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 让对话真正可持久化