对话压缩 | 上下文与 RAG | 做产品
对话压缩
对话会一直变长,窗口却是有限的。所以任何长对话产品,迟早都要处理同一个问题:装不下的时候,扔掉什么。
你会遇到的现象
-
长对话进行到一半,它突然忘了开头定好的规矩
-
它把之前讨论过的一个具体数字记串了
-
你想复现某个问题,但不知道当时上下文里到底还剩什么
为什么要压
两个理由,第二个更常被低估。
**一是装不下。**窗口有硬上限,超了就得处理。
**二是贵。**这一条才是多数产品真正的驱动力。整段上下文每一轮都要重发、重新计费——第十轮的那句话可能只有十个字,但那次调用的输入是前面所有内容的总和。对话长度和累计成本的关系接近平方级,聊得越久涨得越凶。
Agent 场景尤其明显。每一轮工具调用的结果都堆进上下文,跑几十轮下来,光是历史就能把窗口撑满。
三种做法
**① 直接截断。**丢掉最早的几轮,保留最近的。
实现最简单,很多框架的默认行为就是这个。问题也最直接:最早那几轮往往包含最重要的东西——任务的原始描述、用户给的背景、双方确认过的约束。丢了之后模型就开始漂移。
**② 摘要替换。**把前面 N 轮交给模型总结成一小段,用摘要替换原文。
比截断好,主线能保住。但摘要天然是有损的:具体数字、精确措辞、当时排除掉的选项,很容易在总结中蒸发。而且摘要本身也是模型生成的,它可能总结错——一旦错了,后面全部基于错误前提往下走。
**③ 分层保留。**这是比较稳的做法:把内容按重要性分类处理。
硬约束原文照留——用户明确定下的规矩、关键参数、不能改的决定,一个字都不动地保留。中间过程做摘要——讨论、试错、被否决的方案,压成一段。最近几轮保留原文——最新的上下文最需要精确。
多做一点工,但稳定性差别很大。核心思路是:该记死的记死,该模糊的才模糊。
丢掉的通常是什么
这一节最该记住的一句话:压缩最常丢掉的,正是早期定下的约束。
原因很实际。约束通常在对话开头确立——「用中文」「不要超过三段」「必须基于我给的材料」。它们说完就过去了,后面不再重复。于是在摘要里,它们看起来不像「主要内容」,很容易被总结掉。
结果就是那个典型症状:**聊到中途它开始违反一开始说好的规矩。**用户会觉得「它变笨了」,实际上是那条规矩已经不在上下文里了。
第二常丢的是精确的数字和标识:订单号、金额、版本号、人名。摘要倾向于概括——「讨论了几个订单的退款问题」,具体是哪几个就没了。
第三是否定信息:「我们决定不做 A 方案」。摘要容易简化成「讨论了 A 方案」,后面模型可能就把 A 又捡回来了。
摘要是有损的,而且丢的东西很有规律
压缩前
「必须基于我给的材料」 硬约束
订单 A1024,退款 ¥328 精确数字
决定「不」做 A 方案 否定信息
下面是一大堆讨论和试错
摘要之后
整条消失了
「讨论了几个订单的退款问题」
「讨论了 A 方案」 「不」字没了 —— 后面模型很可能把 A 又捡回来 约束不在上下文里了 —— 用户只会觉得「它变笨了」
约束单独存,不进压缩流程 抽到一个固定区域,永远不压缩
压缩要打点 没有日志,线上问题基本没法复现
约束通常在对话开头确立,说完就过去了,后面不再重复——所以在摘要眼里它们「不像主要内容」,最先被总结掉。压缩是不得已的补救,不是默认的好设计:一个明确的「开始新任务」入口往往更有效。
四条实践
**一、约束单独存,不进压缩流程。**把用户确立的硬约束抽出来,放在一个固定区域(通常拼在系统提示后面),永远不压缩。这一条能解决掉大部分漂移问题。
**二、压缩要打点。**什么时候压的、压掉了多少轮、摘要内容是什么,全部记下来。没有这个日志,线上问题基本没法复现——你连当时上下文里有什么都不知道。
**三、考虑让用户可见。**如果压缩会明显影响体验,给个提示(「较早的对话已归纳」)比让用户莫名其妙地发现它失忆要好。有些产品还允许用户把关键信息「置顶」,本质上就是手动加入不压缩区。
**四、先问要不要这么长的对话。**这条最容易被跳过。很多场景其实不需要无限延续的会话——一个明确的「开始新任务」入口,比任何压缩策略都有效,而且成本更低、行为更可预测。压缩是不得已的补救,不是默认的好设计。
长期记忆的实现方式 该跨会话记住的东西,不该靠压缩来保。
上下文中段丢失 就算没被压掉,埋在中间的内容也未必被用上。
上下文窗口 先搞清楚台面有多大,再谈怎么分配。
