Prompt 模板:PromptTemplate / ChatPromptTemplate / MessagesPlaceholder / Few-Shot
Day06 认识了消息,但没人想每次手拼 [SystemMessage(...), HumanMessage(...)]。今天看 libs/core/langchain_core/prompts/——LCEL 链条 prompt | model | parser 里的第一节车厢。弄清四件事:①PromptTemplate 怎么"填空"出字符串;②ChatPromptTemplate.from_messages 怎么把简写元组批量变成消息;③MessagesPlaceholder 怎么给"聊天历史"留座位;④few-shot 范例怎么被装订进最终提示词。
ChatPromptTemplate 是一份剧本:里面既有写死的台词(SystemMessage 人设)、带空的台词("{question}"),还有一行舞台指示"此处插入前情回顾"(MessagesPlaceholder)——开演(invoke)时把前情(聊天历史)原样插进那个位置。整天我们就在这两个世界观里打转。痛点:提示词不能硬编码
Runnable——所以它能直接用 | 接进 LCEL 链(D03 讲过的管道),invoke({"question": "..."}) 输出 PromptValue 喂给模型。一张信纸模板,既是数据、又是流水线的第一道工位。| 类 | 位置 | 产出 |
|---|---|---|
PromptTemplate | prompts/prompt.py:24 | 一根字符串(补全模型/单段文本) |
ChatPromptTemplate | prompts/chat.py:794 | 一列消息(聊天模型) |
MessagesPlaceholder | prompts/chat.py:53 | 把一个变量原样展开成 N 条消息 |
FewShotPromptTemplate | prompts/few_shot.py:121 | 前言 + N 个范例 + 收尾拼成的字符串 |
PromptTemplate:一张填空卷
最基础的字符串模板 PromptTemplate(libs/core/langchain_core/prompts/prompt.py:24):
# libs/core/langchain_core/prompts/prompt.py:24
class PromptTemplate(StringPromptTemplate):
"""Prompt template for a language model.
A prompt template consists of a string template. It accepts a set of
parameters from the user that can be used to generate a prompt for a
language model.
The template can be formatted using either f-strings (default), jinja2,
or mustache syntax.
Example:
# Instantiation using from_template (recommended)
prompt = PromptTemplate.from_template("Say {foo}")
prompt.format(foo="bar")
"""
填空动作在 format(libs/core/langchain_core/prompts/prompt.py:191),短得可以背下来:
# libs/core/langchain_core/prompts/prompt.py:191
def format(self, **kwargs: Any) -> str:
"""Format the prompt with the inputs."""
kwargs = self._merge_partial_and_user_variables(**kwargs) # ① 预填值 + 现填值合并
return DEFAULT_FORMATTER_MAPPING[self.template_format](self.template, **kwargs) # ② 按格式渲染
_merge_partial_and_user_variables模板支持 partial_variables"预填空"——比如"今天日期"这种固定值先填上,剩下的空留给调用时。两拨值在这合并。DEFAULT_FORMATTER_MAPPING[...]按 template_format 查表选渲染器:默认 f-string(就是 Python 的 str.format),也支持 jinja2 / mustache。渲染策略是查表可换的,模板类本身不关心语法。from_template:变量名怎么被自动侦测
为什么推荐 from_template 而不是直接构造?看源码(libs/core/langchain_core/prompts/prompt.py:257):
# libs/core/langchain_core/prompts/prompt.py:257
@classmethod
def from_template(cls, template, *, template_format="f-string",
partial_variables=None, **kwargs) -> PromptTemplate:
input_variables = get_template_variables(template, template_format) # ① ★扫描模板抠出变量名
partial_variables_ = partial_variables or {}
if partial_variables_:
input_variables = [ # ② 预填过的不再算"待填空"
var for var in input_variables if var not in partial_variables_
]
return cls(input_variables=input_variables, template=template,
template_format=template_format,
partial_variables=partial_variables_, **kwargs)
get_template_variables★解析模板字符串,把 {city}、{date} 这些空位名全抠出来,自动生成 input_variables 清单。你不用手写也不会写漏。剔除 partial已经预填的变量从"待填清单"里去掉。之后 invoke 时框架会校验:给的变量和待填清单对不上就报错——填空卷交卷前先对答题卡。p = PromptTemplate.from_template("把下面的{lang}翻译成中文:{text}")→ 自动侦测
input_variables=["lang", "text"]p.format(lang="英文", text="Hello") → "把下面的英文翻译成中文:Hello"p.invoke({"lang": "英文", "text": "Hello"}) → StringPromptValue(text="把下面的英文翻译成中文:Hello")(invoke 走 Runnable 协议,产出可直接喂模型的 PromptValue)。ChatPromptTemplate:批量生产消息
聊天模型要消息列表,主角换成 ChatPromptTemplate(libs/core/langchain_core/prompts/chat.py:794)。入口 from_messages(chat.py:1124)接受五花八门的写法:
# libs/core/langchain_core/prompts/chat.py:1124
@classmethod
def from_messages(cls, messages, template_format="f-string") -> ChatPromptTemplate:
"""Create a chat prompt template from a variety of message formats.
A message can be represented using the following formats:
1. BaseMessagePromptTemplate
2. BaseMessage
3. 2-tuple of (message type, template); e.g., ('human', '{user_input}')
4. 2-tuple of (message class, template)
5. A string which is shorthand for ('human', template)
"""
return cls(messages, template_format=template_format)
真正干活的是 format_messages(libs/core/langchain_core/prompts/chat.py:1174)——逐条"过一遍剧本":
# libs/core/langchain_core/prompts/chat.py:1174
def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
kwargs = self._merge_partial_and_user_variables(**kwargs)
result = []
for message_template in self.messages:
if isinstance(message_template, BaseMessage):
result.extend([message_template]) # ① 写死的台词:原样上
elif isinstance(message_template,
(BaseMessagePromptTemplate, BaseChatPromptTemplate)):
message = message_template.format_messages(**kwargs) # ② 带空的台词:填空后上
result.extend(message) # (可能展开成多条!)
else:
raise ValueError(msg)
return result
BaseMessage 分支剧本里写死的消息(如 SystemMessage("你是客服"))不用填空,直接进结果。模板分支 + extend★注意用的是 extend 不是 append——一个模板项可以展开成多条消息。普通的 ("human", "{q}") 展开成 1 条;而 MessagesPlaceholder 能展开成 N 条(下一节)。这个 extend 就是"留座位"机制的接口。("human", "{q}") 怎么变的from_messages 里每个元组经 _convert_to_message 变成对应的 HumanMessagePromptTemplate 等——底层复用了 D06 提过的 convert_to_messages 家族。MessagesPlaceholder:给聊天历史留座位
MessagesPlaceholder(libs/core/langchain_core/prompts/chat.py:53)就是那行"此处插入前情回顾"的舞台指示。它的 format_messages(chat.py:164):
# libs/core/langchain_core/prompts/chat.py:164
def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
value = (
kwargs.get(self.variable_name, []) # optional=True:没给就空列表
if self.optional
else kwargs[self.variable_name] # 必填:没给直接 KeyError
)
if not isinstance(value, list):
raise ValueError(msg) # 必须是列表,防呆
value = convert_to_messages(value) # ★元组简写 → 真消息对象
if self.n_messages:
value = value[-self.n_messages :] # ★只保留最近 n 条(截断历史)
return value
variable_name座位名。MessagesPlaceholder("history") = "开演时从输入的 history 变量取一列消息,原样坐进这个位置"。optional=True没有历史(第一轮对话)也不报错,返回空列表。源码 docstring 演示了这个对比:不带 optional 直接 KeyError。convert_to_messages允许历史用 ("human", "5+2等于几") 简写传入,这里统一转成 HumanMessage 等对象——和 D06 学的消息体系无缝衔接。n_messages 截断★内置"只记最近 n 条"的滑动窗口——控制 token 成本最朴素的一招,模板层就给你做了。Few-Shot:把范例装订进卷子
FewShotPromptTemplate(libs/core/langchain_core/prompts/few_shot.py:121)的 format(few_shot.py:180)把装订过程写得清清楚楚:
# libs/core/langchain_core/prompts/few_shot.py:180
def format(self, **kwargs: Any) -> str:
kwargs = self._merge_partial_and_user_variables(**kwargs)
examples = self._get_examples(**kwargs) # ① 拿例子:固定列表 或 selector 动态挑
examples = [{k: e[k] for k in self.example_prompt.input_variables} for e in examples]
example_strings = [
self.example_prompt.format(**example) for example in examples # ② 每个例子用小模板渲染
]
pieces = [self.prefix, *example_strings, self.suffix] # ③ 前言 + 例子们 + 收尾
template = self.example_separator.join([piece for piece in pieces if piece])
return DEFAULT_FORMATTER_MAPPING[self.template_format](template, **kwargs) # ④ 最后整体填空
_get_examples(few_shot.py:82)二选一:要么用写死的 examples 列表,要么用 example_selector 按本次输入动态挑(如语义最相似的 2 个)——例子库很大时特别有用。example_prompt.format每个例子本身也是一次"小填空":用一个子 PromptTemplate(如 "问:{q}\n答:{a}")渲染。模板套模板,组合而非硬编码。prefix + examples + suffix三明治装订:前言(任务说明)+ 若干范例 + 收尾(真正的问题),用 example_separator(默认 "\n\n")连接。最后再 format 一次拼好的大模板里 prefix/suffix 的空位(如 {input})最后统一填。所以范例文本里若有花括号要小心转义。example_selector 相当于"来了退款问题就抽 3 张退款工单给他看",比固定 3 张更省注意力(token)。聊天版 FewShotChatMessagePromptTemplate(few_shot.py:397)则把每个例子渲染成 human/ai 消息对,原理相同。串起来 + 今日小结
prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant."), MessagesPlaceholder("history"), ("human", "{question}")])prompt.invoke({"history": [("human", "what's 5 + 2"), ("ai", "5 + 2 is 7")], "question": "now multiply that by 4"})→
ChatPromptValue(messages=[SystemMessage("You are a helpful assistant."), HumanMessage("what's 5 + 2"), AIMessage("5 + 2 is 7"), HumanMessage("now multiply that by 4")])再接上 D05/D06 的知识:这个 PromptValue 直接
| model 喂给 ChatModel,模型读完整"会议纪要"就知道 "that" 指的是 7。👶 小白:("human", "{q}") 元组、HumanMessage、HumanMessagePromptTemplate 三个有啥区别?
👨🏫 老师:元组是简写、Message 是成品、MessagePromptTemplate 是带空的半成品。from_messages 收到元组后:模板串里有空位(如 {q})就转成 HumanMessagePromptTemplate(invoke 时填空产消息);直接给 HumanMessage("你好") 则是写死的成品,原样进结果。记住流向:简写 → 半成品(存在模板里)→ invoke 填空 → 成品消息。
🧠 今天你应该能回答
- PromptTemplate.format 干了哪两步?(合并 partial 变量 + 按 template_format 查表渲染,prompt.py:191)
- from_template 比手动构造好在哪?(自动侦测 input_variables,prompt.py:257)
- ChatPromptTemplate.from_messages 支持哪些写法?(消息对象/元组/字符串等 5 种,chat.py:1124)
- format_messages 为什么用 extend?(一个模板项可展开成 N 条消息,chat.py:1174)
- MessagesPlaceholder 的三个开关?(variable_name 座位名 / optional 可空 / n_messages 截断,chat.py:164)
- few-shot 的装订公式?(prefix + 渲染后的例子们 + suffix,例子可由 selector 动态挑,few_shot.py:180)
✋ 10 分钟动手
cd /Users/bitmart/work/codes/github/AI_WORK/langchain/libs/core/langchain_core
# 1. 字符串模板
sed -n '191,201p' prompts/prompt.py # format:合并 + 查表渲染
sed -n '257,312p' prompts/prompt.py # from_template:变量自动侦测
# 2. 聊天模板
sed -n '1124,1200p' prompts/chat.py # from_messages + format_messages
sed -n '164,190p' prompts/chat.py # MessagesPlaceholder.format_messages
# 3. few-shot 装订
sed -n '180,207p' prompts/few_shot.py # prefix + examples + suffix
grep -n "example_selector" prompts/few_shot.py | head
prompt | model 已经通了,但模型吐出来的是 AIMessage,业务代码要的是字符串、JSON、甚至强类型对象。明天看 output_parsers/:StrOutputParser / JsonOutputParser / PydanticOutputParser,以及大杀器 with_structured_output 是怎么用工具调用逼模型"填表"的。