news 2026/9/26 7:44:17

LangChain摘要中间件实战:解决长会话Token超限与断片问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain摘要中间件实战:解决长会话Token超限与断片问题

1. 长会话为什么会“断片”,以及摘要中间件到底在解决什么

做过对话类应用的人大概率都遇到过这种场景:用户跟你的 AI 助手聊了四五十轮,前面明确说过“我预算八千、主要拍娃、不要单反”,结果聊到后面推荐相机时,它又开始给你推两万的全画幅,或者干脆反问“请问您的预算是多少”。用户当场就炸了,觉得这 AI 是不是失忆了。

这不是模型笨,这是上下文窗口和Token 预算的物理限制在作祟。大模型的上下文窗口是有限的,早期模型 4K、8K,现在主流也就 128K、200K,看着挺大,但你要知道,Token 消耗是平方级增长的——每一轮对话都要把之前所有历史重新塞进去。聊到第 50 轮,光历史记录可能就吃掉几万 Token,成本飙升不说,一旦超出窗口,最早的内容就被直接截断丢弃,于是“断片”就发生了。

摘要中间件(SummarizationMiddleware)就是干这个的:它不让你无脑地把全部历史塞给模型,而是在对话变长时,自动把早期的对话内容压缩成一段摘要,用摘要替代原始历史,从而在保留关键信息的前提下,把 Token 用量压下来。LangChain 生态里这个组件是长会话场景的标配,配合 LangGraph 做状态管理,能实现“聊一百轮也不失忆、成本还可控”的效果。

这篇文章适合三类人看:一是正在用 LangChain 搭对话应用、被长会话 Token 问题折磨的开发者;二是想搞明白摘要中间件内部机制、准备自己魔改的进阶玩家;三是做工业智能体、客服机器人这类需要长期记忆场景的工程同学。我会从设计思路讲到实操代码,再把我踩过的坑和参数调优经验都摊开说。

2. 摘要中间件的整体设计思路与方案选型

2.1 为什么是“摘要”而不是“截断”或“全量保留”

处理长会话历史,业内常见就三条路,我做个对比你就明白为什么摘要中间件是更优解。

方案做法优点致命问题
全量保留每轮把完整历史塞进去信息零丢失Token 爆炸,超窗即崩,成本极高
滑动窗口截断只保留最近 N 轮实现简单,成本低早期关键信息永久丢失,必然断片
摘要压缩早期历史压成摘要,近期保留原文信息保留与成本平衡实现稍复杂,摘要质量依赖模型

滑动窗口截断是最容易踩的坑。我见过不少项目图省事,直接messages[-10:],结果用户第一轮说的核心需求全没了。摘要中间件的思路是:近期对话保留原文(保证细节和语气),早期对话压缩成摘要(保证关键事实不丢)。这就像人开会,你记不住每个人说的每句话,但你会记住“上次会议定了 A 方案、预算 50 万、下周三交付”这种要点。

2.2 触发时机:什么时候该做摘要

摘要不是每轮都做,那样反而浪费。核心是设一个Token 阈值。常见做法是:当历史消息的总 Token 数超过模型上下文窗口的某个比例(比如 60%~70%)时,触发摘要。留出的那 30%~40% 是给当前对话、系统提示词和模型输出用的。

这里有个参数计算的实际例子。假设你用某模型,上下文窗口 128K Token,你设置触发阈值 0.6,那就是 76.8K Token 时触发摘要。摘要后目标压到多少?一般压到触发阈值的 30%~50%,也就是 23K~38K Token 左右。这样一次摘要能撑很多轮,不会频繁触发。

注意:阈值别设太高。我试过设 0.9,结果摘要还没做完,加上系统提示词和输出预留,直接超窗报错。0.6 到 0.7 是比较稳的区间。

2.3 摘要保留多少、丢弃多少:keep 与 summarize 的边界

摘要中间件通常有两个关键参数:keep(保留最近多少条消息原文)和触发阈值。keep设太小,近期上下文细节丢失,模型回答会变得笼统;设太大,摘要省下的 Token 又不够。

我的经验值是keep保留最近 6~10 轮对话(约 12~20 条消息)。这个量级能覆盖大多数“指代消解”需求——用户说“就刚才那个”“换成第二个”,模型能从前几轮找到指代对象。再往前的,交给摘要。

2.4 和 LangGraph 的配合:状态怎么管

单用 LangChain 的ConversationSummaryBufferMemory也能做,但生产环境我更推荐LangGraph + SummarizationMiddleware的组合。原因是 LangGraph 把对话状态显式建模成 State,摘要逻辑作为图中的一个节点或中间件钩子,可控性更强,也方便做 human-in-the-loop、持久化、多轮分支。

LangChain 和 LangGraph 的区别这里顺带说一句:LangChain 偏“链式调用”,适合线性流程;LangGraph 偏“状态图”,适合有循环、分支、需要长期维护状态的 Agent 场景。长会话摘要天然是状态管理问题,所以 LangGraph 更合适。面试里也常问这个区别,答“LangGraph 是 LangChain 之上做有状态编排的框架”基本就对了。

3. 核心细节解析与实操要点

3.1 摘要提示词怎么写才不丢关键信息

摘要质量直接决定长会话会不会断片。很多人随便写个“请总结以上对话”,结果摘要出来全是“用户询问了产品信息,助手进行了回答”这种废话,关键的数字、偏好、约束全没了。

好的摘要提示词要结构化,明确要求保留哪几类信息。我常用的模板是这样的:

SUMMARY_PROMPT = """请将以下对话历史压缩成结构化摘要,务必保留: 1. 用户明确提出的硬性约束(预算、时间、数量、禁忌) 2. 用户表达过的偏好和倾向 3. 已经达成的结论或决定 4. 尚未解决的问题 5. 关键实体名称(产品名、人名、地点) 不要保留:寒暄、重复确认、助手的长篇解释。 对话历史: {history} 结构化摘要:"""

这个模板的关键在于明确“保留什么”和“丢弃什么”。模型做摘要时如果没有明确指令,倾向于保留“看起来重要”的礼貌性内容,反而丢掉数字。我实测下来,加上“硬性约束”“关键实体”这些词后,摘要里数字和名称的保留率明显提升。

3.2 Token 计数:别用字符数估算

Token 和字符数不是一回事。中文大概 1 个汉字 ≈ 1~2 个 Token,英文 1 个单词 ≈ 1.3 个 Token,代码和特殊符号更飘。用len(text)估算 Token 是新手最容易犯的错,会导致阈值判断严重失准。

正确做法是用对应模型的 tokenizer。LangChain 里可以用tiktoken(OpenAI 系)或模型自带的 tokenizer。如果你用的是国产模型,很多也提供了 tokenizer 接口。实在没有,用transformers的AutoTokenizer加载对应模型的分词器。

import tiktoken def count_tokens(text: str, model: str = "gpt-4") -> int: enc = tiktoken.encoding_for_model(model) return len(enc.encode(text))

提示:如果你的应用混用多个模型,Token 计数要按“最贵的那个”或“窗口最小的那个”来算,否则切换模型时会翻车。

3.3 摘要的“递归压缩”问题

聊得特别久的时候,摘要本身也会变长。比如聊了 200 轮,第一次摘要 500 Token,第二次把“旧摘要+新对话”再摘要,可能变成 800 Token,越滚越大。这就是递归压缩膨胀。

解决办法有两个:一是摘要时对旧摘要也做压缩,明确要求“在保留关键信息的前提下尽量精简,目标 X Token 以内”;二是设置摘要的硬上限,超过就强制二次压缩。我在工业智能体项目里就遇到过这个,一个客服机器人聊了三天,摘要滚到 3000 Token,最后加了个“摘要不得超过 800 Token,超出则丢弃最不重要的细节”的约束才压住。

3.4 摘要与原始消息的拼接顺序

摘要生成后,怎么和保留的近期消息拼给模型?顺序很重要。正确顺序是:系统提示词 → 历史摘要 → 近期原文消息 → 当前用户输入。摘要要放在近期消息之前,并明确标注“以下是更早对话的摘要”,让模型知道这是背景而非当前上下文。

我见过有人把摘要放在最后,结果模型把摘要当成了“用户刚说的话”,回答完全跑偏。这个细节不注意,调试半天都找不到原因。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装

先把环境搭起来。LangChain 生态版本迭代快,建议用较新的稳定版,并且用虚拟环境隔离。

conda create -n summary-mw python=3.11 -y conda activate summary-mw pip install langchain langchain-core langgraph tiktoken

如果你要用特定模型,再装对应的 SDK。这里我用一个通用的 ChatModel 接口演示,你替换成自己的模型即可。LangChain 安装时如果遇到依赖冲突,优先保证langchain-core和langgraph版本匹配,这俩是核心。

4.2 定义对话状态与摘要中间件

用 LangGraph 定义状态,把消息列表和摘要都放进 State。

from typing import Annotated, TypedDict from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage, SystemMessage class ChatState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] summary: str # 累积的历史摘要

add_messages是 LangGraph 提供的 reducer,会自动把新消息追加到列表,不用手动管理。summary字段单独存摘要,和消息列表解耦,方便控制。

4.3 摘要触发与压缩的核心逻辑

这是整个中间件的心脏。逻辑是:每次准备调用模型前,先算 Token,超阈值就压缩。

import tiktoken from langchain_core.messages import HumanMessage, AIMessage def count_tokens(text: str, model: str = "gpt-4") -> int: enc = tiktoken.encoding_for_model(model) return len(enc.encode(text)) def maybe_summarize(state: ChatState, llm, threshold: int = 60000, keep_recent: int = 12) -> ChatState: messages = state["messages"] total = sum(count_tokens(m.content) for m in messages) if total < threshold: return state # 没超阈值,不动 # 保留最近 keep_recent 条,其余压缩 to_summarize = messages[:-keep_recent] recent = messages[-keep_recent:] history_text = "\n".join( f"{'用户' if isinstance(m, HumanMessage) else '助手'}: {m.content}" for m in to_summarize ) # 把旧摘要也纳入压缩范围 if state.get("summary"): history_text = f"【已有摘要】{state['summary']}\n\n【新增对话】\n{history_text}" prompt = SUMMARY_PROMPT.format(history=history_text) new_summary = llm.invoke([HumanMessage(content=prompt)]).content return {"messages": recent, "summary": new_summary}

这段代码有几个关键点。第一,to_summarize是除最近keep_recent条之外的所有消息,包括之前已经摘要过的部分——通过把旧摘要拼进history_text实现递归压缩。第二,返回时messages只留近期原文,摘要单独存,这样下次触发时不会重复压缩已经压过的内容。第三,阈值 60000 是针对 128K 窗口的保守值,你可以按自己模型调整。

4.4 接入 LangGraph 主流程

把摘要逻辑挂到图里,在调用模型前执行。

from langgraph.graph import StateGraph, END def build_graph(llm): graph = StateGraph(ChatState) def summarize_node(state): return maybe_summarize(state, llm) def chat_node(state): # 拼接:系统提示 + 摘要 + 近期消息 sys = SystemMessage(content="你是一个有长期记忆的助手。") context = [] if state.get("summary"): context.append(SystemMessage( content=f"以下是更早对话的摘要:\n{state['summary']}" )) context.extend(state["messages"]) resp = llm.invoke([sys] + context) return {"messages": [resp]} graph.add_node("summarize", summarize_node) graph.add_node("chat", chat_node) graph.set_entry_point("summarize") graph.add_edge("summarize", "chat") graph.add_edge("chat", END) return graph.compile()

流程是:每轮先过summarize节点判断要不要压缩,再进chat节点生成回复。chat_node里拼接顺序严格遵循“系统提示 → 摘要 → 近期消息”,这是前面强调过的关键细节。

4.5 参数调优的实测记录

我在一个客服场景里做过对比测试,模型窗口 128K,用户平均单轮输入 80 Token,助手回复 200 Token。测试结果:

配置触发阈值keep_recent平均每轮 Token断片率
无摘要--随轮数线性增长,50 轮后超窗高
激进压缩400006约 8000中(近期细节丢)
平衡配置6000012约 15000低
保守压缩8000020约 25000极低但成本高

最后选了平衡配置。激进压缩虽然省钱,但keep_recent=6时用户说“第二个方案”模型经常找不到指代,断片率反而上升。keep_recent=12是个甜点值。

5. 常见问题与排查技巧实录

5.1 摘要后模型“失忆”了关键数字

这是最高频的问题。原因通常是摘要提示词没强调保留数字,或者摘要模型本身能力弱。排查步骤:先把生成的摘要打印出来看,如果摘要里就没数字,那是提示词问题;如果摘要里有但模型回答时没用,那是拼接顺序或系统提示的问题。

解决办法:提示词里明确“所有数字必须原样保留”,并且摘要用能力较强的模型做,别用最便宜的那个。摘要是一次性成本,值得用好模型。

5.2 Token 计数和实际消耗对不上

常见原因是用了错误的 tokenizer,或者没算系统提示词和工具调用的 Token。排查时把每次请求的完整 payload 打出来,逐段算。另外注意,有些模型的计费 Token 和上下文 Token 口径不同,以官方文档为准。

5.3 摘要递归膨胀导致越压越大

前面提过,解决办法是给摘要设硬上限。在提示词里加“摘要总长度不得超过 800 Token”,并在代码里做二次校验,超了就再压一次。我一般会写个enforce_summary_limit函数兜底。

5.4 多轮工具调用时摘要把工具结果也压没了

Agent 场景里,工具返回的结果(比如查到的订单号、API 返回的 JSON)如果被摘要压掉,后续就没法用了。解决办法是对工具消息单独处理:要么不压缩工具消息,要么在摘要里明确保留“工具调用结论”。我在工业智能体项目里的做法是给工具结果打标记,摘要时优先保留带标记的内容。

5.5 常见问题速查表

现象可能原因排查方向解决
关键数字丢失摘要提示词未强调打印摘要内容提示词加“数字原样保留”
指代消解失败keep_recent 太小检查保留条数调到 10~12
摘要越滚越大递归膨胀看摘要 Token 趋势设摘要硬上限
Token 超窗报错阈值设太高核对窗口与阈值阈值降到 0.6
回答跑偏拼接顺序错检查消息顺序摘要放近期消息前
工具结果丢失工具消息被压检查摘要是否含工具结论工具消息单独保留

5.6 几个我踩过的坑

第一个坑是摘要和消息列表双写。早期我把摘要也塞回 messages 列表,结果下次压缩时摘要被当成普通消息又压一遍,信息层层衰减。后来改成摘要单独字段存储,才解决。

第二个坑是异步场景下的竞态。高并发时多个请求同时触发摘要,可能对同一段历史重复压缩。解决办法是加锁或把摘要做成独立的后台任务,不阻塞主对话流。

第三个坑是摘要语言漂移。用户用中文聊,摘要模型有时输出英文摘要,导致后续模型回答语言混乱。提示词里明确“用与对话相同的语言输出摘要”能解决。

6. 进阶玩法:让摘要中间件更聪明

6.1 分层摘要:短期、中期、长期记忆

单一摘要不够用的时候,可以做分层。近期对话保留原文(短期),中期对话压成详细摘要(中期),更早的压成一句话要点(长期)。这类似人脑的记忆分层,实现上就是维护多个摘要字段,按时间窗口滚动压缩。LangGraph 的状态图天然适合做这种分层,每个层级一个节点。

6.2 摘要 + 向量检索:需要时再召回

摘要会丢细节,但有些细节后面可能突然要用。进阶做法是把被压缩的原始对话存进向量库,摘要里保留“索引提示”,当用户提到相关话题时,用检索把原始细节召回。这就是 RAG 思路用在对话记忆上。基于 LangChain 的 RAG 流程做这个很顺,Chroma 或类似向量库都行。

6.3 摘要质量评估:怎么知道压得好不好

生产环境要有监控。我一般记录三个指标:摘要压缩比(压缩后 Token / 压缩前 Token)、关键实体保留率(摘要里是否含原始对话的关键名词和数字)、断片率(用户纠正“我之前说过”的频率)。断片率是最直接的业务指标,一旦上升就说明摘要策略要调。

6.4 和 human-in-the-loop 结合

有些关键决策不该被摘要模糊掉。用 LangGraph 做 human-in-the-loop 时,可以在摘要节点后加个人工确认环节,让运营人员审核摘要是否保留了关键信息。这在工业智能体、医疗咨询这类高要求场景很有必要。

7. 一些参数选择的经验值汇总

最后把我这些年攒下来的参数经验值整理一下,你可以直接拿去当起点,再按自己场景微调。

参数推荐值说明
触发阈值比例0.6~0.7占模型窗口比例
keep_recent10~12 条约 5~6 轮对话
摘要目标长度触发阈值的 30%~50%压缩后大小
摘要硬上限800~1500 Token防递归膨胀
摘要模型中等能力以上别用最便宜的
摘要语言跟随对话语言提示词明确

这套配置我在几个项目里跑下来,长会话断片率能压到很低,Token 成本相比全量保留能省 60% 以上。当然具体数字因场景而异,关键是理解每个参数背后的权衡,而不是照抄。

摘要中间件这东西,说穿了就是“用一次性的摘要成本,换长期的上下文稳定”。它不复杂,但细节多,尤其是提示词、拼接顺序、递归压缩这几个点,不注意就会踩坑。我个人的体会是,先把基础版跑通,再根据实际断片情况针对性调参,比一上来就搞分层、搞向量召回要务实得多。等你把单层摘要玩明白了,再往上叠进阶玩法,心里才有底。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 7:43:28

MAC地址批量提取与白名单登记实战:告别手工抄录

做了这么多年网络运维&#xff0c;最烦的活儿之一就是给全网的机器做MAC地址登记。台式机、笔记本、一体机、瘦客户机、考试机、收银机&#xff0c;少则几十台&#xff0c;多则几百台&#xff0c;以前都是拿着设备管理器一台台抄&#xff0c;抄完再手工填Excel&#xff0c;第二…

作者头像 李华
网站建设 2026/9/26 7:42:18

QT6 PDF阅读器开发:标签页码定位与关键字搜索实战

简介&#xff1a;这是一份基于QT6框架开发的PDF阅读器完整工程源码&#xff0c;面向具备一定C与Qt基础的开发者&#xff0c;尤其适合需要实现文档阅读、内容检索与页面定位功能的学习者参考。项目在基础阅读能力之上&#xff0c;重点实现了标签与页码双维度定位&#xff0c;以及…

作者头像 李华
网站建设 2026/9/26 7:41:59

ResNet50特征提取+逻辑回归:快速构建猫狗分类基线

简介&#xff1a;这是一份面向深度学习入门与计算机视觉实践者的完整案例源码&#xff0c;围绕ResNet50特征提取与逻辑回归分类展开&#xff0c;帮助读者理解如何将预训练卷积网络与传统机器学习方法结合&#xff0c;解决猫狗二分类这一经典问题。压缩包共43个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/26 7:41:28

数据库内存省一半?NVMatrix块存储EBS实战解析

内存价格这一轮涨得实在离谱&#xff0c;DDR4 从底部翻倍都不止&#xff0c;DDR5 更是让人不敢直视。做数据库运维的同学应该都体会过那种痛&#xff1a;业务说慢&#xff0c;开发说加内存&#xff0c;领导说看预算。一台 512G 内存的数据库服务器&#xff0c;光内存成本就能顶…

作者头像 李华
网站建设 2026/9/26 7:38:11

YOLO目标检测与云台伺服控制的工业级闭环实现

简介&#xff1a;本资源是一套基于YOLO的智能追踪云台完整实现方案&#xff0c;面向深度学习初学者、毕业设计与课程设计学生&#xff0c;解决实时目标检测与物理云台协同控制这一典型AI硬件落地问题。项目融合YOLOv8目标检测&#xff08;含训练好的yolov8n.pt模型&#xff09;…

作者头像 李华
网站建设 2026/9/26 7:38:07

别再盲目学Python了,这3个坑千万别踩

坑一&#xff1a;把语法当终点&#xff0c;从不写完整项目变量、循环、函数、类&#xff0c;这些语法半个月就能过一遍。很多人学完这些就觉得自己会Python了&#xff0c;然后开始刷面试题&#xff0c;背八股文。可一到实际项目&#xff0c;连一个文件读取加数据清洗都写不出来…

作者头像 李华