全栈开发知识库

13. 上下文压缩

上下文放不下了怎么办?

  • 截断,那前面的上下文信息没有了怎么办?
  • 摘要,把旧消息压缩成一段总结塞回去,但你要是把二十轮全压成一段话,用户第三轮说的"预算五万"和第十轮说的"我随口一提五万可能不太够",在你摘要里变成同一句话。到底五万是硬约束还是随口一说?你的 Agent 不知道。

这道题的本质不是怎么省token,而是怎么在有限窗口里保住决策所需的最小信息集。

截断是物理删除,摘要是信息压缩,但真正要解决的问题是 --- 哪些信息必须以什么形态、什么精度活下来。

答案

上下文窗口管理不是满了就砍,而是一套信息蒸馏 + 分层卸载 + 选择性保留的工程系统。

简单截断会丢失关键约束,粗暴摘要会抹平信息精度差异。

正确的做法是把上下文分成:

  • 绝对不能丢的
  • 可以压缩但要保持精度的
  • 可以卸载到外部存储按需找回的

通过这三层,分别用不同策略处理。

一句话总结: 上下文管理不是垃圾桶满了倒垃圾,是搬家时决定哪些东西随身带、哪些装箱、哪些扔掉。

2023 年 Stanford 那篇Lost in the Middle(Liu et al.)已经证明了一件事:LLM 对上下文中间位置的信息关注度最低,头尾关注度高。这意味着就算你不截断,把二十轮全塞进去,模型对第八轮说的"预算五万"的注意力本来就已经很弱了。

粗暴摘要的代价:你把一段对话压成一句话的时候,不是在做无损压缩,你是在做有损压缩,而且你控制不了损失的是哪部分。

上下文压缩的本质: 不是删,是蒸馏

上下文管理的本质是什么?

是在有限窗口里,保住当前决策所需的最小信息集。

注意三个关键词:当前、决策、最小。

不是"保住所有信息"——窗口放不下。不是"保住最新信息"——旧约束可能比新闲聊重要一万倍。是保住此刻做决策真正需要的那些东西。

工程上要怎么做? 四种策略叠加用

第一种:滑动窗口 + 锚定

最基础的一招。窗口只保留最近 N 轮对话,但把关键锚点钉死在窗口头部,不参与滑动。

什么是锚点?就是那些"丢了就完蛋"的信息。用户的硬约束("花生过敏""预算五万")、当前任务的目标描述、系统级指令。

这些东西不管对话滚到第几轮,都钉在上下文最前面。后面的对话可以滑走,锚点不动。

MemGPT(2023,UC Berkeley,Packer et al.)的核心思路就是这个:把上下文窗口当成操作系统的 RAM,把核心指令和用户画像钉在 RAM 的固定区域,其他信息按需 page in / page out。不是所有信息都平等地占窗口空间,有些信息是"常驻进程",有些是"临时线程"。

第二种:递归摘要

不是把二十轮压成一段话,而是分层递进地压。

第一轮压缩:把最早的 5 轮压成一段摘要,保留关键实体和约束。 第二轮压缩:等这段摘要也老了,再把它和接下来的 5 轮合并,压成更高层的摘要。 以此类推。

每一层压缩都保留一个"精度标签":这条信息是硬约束、是偏好、是临时状态、还是闲聊。压缩的时候,硬约束必须原样保留,临时状态可以模糊化,闲聊可以直接丢。

LangChain 里的ConversationSummaryBufferMemory就是干这个的:最近的对话保留原文,更早的对话递归压缩成摘要,两部分拼接后注入上下文。

但注意,这里有个坑:摘要本身也会占 token。如果你的摘要写得比原文还长,那压缩了个寂寞。所以摘要必须有长度约束,而且要定期再压缩。

第三种:分层卸载到外部记忆

窗口实在放不下的,别硬塞,卸载到外部存储。

这就是长期记忆的用武之地。上下文窗口是 RAM,外部记忆库是磁盘。当前任务用不到的历史信息,从 RAM 卸载到磁盘。需要的时候再通过检索召回。

但这里有个特别容易踩的坑,小林在 MewCode 项目文档里专门提过:刚注入的长期记忆,下一步就被上下文压缩器给压掉了。

为什么?因为压缩器不知道这条信息是刚从外部记忆召回的、当前任务正在用的,还是很久以前的旧对话。它一视同仁地压。

解法是给每条信息打一个"不可压缩"标记,或者在压缩策略里排除最近 N 轮内刚注入的外部记忆。

第四种:选择性保留 + 重要性评分

最精细的一招。不是按时间顺序决定谁留谁走,而是给每条消息打一个重要性分数。

怎么打?三个维度:

  • 相关性:这条信息跟当前任务有多相关?
  • 约束强度:是硬约束("必须""不能")还是软偏好("最好""尽量")?
  • 时效性:这条信息现在还有效吗?还是已经被后续对话更新了?

分数低的先压缩或卸载,分数高的保留原文。

Anthropic 在 Claude 的系统提示词里就用了类似思路:它要求模型在动手之前先读 SKILL.md,因为"你的训练数据可能已经过时"。这本质上就是在说:不要相信你的记忆,去查最新的、最相关的信息源。上下文管理也是同一个逻辑——不要相信"所有历史都同等重要",去判断此刻真正需要的是什么。

压缩完之后,怎么注入回去?

好,压缩做完了,摘要生成了,外部记忆也卸载了。下一步是什么?

注入。

这一步很多人忽略,但它直接决定了压缩是"有效瘦身"还是"信息黑洞"。

注入不是把摘要往上下文开头一扔就完事。你得考虑三个问题:

第一,注入的位置。

摘要放在哪?放在系统指令后面、用户最新消息前面?还是穿插在对话流里?

Lost in the Middle那篇论文告诉你:模型对上下文中间部分的注意力最弱。所以关键摘要别放中间,放头部或尾部。

第二,注入的格式。

别把摘要写成一段自然语言散文。写成结构化的、带标签的格式:

[用户约束] 花生过敏(硬约束);预算上限五万(当前有效) [任务状态] 技术方案已对齐,待实施 [历史要点] 用户偏好简洁风格;项目使用Postgres 结构化格式比自然语言段落更容易被模型准确解析。你给它一段话,它可能漏掉里面的关键约束;你给它一个带标签的列表,它漏的概率小得多。

第三,注入的时机。

不是每轮都重新压缩重新注入。设一个阈值,比如上下文超过窗口的 70% 时触发一次压缩,压缩完注入,然后继续正常对话。别每轮都压,那跟每轮都搬家一样,累死。

Claude Code 的/compact命令就是这个思路:你手动触发一次压缩,它把当前对话历史压缩成一段摘要,替换掉原始消息,释放窗口空间。但注意,它是你主动触发的,不是自动的。因为在工程里,什么时候压、压多少,是一个需要权衡的决策,不能完全交给自动策略。

"上下文管理的目标不是让窗口永远不爆,是让窗口里活下来的每一条信息,都配得上占这个位置。"

本页由 Markdown 自动渲染 · 共 628 篇笔记