# 13 - 提示词与消息模板

---

**本章课程目标：**

- 理解 **Prompt** 是什么，知道它为什么会从“一段字符串”逐步演化成“多角色消息 + 模板 + 占位符”。
- 掌握 LangChain 中与输入组织最相关的三块内容：**消息类型（Message）**、**模型调用方式（invoke / stream / batch）**、**提示词模板（PromptTemplate / ChatPromptTemplate）**。
- 会运行并理解本章全部案例：**输入类型、同步与异步调用、文本模板、对话模板、消息占位符、从 JSON / YAML 加载提示词**，为后续 [输出解析器](14-输出解析器.md)、[LCEL 与链式调用](15-LCEL与链式调用.md)、[记忆与对话历史](16-记忆与对话历史（含Redis基础）.md) 打基础。

**学习建议：** 读这一章时，拿一条普通用户问题练手，把它改造成消息列表：系统消息放规则，用户消息放任务，历史消息放上下文。先理解模型到底吃进去什么，再看 `PromptTemplate`、`ChatPromptTemplate`、`MessagesPlaceholder` 怎么帮你复用这些结构。读完后最好能判断：什么时候写死提示词，什么时候抽成模板，什么时候需要把模板放到文件里。

**官方文档与资源**：详见 [工具导航与参考资料索引 - 提示词与结构化输出](工具导航与参考资料索引.md#提示词与结构化输出)。

---

## 1、Prompt 简介

本章对应 [第 11 章 Model I/O](11-Model-I-O与模型接入.md) 里的 **输入格式化（Format）** 与 **模型调用（Predict）** 两部分。模型并不是“凭空思考”，它始终是在读取我们提供的输入后再生成输出；而 Prompt、Message、Template，正是组织这份输入的核心工具。

如果你已经学过 [1-2 提示词工程基础](1-2-提示词工程基础.md)，那么本章可以直接理解为它的代码实现版：前一章讲“怎么把角色、任务、上下文、输入、输出和约束说清楚”，这一章讲“这些内容进入 LangChain 后，应该以什么输入形态存在、如何复用、如何插入多轮历史、如何外置到文件”。

### 1.1 定义

**Prompt（提示词）**，就是你发给大模型的输入内容。

最简单的 Prompt，是一句自然语言，比如“什么是 LangChain？”；再进一步，你会给它增加角色，比如“你是一名法律顾问，请用 50 字介绍广告法”；再往后，为了让代码可复用、可维护、可协作，你会把这段输入写成模板，并把会变化的部分改成占位符。这就是从“随手提问”走向“工程化输入管理”的过程。

入门阶段可先把握一个基本判断：**Prompt 不只是把一句话写漂亮，而是把模型输入组织清楚。**

在真实项目中，Prompt 通常承担几件事：

- 告诉模型它是谁，要扮演什么角色。
- 告诉模型当前任务是什么，回答边界是什么。
- 告诉模型输出格式应该长什么样。
- 把用户问题、历史对话、检索结果、工具结果组合成一次完整输入。

这也是为什么随着项目复杂度提高，Prompt 会从“一个字符串”逐步演化成“多角色消息 + 模板 + 占位符 + 外部配置文件”。

![DeepSeek 官方提示词示例库入口](images/13/13-1-1-1.jpeg)

### 1.2 Prompt 的作用

很多人在学 LangChain 时，会把注意力都放在“接哪个模型”“换哪个平台”“怎么配 API Key”上，但真正到了项目里，最容易让系统效果不稳定的，往往不是模型接入，而是**输入组织得不够清楚**。

下面这些真实开发场景，都离不开本章内容：

- **智能客服**：需要系统提示词规定语气、身份和拒答策略。
- **企业知识库问答**：需要把“检索出来的上下文 + 用户问题”组合成一条清晰提示。
- **多轮聊天**：需要把历史对话插回当前输入，而不是每轮都从头问。
- **结构化输出**：需要提前在 Prompt 里写清楚输出格式要求，方便后面交给解析器处理。
- **团队协作与 A/B 测试**：需要把 Prompt 模板从代码里抽出来，放到 JSON / YAML 中做版本管理。

一句话：**模型能力决定上限，Prompt 设计决定你能不能稳定接近这个上限。**

### 1.3 本章在项目中的位置

本章对应仓库中的 `案例与源码-2-LangChain框架/04-prompt` 目录，按学习顺序可以分成 4 类：

| 目录                    | 作用         | 你会学到什么                               |
| ----------------------- | ------------ | ------------------------------------------ |
| `invoke/`               | 模型调用方式 | `invoke`、`stream`、`batch` 及异步版本     |
| `prompt_templates/`     | 文本模板     | `PromptTemplate` 的创建、格式化、复用      |
| `chat_prompt_template/` | 对话模板     | `ChatPromptTemplate`、消息参数、消息占位符 |
| `load_external/`        | 外部文件加载 | 如何从 JSON / YAML 加载 Prompt             |

这一章不是零散的 API 介绍，而是在回答一个问题：**开发 LLM 应用时，怎样把 [1-2 提示词工程基础](1-2-提示词工程基础.md) 里学到的写法和原则，组织成一套可复用、可维护、可扩展的结构？**

---

## 2、调用大模型的入参类型

初学者容易误以为：调用聊天模型时，输入永远只能是“一段字符串”。实际上，LangChain 聊天模型为了适配真实对话场景，通常支持多种输入形态。这些写法表面不同，核心都一样：**把 [1-2 章](1-2-提示词工程基础.md) 里讲过的角色、任务、上下文、输入和约束，以合适的消息结构交给模型。**

### 2.1 入参形态总览

同一次 `invoke`，左侧可以是多种类型的输入，中间由聊天模型处理，右侧典型返回值是 `AIMessage`。这也是为什么你在 [第 11 章](11-Model-I-O与模型接入.md) 会经常看到“正文一般通过 `.content` 读取”。

![聊天模型 invoke：常见入参类型与 AIMessage 输出](images/13/13-2-1-1.png)

常见对应关系如下：

| 入参形态                                     | 调用时大致长什么样                                      | 适合场景                           |
| -------------------------------------------- | ------------------------------------------------------- | ---------------------------------- |
| `str`                                        | `model.invoke("请解释什么是 LangChain")`                | 单轮、轻量、快速试接口             |
| `PromptTemplate.format(...)` 后的字符串      | `model.invoke(prompt_str)`                              | 固定句式 + 少量变量替换            |
| 消息对象列表                                 | `model.invoke([SystemMessage(...), HumanMessage(...)])` | 推荐写法，适合系统提示、多轮对话   |
| `(role, content)` 元组列表                   | `[("system", "..."), ("user", "...")]`                  | 简洁、贴近 ChatPromptTemplate 风格 |
| `{"role": "...", "content": "..."}` 字典列表 | `[{"role":"system","content":"..."}, ...]`              | 贴近 OpenAI 风格 JSON 数据         |

【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke_InputTypes.py`

[LLM_Invoke_InputTypes.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke_InputTypes.py ":include :type=code")

### 2.2 写法一：纯字符串

最简单的输入方式，把整段说明写在一个字符串里交给模型，适合快速试接口、或单轮一句话任务。

```python
resp = model.invoke("用一句话解释什么是 LangChain")
print(resp.content)
```

不过在真实项目里，纯字符串也有明显局限：不方便表达系统角色与用户问题的边界；不方便插入历史对话；不利于后期维护和多人协作。

所以，**纯字符串适合起步，不适合复杂对话场景长期使用。**

### 2.3 写法二：模板 + 占位符

如果一句 Prompt 里只有少数变量会变化，就没必要每次手写整段长文本。更合理的做法，是把固定部分写成模板，把变化部分留成占位符。

这一节本质上仍然是“字符串输入”，只是我们把字符串的生成过程工程化了。

```python
from langchain_core.prompts import PromptTemplate

template = PromptTemplate.from_template(
    "用不超过 50 字介绍：{topic} 是什么？"
)
prompt_str = template.format(topic="LangChain")
resp = model.invoke(prompt_str)
print(resp.content)
```

也就是说：

- **`PromptTemplate` 负责生产输入**
- **`model.invoke(...)` 负责把输入发给模型**

它和 2.2 的区别不在于模型收到了不同类型，而在于**我们是手写字符串，还是用模板生成字符串**。

### 2.4 写法三：多角色消息列表

当你开始做聊天机器人、问答助手、企业知识库、代码助手时，最推荐的入参方式通常不是字符串，而是**消息列表**。

原因很简单：聊天模型更擅长理解“谁在说话”。把输入拆成 `SystemMessage`、`HumanMessage`、`AIMessage` 等不同角色，模型更容易正确理解上下文结构，而不是把所有东西都当成一大段平铺文本。

```python
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

messages = [
    SystemMessage(content="你是只回答技术问题的助手，回答要简短。"),
    HumanMessage(content="什么是 LangChain？"),
    # 多轮示例：
    # AIMessage(content="LangChain 是用于编排 LLM 应用的框架……"),
    # HumanMessage(content="它和直接调 API 有什么区别？"),
]
resp = model.invoke(messages)
print(resp.content)
```

在实际项目里，这种写法特别常见：

- **系统提示词**放在 `SystemMessage`
- **用户问题**放在 `HumanMessage`
- **历史回复**可放回 `AIMessage`
- **工具执行结果**后续可用 `ToolMessage`

如果你后面要做多轮对话、Agent、RAG，这种消息列表思维会反复用到。

还有一种等价包装是 `ChatPromptValue`：链式编排里更常见的是由 `ChatPromptTemplate.invoke(...)` 得到 `ChatPromptValue`；下面直接构造对象，便于理解「`PromptValue` → `to_messages()` → `model.invoke`」的衔接。

```python
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.prompt_values import ChatPromptValue

prompt_value = ChatPromptValue(
    messages=[
        SystemMessage(content="You are a helpful AI bot. Your name is Bob."),
        HumanMessage(content="Hello, how are you doing?"),
        AIMessage(content="I'm doing well, thanks!"),
        HumanMessage(content="What is your name?"),
    ]
)
resp = model.invoke(prompt_value.to_messages())
```

### 2.5 写法四：元组列表与字典列表

除了显式使用 `SystemMessage`、`HumanMessage` 等类，LangChain 聊天模型通常还支持两种常见简写。

- **元组列表**：每项写成 `(role, content)`
- **字典列表**：每项写成 `{"role": "...", "content": "..."}`

这两种写法与消息对象列表在语义上基本等价，只是更接近手写列表或 OpenAI 风格的数据结构。

```python
import os
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0.0,
    base_url=os.getenv("OPENAI_BASE_URL"),
    api_key=os.getenv("OPENAI_API_KEY"),
)

messages_as_tuples = [
    ("system", "你是一个专业的数学助手"),
    ("user", "你好，你是谁"),
]
messages_as_dicts = [
    {"role": "system", "content": "你是一个专业的数学助手"},
    {"role": "user", "content": "你好，你是谁"},
]

resp = llm.invoke(messages_as_tuples)
print(type(resp))
print(resp.content)

resp2 = llm.invoke(messages_as_dicts)
print(resp2.content)
```

关于选择策略，可按下面的思路理解：

- **想学得最清楚**：优先用 `Message` 类
- **想写得最简洁**：可以用元组列表
- **想和 OpenAI 风格请求体对齐**：可以用字典列表

无论哪种方式，同步 `invoke` 的返回值通常仍是 `AIMessage`。

### 2.6 扩展：Java 生态中的多角色

“System / User / Assistant / Tool” 这套思路并不是 LangChain Python 独有的，Java 生态中的 **LangChain4J**、**Spring AI** 也有类似设计。理解这一点很有价值，因为它说明：

**多角色消息并不是某个框架的语法技巧，而是现代聊天模型交互的一种通用抽象。**

**LangChain4J：**

```java
package dev.langchain4j.data.message;

public enum ChatMessageType {
    SYSTEM(SystemMessage.class),
    USER(UserMessage.class),
    AI(AiMessage.class),
    TOOL_EXECUTION_RESULT(ToolExecutionResultMessage.class),
    CUSTOM(CustomMessage.class);

    private final Class<? extends ChatMessage> messageClass;

    ChatMessageType(Class<? extends ChatMessage> messageClass) {
        this.messageClass = messageClass;
    }

    public Class<? extends ChatMessage> messageClass() {
        return messageClass;
    }
}
```

**Spring AI：**

```java
package org.springframework.ai.chat.messages;

public enum MessageType {
    USER("user"),
    ASSISTANT("assistant"),
    SYSTEM("system"),
    TOOL("tool");

    private final String value;

    MessageType(String value) {
        this.value = value;
    }

    public static MessageType fromValue(String value) { ... }

    public String getValue() {
        return this.value;
    }
}
```

---

## 3、入参的消息类型

当你把输入组织成“消息列表”之后，就需要知道每种消息类型代表什么。在 LangChain 官方语境里，最核心的几类消息是：**SystemMessage**、**HumanMessage**、**AIMessage**、**ToolMessage**。

**文档**：https://docs.langchain.com/oss/python/langchain/messages （英文）；https://docs.langchain.org.cn/oss/python/langchain/messages (中文)

### 3.1 四类核心消息

| 类型            | 说明                                             | 项目里最常见的用途                  |
| --------------- | ------------------------------------------------ | ----------------------------------- |
| `SystemMessage` | 系统消息，通常用于规定角色、风格、边界、输出格式 | 设定人设、回答规则、拒答策略        |
| `HumanMessage`  | 用户消息，对应用户当前输入                       | 放用户问题、补充条件、后续追问      |
| `AIMessage`     | 模型回复消息                                     | 保存上一轮回复，支持多轮上下文      |
| `ToolMessage`   | 工具执行结果消息                                 | 把外部工具/函数的返回结果回传给模型 |

需要注意两个细节：

1. **LangChain 的类名叫 `HumanMessage`，但很多平台的角色字段写的是 `user`。**这不是冲突，而是不同层的命名习惯。你可以把它们理解成同一个角色。

2. **旧版本资料里可能会看到 `FunctionMessage`。**在 LangChain 1.x 语境下，更常见的是 `ToolMessage`。如果你阅读旧教程或旧代码，看到这类差异，先把它理解为工具调用结果的旧命名即可。

### 3.2 SystemMessage 的作用

很多新手会把系统提示词和用户问题混在一段字符串里写，这样虽然也能跑，但可维护性很差。更稳妥的做法是把“规则”和“问题”拆开：

- **SystemMessage** 负责定义系统层规则
- **HumanMessage** 负责承载当前用户问题

例如：

- “你是一个法律助手，只回答法律问题”
- “输出请控制在 80 字内”
- “如果超出范围，请明确拒答”

这些内容更适合放在 `SystemMessage` 里，因为它们属于“长期规则”，而不是某一轮具体问题。

这正对应了 [1-2 提示词工程基础](1-2-提示词工程基础.md) 里强调的那条原则：**稳定约束和动态输入要拆开写。**在 LangChain 里，这条原则最直接的落地方式就是把稳定规则放进 `SystemMessage`，把当前任务放进 `HumanMessage`。

在真实项目里，SystemMessage 往往决定：

- 回答口吻是否专业
- 是否允许推测
- 是否必须引用上下文
- 是否必须按 JSON 输出
- 是否需要保守拒答

这也是为什么 Prompt 工程里经常会说：**系统提示词是行为边界，用户提示词是当前任务。**

### 3.3 ToolMessage 什么时候会出现

`ToolMessage` 不会在普通问答里频繁出现，它更常见于函数调用、工具调用、Agent 编排场景。

简单理解就是：

- 模型先在 `AIMessage` 里表达“我想调用某个工具”
- 代码真的去执行工具
- 工具结果再以 `ToolMessage` 的形式回传给模型

这样模型才能基于工具结果继续回答。

下面是一个简化示例：

```python
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage

messages = [
    SystemMessage(content="你是一位乐于助人的智能小助手"),
    HumanMessage(content="你好，请你介绍一下你自己"),
    AIMessage(content="我是一名人工智能助手，请问您有什么想问的吗？"),
    ToolMessage(
        content='{"population": 21540000, "area": "16410平方公里"}',
        tool_call_id="call_abc123",
    ),
]
print(messages)
```

对当前章节来说，你只需要先建立基本印象：**最常用的是 System / Human / AI 三类；ToolMessage 是后续 Agent、工具调用章节的重要铺垫。**

---

## 4、调用大模型的调用方式

当输入组织好之后，下一步就是把它交给模型。LangChain 聊天模型常见的调用方式有四类：普通调用、流式调用、批量调用，以及它们各自的异步版本。

这部分看起来像 API 记忆题，其实可以用一个更直观的方式理解：

- **invoke / ainvoke**：一次发一条
- **stream / astream**：一边生成一边返回
- **batch / abatch**：一次发很多条

### 4.1 普通调用（invoke / ainvoke）

【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke.py`、`LLM_aInvoke.py`

- `invoke`：同步调用，最常用，适合单轮问答与脚本演示。
- `ainvoke`：异步调用，适合异步 Web 服务、并发任务和高吞吐场景。

```python
import os
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, SystemMessage

model = init_chat_model(
    model="qwen-plus",
    model_provider="openai",
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
messages = [
    SystemMessage(content="你是一个法律助手，只回答法律问题，超出范围回答：非法律问题无可奉告"),
    HumanMessage(content="简单介绍下广告法，一句话 50 字以内")
]
response = model.invoke(messages)
print(type(response))
print(response.content)
```

实际项目里怎么选：

- **命令行脚本、教学示例、简单后台任务**：优先 `invoke`
- **FastAPI、异步服务、并发请求**：优先 `ainvoke`
- **本地开源模型（Ollama 等）**：实例化与端点配置见 [第 12 章 Ollama 本地部署与调用](12-Ollama本地部署与调用.md)，本章的 `invoke` / `stream` / `batch` 用法同样适用。

[LLM_Invoke.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke.py ":include :type=code")

[LLM_aInvoke.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_aInvoke.py ":include :type=code")

### 4.2 流式调用（stream / astream）

【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Stream.py`、`LLM_aStream.py`

- **stream**：同步流式输出
- **astream**：异步流式输出

流式的最大价值不是“更快算完”，而是**更快把正在生成的内容展示给用户**。在聊天机器人、报告生成、代码生成等场景里，用户体验会明显更好。

```python
messages = [
    SystemMessage(content="你叫小问，是一个乐于助人的AI助手"),
    HumanMessage(content="你是谁")
]
for chunk in model.stream(messages):
    print(chunk.content, end="", flush=True)
print()
```

真实项目里，`stream` / `astream` 很常用于：

- 聊天界面的“打字机效果”
- 长回答提前回显
- 减少用户等待焦虑

[LLM_Stream.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Stream.py ":include :type=code")

[LLM_aStream.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_aStream.py ":include :type=code")

### 4.3 批处理（batch / abatch）

【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Batch.py`、`LLM_aBatch.py`

- **batch**：一次提交多条输入，统一获得多条结果
- **abatch**：异步批处理

它特别适合离线任务，而不是交互式聊天。例如：

- 批量摘要一批文档
- 批量清洗问答数据
- 批量评估 Prompt 效果
- 批量为商品、评论、工单做标签分类

```python
questions = [
    "什么是 Redis？简洁 100 字以内",
    "Python 的生成器是做什么的？简洁 100 字以内",
]
response = model.batch(questions)
for q, r in zip(questions, response):
    print(f"问题：{q}\n回答：{r.content}\n")
```

[LLM_Batch.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Batch.py ":include :type=code")

[LLM_aBatch.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_aBatch.py ":include :type=code")

### 4.4 小结

| 场景     | 同步     | 异步      | 适合什么情况        |
| -------- | -------- | --------- | ------------------- |
| 单条调用 | `invoke` | `ainvoke` | 单轮问答、接口服务  |
| 流式输出 | `stream` | `astream` | 聊天 UI、长文本生成 |
| 批量处理 | `batch`  | `abatch`  | 离线任务、批量评估  |

如果你一时记不住，也没关系，先记住这条基本规则：

**先会 `invoke`，再学 `stream`，最后再补 `batch` 和异步版本。**

---

## 5、提示词模板概览

### 5.1 提示词简介

在真正的项目里，Prompt 几乎不可能永远写死在代码中。原因很现实：

- 用户问题会变
- 角色设定会变
- 输出要求会变
- 业务策略会调整
- 团队成员需要一起维护

如果每次都手写一整段 Prompt，不仅容易重复，还非常难维护。提示词模板的作用，就是把**固定部分沉淀下来，把变化部分改成变量**，从而让同一套提示逻辑可以反复复用。

这和 Python 的 `f-string` 很像：

```python
def hello(name: str) -> None:
    print(f"你好：{name}")

if __name__ == "__main__":
    hello("李四")
```

这里的 `{name}` 就像 Prompt 模板里的占位符，直接把它看成“先留坑，后填值”就行。

### 5.2 提示词模板类型

LangChain 中常见的提示词模板主要有下面几类，本课程重点掌握前两种即可：

| 类型                      | 说明                                         | 本课程定位 |
| ------------------------- | -------------------------------------------- | ---------- |
| **PromptTemplate**        | 面向纯文本模板，填值后通常得到一条字符串     | 重点掌握   |
| **ChatPromptTemplate**    | 面向聊天模型的多角色模板，填值后得到多条消息 | 重点掌握   |
| **FewShotPromptTemplate** | 把若干“示例输入-输出”嵌入提示词              | 了解即可   |
| **PipelinePrompt**        | 把多个子提示按顺序组合                       | 了解即可   |

入门阶段可以先作一个简化理解：

- **单条文本任务**，先看 `PromptTemplate`
- **聊天模型、多角色、多轮对话**，重点看 `ChatPromptTemplate`

### 5.3 Few-shot 模板

Few-shot 的意思是：不要只告诉模型“按什么规则做”，还给它几组“输入应该怎么变成输出”的示例。

它适合下面这类场景：

- 分类标签容易混，需要给几个标准样例
- 输出风格有要求，单靠文字说明不够稳
- 任务规则不复杂，但希望模型模仿固定格式

入门阶段不用急着背 `FewShotPromptTemplate` 的所有参数，先记住一句话：**Few-shot 是把示例变成提示词的一部分，让模型照着样子做。**等你后面做分类、抽取、客服话术生成时，再把它作为提高稳定性的手段即可。

---

## 6、文本提示词模板（PromptTemplate）

### 6.1 简介

`PromptTemplate` 是 LangChain 中最基础的模板类，适合把一段文本 Prompt 做成“固定骨架 + 动态变量”的形式。

它最适合的场景是：

- 摘要、改写、翻译、分类等单轮文本任务
- 还不需要明确区分 system / user 角色
- 需要频繁替换少量变量

如果你把它和第 2 节联系起来看，会更容易理解：**PromptTemplate 的结果通常还是字符串，只不过这条字符串不再靠手写，而是由模板生成。**

### 6.2 常用参数

| 参数                | 说明                                     |
| ------------------- | ---------------------------------------- |
| `template`          | 模板字符串，内部可包含 `{变量名}` 占位符 |
| `input_variables`   | 调用时需要传入的变量名列表               |
| `partial_variables` | 在模板创建阶段就预先固定的一部分变量     |

其中，`partial_variables` 尤其值得理解。它的作用很直接：

**把那些“经常不变”的变量先固定住，后续每次只传真正会变化的部分。**

典型例子：

- 系统角色长期固定为“Python 工程师”
- 但用户问题每次都不同

这样一来，你就不用每次都重复传 `role="Python 工程师"`。

### 6.3 常用方法

| 方法            | 返回值                | 适合场景                                     | 案例源码                          |
| --------------- | --------------------- | -------------------------------------------- | --------------------------------- |
| `format(...)`   | `str`                 | 最常用，拿到字符串后直接给模型或自己继续拼接 | `PromptTemplate_FormatMethod.py`  |
| `invoke({...})` | `PromptValue`         | 需要接入 LangChain 链时更自然                | `PromptTemplate_InvokeMethod.py`  |
| `partial(...)`  | 新的 `PromptTemplate` | 先固定部分变量，再多次复用                   | `PromptTemplate_PartialMethod.py` |

```python
from langchain_core.prompts import PromptTemplate

template = PromptTemplate.from_template(
    "你是一个专业的{role}工程师，请回答我的问题，我的问题是：{question}"
)

# 1）format：得到 str
prompt_str = template.format(role="python开发", question="二分查找怎么写？")

# 2）invoke：得到 PromptValue
prompt_value = template.invoke({"role": "python开发", "question": "冒泡排序怎么写？"})
prompt_value.to_string()
prompt_value.to_messages()

# 3）partial：固定 role，得到新模板
new_template = template.partial(role="python开发")
prompt_str = new_template.format(question="快速排序怎么写？")
```

对初学者的实用建议是：

- **刚入门时优先用 `format`**
- **做 LCEL 或链式调用时再逐渐理解 `invoke`**
- **同一模板长期复用时再考虑 `partial`**

【案例源码】  
format：`案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_FormatMethod.py`  
invoke：`案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_InvokeMethod.py`  
partial：`案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_PartialMethod.py`

[PromptTemplate_FormatMethod.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_FormatMethod.py ":include :type=code")

[PromptTemplate_InvokeMethod.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_InvokeMethod.py ":include :type=code")

[PromptTemplate_PartialMethod.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_PartialMethod.py ":include :type=code")

### 6.4 创建方式

`PromptTemplate` 常见有两种创建方式：

- 构造函数：手动指定 `template` 和 `input_variables`
- `from_template(...)`：由 LangChain 自动推断变量名

```python
from langchain_core.prompts import PromptTemplate

# 方式一：构造函数
template = PromptTemplate(
    template="你是一个专业的{role}工程师，请回答：{question}",
    input_variables=["role", "question"]
)
prompt = template.format(role="python开发", question="快速排序怎么写？")

# 方式二：from_template
template = PromptTemplate.from_template("请给我一个关于{topic}的{type}解释。")
prompt = template.format(topic="量子力学", type="详细")
```

从经验上看，可按下面的方式选择：

- **模板简单**：优先 `from_template(...)`
- **你想显式表达变量名**：用构造函数

【案例源码】构造函数：`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Constructor.py`  
【案例源码】`from_template`：`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_FromTemplate.py`

[PromptTemplate_Constructor.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Constructor.py ":include :type=code")

[PromptTemplate_FromTemplate.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_FromTemplate.py ":include :type=code")

除了创建方式，本章还保留了两个补充案例。

**第一，组合多个模板。**  
当一个 Prompt 由多个子部分拼起来时，可以通过 `+` 组合模板，而不是手写超长字符串。真实项目里，这在“角色说明 + 业务规则 + 当前任务”这种分段组织里很有用。

【案例源码】`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Combined.py`

[PromptTemplate_Combined.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Combined.py ":include :type=code")

**第二，比较 `partial_variables` 与 `partial()`。**  
二者都能做到“先固定一部分变量，后续只传剩余变量”，但一个发生在模板创建阶段，一个发生在已有模板基础上。

【案例源码】`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_PartialVariables.py`

[PromptTemplate_PartialVariables.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_PartialVariables.py ":include :type=code")

---

## 7、对话提示词模板（ChatPromptTemplate）

### 7.1 简介

如果说 `PromptTemplate` 更适合“单条文本输入”，那么 `ChatPromptTemplate` 就是为“聊天模型场景”准备的模板类。

它比 `PromptTemplate` 更贴合真实项目，因为真实聊天应用往往不只是“一句话”，而是：

- 一条系统设定
- 一条或多条用户消息
- 可能还有历史 AI 回复
- 可能再插入工具结果或历史上下文

这时，把所有内容写成一大段纯文本虽然也能跑，但不如把它们拆成带角色的消息来得清晰。

```python
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

messages = [
    SystemMessage(content="你是一个AI开发工程师"),
    HumanMessage(content="你能开发哪些AI应用?"),
    AIMessage(content="我能开发很多AI应用，比如聊天机器人、图像识别等")
]
```

所以你可以把 `ChatPromptTemplate` 简单理解成：**“面向多角色消息的模板系统”。**

### 7.2 常用参数

`ChatPromptTemplate` 的核心不是单个 `template` 字符串，而是一组“消息模板”。每一项都可以是下面这些形式：

| 类型                  | 说明                                          | 案例源码                             |
| --------------------- | --------------------------------------------- | ------------------------------------ |
| 元组                  | `("system", "你是{name}")`                    | `ChatPromptTemplate_TupleParam.py`   |
| 字典                  | `{"role": "system", "content": "你是{name}"}` | `ChatPromptTemplate_DictParam.py`    |
| Message 类            | `SystemMessage(content="你是{name}")`         | `ChatPromptTemplate_MessageParam.py` |
| `MessagesPlaceholder` | 在模板中预留一段“消息列表占位”                | 见 7.5 节                            |

三种常规写法里，没有绝对的谁对谁错，可以按下面的经验来选：

- **元组**：最简洁，教学和业务代码里都很常见
- **字典**：更贴近 OpenAI 风格 JSON，方便和网关数据结构对齐
- **Message 类**：最显式，角色最清楚，适合教学和复杂场景

【案例源码】  
元组：`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_TupleParam.py`  
字典：`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_DictParam.py`  
Message 类：`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_MessageParam.py`

[ChatPromptTemplate_TupleParam.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_TupleParam.py ":include :type=code")

[ChatPromptTemplate_DictParam.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_DictParam.py ":include :type=code")

[ChatPromptTemplate_MessageParam.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_MessageParam.py ":include :type=code")

下面这个基础示例能帮助你直观看懂三种写法的共同点：它们都是在定义“系统说什么、用户说什么、哪些部分由运行时填值”。

```python
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate

prompt1 = ChatPromptTemplate.from_messages([
    ("system", "你是助手，名字叫{name}。"),
    ("human", "{question}")
])

prompt2 = ChatPromptTemplate.from_messages([
    {"role": "system", "content": "你是助手，名字叫{name}。"},
    {"role": "user", "content": "{question}"}
])

prompt3 = ChatPromptTemplate.from_messages([
    SystemMessage(content="你是助手，名字叫{name}。"),
    HumanMessage(content="{question}")
])
```

### 7.3 常用方法

| 方法                   | 返回值              | 使用建议                                    |
| ---------------------- | ------------------- | ------------------------------------------- |
| `format_messages(...)` | `List[BaseMessage]` | 最直观，得到消息列表后交给模型              |
| `invoke({...})`        | `ChatPromptValue`   | 适合与 LangChain 链条衔接，也可直接交给模型 |
| `format(...)`          | `str`               | 适合查看最终拼接效果，不推荐作为聊天主写法  |

这三个方法最容易混淆，建议这样理解：

- `format_messages`：我要的是“消息列表”
- `invoke`：我要的是“PromptValue 对象”
- `format`：我要的是“纯字符串”

```python
from langchain_core.prompts import ChatPromptTemplate

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个{role}，请回答我提出的问题"),
    ("human", "请回答：{question}")
])

# 方式一：得到消息列表
messages = chat_prompt.format_messages(
    role="python开发工程师",
    question="堆排序怎么写"
)
result = model.invoke(messages)

# 方式二：得到 ChatPromptValue
prompt_value = chat_prompt.invoke({
    "role": "python开发工程师",
    "question": "快速排序怎么写"
})
result = model.invoke(prompt_value)

# 方式三：得到纯字符串
prompt_str = chat_prompt.format(
    role="python开发工程师",
    question="快速排序怎么写"
)
print(prompt_str)
```

对实际项目来说，建议优先采用这两种：

- `format_messages(...) -> model.invoke(messages)`
- `invoke({...}) -> model.invoke(prompt_value)`

因为这两种方式都能保留清晰的消息角色结构。

【案例源码】`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_FormatMessages.py`

[ChatPromptTemplate_FormatMessages.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_FormatMessages.py ":include :type=code")

### 7.4 创建方式

`ChatPromptTemplate` 常见也有两种创建方式：

- `ChatPromptTemplate.from_messages([...])`
- `ChatPromptTemplate([...])`

它们的核心差别不大，本质上都是把一组消息模板交给 `ChatPromptTemplate`。

```python
from langchain_core.prompts import ChatPromptTemplate

messages = [
    ("system", "你是一个{role}，请回答我提出的问题"),
    ("human", "请回答：{question}")
]

chat_prompt1 = ChatPromptTemplate.from_messages(messages)
chat_prompt2 = ChatPromptTemplate(messages)

print(chat_prompt1.format_messages(role="python开发工程师", question="堆排序怎么写"))
print(chat_prompt2.format_messages(role="python开发工程师", question="堆排序怎么写"))
```

经验上更推荐优先使用 `from_messages(...)`，因为可读性更好，也更符合官方文档和社区示例的主流写法。

【案例源码】`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_Constructor.py`

[ChatPromptTemplate_Constructor.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_Constructor.py ":include :type=code")

### 7.5 MessagesPlaceholder

MessagesPlaceholder（消息占位符）这是本章最重要的知识点之一，很多人一开始会觉得它抽象，但一旦理解，就会发现它几乎是多轮对话、记忆、历史上下文拼接的关键。

**先说结论：`MessagesPlaceholder` 的作用，就是在模板里先留出一段“消息列表的位置”，等真正调用时再把历史对话整块塞进去。**

![MessagesPlaceholder 将历史消息动态插入 ChatPromptTemplate：模板先留占位，运行时再填入多轮消息](images/13/13-7-5-1.svg)

为什么它重要？因为真实项目里的“历史对话”往往不是固定写死的：

- 有时只有 2 轮
- 有时有 10 轮
- 有时还要先裁剪、总结、过滤

如果没有占位符，你就只能在代码里手动拼接 `HumanMessage`、`AIMessage`，又乱又难维护。

它常见有两种写法：

- **显式写法**：`MessagesPlaceholder("memory")`
- **隐式写法**：`("placeholder", "{memory}")`

二者的核心思想完全一样，只是语法风格不同。

典型结构通常是：

- 系统设定
- 历史消息占位
- 当前用户问题

```python
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个资深的Python应用开发工程师，请认真回答我提出的Python相关的问题"),
    MessagesPlaceholder("memory"),
    ("human", "{question}")
])

prompt_value = prompt.invoke({
    "memory": [
        HumanMessage(content="我的名字叫亮仔，是一名程序员"),
        AIMessage(content="好的，亮仔你好")
    ],
    "question": "请问我的名字叫什么？"
})

print(prompt_value.to_string())
```

这段代码的价值在于，它让模板本身变得非常稳定，而把“历史有几轮、具体内容是什么”延迟到运行时再决定。后面学 [第 16 章 记忆与对话历史](16-记忆与对话历史（含Redis基础）.md) 时，你会频繁看到这种模式。

【案例源码】  
显式：`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ExplicitPlaceholder.py`  
隐式：`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ImplicitPlaceholder.py`

[ChatPromptTemplate_ExplicitPlaceholder.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ExplicitPlaceholder.py ":include :type=code")

[ChatPromptTemplate_ImplicitPlaceholder.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ImplicitPlaceholder.py ":include :type=code")

---

## 8、从文件加载提示词

当 Prompt 还很短时，把它直接写在 Python 代码里问题不大；但只要你开始做真实项目，就会很快遇到几个问题：Prompt 越来越长，代码越来越乱；产品、运营、算法同学希望一起改 Prompt；需要保留多个版本做 A/B 测试；想把 Prompt 与代码逻辑分离。

这时候，把 Prompt 放到 **JSON / YAML** 等外部文件中，就会更工程化。LangChain 提供了 `load_prompt(...)`，可以根据文件内容加载为模板对象。对于本章案例来说，最常见的是 `_type: "prompt"`，即加载为 `PromptTemplate`。

### 8.1 从 JSON 加载

```json
{
  "_type": "prompt",
  "input_variables": ["name", "what"],
  "template": "请{name}讲一个{what}的故事"
}
```

```python
from langchain_core.prompts import load_prompt

template = load_prompt("prompt.json", encoding="utf-8")
print(template.format(name="张三", what="搞笑"))
```

### 8.2 从 YAML 加载

YAML 与 JSON 的核心思路完全相同，只是文件格式更适合人工阅读，也更方便写注释。对团队协作来说，很多场景会更喜欢 YAML。

运行这类案例时，先确认**当前工作目录**，否则相对路径可能找不到文件。这一点和 [第 10 章 HelloWorld](10-LangChain快速上手与HelloWorld.md) 中的 `.env` 读取问题本质很像，都是“脚本运行位置”和“相对路径”的关系。

【案例源码】  
JSON：`案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo01.py`  
YAML：`案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo02.py`  
配套文件：`prompt.json`、`prompt.yaml`

[PromptLoadDemo01.py](案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo01.py ":include :type=code")

[PromptLoadDemo02.py](案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo02.py ":include :type=code")

到这里你可以看到，本章知识已经形成了一个比较完整的工程化闭环：

- **消息类型** 解决“输入按什么角色组织”
- **调用方式** 解决“输入如何发给模型”
- **PromptTemplate / ChatPromptTemplate** 解决“输入如何复用”
- **MessagesPlaceholder** 解决“历史对话如何动态插入”
- **JSON / YAML 外置文件** 解决“模板如何协作与版本管理”

---

**章节思考题：**

1. 普通字符串 Prompt 和消息模板最大的工程差别是什么？

   **参考思路：** 普通字符串适合临时调用，消息模板更适合长期维护。它把系统规则、用户输入、历史消息和变量占位分清楚，后续改规则、换输入、接多轮历史都会更稳。

2. 什么时候应该用 `MessagesPlaceholder`，而不是把历史对话拼成一大段字符串？

   **参考思路：** 需要保留消息角色、顺序和多轮结构时，应使用 `MessagesPlaceholder`。拼字符串会丢掉角色信息，也不利于后续和记忆、工具调用、消息对象体系衔接。

3. 如果一个模板变量越来越多，你会如何判断它是不是该拆分？

   **参考思路：** 看变量是否服务同一个任务、是否来自同一层上下文、是否经常一起变化。如果一个模板同时管规则、业务输入、检索结果、历史消息和输出格式，可能就该拆成更小的模板或链路节点。

4. 把提示词放到外部文件有什么好处和风险？

   **参考思路：** 好处是便于版本管理、运营调整和复用；风险是变量名、格式和代码调用容易不一致。外置后要配套校验、示例输入和变更记录，不能只把文本搬出去。

**本章小结：**

- **Prompt 的本质**：Prompt 不是“随便写一句话”，而是对模型输入进行结构化组织。随着项目复杂度提升，输入会从纯字符串演化成多角色消息，再进一步演化成模板、占位符与外部配置文件。
- **消息与调用**：聊天模型常见输入包括 `str`、消息对象列表、元组列表、字典列表；常见调用方式包括 `invoke / ainvoke`、`stream / astream`、`batch / abatch`。返回值通常是 `AIMessage`，正文一般通过 `.content` 读取。
- **模板与工程化**：`PromptTemplate` 适合文本模板，`ChatPromptTemplate` 适合聊天模型与多角色场景，`MessagesPlaceholder` 是多轮历史拼接的关键；将 Prompt 放入 JSON / YAML 更适合真实项目中的版本管理、多人协作与 A/B 测试。
- 学完本章后，你至少应该能区分四类输入组织方式：**纯字符串、消息列表、模板 + 占位符、外部文件加载**；也要知道 `PromptTemplate`、`ChatPromptTemplate`、`MessagesPlaceholder` 分别适合什么场景。

**建议下一步：** 继续学习 [第 14 章 输出解析器](14-输出解析器.md)，把本章的“输入组织”与下一章的“输出结构化”连起来；再配合 [第 15 章 LCEL 与链式调用](15-LCEL与链式调用.md)，就能形成 LangChain 中最核心的“输入 -> 模型 -> 输出 -> 链式编排”主线。如果你读到这里，仍然对“为什么要这样拆 Prompt”不够踏实，也可以回看 [1-2 提示词工程基础](1-2-提示词工程基础.md) 中的六要素、Few-shot 和结构化组织方式部分。
