LangChain 中如何实现多轮对话的上下文管理?
- 标签:LangChain、上下文管理
🏛 业务架构师
LangChain 里多轮对话的上下文管理,我按"三层递进"讲:
第一层·RunnableWithMessageHistory(会话历史):按 session_id 存取消息列表(内存/Redis/DB 后端),每轮把历史+新输入装配进提示。这是基础机制——解决"连续对话"问题。配置要点:会话 ID 的设计(用户 ID+场景 ID),历史存储的 TTL 与清理策略。
第二层·历史裁剪策略:直接全量注入历史会随轮次膨胀——用 trim_messages 或 window 策略只保留最近 N 轮,更早的滚动摘要为"会话纪要"注入 System Prompt。token 预算 = 固定部分(系统提示+工具)+ 动态部分(历史+检索内容),要配额化管理。
第三层·状态外置(长任务/Agent 场景):把关键业务状态(用户确认过的订单号、任务进度)抽取到结构化存储,上下文里只保留必要引用——LangGraph 的 State+Checkpointer 是这一层的标准实现:状态显式定义、每步持久化、支持暂停恢复。
业务落地建议:普通客服对话,第一层+第二层(窗口+摘要)就够;跨天任务型 Agent(工单跟进)必须上第三层。高频踩坑点:多实例部署时历史存储要集中(Redis/DB),用内存存储会导致负载均衡后"失忆";历史里的工具调用消息要与文本消息正确配对,否则模型上下文错乱。
成本提醒:上下文管理的每个决策都是成本决策——窗口开多大、摘要多久做一次,直接体现在账单上,建议按对话类型分档配置。
🏗 系统架构师
多轮对话上下文管理在 LangChain 里的实现我分三代方案讲,重点讲现代姿势:
方案一:runnable history(1.0 推荐)——把历史注入注入消息列表:
python
from langchain_core.runnables.history import RunnableWithMessageHistory
chain = prompt | model | parser
with_history = RunnableWithMessageHistory(
chain,
get_session_history, # 按 session_id 从存储读取
input_messages_key="input",
history_messages_key="history",
)
# 调用时传 config={"configurable": {"session_id": "user-123"}}方案二:LangGraph checkpointer(Agent 场景标准):对话状态进图状态,每步自动快照持久化(Postgres/Redis),天然支持多轮、断点恢复、时间旅行;thread_id 即会话 ID。
方案三:手动管理:ChatMessageHistory 对象 + 自己拼 messages 列表——最灵活,适合定制化强的网关层。
架构要点(比 API 更重要):
- 会话存储选型:Redis(热会话,TTL)+ DB(冷归档),无状态服务 + 有状态存储分离,支撑水平扩展;
- 上下文预算控制:轮次多了要截断/摘要(滑动窗口 + 滚动摘要组合),管理逻辑要在 history 读取层做,不散落在业务代码;
- 并发一致性:同一会话并发请求要加锁或串行化,否则历史错乱;
- 可观测:每轮记录 token 数与截断行为,监控上下文膨胀。
一句话:多轮对话的本质是"外部状态管理"——LLM 无状态,连续性是工程给的。
💻 全栈开发专家
多轮对话上下文管理我做四件事:带历史、会话隔离、预算裁剪、摘要兜底。
1. 会话历史管理(新版标准做法):
python
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import RedisChatMessageHistory
chain_with_history = RunnableWithMessageHistory(
chain, # 你的 prompt|llm|parser 链
get_session_history=lambda sid: RedisChatMessageHistory(sid, url="redis://..."),
input_messages_key="question",
history_messages_key="history")
chain_with_history.invoke(
{"question": "那它的价格呢"}, # 指代上一轮的"它"
config={"configurable": {"session_id": "user-123"}}) # 会话隔离键session_id 的粒度要设计好(用户级/会话级),Redis 存储 TTL 过期清理。
2. 长对话裁剪:trim_messages(strategy="last", max_tokens=N) 保最近轮次;注意 system 消息和摘要要标记 include=True 不被裁掉。
3. 滚动摘要:超过阈值(如窗口 70%)触发旧历史摘要化,保留最近 2~4 轮原文——长任务的标配。
4. 工程细节(踩坑):
- 指代消解:「它/上面那个」跨轮指代,简单裁剪会丢指代对象。轻量做法:最近 3 轮永远成组保留;讲究的加一步 LLM 查询改写(把「它」解析成实体名再检索/回答);
- 并发生一致:同一会话并发请求会导致历史错乱,会话级锁或单线程队列处理;
- 历史入库脱敏:聊天记录含 PII 时存储前打码;
- 失败恢复:历史在 Redis 挂了要降级到无记忆模式并提示用户,别让整个服务不可用。