RunnableWithMessageHistory是对langchain LCEL 原始chain的包装,使得不具备记忆功能的原始chain具备了记忆功能,被包装后的chain平等的参与后续chain的组合。
一、核心定位与设计思想
1. 本质定义
RunnableWithMessageHistory是LangChain 基于 LCEL 协议实现的包装类,属于装饰器模式应用:
- 目标对象:任意标准 LCEL
Runnable(Prompt + LLM、普通 Chain、自定义 Runnable 等无原生记忆的链路) - 核心能力:无感追加对话历史记忆,不修改原有链的代码与逻辑
- 接口一致性:包装后实例依然是标准
Runnable,完全遵循 LCEL 规范,和原生链具备同等组合能力,可使用|、RunnableParallel、RunnableBranch等任意 LCEL 语法拼接、嵌套、复用!!!这正是该消息的强大之处!!!!
2. 解决的痛点
- 原生 LCEL 链是无状态的:单次调用独立,无法留存多轮对话上下文;
- 传统记忆组件(
ConversationBufferMemory)和老版 Chain 强耦合,难以适配 LCEL 流式、组合式的编程范式; - 统一记忆逻辑与业务链逻辑,实现记忆层与业务层解耦。
二、工作原理
1. 整体执行流程
- 接收入参:调用时传入业务输入 +专属
session_id(会话唯一标识); - 加载历史:通过你定义的
get_session_history函数,根据session_id读取当前会话的历史消息; - 拼接上下文:将「历史对话 + 当前用户提问」自动整合,注入到原始 Chain 的 Prompt 中;
- 执行原始链:调用被包装的原生 LCEL 链路,得到模型回复;
- 持久化历史:把「当前提问 + 模型回答」追加到对应
session_id的会话历史中; - 返回结果:输出模型响应,整个过程对上层调用透明。
2. 为什么能平等参与 LCEL 组合
LCEL 的核心是统一 Runnable 协议:所有组件(Prompt、LLM、解析器、包装器)都实现了invoke/stream/batch等标准方法。RunnableWithMessageHistory只是在标准方法内部增加了记忆读写逻辑,对外暴露的接口、入参出参格式和普通链完全一致,因此:
- 可放在链路任意位置,用管道符
|串联; - 可并行、分支、嵌套组合;
- 支持流式输出、批量调用等 LCEL 全部特性。
三、核心参数解析
python
运行
RunnableWithMessageHistory( runnable: Runnable, # 必选:被包装的原始LCEL链 get_session_history: Callable[[str], BaseChatMessageHistory], # 必选:会话历史获取函数 input_messages_key: str = "input", # 用户输入字段名 history_messages_key: Optional[str] = None, # 历史消息注入字段名 output_messages_key: Optional[str] = None # 输出消息字段名 )- runnable待增强的原始 LCEL 链,纯业务逻辑,本身不处理记忆。
- get_session_history回调函数,入参为
session_id,返回一个消息历史存储实例。- 内置实现:内存存储、文件、Redis、数据库等;
- 作用:按会话隔离历史,支持多用户、多对话并发。
- input_messages_keyPrompt 模板中,接收当前用户输入的变量名。
- history_messages_keyPrompt 模板中,接收拼接后的历史对话的变量名。
四、完整实战示例(分两种常用写法)
场景 1:标准对话模板(显式指定历史变量)
最规范写法,手动在 Prompt 中预留历史消息位。
python
运行
from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_openai import ChatOpenAI from langchain_community.chat_message_histories import ChatMessageHistory # 1. 构建无记忆的原始 LCEL 链 # MessagesPlaceholder 专门用来承载动态对话历史 prompt = ChatPromptTemplate.from_messages([ ("system", "你是专业助手,根据上下文回答问题"), MessagesPlaceholder(variable_name="history"), # 历史对话占位 ("human", "{question}") # 当前用户问题 ]) llm = ChatOpenAI(model="gpt-3.5-turbo") # 原始链:纯业务逻辑,无记忆 raw_chain = prompt | llm # 2. 定义会话历史获取函数(内存版) def get_history(session_id: str) -> ChatMessageHistory: return ChatMessageHistory() # 3. 包装为带记忆的链 history_chain = RunnableWithMessageHistory( runnable=raw_chain, get_session_history=get_history, input_messages_key="question", # 当前输入变量名 history_messages_key="history" # 历史消息变量名 ) # 4. 调用测试(同一个session_id 共享历史) session_cfg = {"configurable": {"session_id": "user_001"}} # 第一轮对话 res1 = history_chain.invoke({"question": "我叫小王"}, config=session_cfg) print(res1.content) # 第二轮对话,自动带上历史 res2 = history_chain.invoke({"question": "我叫什么名字?"}, config=session_cfg) print(res2.content)场景 2:极简写法(自动推导字段)
适合简单对话,框架自动处理消息拼接。
python
运行
# 简化 Prompt prompt = ChatPromptTemplate.from_messages([ ("system", "友好聊天"), ("human", "{input}") ]) raw_chain = prompt | llm # 包装记忆链 history_chain = RunnableWithMessageHistory( raw_chain, get_history, input_messages_key="input" ) # 继续 LCEL 组合:包装后的链正常拼接解析器 from langchain_core.output_parsers import StrOutputParser final_chain = history_chain | StrOutputParser() # 链式组合,完全兼容 # 调用 print(final_chain.invoke({"input": "今天天气如何"}, config={"configurable": {"session_id":"user_002"}}))五、关键特性与进阶说明
1. 会话隔离(session_id)
- 不同
session_id对应独立的对话历史,天然支持多用户、多会话; - 内存存储重启后历史清空,生产环境建议替换为 Redis / 数据库持久化。
2. LCEL 组合能力验证
包装后的链 = 标准Runnable,支持所有 LCEL 组合语法:
python
运行
# 并行组合示例 from langchain_core.runnables import RunnableParallel # 带记忆的链 和 普通链 并行执行 parallel_chain = RunnableParallel({ "chat": history_chain, "summary": raw_chain }) # 正常调用 parallel_chain.invoke({"question": "你好"}, config={"configurable":{"session_id":"s1"}})3. 流式、批量支持
原生stream/batch方法完全保留,包装不影响流式输出等高级特性。
4. 优势总结
- 解耦:记忆逻辑和业务链完全分离,原始链可单独测试、复用;
- 通用:任意 LCEL 链都可一键加装记忆,无需改造原有代码;
- 兼容:100% 适配 LCEL 生态,组合、嵌套、分支不受限制;
- 灵活:历史存储可自由切换(内存、Redis、MySQL 等)。
六、和传统 Memory 的核心区别
表格
| 维度 | 传统 Memory + 老式 Chain | RunnableWithMessageHistory + LCEL |
|---|---|---|
| 耦合度 | 强耦合,代码侵入高 | 装饰器包装,零侵入 |
| 组合性 | 难以自由拼接、嵌套 | 原生支持 LCEL 全组合能力 |
| 状态管理 | 链内部维护状态 | 独立会话存储,多会话隔离清晰 |
| 适用范式 | 旧版命令式编程 | LCEL 声明式、流式编程 |
七、常见使用注意点
- 必须通过
config={"configurable": {"session_id": "xxx"}}传入会话 ID,否则无法区分历史; - Prompt 中使用
MessagesPlaceholder承载历史消息是最佳实践,格式更标准; - 内存版
ChatMessageHistory仅适用于测试,线上务必使用持久化存储; - 包装顺序:先构建业务 LCEL 链,最后统一包装记忆,逻辑更清晰。