MessagesPlaceholder是什么?
MessagesPlaceholder是 LangChain 中专门用于ChatPromptTemplate的一个特殊占位符组件。它与普通字符串占位符(如{input})最大的不同在于:它用来接收并注入一整个“消息列表(List[BaseMessage])”,而不是一个普通的字符串值。
在代码中,它的写法是:
MessagesPlaceholder("变量名")其中"变量名"是运行时传入的字典中对应的键名。
一句话定义:
MessagesPlaceholder是 LangChain 在提示模板中预留的“插槽”,用于在运行时把一整套带有角色(System/Human/AI/Tool)的对话消息无损地插入到模板中。
为什么需要它?(普通占位符 vs MessagesPlaceholder)
如果我们只用普通字符串占位符{chat_history},我们需要手动把历史消息列表拼接成一个长字符串(例如:"用户:你好\nAI:你好,有什么可以帮您?\n用户:...")。这样做有两个致命问题:
| 问题 | 说明 |
|---|---|
| 丢失角色信息 | 拼接成的字符串失去了HumanMessage、AIMessage这类结构化标签,模型无法区分哪句是谁说的(除非你在字符串里强行加“用户:”前缀,但这是纯文本,不是原生结构)。 |
| 无法传输工具消息(ToolMessage) | RAG 或 Agent 中涉及工具调用时,历史里可能包含ToolMessage(工具返回结果)。纯文本拼接无法保留其特殊结构。 |
MessagesPlaceholder恰恰解决了这两个问题:它直接传递BaseMessage对象列表,保持每条消息的完整角色、内容和元数据,让聊天模型(ChatModel)可以原生解析,不用再手动去做字符串拼接。
在代码中如何使用?
1. 定义模板
fromlangchain_core.promptsimportChatPromptTemplate,MessagesPlaceholder prompt=ChatPromptTemplate.from_messages([("system","你是一个乐于助人的助手。"),MessagesPlaceholder("chat_history"),# ① 预留历史消息插槽("human","{input}")# ② 当前用户输入])- 运行时,
chat_history会替换成一整段历史消息列表。 input仍是一个普通的字符串占位符。
2. 运行时注入
fromlangchain_core.messagesimportHumanMessage,AIMessage# 模拟历史对话history_msgs=[HumanMessage(content="我叫小明"),AIMessage(content="你好小明!很高兴认识你。")]# 注入数据final_messages=prompt.invoke({"chat_history":history_msgs,"input":"你还记得我叫什么吗?"}).to_messages()# 此时 final_messages 包含:# [SystemMessage, HumanMessage("我叫小明"), AIMessage("..."), HumanMessage("你还记得我叫什么吗?")]最终生成的final_messages会保留每一条消息的角色,直接传给模型。
与RunnableWithMessageHistory的关系
MessagesPlaceholder负责“占位”,RunnableWithMessageHistory负责“填充”。
在实战中,MessagesPlaceholder最常见的搭配就是RunnableWithMessageHistory(如你之前写的代码)。它的工作流程是:
RunnableWithMessageHistory根据session_id从chat_history_store里取出一整个消息列表。- 在调用
base_chain时,它自动把这个列表塞进invoke字典的chat_history键里。 prompt里的MessagesPlaceholder("chat_history")收到这个列表,完成注入。- 模型生成回复。
- 框架自动把新的
UserMessage和AIMessage追加回存储中。
没有MessagesPlaceholder,RunnableWithMessageHistory就无处安放历史消息;没有RunnableWithMessageHistory,MessagesPlaceholder就只能手动传参。
面试高频追问与回答
Q1:如果我用普通字符串占位符{chat_history},然后传一个 JSON 字符串,效果一样吗?
A:不一样。ChatModel(如通义千问、GPT-4)的 API 接收的是结构化的消息数组([{"role": "user", "content": "..."}])。如果传 JSON 字符串,模型会把它当成一段普通的content,而无法识别其中的角色信息。这会导致系统提示和用户消息界限模糊,严重影响模型的指令遵循能力。
Q2:MessagesPlaceholder能传ToolMessage吗?
A:可以。只要是BaseMessage的子类(HumanMessage,AIMessage,SystemMessage,ToolMessage等),MessagesPlaceholder都能无损传递。这在 Agent 场景中非常关键,因为工具调用结果必须原样送回模型,模型才能据此做下一步推理。
Q3:MessagesPlaceholder和StringPromptTemplate能混用吗?
A:不能。StringPromptTemplate生成的是纯文本提示,不区分角色。MessagesPlaceholder是专为ChatPromptTemplate设计的。二者不能混用。
Q4:如何限制MessagesPlaceholder里消息的数量?
A:MessagesPlaceholder本身不提供限制功能。你需要在使用RunnableWithMessageHistory时,配合trim_messages工具,或者在自定义的ChatMessageHistory的add_messages方法里做截断。官方推荐的做法是在传给MessagesPlaceholder之前,用trim_messages对历史列表进行裁剪。
总结
MessagesPlaceholder是 LangChain 实现结构化多轮对话的基石。它让历史消息得以“原汁原味”地传入模型,让 Agent 和 RAG 系统能精准理解上下文。在面试中,能清晰区分MessagesPlaceholder和普通字符串占位符,并能解释其在RunnableWithMessageHistory中的协作关系,是体现你对 LangChain 理解深度的标志性知识点之一。