Skip to main content

03 · ChatPromptTemplate:从手写消息列表到模板的必然性

本片目标:回答”为什么要模板”——当消息列表需要动态拼装、插入历史、复用系统提示时,手写列表会变得脆弱;模板把”消息结构”与”每次的数据”分离。 新增规定性:3(拼消息用 ChatPromptTemplate,历史用 MessagesPlaceholder数据字典:ChatPromptTemplate / MessagesPlaceholder / PromptTemplate / PromptValue。 进程线程模型:无变化(同步阻塞,模板 invoke 是纯内存操作,毫秒级)。 网络模型:模板本身不联网;它把消息拼好后,交给模型 invoke 才发 HTTP。

1. 上集回顾

第 02 篇我们学会了构造 SystemMessage + HumanMessage 列表传给模型。但现在遇到两个场景,手写列表开始难受: 1要插入历史消息:多轮对话时,消息列表是 [system, 历史user, 历史ai, 当前user]——历史是动态的、可长可短。 2要动态填变量"你是{company}的助手,请回答关于{product}的问题"——每次拼字符串吗?f-string 拼多了就乱。 本质问题:消息列表的结构(哪些是 system、哪里插历史、哪里放当前问题)是固定的,变的是数据(问题内容、公司名、历史消息)。把”固定结构”和”变化数据”混在一起写,必然导致重复代码和拼错。 模板 = 把结构写一次,数据每次填进去。

2. 数据字典:模板类

2.1 ChatPromptTemplate(聊天模板,本系列主力)

MessageLikeRepresentation(模板里每个元素,三种写法)

2.2 MessagesPlaceholder(历史槽位——关键)

为什么需要它(规定性 3 的核心):普通 {question} 只能塞一个字符串。而历史是一组消息对象。如果不用占位符而是 ("human", "{history}"),传列表进去会变成字符串字面量 "['AIMessage(...)', 'AIMessage(...)']"——模型看到的是 Python 列表的 repr,不是消息!

2.3 PromptValue(invoke 的返回)

2.4 PromptTemplate(单条字符串模板,偶尔用)


3. 关键方法


4. 进程线程模型

模板这一步不联网、不阻塞。它是纯 Python 字符串/对象操作。所以放心在任意线程调用。

5. 网络模型

注意:消息的 type 字段("system"/"human"/"ai")在序列化时被映射成协议的 rolesystem/user/assistant)。

6. 验证:跑起来

配套代码 code/03_prompt.py
  1. 建一个带 system 变量 + history 槽位 + question 的模板;
  2. 第一轮(空历史)invoke,打印拼出的消息列表;
  3. 第二轮(有历史)invoke,打印历史如何被展开插入;
  4. 演示 {history} 普通变量 vs MessagesPlaceholder 的差异(传列表会变成字面量字符串)。
预期输出(节选)

7. 边界

  • 模板不是唯一方式——create_agent 内部就不用模板(直接列表拼接)。但本系列目标是 RAG,模板是拼 prompt 的正道。
  • {变量} 只塞字符串——塞列表会变成 repr 字符串。塞消息组必须用 MessagesPlaceholder
  • f-string 语法限制——模板里别写裸 { }(如 JSON 示例),要么转义 {{,要么用 template_format="jinja2"
  • system 提示也可以插值——("system", "你是{company}的助手") 完全合法,变量在 invoke 时填。

推荐资料(延伸阅读)


8. 未完待续

现在”输入”这块齐了:模型 + 消息 + 模板。但输出还有问题:
  1. invoke 返回的是 AIMessage,我想要纯文本——每次手动 .content
  2. 模型回答慢,用户等 2 秒才看到整段文字——能不能流式输出,像 ChatGPT 那样一个字一个字蹦?
这就是第 04 篇:输出解析与流式。 04 · 输出解析与流式