大模型:RunnableWithMessageHistory 短期记忆案例
·
代码分析:基于 LangChain 的多轮对话记忆实现
这段代码演示了如何使用 LangChain 的 RunnableWithMessageHistory 组件,结合通义千问模型(ChatTongyi)构建一个具备多轮对话记忆能力的聊天链。它通过 InMemoryChatMessageHistory 在内存中按会话 ID(session_id)存储对话历史,并自动在每次请求时将历史消息注入提示模板。
📌 代码结构与关键组件
1. 模型与提示模板
model = ChatTongyi(model="qwen3-max")
prompt = ChatPromptTemplate.from_messages([
("system", "你需要根据会话历史回应用户问题。对话历史:"),
MessagesPlaceholder("chat_history"),
("human", "请回答如下问题:{input}")
])
- 使用
ChatPromptTemplate构建聊天消息模板,包含三个部分:- 系统提示(固定角色设定)。
MessagesPlaceholder("chat_history"):这是一个占位符,运行时会被传入的历史消息列表替换。它是实现记忆注入的关键。- 用户问题(
{input})。
2. 基础链(base_chain)
str_parser = StrOutputParser()
def print_prompt(full_prompt):
print("="*20, full_prompt.to_string(), "="*20)
return full_prompt
base_chain = prompt | print_prompt | model | str_parser
- 使用 LCEL(LangChain Expression Language)将
prompt、调试函数、模型、输出解析器串联成一个Runnable链。 print_prompt用于在控制台打印最终生成的提示,方便调试。
3. 历史存储与 RunnableWithMessageHistory
store = {} # session_id -> InMemoryChatMessageHistory
def get_history(session_id):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
conversation_chain = RunnableWithMessageHistory(
base_chain,
get_history,
input_messages_key="input",
history_messages_key="chat_history"
)
store是内存字典,键为session_id,值为InMemoryChatMessageHistory实例。RunnableWithMessageHistory是一个包装器,它接收一个基础链和一个“历史获取函数”,并在每次调用时自动完成:- 根据
session_id从store中获取对应历史消息列表。 - 将历史消息注入到
base_chain输入中的chat_history字段。 - 执行
base_chain。 - 将本轮的用户输入和模型输出追加到历史存储中(通过
add_user_message和add_ai_message)。
- 根据
🔄 运行时流程(以三次执行为例)
-
第一次执行(用户说:“小明有2个猫”):
conversation_chain.invoke({"input": "小明有2个猫"}, session_config)- 从
store["user_001"]获取空历史(首次)。 - 传入
base_chain的输入为:{"input": "...", "chat_history": []}。 prompt生成的消息列表中,chat_history为空,只有系统消息和当前用户提问。- 模型生成回复(如“好的,记住了”)。
- 执行后,
RunnableWithMessageHistory自动将HumanMessage("小明有2个猫")和AIMessage("...")追加到store["user_001"]。
-
第二次执行(“小刚有1只狗”):
- 历史已有两轮(用户+AI)。
chat_history注入完整历史,模型看到上下文,生成新回复。- 历史再次扩展。
-
第三次执行(“总共有几个宠物”):
- 历史包含前两轮所有消息。
- 模型能从历史中提取“小明有2个猫,小刚有1只狗”,正确回答“3个”。
🧠 关键知识点
| 组件 | 作用 |
|---|---|
MessagesPlaceholder |
在 ChatPromptTemplate 中预留一个位置,运行时注入完整的消息列表,而非单个字符串。这保留了消息角色(human/ai)的结构。 |
RunnableWithMessageHistory |
这是一个 Runnable 包装器,它不影响原链的逻辑,仅在其前后插入“加载历史”和“保存历史”的操作。适用于任何 Runnable,不限于 Chain。 |
InMemoryChatMessageHistory |
一个简单的内存存储实现,支持 add_messages、messages 属性(返回 BaseMessage 列表)和 clear。适合开发测试,但生产环境可用 RedisChatMessageHistory 或 PostgresChatMessageHistory。 |
input_messages_key / history_messages_key |
指定在 invoke 输入字典中,哪两个键分别代表当前用户输入和历史消息列表。这些键必须与提示模板中的占位符名称匹配。 |
✅ 优点
- 简单:只需几行代码即可为任何链添加记忆。
- 灵活:历史存储可插拔(内存、Redis、数据库)。
- 无需修改模型:通过提示工程注入历史,不依赖模型的特殊记忆功能。
⚠️ 局限与改进方向
| 局限 | 解决方案 |
|---|---|
| 历史无限增长,超出上下文窗口限制 | 结合 ConversationBufferWindowMemory 或 ConversationSummaryMemory 进行裁剪/摘要。 |
| 内存存储,程序重启丢失 | 改用持久化存储,如 RedisChatMessageHistory 或自定义 FileChatMessageHistory。 |
| 无法跨会话共享信息 | 需额外实现“长期记忆”组件(如 PostgresStore),手动读写用户画像。 |
高并发下 store 字典可能线程不安全 |
使用线程安全的存储或外部缓存(如 Redis)。 |
💡 面试考点速查
-
RunnableWithMessageHistory和直接手动维护messages列表的区别?- 手动维护需要自己在每次调用时处理历史注入和更新,代码分散且易错;
RunnableWithMessageHistory将这些逻辑封装成标准组件,便于复用和测试。
- 手动维护需要自己在每次调用时处理历史注入和更新,代码分散且易错;
-
MessagesPlaceholder与普通字符串占位符({var})有何不同?- 普通占位符只能传入单个字符串;
MessagesPlaceholder传入的是BaseMessage序列,能保留角色和结构信息,更符合聊天模型的输入格式。
- 普通占位符只能传入单个字符串;
-
如何在生产环境中实现多轮对话记忆?
- 使用持久化历史存储(如 Redis)替换
InMemoryChatMessageHistory,结合RunnableWithMessageHistory,并配合消息裁剪/摘要策略以控制 Token 消耗。
- 使用持久化历史存储(如 Redis)替换
这段代码清晰地展示了 LangChain 处理对话记忆的标准范式,是面试中常见的实战示例。掌握其原理和扩展方式,能够体现你对 LangChain 核心组件的深度理解。
更多推荐

所有评论(0)