LangChain 提示模板

QuibblerAgentQuibblerAgent 2026-11-06 约 16 分钟 1 次阅读

第 1 篇的结尾留了个钩子:单轮调用裸 SDK 反而更短,框架的价值要从提示词复用、输出解析、流程编排这三类重复劳动里兑现。本篇是《LangChain 从入门到实战》第 2 篇,讲第一类——提示模板(见官方文档):先说清 f-string 拼提示到底欠了什么账,再用 ChatPromptTemplate 重建消息组装,重点看历史消息插槽 MessagesPlaceholder,最后揭开模板的 Runnable 本质——它直接通向第 5 篇的链式编排。

1、为什么需要提示模板

多数人的起点是 f-string 直接拼,一段典型的审查提示长这样:

prompt = f"""你是代码审查员,审查这段 {language} 代码:
{code}
只输出 JSON:{{"issues": [], "score": 0}}"""

为了在提示里放一个 JSON 输出示例,字面量大括号全部要双写成 {{}},变量和字面量在同一层语法里纠缠,提示越长越难改。转义还只是表层,f-string 的账可以数出三笔:

  • 转义:JSON、代码示例里的大括号手工双写,改一次数一次
  • 复用:同一段系统提示散落各调用点,改一处漏一处
  • 版本:提示词是效果的核心变量,却埋在业务代码里无法单独评审回滚

第三笔最贵。提示词每一次措辞调整都会改变输出质量,工程上它理应像配置一样被单独管理:谁改的、为什么改、能否回滚——前提是提示词先从代码里解耦,变成一份显式声明了变量的数据。ChatPromptTemplate 就是这个载体:模板对象持有消息结构与变量表,填充变量时才生成消息。顺带一提,模板默认沿用 f-string 语法,也支持 template_format="mustache"——该语法下变量写成 {{name}}、其余大括号一律是字面量,JSON 示例多的提示可以换它卸掉转义负担。

2、用 from_messages 组装消息

先定口径:1.x 中提示模板的本体在 langchain-core(当前 1.6.x,见 PyPI 主页),官方 API 参考的推荐导入是 from langchain_core.prompts import ChatPromptTemplate。

老教程里常见的 from langchain.prompts import ... 在 1.x 仍能用——主包对 core 做了再导出;但新代码建议直接从 langchain_core.prompts 导入,与官方文档口径一致。

聊天模型吃的是消息列表,模板的最小单元因此不是字符串,而是"角色 + 内容"。from_messages 接收一组消息模板,每条用一个二元组声明:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是 {language} 代码审查员,逐条给出改进建议。"),
    ("human", "审查这段 {language} 代码:\n{code}"),
])

print(prompt.input_variables)   # ['code', 'language']

三点解读。其一,角色用字符串声明,system、human、ai 各对应一类消息,列表里的纯字符串等价于 ("human", ...);其二,变量以单层大括号标记,input_variables 自动推断——填漏变量时错误发生在填充那一刻且指名道姓,而不是像 f-string 那样拼出一段残缺提示发给模型;其三,{language} 出现在两条消息里只需提供一次,模板负责渲染到每一处。

再补一个容易忽略的细节:这些元组在填充后会被转换成 SystemMessage、HumanMessage、AIMessage 等 langchain-core 统一定义的消息对象——正是第 1 篇说"换厂商后原样可用"的那批类型。模板层产出的结构天然与模型抽象同构,这是后面能用管道符直接串联的前提。

还有一个更简的构造器 from_template:接收单个字符串,生成只含一条 human 消息的模板,单轮问答够用;但它表达不了系统提示与多角色结构,实际项目的主力是 from_messages,前者可视为后者的单消息特例。两个构造器返回同一类型,替换不存在迁移成本。

3、历史消息插槽

单轮场景里模板与 f-string 的差距还不明显,MessagesPlaceholder 才是拉开差距的第一处。多轮对话里历史消息"长度不定、类型确定":不知道用户聊了三轮还是三十轮,只知道它们应插在系统提示之后、最新问题之前。裸 SDK 只能在每个调用点手工拼接:

messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages += history                       # 手工拼接历史
messages.append({"role": "user", "content": question})

这段三行逻辑会随调用点数量无限复制,漏拼、错序、重复代码都从这里长出来。模板的解法是把"历史插在哪"声明为结构的一部分:

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

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是耐心的编程助教。"),
    MessagesPlaceholder("history"),
    ("human", "{question}"),
])

result = prompt.invoke({
    "history": [HumanMessage("什么是递归?"), AIMessage("函数调用自身的做法。")],
    "question": "给我一个 Python 例子",
})
print(result.messages)

关键差异在职责划分:模板声明消息的形状(历史插在哪、前后是什么),调用方只提供数据(一个消息列表)。裸 SDK 里两件事缠在同一处拼接代码上,改提示结构要动每个调用点;模板里改一处结构、全部调用点同时生效。看渲染结果更直观:上面那次 invoke 之后,history 插槽被展开成两条消息,result.messages 共四条——一条 system、两条历史、一条 human,顺序与模板声明完全一致。

两个参数顺带记住:optional=True 允许不传该变量,首轮对话没有历史时不报错;n_messages=10 只取最近若干条,用来控制上下文长度,细节见 MessagesPlaceholder 参考。对照裸 SDK 做同样两件事:先 history[-10:] 手工截断,再小心翼翼维持拼接顺序,逻辑分散在调用点且极易漏改——模板把它们都变成了声明。另外元组写法 ("placeholder", "{history}") 与 MessagesPlaceholder 等价,读旧代码时认得即可。

4、模板即 Runnable

上面反复出现的 prompt.invoke(...) 不是普通方法。第 1 篇说过,1.x 的地基是 Runnable 抽象——统一的 invoke、stream、batch 接口;ChatPromptTemplate 本身就实现了它:输入变量字典,输出 ChatPromptValue(用 .messages 取消息列表)。这带来一个直接后果:模板可以用管道符与模型串联。数据在管道里的流转如下:

flowchart LR A[变量字典] --> B[提示模板] B --> C[消息列表] C --> D[模型] D --> E[AI 消息]

模板把字典渲染成消息列表,模型把消息列表变成 AI 消息,两步各司其职、中间没有手工胶水。代码只需一行管道(prompt 沿用上一节):

from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-6.1-terra", temperature=0)
chain = prompt | model            # 模板在前,模型在后

resp = chain.invoke({
    "history": [HumanMessage("什么是递归?"), AIMessage("函数调用自身的做法。")],
    "question": "给我一个 Python 例子",
})
print(resp.content)

chain.invoke 收变量字典,直接返回 AIMessage——"渲染消息"这一步被管道吞掉了。| 背后是 LCEL 的组装规则,能串的远不止模板与模型:解析器、检索器、条件分支都能进管道,那是本系列第 5 篇的主场,本篇止步于两步串联。

把模板做成 Runnable 还有一层工程红利:prompt.invoke 不联网、不耗 token,可以在单元测试里直接断言渲染出的消息序列——提示词的每次改动都能进 CI 回归。这是第 1 节"版本"那笔账的另一半答案:提示词不仅要能单独 diff,还要能自动验证。

5、实战代码审查提示模板

把前三节合进一个可复用的完整示例:多变量加 few-shot(少样本提示,在提示里放输入输出示例让模型模仿格式与分寸)的代码审查模板。示例消息放进模板,就是一组写死的 human 与 ai 字面量消息:

from langchain_core.prompts import ChatPromptTemplate

review_prompt = ChatPromptTemplate.from_messages([
    ("system",
     "你是 {language} 代码审查员,按严重程度逐条输出问题,"
     "每条格式:[级别] 位置:说明;无问题输出“无”。"),
    # few-shot 示例:一组写死的输入输出对
    ("human", "审查目标:配置读取函数\n背景:内部脚本"),
    ("ai", "[建议] 第 4 行:裸 except 吞掉异常,至少记录日志"),
    ("human", "审查目标:{title}\n背景:{context}"),
])

messages = review_prompt.invoke({
    "language": "Python",
    "title": "文件上传接口",
    "context": "面向外部用户的 API",
}).messages

for m in messages:
    print(f"{m.type}: {m.content}")

四点解读:系统提示声明角色与输出格式,变量 language 让同一模板覆盖多语言项目;中间两条是字面量示例消息,示范"什么输入对应什么输出";真正的请求在最后一条 human 消息,复用 title 与 context 两个变量;拿到的 messages 可直接喂模型,更顺手的做法是 review_prompt | init_chat_model("openai:gpt-6.1-terra") 串成链。至此这份提示成了一份独立资产:可以存盘、进版本库、在评审里单独 diff——第 1 节说的"提示词工程化"在这里落地。

few-shot 的摆放另有两个实践提醒:示例要贴着真实请求放,离最终问题越近模型模仿得越稳;示例本身占上下文窗口和费用,两三条通常够用,别把模板写成题库。当输出格式复杂到示例描述不清时,就该换结构化输出的路子——那是本系列第 3 篇的内容。

6、与裸 SDK 对照

同一份审查提示,裸 SDK 的写法是系统提示存常量、f-string 填变量、调用点手工排消息顺序。两种写法逐维对照如下:

维度 裸 SDK 拼接 ChatPromptTemplate
变量管理 f-string 隐式引用 模板显式声明并自动推断
缺变量后果 拼出残缺提示发给模型 填充时报错且指名变量
消息结构 手工维护 role 列表 角色元组声明一次成型
历史消息 每个调用点手工拼接 MessagesPlaceholder 插槽
复用形态 字符串常量散落各处 单一对象可存盘可 diff
与模型衔接 手动组装后再调用 Runnable 直接进管道

诚实地说,前三行在小脚本里感知不强,模板的分量集中在后三行:历史插槽与管道衔接让提示层天然长在第 1 篇的统一模型抽象上——换厂商、加解析器、接编排都不必重写提示代码。裸 SDK 也能自己封函数模仿,但那等于手写一个小型模板库,而这条路 LangChain 已经铺完并持续维护着。选择建议维持第 1 篇的判断:一次性脚本、单轮固定提示,裸 SDK 的三行拼接仍是更短路径;但凡提示要复用、对话有历史、后续要接解析或编排,模板层从第一天就值得引入。

7、总结与练习

回顾全文:f-string 拼提示在转义、复用、版本三处积累隐性成本;ChatPromptTemplate.from_messages 用角色元组把消息结构声明成数据,from_template 是它的单消息特例;MessagesPlaceholder 把历史消息的位置收编进模板结构,optional 与 n_messages 覆盖空历史与长度控制;模板本身是 Runnable,invoke 产消息、| 接模型,完整编排语义在第 5 篇展开。对于手头正在写的应用,建议先把散落的提示词收进一个 from_messages 模板——langchain-core 随主包安装、不引入新依赖,而这一步是第 3 篇结构化输出与第 5 篇编排的共同地基。

练习两道:

  • 给第 5 节的审查模板加上 MessagesPlaceholder("history", optional=True),让模型能参考上一轮已指出的问题、避免重复
  • 用 template_format="mustache" 重写一个含 JSON 输出示例的模板,对比 f-string 版的转义负担

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

83