文章目录
- [1. Prompt 模板是什么?为什么需要它?](#1. Prompt 模板是什么?为什么需要它?)
-
- [1.1 从硬编码到模板](#1.1 从硬编码到模板)
- [1.2 LangChain Prompt 模块做了什么](#1.2 LangChain Prompt 模块做了什么)
- [2. PromptTemplate:最基础的字符串模板](#2. PromptTemplate:最基础的字符串模板)
-
- [2.1 基本用法](#2.1 基本用法)
- [2.2 它适合用在哪?](#2.2 它适合用在哪?)
- [3. ChatPromptTemplate:为聊天模型而生的模板](#3. ChatPromptTemplate:为聊天模型而生的模板)
-
- [3.1 聊天模型的 API 需要什么格式?](#3.1 聊天模型的 API 需要什么格式?)
- [3.2 基本用法](#3.2 基本用法)
- [3.3 三种角色的用途](#3.3 三种角色的用途)
- [3.4 ChatPromptTemplate使用场景(PromptTemplate vs ChatPromptTemplate)](#3.4 ChatPromptTemplate使用场景(PromptTemplate vs ChatPromptTemplate))
- [3.5 ChatPromptTemplate 的推荐结构](#3.5 ChatPromptTemplate 的推荐结构)
- [4. MessagesPlaceholder:让模板记住对话历史](#4. MessagesPlaceholder:让模板记住对话历史)
-
- [4.1 为什么需要 MessagesPlaceholder?](#4.1 为什么需要 MessagesPlaceholder?)
- [4.2 基本用法](#4.2 基本用法)
- [4.3 一个很容易踩的坑:首轮对话报错](#4.3 一个很容易踩的坑:首轮对话报错)
- [4.4 两种等价写法](#4.4 两种等价写法)
- [5. FewShotChatMessagePromptTemplate:给模型看几个示例](#5. FewShotChatMessagePromptTemplate:给模型看几个示例)
-
- [5.1 Zero-Shot 与 Few-Shot 的差别](#5.1 Zero-Shot 与 Few-Shot 的差别)
- [5.2 基本用法](#5.2 基本用法)
- [6. LCEL 管道:模板和模型的无缝拼接](#6. LCEL 管道:模板和模型的无缝拼接)
-
- [6.1 什么是管道?](#6.1 什么是管道?)
- [6.2 三种最实用的管道模式](#6.2 三种最实用的管道模式)
- 补充:模板类的format和invoke方法
🔗 前置知识:LangChain 核心概念和模型调用笔记
📅 更新日期:2026-06-11
1. Prompt 模板是什么?为什么需要它?
1.1 从硬编码到模板
想象一个场景:你要让大模型当翻译官,把不同的句子翻成不同的语言。你第一反应可能会这么写:
python
# 直接硬编码:翻一句写一句
response = model.invoke("请把以下内容翻译成英文:今天天气真好")
# 换一个句子、换一种语言?那就拼字符串呗
text = "我喜欢吃火锅"
language = "法语"
response = model.invoke(f"请把以下内容翻译成{language}:{text}")
看起来还行?但如果你的应用里有 100 个地方都需要构造提示词,每次都是各种 f"...{var1}...{var2}" 拼来拼去,很快你就会发现三个痛点:
- 逻辑与文本耦合:提示词的骨架和具体数据搅在一起,改一句提示词模板,可能要找遍全项目。
- 维护灾难:当提示词结构变得复杂(比如要加系统人设、历史对话、少样本示例),纯字符串拼接的代码会变得极其丑陋且容易出错。
- 复用困难:同一个提示词结构,在不同的功能里,你又要重新拼一遍。
模板的思路很简单:把提示词当成一个填空题,可变的部分用 {变量名} 占位,需要的时候再填入具体值。这跟 Python 字符串的 format() 方法是一回事:
python
# ✅ 模板化:卷子是卷子,答案是答案,泾渭分明
template = "请把以下内容翻译成{target_language}:{text}"
# 无论要填什么,都是同一张卷子
prompt1 = template.format(target_language="英文", text="今天天气真好")
prompt2 = template.format(target_language="日文", text="我喜欢吃火锅")
这样做的好处是:模板结构只定义一次,之后所有调用只需要关心填什么数据进去。改提示词的时候也只改模板这一处,不用满世界去找代码。
1.2 LangChain Prompt 模块做了什么
| 能力 | 它能帮你做什么 |
|---|---|
| 变量插值 | {variable} 占位符,运行时自动填入,告别手工拼字符串 |
| 对话历史注入 | 通过 MessagesPlaceholder 动态插入一整串多轮对话记录 |
| 少样本示例 | 内嵌输入→输出范例,手把手教模型按规矩办事 |
| LCEL 管道组合 | 模板直接用 ` |
接下来的每一节,我们都会把它们一个一个拆开来吃透。
2. PromptTemplate:最基础的字符串模板
PromptTemplate 是 LangChain 里最基础也最直接的模板工具,它适用于不区分角色、只需要生成一段纯文本提示词的场景。
2.1 基本用法
来看一个实际的例子。你现在大三,计算机专业,想让 AI 给你一些学习规划建议:
python
from langchain.chat_models import init_chat_model
from langchain_core.prompts import PromptTemplate
# 1. 初始化模型(这里用的是 DeepSeek,你也可以换成任何其他模型)
model = init_chat_model(
model="deepseek-v4-pro"
)
# 2. 创建模板:{变量} 就是占位符,名字自己起
prompt_template = PromptTemplate.from_template(
"我现在大学{grade_number}年级,专业是{major},请给我一些学习规划"
)
# 3. 填入变量值,生成完整的提示词字符串
prompt = prompt_template.format(
grade_number=3,
major="计算机科学"
)
print(prompt) # 输出:我现在大学3年级,专业是计算机科学,请给我一些学习规划
# 4. 把生成的提示词传给模型,获取回复
response = model.invoke(prompt)
print(response.content)
这里有一个容易忽略的细节:PromptTemplate.from_template() 会自动从你的模板字符串中提取变量名。上面的例子中,它识别出了 {grade_number} 和 {major} 两个变量。你也可以显式声明:
python
# 显式声明变量名(更严谨,也方便团队其他人一眼看出需要传什么参数)
prompt_template = PromptTemplate(
template="我现在大学{grade_number}年级,专业是{major},请给我一些学习规划",
input_variables=["grade_number", "major"], # 显式声明
)
2.2 它适合用在哪?
PromptTemplate 适合的场景是:你只需要一段纯文本作为提示词,不关心什么系统角色、对话历史这些。比如文本摘要、代码生成、风格转换这类一次性任务。
但问题是:现代的大模型(GPT、DeepSeek、Claude 等)都是聊天模型,它们的 API 期望接收的不是一段裸字符串,而是一个带有角色标签的消息列表。这就引出了我们的主角 ChatPromptTemplate。
3. ChatPromptTemplate:为聊天模型而生的模板
3.1 聊天模型的 API 需要什么格式?
聊天模型的 API 和传统文本模型的 API 有一个本质区别:输入不是一段字符串,而是一个消息列表,每条消息必须标明是哪个角色说的。
以 OpenAI 的 API 为例:
python
# 聊天模型 API 期望的格式
messages = [
{"role": "system", "content": "你是专业翻译官"},
{"role": "user", "content": "翻译成英文:你好"},
]
原生调用时需要手动拼装这些字典,很繁琐。ChatPromptTemplate 就是管理这个结构的。
3.2 基本用法
我们用回上面学业规划的例子,但这次用 ChatPromptTemplate 来写:
python
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
# 初始化模型
model = init_chat_model(model="deepseek-v4-pro")
# 用 from_messages 构建带角色的模板
# 每个元组:(角色, 模板内容)
prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一位经验丰富的学业规划专家,擅长为大学生制定个性化的学习路线。"),
("human", "我现在大学{grade_number}年级,专业是{major},请给我一些学习规划")
])
# 注意:ChatPromptTemplate 用 format_messages(),不是 format()
# format() 返回的是字符串,format_messages() 返回的是消息对象列表
messages = prompt_template.format_messages(
grade_number=3,
major="计算机科学"
)
# messages 现在是一个列表:
# [SystemMessage(content="你是一位经验丰富..."),
# HumanMessage(content="我现在大学3年级,专业是计算机科学...")]
response = model.invoke(messages) # 传入消息列表
print(response.content)
注意到,ChatPromptTemplate 用的是 format_messages() 而不是 format()。这是因为 ChatPromptTemplate 的输出是一个消息对象列表(List[BaseMessage]),而不是裸字符串。如果你用 format(),它会尝试把消息序列化成字符串,这通常不是你想要的。
不过在实际开发中,当你把模板放入 LCEL 管道时,调用的是 invoke() 方法(传入一个变量字典),LangChain 内部会自动帮你处理格式化。这个我们后面会讲到。
text
ChatPromptTemplate.from_messages([
("system", "你是{role}..."), ← 系统人设(定义 AI 的行为边界)
("human", "{question}"), ← 用户当前的问题
])
│
▼ .format_messages(role="...", question="...")
│
[SystemMessage, HumanMessage] ← 可以直接传给 model.invoke()
3.3 三种角色的用途
from_messages 的元组中,第一个元素是角色标识。LangChain 支持三种角色:
| 元组写法 | 对应消息类型 | 什么时候用 |
|---|---|---|
("system", "...") |
SystemMessage |
设定 AI 的行为边界:它是谁、该怎么做、输出什么格式 |
("human", "...") |
HumanMessage |
用户提出的问题或指令,通常是模板中变量最多的部分 |
("ai", "...") |
AIMessage |
预设 AI 的回复内容,主要用于少样本示例中展示「理想的回答长什么样」 |
疑问: from_messages 是不是只能用元组 ("system", "...") 这种元组写法?我能不能直接写一个 SystemMessage 对象,然后在里面放 {变量} 占位符?
python
from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate
from langchain_core.messages import SystemMessage
# ❌ 直接传 SystemMessage,{role} 只是一个普通字符串,不会被替换
prompt_bad = ChatPromptTemplate.from_messages([
SystemMessage(content="你是{role}"), # 运行时不会把 {role} 换成你指定的值
("human", "{question}")
])
# 调用时会原样输出 "你是{role}",变量没被填充
# ✅ 方式一:元组快捷写法(最常用)
prompt_good1 = ChatPromptTemplate.from_messages([
("system", "你是{role}"), # 元组第二个元素自动识别为模板
("human", "{question}")
])
# ✅ 方式二:显式使用 SystemMessagePromptTemplate
prompt_good2 = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template("你是{role}"), # 显式声明这是一个模板
("human", "{question}")
])
所以,元组写法只是"快捷方式",底层 LangChain 会自动把 ("system", "...") 转换成 SystemMessagePromptTemplate。
如果你想写得"重量级"一点,完全可以显式创建 SystemMessagePromptTemplate;但在实际开发中,用元组就足够简洁,也是官方推荐的主流方式。
思考一下 :为什么没有
("tool", "...")这种角色?因为工具调用返回的消息(
ToolMessage)是在程序运行过程中由外部工具动态生成的,不能预先在静态模板里写好。如果你需要在模板中给工具消息留位置,应该使用等会儿要学的MessagesPlaceholder。
3.4 ChatPromptTemplate使用场景(PromptTemplate vs ChatPromptTemplate)
| 对比维度 | PromptTemplate |
ChatPromptTemplate |
|---|---|---|
| 输出内容 | 纯字符串 | 消息对象列表 |
| 能不能设 system 角色 | 不能 | 能 |
| 适合什么模型 | 旧式文本补全模型 | 现代聊天模型(GPT / DeepSeek / Claude / Gemini 等) |
| 推荐程度 | 少数简单场景 | 首选,覆盖 95% 的场景 |
如果你只是想让模型把一段文字总结一下、翻译一下、改写一下,用 PromptTemplate 传字符串完全够用。但一旦你需要设定系统角色、管理对话历史、构建多轮对话,就必须用 ChatPromptTemplate。因为现代聊天模型的 API 就是这么设计的,用字符串传进去也能跑,但 LangChain 内部会帮你把字符串包装成 HumanMessage,等于你主动放弃了 system 角色的能力。
3.5 ChatPromptTemplate 的推荐结构
一个设计良好的聊天模板,通常长成下面这个样子
python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
# 第一层:系统人设(固定基调,定义行为边界和输出格式)
("system", "你是{role}。{behavior_rule}。请用{output_format}格式回复。"),
# 第二层:对话历史占位(动态注入,运行时才知道有几条消息)
MessagesPlaceholder(variable_name="history"),
# 第三层:用户当前输入(每次请求都在变)
("human", "{user_input}"),
])
- 第一层:系统消息,一般写一次就很少改,它决定了 AI 的"人设"。
- 第二层 :历史记录插槽,由
MessagesPlaceholder动态注入上一轮甚至上 N 轮的完整对话。 - 第三层:用户刚刚问的问题,变化最频繁。
4. MessagesPlaceholder:让模板记住对话历史
4.1 为什么需要 MessagesPlaceholder?
多轮对话有一个铁打的需求:每次跟模型聊天,都要把前面所有的聊天记录一起传回去 ,这样模型才记得上下文。但问题是,第一轮聊天时历史记录是 0 条,第五轮聊天时历史可能有 10 条。历史消息的数量和内容完全不可预知 ,根本没法用固定的 {history} 变量去表达。
这时候 MessagesPlaceholder 就派上用场了:它在模板里预先挖好一个"插槽",声明这个槽会接收一个不定长的消息列表,运行时你把整个列表塞进去就行。
4.2 基本用法
python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
prompt = ChatPromptTemplate.from_messages([
("system", "你是专业的旅游顾问。"),
MessagesPlaceholder(variable_name="history"), # ← 历史消息的插槽
("human", "{question}"),
])
# 第1轮:没有历史,塞个空列表
messages_round1 = prompt.format_messages(
history=[],
question="推荐一个北京三日游路线"
)
# 第2轮:把第1轮的一问一答作为历史传进去
messages_round2 = prompt.format_messages(
history=[
HumanMessage(content="推荐一个北京三日游路线"),
AIMessage(content="第一天:故宫→天安门→前门大街\n第二天:八达岭长城..."),
],
question="第二天有哪些必吃的美食?"
)
# 第3轮:传前两轮的完整历史,模型会结合所有上下文回答
messages_round3 = prompt.format_messages(
history=[
HumanMessage(content="推荐一个北京三日游路线"),
AIMessage(content="第一天:故宫→天安门→前门大街\n第二天:八达岭长城..."),
HumanMessage(content="第二天有哪些必吃的美食?"),
AIMessage(content="推荐全聚德烤鸭、炸酱面..."),
],
question="第三天怎么安排?"
)
4.3 一个很容易踩的坑:首轮对话报错
如果你不加 optional=True,而首轮对话又不传 history 变量,LangChain 会直接报错说缺少变量。有两种解决方式:
python
# 方式一:首轮传空列表
prompt.format_messages(history=[], question="你好")
# 方式二:声明为可选(更优雅)
prompt = ChatPromptTemplate.from_messages([
("system", "你是专业的代码助手。"),
MessagesPlaceholder(variable_name="history", optional=True), # 加了这个,不传也行
("human", "{question}"),
])
# 首轮不传 history 也不会报错
prompt.invoke({"question": "什么是闭包?"})
推荐用方式二,代码更干净,调用者不需要知道「首轮要传空列表」这个隐含约定。
4.4 两种等价写法
你可能看到某些代码里写着 ("placeholder", "{history}"),然后觉得困惑:这是什么写法?
python
# 这两种写法完全等价
MessagesPlaceholder(variable_name="history")
("placeholder", "{history}")
("placeholder", "{history}") 是 LangChain 提供的语法糖,内部会自动把它解析为 MessagesPlaceholder。你只需要知道它们能干一样的事即可。不过为了代码可读性,建议优先使用 MessagesPlaceholder(...) 。
思考 :你可能会想,"那用
("human", "{history}")能不能传一个历史列表进去?"不行。
{history}如果放在human角色里,它只会被当做一个普通字符串变量去填值。而MessagesPlaceholder的特殊之处在于,它会将传入的整个消息列表原样"展开"插入到模板的对应位置,保持每条消息原有的角色和内容不变。这是普通变量做不到的。
5. FewShotChatMessagePromptTemplate:给模型看几个示例
5.1 Zero-Shot 与 Few-Shot 的差别
你有没有遇到这种情况:你跟模型说"请用 JSON 格式输出",它却给你输出了一大堆废话,或者 JSON 结构完全不是你想要的?这是因为光靠系统提示"描述规则",有时对模型的约束力不够。
一个简单又强大的办法是:直接给它看几个标准答案的例子 。这就是少样本提示(Few-Shot Prompting):
- Zero-Shot :只给指令,不给范例。
"请把下面的句子翻译成英文。" - Few-Shot :给指令的同时,附上几个输入→输出的范例。
"示例1:'苹果' → 'apple';示例2:'香蕉' → 'banana';现在翻译:'橙子'"
5.2 基本用法
FewShotChatMessagePromptTemplate 帮你把示例的管理和格式化统一起来,而不是每次手动拼装。
python
from langchain_core.prompts import (
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
)
# 第一步:准备示例数据,每个示例是一个字典,字段名随意起
examples = [
{"input": "今天天气真好", "output": "positive"},
{"input": "我等了三个小时还没到", "output": "negative"},
{"input": "还行吧,就那样", "output": "neutral"},
]
# 第二步:定义每个示例的展示格式
# 这个模板决定了每条示例会被格式化成什么样的消息
example_prompt = ChatPromptTemplate.from_messages([
("human", "文本:{input}"), # 示例中的「用户输入」部分
("ai", "情感:{output}"), # 示例中的「AI 应该给出的回复」部分
])
# 第三步:创建 Few-Shot 模板,把示例数据和格式模板绑定
few_shot_prompt = FewShotChatMessagePromptTemplate(
examples=examples, # 示例列表
example_prompt=example_prompt, # 每条示例怎么展示
)
# 第四步:组装最终模板,把 few-shot 模板作为一个消息块嵌入
final_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个情感分析专家。请将文本分类为 positive、negative 或 neutral。直接输出分类结果,不要解释。"),
few_shot_prompt, # 示例会自动展开成多组 human/ai 消息
("human", "文本:{input}"), # 真正的用户输入
])
# 第五步:使用
messages = final_prompt.format_messages(input="这部电影简直浪费时间")
# messages 的内容(简化表示):
# [
# SystemMessage("你是情感分析专家..."),
# HumanMessage("文本:今天天气真好"), ← 示例1的用户输入
# AIMessage("情感:positive"), ← 示例1的AI输出
# HumanMessage("文本:我等了三个小时还没到"), ← 示例2
# AIMessage("情感:negative"),
# HumanMessage("文本:还行吧,就那样"), ← 示例3
# AIMessage("情感:neutral"),
# HumanMessage("文本:这部电影简直浪费时间"), ← 真正的用户输入
# ]
关键理解一点:few_shot_prompt 被嵌入到 final_prompt 后,在格式化时会自动把每个示例展开成 human + ai 两条消息,插入到模板中。你不需要关心展开细节,只需要定义好示例数据和展示格式。
6. LCEL 管道:模板和模型的无缝拼接
6.1 什么是管道?
将组件串联,上一个组件的输出作为下一个组件的输入 是LangChain链(尤其是|管道链)的核心工作原理,这也是链式调用的核心价值:实现数据的自动化流转与组件的协同工作。


在 LangChain 的世界里,你不会孤立地使用一个 Prompt 模板,真正的魔法开始于你把它跟模型连接起来:
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
model = init_chat_model("deepseek-chat")
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个{role}。"),
("human", "{question}"),
])
# ✅ 核心魔法:用 | 符号把组件串成流水线
chain = prompt | model | StrOutputParser()
# ↑ ↑ ↑
# 模板 模型调用 提取纯文本
result = chain.invoke({"role": "Python专家", "question": "什么是装饰器?"})
print(result) # 直接输出 AI 的文字回答,无需手动取 .content
这个 | 是怎么做到的?在 Python 里,| 操作符可以被类通过 __or__ 方法重载。LangChain 的所有核心组件(PromptTemplate、ChatPromptTemplate、聊天模型、输出解析器等)都是 Runnable 的子类,而 Runnable 重载了 __or__ 方法,使得 a | b 会自动变成"把 a 的输出作为 b 的输入"的管道。
6.2 三种最实用的管道模式
模式一:基础三件套
python
# 最常用的公式
chain = prompt | model | StrOutputParser()
result = chain.invoke({"变量1": "值1", "变量2": "值2"})
模式二:多步串联
有些任务需要分两步完成,比如先把一篇长文章总结成中文摘要,再把中文摘要翻译成英文。每一步的输出是下一步的输入:
python
# 第一段管道:文章 → 中文摘要
summarize_prompt = ChatPromptTemplate.from_messages([
("human", "请用 50 字总结以下内容:{text}")
])
summarize_chain = summarize_prompt | model | StrOutputParser()
# 第二段管道:中文摘要 → 英文翻译
translate_prompt = ChatPromptTemplate.from_messages([
("human", "请翻译成英文:{chinese_text}")
])
translate_chain = translate_prompt | model | StrOutputParser()
# 串联:先摘要再翻译
# 中间 lambda 的作用是将上一段的字符串输出包装成字典,让下一段管道能正确取值
full_chain = (
{"text": lambda x: x["article"]} # 从原始输入中提取 article 字段
| summarize_chain # 生成中文摘要(字符串)
| (lambda summary: {"chinese_text": summary}) # 包装成字典传给翻译管道
| translate_chain # 翻译成英文
)
result = full_chain.invoke({"article": "(一篇很长的中文文章)..."})
print(result) # 英文摘要
模式三:并行分支
同一个问题,你想同时生成两个不同风格的回复。比如既要一个笑话也要一首诗:
python
from langchain_core.runnables import RunnableParallel
# 一条管道生成笑话
joke_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个幽默的段子手。"),
("human", "用一个笑话介绍{topic}")
])
joke_chain = joke_prompt | model | StrOutputParser()
# 一条管道生成诗歌
poem_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个浪漫的诗人。"),
("human", "用一首短诗赞美{topic}")
])
poem_chain = poem_prompt | model | StrOutputParser()
# 并行执行:两条管道同时跑,互不等待
parallel_chain = RunnableParallel(
joke=joke_chain, # 分支1
poem=poem_chain, # 分支2
)
result = parallel_chain.invoke({"topic": "程序员"})
print(result["joke"]) # 笑话结果
print(result["poem"]) # 诗歌结果
RunnableParallel 内部会并发执行两条管道,总耗时约等于最慢的那条管道的耗时,而不是两条之和。
补充:模板类的format和invoke方法

python
# 方式1:format_messages() → 返回消息列表
messages = prompt.format_messages(role="助手", user_input="你好")
# 返回 List[BaseMessage],可以直接传给 model.invoke()
# 方式2:invoke() → 返回 PromptValue(LCEL 兼容)
prompt_value = prompt.invoke({"role": "助手", "user_input": "你好"})
# 返回 ChatPromptValue,它有 .messages 和 .to_string() 两个便利属性
它们之间的区别是:format_messages() 返回的是单纯的消息列表,而 invoke() 返回的是一个更"高级"的 PromptValue 对象。这个对象除了包含消息列表外,还可以与 LangChain 的 LCEL 管道无缝集成。
- PromptTemplate:通用提示词模板,支持动态注入信息。
- FewShotChatPromptTemplate:支持基于模板注入任意数量的示例信息。
- ChatPromptTemplate:支持注入任意数量的历史会话信息。
以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。如果文章对你有帮助,别忘了点个赞、留个言,让更多的小伙伴看到~ 我们下篇再见!