> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

# 03 · ChatPromptTemplate：从手写消息列表到模板的必然性

# 03 · ChatPromptTemplate：从手写消息列表到模板的必然性

> **本片目标**：回答"为什么要模板"——当消息列表需要动态拼装、插入历史、复用系统提示时，手写列表会变得脆弱；模板把"消息结构"与"每次的数据"分离。
> **新增规定性：3**（拼消息用 `ChatPromptTemplate`，历史用 `MessagesPlaceholder`）
> **数据字典**：ChatPromptTemplate / MessagesPlaceholder / PromptTemplate / PromptValue。
> **进程线程模型**：无变化（同步阻塞，模板 invoke 是纯内存操作，毫秒级）。
> **网络模型**：模板本身不联网；它把消息拼好后，交给模型 invoke 才发 HTTP。

***

## 1. 上集回顾

第 02 篇我们学会了构造 `SystemMessage` + `HumanMessage` 列表传给模型。但现在遇到两个场景，手写列表开始难受：

1**要插入历史消息**：多轮对话时，消息列表是 `[system, 历史user, 历史ai, 当前user]`——历史是动态的、可长可短。
2**要动态填变量**：`"你是{company}的助手，请回答关于{product}的问题"`——每次拼字符串吗？`f-string` 拼多了就乱。

**本质问题**：消息列表的结构（哪些是 system、哪里插历史、哪里放当前问题）是**固定的**，变的是**数据**（问题内容、公司名、历史消息）。把"固定结构"和"变化数据"混在一起写，必然导致重复代码和拼错。

**模板 = 把结构写一次，数据每次填进去。**

***

## 2. 数据字典：模板类

### 2.1 `ChatPromptTemplate`（聊天模板，本系列主力）

```python theme={null}
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是{company}公司的智能助手，说话简洁。"),  # ① 元组写法: (角色, 模板文本)
    MessagesPlaceholder("history"),                         # ② 历史槽位
    ("human", "{question}"),                                # ③ 当前问题
])
```

| 构造方式             | 签名                                                                                                           | 说明                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------- |
| `from_messages`  | `from_messages(messages: list[MessageLikeRepresentation], template_format="f-string") -> ChatPromptTemplate` | 主力入口                       |
| `invoke`         | `invoke(input: dict, config=None) -> PromptValue`                                                            | 传入变量 dict，得到 PromptValue   |
| `.to_messages()` | `prompt_value.to_messages() -> list[BaseMessage]`                                                            | PromptValue → 消息列表（真正给模型的） |

**MessageLikeRepresentation（模板里每个元素，三种写法）**：

| 写法   | 例子                               | 说明                   |
| ---- | -------------------------------- | -------------------- |
| 元组   | `("system", "你是{company}的助手")`   | 角色 + 文本模板（`{变量}` 插值） |
| 消息对象 | `SystemMessage(content="...")`   | 静态消息（不插值）            |
| 插槽   | `MessagesPlaceholder("history")` | 运行时塞进一**组**消息        |

### 2.2 `MessagesPlaceholder`（历史槽位——关键）

```python theme={null}
MessagesPlaceholder(variable_name="history", optional=False, n_messages=None)
```

| 参数              | 类型     | 说明                   |                   |
| --------------- | ------ | -------------------- | ----------------- |
| `variable_name` | `str`  | 对应 invoke 时 dict 里的键 |                   |
| `optional`      | `bool` | 该键缺省时是否报错            |                   |
| `n_messages`    | \`int  | None\`               | 只取最近 N 条（None=全取） |

**为什么需要它**（规定性 3 的核心）：普通 `{question}` 只能塞一个字符串。而历史是**一组消息对象**。如果不用占位符而是 `("human", "{history}")`，传列表进去会变成字符串字面量 `"['AIMessage(...)', 'AIMessage(...)']"`——模型看到的是 Python 列表的 repr，不是消息！

```python theme={null}
prompt_value = prompt.invoke({
    "company": "Z.ai",
    "history": [HumanMessage(content="我是小林"), AIMessage(content="你好小林！")],
    "question": "我叫什么名字？",
})
messages = prompt_value.to_messages()
# → [SystemMessage("你是Z.ai公司的..."), HumanMessage("我是小林"), AIMessage("你好小林！"), HumanMessage("我叫什么名字？")]
```

### 2.3 `PromptValue`（invoke 的返回）

| 属性/方法            | 类型                  | 说明                     |
| ---------------- | ------------------- | ---------------------- |
| `.to_messages()` | `list[BaseMessage]` | 转消息列表（给模型用这个）          |
| `.to_string()`   | `str`               | 转纯文本（给非聊天模型用）          |
| `.to_prompt()`   | `Prompt`            | 转 OpenAI prompt 结构（少用） |

### 2.4 `PromptTemplate`（单条字符串模板，偶尔用）

```python theme={null}
from langchain_core.prompts import PromptTemplate
t = PromptTemplate.from_template("你是{company}的助手", template_format="f-string")
```

***

## 3. 关键方法

| 方法                                 | 签名                                                | 说明         |
| ---------------------------------- | ------------------------------------------------- | ---------- |
| `ChatPromptTemplate.from_messages` | `(messages, template_format="f-string")`          | 建模板        |
| `prompt.invoke(dict)`              | `invoke(input: dict, config=None) -> PromptValue` | 填变量        |
| `prompt_value.to_messages()`       | `-> list[BaseMessage]`                            | 取消息列表      |
| `prompt.format(...)`               | `format(**kwargs) -> str`                         | 直接得文本（调试用） |
| `prompt.partial(...)`              | `partial(**kwargs)`                               | 预填部分变量（少用） |

***

## 4. 进程线程模型

```
prompt.invoke({...})   ← 纯内存操作：模板字符串插值 + 消息对象组装
  ↓ 毫秒级返回
model.invoke(messages) ← 这里才开始联网，阻塞 1~3 秒
```

模板这一步**不联网、不阻塞**。它是纯 Python 字符串/对象操作。所以放心在任意线程调用。

***

## 5. 网络模型

```
prompt.invoke → to_messages() → [SystemMessage, HumanMessage, AIMessage, HumanMessage]
                                          │
                                          ▼
model.invoke(messages)
  └─ 序列化为 Anthropic Messages JSON:
     {"model": "glm-4.7", "max_tokens": 512,
      "messages": [
        {"role":"system","content":"你是Z.ai公司的智能助手，说话简洁。"},
        {"role":"user","content":"我是小林"},
        {"role":"assistant","content":"你好小林！"},
        {"role":"user","content":"我叫什么名字？"}
      ]}
```

注意：消息的 `type` 字段（`"system"`/`"human"`/`"ai"`）在序列化时被映射成协议的 `role`（`system`/`user`/`assistant`）。

***

## 6. 验证：跑起来

配套代码 `code/03_prompt.py`：

1. 建一个带 system 变量 + history 槽位 + question 的模板；
2. 第一轮（空历史）invoke，打印拼出的消息列表；
3. 第二轮（有历史）invoke，打印历史如何被展开插入；
4. 演示 `{history}` 普通变量 vs `MessagesPlaceholder` 的差异（传列表会变成字面量字符串）。

```powershell theme={null}
cd enterprise-rag-course\code
python 03_prompt.py
```

**预期输出（节选）**：

```
===== 第 1 轮（history 为空） =====
拼出的消息列表:
  [SystemMessage] 你是Z.ai公司的智能助手，说话简洁。
  [HumanMessage]  我叫小林，是后端工程师

===== 第 2 轮（history 有 2 条） =====
拼出的消息列表:
  [SystemMessage] 你是Z.ai公司的智能助手，说话简洁。
  [HumanMessage]  我叫小林，是后端工程师
  [AIMessage]     收到，小林。
  [HumanMessage]  我主要做什么工作？
```

***

## 7. 边界

* **模板不是唯一方式**——`create_agent` 内部就不用模板（直接列表拼接）。但本系列目标是 RAG，模板是拼 prompt 的正道。
* **`{变量}` 只塞字符串**——塞列表会变成 repr 字符串。塞**消息组**必须用 `MessagesPlaceholder`。
* **f-string 语法限制**——模板里别写裸 `{` `}`（如 JSON 示例），要么转义 `{{`，要么用 `template_format="jinja2"`。
* **system 提示也可以插值**——`("system", "你是{company}的助手")` 完全合法，变量在 invoke 时填。

***

## 推荐资料（延伸阅读）

* [LangChain 官方文档 · 提示词模板](https://docs.langchain.com/oss/python/langchain/messages#prompt-templates) —— 模板与 MessagesPlaceholder 权威说明
* [langchain-core API 参考 · ChatPromptTemplate](https://reference.langchain.com/python/langchain-core/prompts/ChatPromptTemplate) —— 模板类的完整方法签名

***

## 8. 未完待续

现在"输入"这块齐了：模型 + 消息 + 模板。但**输出**还有问题：

1. `invoke` 返回的是 AIMessage，我想要纯文本——每次手动 `.content`？
2. 模型回答慢，用户等 2 秒才看到整段文字——能不能**流式**输出，像 ChatGPT 那样一个字一个字蹦？

这就是第 04 篇：输出解析与流式。

→ [04 · 输出解析与流式](/doc/doc/enterprise-rag-course/04-输出解析与流式)
