LangChain 第四篇:多轮对话实践 RunnableWithMessageHistory + PostgreSQL

41 阅读3分钟

LangChain 的多轮对话,本质不是“模型记住了历史”,而是 Runnable 在每次调用前,把历史消息重新拼进 Prompt,再把新结果写回存储。

这一篇将基于一个完整的多轮对话流程,从零梳理:

  • 多轮对话在 LangChain 中是如何工作的
  • RunnableWithMessageHistory 到底做了什么
  • 使用 PostgreSQL或其他数据库进行持久化存储
  • 多用户 / 多会话是如何隔离的

1.多轮对话的真实需求

多轮对话通常至少要满足:

  • 同一个用户,多次提问能接着聊
  • 不同用户 / 不同会话互不干扰
  • 服务重启后,对话不能丢
  • Prompt、LLM、存储可以独立替换

下面我们用完整代码 + 调用流程,学习 LangChain 的多轮对话机制

2.多轮对话整体流程

用户输入
↓
RunnableWithMessageHistory
↓
get_session_history(user_id, conversation_id)
↓
ChatMessageHistory / PostgresChatMessageHistory
↓
Prompt + LLM
↓
模型输出(并写回历史)

3.初始化 LLM

这里以通义千问 ChatTongyi 为例,开启流式 + 思考模式:

from langchain_community.chat_models import ChatTongyi
from pydantic import SecretStr

llm = ChatTongyi(
    model="qwen3-plush
    api_key=SecretStr("sk-xxx"),
    streaming=True, # 思考模式必须启用流式输出
    model_kwargs={
        "enable_thinking": True, # 开启思考模式
        "incremental_output": True
    }
)

参数说明:

  • streaming=True: 开启流式输出,否则思考模式不生效
  • enable_thinking=True # 启用“思考”模式(流式调用才有效)
  • incremental_output=True # 模型 token 级增量返回,适合 SSE / WebSocket

4.定义聊天提示模板(历史是怎么插进去的)

使用 ChatPromptTemplate.from_messages 创建多轮对话模板

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个擅长写作的智能聊天助手",
        ),
        MessagesPlaceholder(variable_name="history"),  # 历史记录占位符,实现多轮对话关键的一行
        ("human", "{input}"),  # 最新用户输入占位符
    ]
)

参数说明:

  • system: 系统指令,指定模型身份或能力
  • MessagesPlaceholder: 历史消息占位符,自动填充历史对话
  • human: 用户输入

5.将 prompt 与 LLM 组成管道

通过 | 操作符可以将 Prompt 输出作为 LLM 输入

runnable = prompt | llm

6.连接 PostgreSQL 数据库(持久化前提)

import psycopg

sync_conn = psycopg.connect(
    "postgresql://user:password@host:5432/db"
)

7.初始化聊天记录表(会自动在数据库创建相应的表)

from langchain_postgres import PostgresChatMessageHistory

PostgresChatMessageHistory.create_tables(
    sync_conn, 
    "message_store"
)

说明:

  • LangChain 会自动创建表结构
  • 只需要在项目初次启动时执行一次
  • 后续直接复用表

8.定义获取聊天历史的函数(关键)

import uuid

def get_session_history(user_id: str, conversation_id: str):
    # 会话的唯一标识
    session_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{user_id}_{conversation_id}"))
    return PostgresChatMessageHistory(
        "message_store",       # table_name
        session_id,            # session_id
        sync_connection=sync_conn
    )

这里完成了三件非常重要的事:

  • 同一个 user + conversation → 同一个会话
  • 不同用户 / 会话自动隔离
  • 历史记录真正落库,可跨服务实例

9.RunnableWithMessageHistory:多轮对话的核心

from langchain_core.runnables import ConfigurableFieldSpec, RunnableWithMessageHistory

with_message_history = RunnableWithMessageHistory(
    runnable,  # type: ignore
    get_session_history,
    input_messages_key="input",  # 最新输入的 key
    history_messages_key="history",  # 历史消息的 key
    history_factory_config=[
        ConfigurableFieldSpec(
            id="user_id",  # 对应工厂函数参数名
            annotation=str,
            name="用户 ID",
            description="用户的唯一标识符。",
            default="",
            is_shared=True,
        ),
        ConfigurableFieldSpec(
            id="conversation_id",  # 对应工厂函数参数名
            annotation=str,
            name="对话 ID",
            description="对话的唯一标识符。",
            default="",
            is_shared=True,
        ),
    ],
)

参数说明:

  • runnable: 定义大模型的管道
  • get_session_history: 历史工厂函数(定义获取聊天历史的函数)
  • input_messages_key: 指定输入消息的 key,对应 prompt 中的 {input}
  • history_messages_key: 指定历史消息的 key,对应 MessagesPlaceholder 的 variable_name
  • history_factory_config: 配置需要传入工厂函数的参数

它在每一次 invoke 时,固定做 4 件事:

  • 根据 config 调用 get_session_history
  • 读取历史消息
  • 把历史注入 Prompt,再调用 runnable
  • 把「本轮问 + 答」写回存储

10.调用示例

# 第一次调用
result = with_message_history.invoke(
    {"input": "你好"},
    config={"configurable": {"user_id": "123", "conversation_id": "1"}},
)
print(result)

# 第二次调用(自动带上历史)
result2 = with_message_history.invoke(
    {"input": "我刚才说了什么"},
    config={"configurable": {"user_id": "123", "conversation_id": "1"}},
)
print(result2)

11.小结

  • Prompt 是无状态的
  • Runnable 是无状态的
  • 状态只存在于 MessageHistory
  • RunnableWithMessageHistory 负责“粘合”
  • PostgreSQL 让对话真正可持久化