在构建聊天机器人Chatbot时**“记忆”Memory**是核心能力之一。早期的 LangChain 使用ConversationChainMemory对象来管理历史但在 LCELLangChain Expression Language时代官方推荐使用更灵活、更解耦的RunnableWithMessageHistory。本文将以一个完整的 Python 示例为基础深入剖析RunnableWithMessageHistory的工作原理、核心参数及最佳实践。1. 为什么需要它在没有RunnableWithMessageHistory之前手动管理对话历史通常需要以下繁琐步骤查询根据 User ID 从数据库查出历史记录。拼接手动把历史记录塞进 Prompt 中。调用执行 LLM。保存手动把 User Input 和 AI Output 追加保存回数据库。RunnableWithMessageHistory就像一个自动化的切面Aspect它包装了你的 Chain自动在后台完成了上述“查询-注入-保存”的所有工作让你只需要关注当前轮次的交互。2. 实战代码演示以下是一个可运行的完整示例 (src/examples/memory/demo_runnable_with_history.py)。2.1 准备工作首先我们需要定义基础组件LLM、Prompt 以及历史记录的存储位置。fromlangchain_core.promptsimportChatPromptTemplate,MessagesPlaceholderfromlangchain_core.runnables.historyimportRunnableWithMessageHistoryfromlangchain_community.chat_message_historiesimportChatMessageHistoryfromsrc.llm.gemini_chat_modelimportget_gemini_llm# 1. 定义存储这里使用内存字典模拟数据库store{}# 2. 定义工厂函数告诉系统如何根据 session_id 获取历史对象defget_session_history(session_id:str):ifsession_idnotinstore:store[session_id]ChatMessageHistory()returnstore[session_id]# 3. 初始化 LLMllmget_gemini_llm()# 4. 定义 Prompt必须包含历史记录的占位符promptChatPromptTemplate.from_messages([(system,You are a helpful assistant.),MessagesPlaceholder(variable_namehistory),# --- 关键点预留位置(human,{input}),])# 5. 创建基础 Chainchainprompt|llm2.2 核心包装这是最关键的一步。我们将无状态的chain包装成有状态的with_message_history。with_message_historyRunnableWithMessageHistory(chain,get_session_history,input_messages_keyinput,history_messages_keyhistory,)2.3 调用执行调用时我们需要通过config传递session_id。# 第一轮告诉我是 Bobresponse1with_message_history.invoke({input:Hi! My name is Bob.},config{configurable:{session_id:session_1}})print(fAI:{response1.content})# 第二轮问我的名字response2with_message_history.invoke({input:What is my name?},config{configurable:{session_id:session_1}})print(fAI:{response2.content})# 输出: AI: Your name is Bob. (成功记住了)3. 参数深度剖析 (非常重要)RunnableWithMessageHistory的构造函数接收 4 个核心参数理解它们是掌握 LCEL Memory 的关键。1.runnable(位置参数 1)含义被包装的基础对象通常是Prompt | LLM组成的 Chain。要求这个 Chain 本身必须是无状态的。它不知道“历史”的存在它只知道接收一个包含消息列表的 Prompt 并输出结果。历史的注入是在它运行之前由包装器完成的。2.get_session_history(位置参数 2)含义一个工厂函数Factory Function。签名(session_id: str) - BaseChatMessageHistory。作用这是 LangChain 与外部存储Redis, Postgres, Memory交互的接口。当invoke开始时系统调用此函数加载旧记录。当invoke结束时系统调用此函数保存新记录。实战生产环境中这里通常返回RedisChatMessageHistory或PostgresChatMessageHistory的实例。3.input_messages_key(关键字参数)含义“哪个 Key 代表用户的新消息”背景Chain 的输入通常是一个字典例如{input: 你好, style: 幽默}。系统需要知道要把哪一个值作为HumanMessage保存到历史记录中。设定在上面的例子中我们调用 invoke 时用了{input: ...}所以这里填input。4.history_messages_key(关键字参数)含义“历史记录应该填到 Prompt 的哪个坑里”背景在 Prompt 中我们预留了一个占位符MessagesPlaceholder(variable_namehistory)。设定系统加载出历史记录List[Message]后会自动将其注入到这个 key 中。所以这里必须填history以匹配 Prompt 中的变量名。4. 运行流程图解当你执行with_message_history.invoke({...}, config{...})时内部发生了什么提取 ID从config中读取session_id。加载历史调用get_session_history(session_id)获取当前的历史消息列表。注入 Prompt创建一个新的输入字典。将用户输入 (input) 放入。将历史列表 (history) 放入。执行 Chain运行基础 Chain (prompt | llm)。保存历史将用户的输入 (HumanMessage) 追加到历史对象。将 AI 的输出 (AIMessage) 追加到历史对象。返回结果将 AI 的输出返回给用户。5. 总结RunnableWithMessageHistory是 LangChain 中优雅管理状态的瑞士军刀。它通过配置化的方式将繁琐的历史记录读写逻辑从业务逻辑中剥离出来极大地简化了代码结构。记忆口诀Config定身份 (session_id)。Factory找仓库 (get_session_history)。Keys做映射 (input/history keys)。6. 架构关系图 (Mermaid)下图直观地展示了RunnableWithMessageHistory、get_session_history、MessagesPlaceholder和ChatPromptTemplate之间的协作关系。LLMMessagesPlaceholderChatPromptTemplateChatMessageHistory (Storage)get_session_historyRunnableWithMessageHistoryUserLLMMessagesPlaceholderChatPromptTemplateChatMessageHistory (Storage)get_session_historyRunnableWithMessageHistoryUser1. 用户发起调用2. 加载历史记录3. 准备 Prompt 输入4. 渲染 Prompt5. 执行推理6. 保存新对话7. 返回结果invoke(inputHi, config{session_id: 1})1调用工厂函数(session_id1)2获取/创建历史对象3返回 List[BaseMessage] (旧历史)4传入 {input: Hi, history: [旧历史]}5遇到 variable_namehistory6展开 [旧历史] 列表7填充 {input} 到 HumanMessage8生成最终完整消息列表9发送完整消息列表10返回 AI 回复 (Hello!)11add_user_message(Hi)12add_ai_message(Hello!)13返回 Hello!14图解说明RunnableWithMessageHistory是总指挥负责协调所有组件。get_session_history是仓库管理员负责根据 ID 找到对应的历史记录本。MessagesPlaceholder是 Prompt 里的“占位符”负责把从仓库拿出来的历史记录“平铺”到对话中。ChatPromptTemplate是最终的拼装车间输出给 LLM 的是包含历史和新输入的完整列表。