从模板语法、ChatPromptTemplate、MessagesPlaceholder 到源码级执行链路
一、为什么 Prompt 不能一直写在代码里?
很多人刚接触大模型应用时,第一反应是:不就是把一句话发给模型吗?比如“你是一个客服助手,请回答用户问题”。这在 Demo 里可以跑通,但一旦进入真实项目,问题马上出现。
智能客服要拼接用户问题、历史对话、知识库资料、系统规则;股票分析助手要拼接行情、公告、资金流、风险提示;智能营销助手要拼接人群、活动目标、历史案例、输出格式。所有内容如果都靠字符串拼接,后面维护会非常痛苦。
问题 | 项目里会发生什么 | 后果 |
散落在代码里 | 每个接口、每个 Agent 都写一份 Prompt | 改一次要全项目搜索 |
变量靠手动拼 | context、question、history 容易传错 | 模型回答不稳定 |
输出格式不统一 | 一会儿文本,一会儿 JSON,一会儿表格 | 后端很难解析 |
没有版本管理 | Prompt 改坏后无法快速回滚 | 线上问题难复盘 |
没有评测体系 | 只能凭感觉判断效果好坏 | 迭代方向不清楚 |
所以,Prompt Template 的核心价值不是“把一句话写得更漂亮”,而是把大模型请求变成一个结构化、可维护、可复现的工程对象。
二、通俗理解:Prompt Template 就像合同模板
最简单的比喻:Prompt Template 就像一份合同模板。合同里有固定条款,也有变量位置,比如甲方、乙方、金额、日期。每次签合同时,不会重新写一份合同,而是把变量填进去。
大模型应用也是一样。固定部分是角色、任务、规则、输出格式;动态部分是用户问题、知识库资料、历史对话、工具结果。Prompt Template 做的事情就是把这些东西按照固定结构组装起来。
# 一个最简单的 PromptTemplate |
这段代码里,{topic} 就是变量。业务系统只需要传入不同 topic,就能复用同一套提示词结构。
三、PromptTemplate 和 ChatPromptTemplate 有什么区别?
LangChain 里最常见的两个 Prompt 组件是 PromptTemplate 和 ChatPromptTemplate。两者都叫模板,但适用场景不同。
组件 | 输入结构 | 输出结果 | 适合场景 |
PromptTemplate | 一个字符串模板 | 字符串 Prompt | 简单生成、翻译、摘要、分类 |
ChatPromptTemplate | 一组带角色的消息模板 | ChatPromptValue | Chat Model、多轮对话、Agent、RAG |
现在企业项目里更常用 ChatPromptTemplate,因为主流模型基本都是 Chat Model。它不是简单拼一大段字符串,而是按 System、Human、AI、Tool 等角色组织消息。
from langchain_core.prompts import ChatPromptTemplate |
四、源码地图:Prompt 相关类怎么分层?
看 LangChain 源码时,不要一开始就钻进所有细节。先抓住类之间的关系。大体可以分成三层:字符串模板、消息模板、聊天模板。
源码中,PromptTemplate 是字符串模板;ChatPromptTemplate 是聊天模板;MessagesPlaceholder 是历史消息占位符。它们最后都会服务于一个目标:把业务输入格式化为模型可以接收的 PromptValue 或 Message 列表。
类名 | 源码职责 | 你可以怎么理解 |
PromptTemplate | 保存 template、input_variables、template_format | 普通字符串模板 |
StringPromptTemplate | 抽象字符串模板的 format / format_prompt 行为 | 字符串模板基类 |
BaseStringMessagePromptTemplate | 把字符串模板包装成某种消息 | 字符串转角色消息 |
ChatPromptTemplate | 维护一组 message templates | 聊天消息模板容器 |
MessagesPlaceholder | 接收一组已有 messages 并插入模板 | 历史对话插槽 |
ChatPromptValue | 保存最终 messages | 即将交给 ChatModel 的请求对象 |
五、PromptTemplate.from_template 做了什么?
PromptTemplate 由一个字符串模板组成,它接受一组参数,用来生成发送给语言模型的 Prompt;模板格式支持 f-string、jinja2、mustache,其中官方更推荐 f-string,因为 jinja2 模板如果来自不可信来源会有安全风险。
按源码逻辑整理成伪代码,大概是这样:
# 这是按源码逻辑整理的伪代码,不是逐行复制 |
这段逻辑背后的意义很重要:LangChain 不只是保存了一段字符串,它会提前分析这段字符串里有哪些变量。这样在运行时少传一个参数,框架就能提前报错,而不是把残缺 Prompt 发给模型。
六、partial_variables:把固定变量提前塞进去
partial_variables 可以理解成“预填字段”。比如你的系统里,语言、公司名、输出风格是固定的,就不用每次调用都传。
from langchain_core.prompts import PromptTemplate |
工程建议 |
七、ChatPromptTemplate.from_messages 做了什么?
ChatPromptTemplate 更适合现代 Chat Model。官方 API 参考说明,from_messages 可以接收多种消息表示方式,包括 BaseMessagePromptTemplate、BaseMessage、(message type, template) 二元组、(message class, template) 二元组,或者一个字符串。源码里会把这些不同写法统一转换成标准消息模板。
按源码逻辑整理成伪代码,大概是这样:
# 这是按源码逻辑整理的伪代码,不是逐行复制 |
这就是为什么你写下面这种形式时,LangChain 能自动知道 question 是必填变量,history 可以作为历史消息插入。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder |
八、MessagesPlaceholder:多轮对话不要硬拼字符串
多轮对话里,很多人会把历史聊天记录拼成一大段字符串塞进 Prompt。这样做能跑,但有两个问题:第一,角色信息容易丢;第二,工具消息、AI 消息、用户消息混在一起,模型很难准确理解上下文。
MessagesPlaceholder 的作用,就是把一组已经存在的 messages 原样插入到 ChatPromptTemplate 中。源码中它会检查传入值是不是列表,并把 tuple 等 message-like 对象转换成标准 BaseMessage;还可以通过 n_messages 限制只保留最近几条。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder |
九、invoke 到 format_messages 的执行链路
在 LangChain 里,PromptTemplate 和 ChatPromptTemplate 都可以被当成 Runnable 使用,所以你经常会看到 prompt.invoke(...)。但 prompt.invoke 并不会调用大模型,它只是把输入变量格式化成 PromptValue。后面再通过管道交给模型。
BaseChatPromptTemplate 的源码逻辑可以这样理解:format_prompt 会调用 format_messages,然后把消息列表包装成 ChatPromptValue。
# 这是按源码逻辑整理的伪代码,不是逐行复制 |
所以,完整流程是:业务输入进来,PromptTemplate 负责格式化,生成 PromptValue;ChatModel 读取 PromptValue 里的 messages,再发给模型供应商。
十、和 LCEL 组合:Prompt 可以像积木一样接模型
LangChain 的 Prompt 不是孤立组件,它可以和模型、输出解析器组成一条链。你可以把它理解成 Java 里的流水线:先构造请求,再调用服务,最后解析响应。
from langchain.chat_models import init_chat_model |
这里的管道写法可以读成:先用 prompt 把变量组装成 messages,再把 messages 交给 model。Prompt 和 Model 解耦以后,后面切模型、改 Prompt、加解析器都更容易。
十一、实战案例一:智能客服 Prompt Template
下面是一个客服系统里更接近真实项目的 Prompt。它不是只告诉模型“你是客服”,而是明确角色、边界、知识库、输出格式和兜底策略。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder |
这个模板里,system 是固定规则,history 是多轮对话,question 是用户当前问题,context 是 RAG 检索出来的资料。这样设计以后,客服回答会更稳定,也更容易排查问题。
十二、实战案例三:RAG 问答 Prompt Template
RAG 的关键不是让模型凭记忆回答,而是让模型基于检索到的资料回答。因此 Prompt 里要明确告诉模型:资料里没有,就说不知道。
rag_prompt = ChatPromptTemplate.from_messages([ |
这个模板简单,但非常重要。它给模型加了一个“证据边界”:只根据 context 回答。后续配合 Retriever、Rerank、引用来源,才能组成真正可用的企业知识库问答系统。
十三、企业级 Prompt 应该怎么管理?
在真实公司里,Prompt 不能只放在 Python 文件里。因为 Prompt 本身会不断迭代,可能需要灰度、回滚、AB 测试、效果评测和问题复盘。
能力 | 具体做法 | 为什么重要 |
版本管理 | 每次 Prompt 变更记录 version | 线上出错可以回滚 |
配置中心 | Prompt 放数据库或配置平台 | 不用每次发版才能修改 |
变量规范 | 统一 question/context/history/tools 等命名 | 减少调用错误 |
日志追踪 | 记录 Prompt 版本、输入变量、模型输出 | 方便定位问题 |
离线评测 | 用固定测试集跑回归 | 防止改好一个场景,改坏另一个场景 |
安全策略 | 高危操作加边界和人工确认 | 避免模型越权 |
十四、常见坑:为什么你的 Prompt Template 会报错?
LangChain 官方错误说明里提到,INVALID_PROMPT_INPUT 通常和缺失变量、变量格式不正确、MessagesPlaceholder 传参错误、花括号转义有关。项目里最常见的是下面几个。
坑 | 错误写法 | 正确理解 |
少传变量 | 模板里有 {question},调用时没传 | input_variables 必须都传齐 |
花括号没转义 | 想输出 JSON 却直接写 { } | f-string 中普通大括号要写成 {{ 和 }} |
history 传字符串 | MessagesPlaceholder("history") 传了普通字符串 | 应该传 message list 或 message-like 对象 |
滥用 jinja2 | 从用户输入加载 jinja2 模板 | 不可信模板不要用 jinja2 |
Prompt 太长 | 把所有历史和文档都塞进去 | 应该做裁剪、摘要、Rerank |
十五、源码级总结:Prompt Template 的本质
学完这一章,你应该把 Prompt Template 理解成一个“模型请求构造器”。它不是魔法,不是咒语,而是大模型应用里的工程边界。
你看到的表面 | 源码里的真实动作 |
写一个模板字符串 | 解析变量名,保存 template_format |
传入变量 | 校验 input_variables 是否完整 |
调用 prompt.invoke | 格式化成 PromptValue |
使用 ChatPromptTemplate | 生成一组 BaseMessage |
使用 MessagesPlaceholder | 把历史 messages 插入固定位置 |
接到模型后面 | PromptValue 进入 ChatModel |
一句话记住 |
下一章预告
下一章讲 Structured Output:如何让模型稳定输出 JSON。它会接在 Prompt Template 后面,解决另一个工程难题:模型回答再好,如果输出格式不稳定,后端也很难真正接入业务系统。
内容来源:Prompt Template:提示词如何从“玄学”变成工程能力?:功能变化与行业影响解析_热闻岛