代码分析:基于 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 是一个包装器,它接收一个基础链和一个“历史获取函数”,并在每次调用时自动完成:
    1. 根据 session_idstore 中获取对应历史消息列表。
    2. 将历史消息注入到 base_chain 输入中的 chat_history 字段。
    3. 执行 base_chain
    4. 将本轮的用户输入和模型输出追加到历史存储中(通过 add_user_messageadd_ai_message)。

🔄 运行时流程(以三次执行为例)

  1. 第一次执行(用户说:“小明有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"]
  2. 第二次执行(“小刚有1只狗”):

    • 历史已有两轮(用户+AI)。
    • chat_history 注入完整历史,模型看到上下文,生成新回复。
    • 历史再次扩展。
  3. 第三次执行(“总共有几个宠物”):

    • 历史包含前两轮所有消息。
    • 模型能从历史中提取“小明有2个猫,小刚有1只狗”,正确回答“3个”。

🧠 关键知识点

组件 作用
MessagesPlaceholder ChatPromptTemplate 中预留一个位置,运行时注入完整的消息列表,而非单个字符串。这保留了消息角色(human/ai)的结构。
RunnableWithMessageHistory 这是一个 Runnable 包装器,它不影响原链的逻辑,仅在其前后插入“加载历史”和“保存历史”的操作。适用于任何 Runnable,不限于 Chain
InMemoryChatMessageHistory 一个简单的内存存储实现,支持 add_messagesmessages 属性(返回 BaseMessage 列表)和 clear。适合开发测试,但生产环境可用 RedisChatMessageHistoryPostgresChatMessageHistory
input_messages_key / history_messages_key 指定在 invoke 输入字典中,哪两个键分别代表当前用户输入和历史消息列表。这些键必须与提示模板中的占位符名称匹配。

✅ 优点

  • 简单:只需几行代码即可为任何链添加记忆。
  • 灵活:历史存储可插拔(内存、Redis、数据库)。
  • 无需修改模型:通过提示工程注入历史,不依赖模型的特殊记忆功能。

⚠️ 局限与改进方向

局限 解决方案
历史无限增长,超出上下文窗口限制 结合 ConversationBufferWindowMemoryConversationSummaryMemory 进行裁剪/摘要。
内存存储,程序重启丢失 改用持久化存储,如 RedisChatMessageHistory 或自定义 FileChatMessageHistory
无法跨会话共享信息 需额外实现“长期记忆”组件(如 PostgresStore),手动读写用户画像。
高并发下 store 字典可能线程不安全 使用线程安全的存储或外部缓存(如 Redis)。

💡 面试考点速查

  • RunnableWithMessageHistory 和直接手动维护 messages 列表的区别?

    • 手动维护需要自己在每次调用时处理历史注入和更新,代码分散且易错;RunnableWithMessageHistory 将这些逻辑封装成标准组件,便于复用和测试。
  • MessagesPlaceholder 与普通字符串占位符({var})有何不同?

    • 普通占位符只能传入单个字符串;MessagesPlaceholder 传入的是 BaseMessage 序列,能保留角色和结构信息,更符合聊天模型的输入格式。
  • 如何在生产环境中实现多轮对话记忆?

    • 使用持久化历史存储(如 Redis)替换 InMemoryChatMessageHistory,结合 RunnableWithMessageHistory,并配合消息裁剪/摘要策略以控制 Token 消耗。

这段代码清晰地展示了 LangChain 处理对话记忆的标准范式,是面试中常见的实战示例。掌握其原理和扩展方式,能够体现你对 LangChain 核心组件的深度理解。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐