六大Agent上下文压缩策略对比,第七个方案实测推荐

2026-06-13阅读 0热度 0
Claude

前言

上下文压缩这事儿,现在差不多是每个Agent的必修课了。但怎么压,各家方案差得可不止一星半点。最近花了不少时间把主流方案看清了一遍,发现背后都是不同的设计哲学在打架。

这篇文章就三块内容:先横向看看几个主流Agent的具体做法和设计考量;再从中提炼几条接近共识的原则;最后聊聊我们在MUR AI上最终落地的方案——四级水平线加增量摘要,以及云端多用户场景下额外需要处理的几层设计。

MUR AI是一个面向用研场景的云端多用户Agent。跑在云端、服务多人,这两个特点让压缩方案比本地CLI工具多了几层麻烦事儿。

1. 同一个问题,六种做法

先看一个快速对照表,感受一下差异有多大:

产品 核心策略 一句话概括
Claude Code 五段流水线,按成本递增排列 便宜的本地操作先上,LLM摘要兜底
Codex CLI 保留近期用户消息原文,其余全部替换为handoff摘要 用户说的话最准确,模型说的可以重写
OpenCode 时间戳标记隐藏 + 结构化摘要 + 回放最后一条用户消息 不真删,理论上可恢复
Cline /smol ( /compact )生成摘要后在同一任务内接续 自动 + 手动双模式
Cursor 自动摘要 + 提示开新对话 + 历史可搜索 压缩后仍能回溯原始历史
Amp 不做递归压缩,用/handoff开新线程携带要点 长对话本身就是问题,换线程比压缩好
MemGPT / Letta 上下文 = RAM,历史 = 磁盘,Agent自主换入换出 操作系统级的内存调度

六家产品,六种哲学。说明这事儿没有标准答案,每一种选择背后都是取舍。

2. 反面教材:第一代"撑不住才动手"

想知道新方案为什么长这样,先得看清它们想甩掉什么。第一代压缩的逻辑一句话说透:

直观,好实现。但用过的人都知道体验有多糟糕——下面五条是痛点的根源。你会发现,新方案各自的设计选择,本质上就是重点解决其中某一两条。

  • 悬崖式触发。不溢出时一动不动,对话越来越胖,模型注意力越来越散。一旦溢出就全量出手,把前面几十轮一口气捏成一段摘要。这种"要么零要么全",意味着触发的那一刻质量已经塌了——模型刚因为信息过载走神,紧接着又被剥夺大半上下文。
  • 全量摘要丢细节。几十轮消息压成几百字,无论prompt写得多好,变量名、函数签名、错误堆栈、用户的具体措辞都会被丢掉——偏偏这些是Agent继续干活最需要的东西。
  • Token估算粗糙。不少实现用text.length / 3估算token。中英混合场景下误差能到30-50%,以为安全实际已经溢出,或者以为该压了实际还早。
  • 不区分信息价值。5000行grep输出和5000token的关键诊断被同等对待——前者裁了不影响任务,后者裁了Agent立刻不会干活。
  • 用户内容被一刀切。用户贴的代码代表输入意图,和工具输出性质不同。但如果一视同仁地压,就会出现"用户贴的代码被压没了,模型忘了要改什么"。

第一代做法的根本问题:它把压缩当突发事件处理,而不是一种需要持续维护的能力。

3. 第二代:各家的做法

过去一年,几个主流Agent不约而同走向"分层 + 渐进",但味道各不相同。逐个拆来看看。

3.0 Claude Code(Anthropic):五段流水线 + 结构化摘要

Claude Code把上下文管理做成了一条严格按成本递增排列的流水线:

  1. Budget Reduction——调整工具输出的截断预算
  2. Snip——截短老的工具输出,留"做过什么"的摘要行
  3. Microcompact——对工具输出内容做局部内联压缩
  4. Context Collapse——对更久远的历史做粒度更细的折叠
  5. Auto-Compact——兜底,调LLM生成结构化摘要

前四步都是纯本地操作,零API调用。只有第五步才会请求LLM。摘要本身是结构化的,固定包含九个章节:用户意图、主要请求、技术概念、文件与代码段、错误与修复、问题解决过程、用户消息、待办任务、下一步。

有个值得注意的细节:它在压缩时刻意保持消息序列前缀稳定,让Prompt Cache命中率不会因为压缩而掉下来。

Claude Code内部其实还有两条更激进的路径,思路都是"把脏活交给服务端,客户端一个字节都别改":

  • cached_microcompact:把"删掉旧tool结果"包装成API层的cache_edits指令。客户端发出去的prompt原封不动,服务端在已缓存的前缀上直接抠掉指定内容——字节没变,缓存不失效。
  • apiMicrocompact:更彻底,直接调Anthropic的context_management API(beta context-management-2025-06-27,Vertex / Bedrock也支持),让服务端按input_tokens阈值自动裁剪旧的工具调用。

换句话说,前面那条本地五段流水线在很多场景下已经退居二线了。能让服务端做的就让服务端做,本地操作永远是兜底。

3.1 Codex CLI(OpenAI):近期用户消息优先保护

Codex的策略相对简单——在约95%容量时触发,生成一份handoff摘要替换掉旧历史。重建后的上下文只剩三部分:

所有assistant回复和工具结果被物理删除,由摘要替代。近期约20k token内的用户消息原样保留,更早的用户消息则被蒸馏进摘要里——关键请求和约束会被保留下来。

设计哲学是把压缩当作"同事间的工作交接":进展、约束、剩余任务,足够下一个模型接手继续干。

3.2 OpenCode:可逆隐藏 + 回放最后一条指令

OpenCode的做法很有意思,分两步走:

第一步Prune(轻量,无LLM调用):每次成功响应后自动触发。往回遍历消息,跳过最近2轮用户对话,保护最近40k token的工具输出不动,把更老的工具输出用时间戳标记为"已压缩"。关键是——数据还在数据库里,只是模型看到的变成了占位符[Old tool result content cleared]

第二步Summary(重量,调LLM):只在token用量超过模型输入上限时触发。生成一份五段式结构化摘要(目标 / 指令 / 发现 / 已完成 / 相关文件),然后自动回放用户最后一条消息——模型不会从摘要继续,而是从你最近的指令继续。

值得一提的是,它只有一份摘要同时服务于模型和界面展示,并没有做"给模型看的详细版"和"给用户看的精简版"的分离。

3.3 Cline:自动 + 手动双模式

Cline从v3.25开始有两种压缩模式:

  • /smol(别名/compact):手动触发,生成摘要后在同一任务内接续。决策、代码变更、状态都保留在摘要里,不用切新会话。
  • Auto-Compact:接近上下文上限时自动触发,行为和/smol一致。

如果开启了Focus Chain(v3.25默认开启),待办列表会穿越压缩存活下来,作为进度锚点。

3.4 Cursor:压缩 + 可回溯

Cursor在上下文超出模型窗口时自动压缩旧消息,同时会提示用户"开一个带摘要的新对话"。

2026年他们加了个有意思的能力——Dynamic Context Discovery:把聊天历史变成可搜索的文件,即使压缩后Agent也能回头检索原始细节。据说在A/B测试里减少了46.9%的总token消耗。

不过社区反馈里有个已知问题:压缩后模型有时会"忘掉"刚才的编辑,Cursor团队确认这是高优bug在修。

3.5 Amp(Sourcegraph):不压缩,换线程

Amp的立场很鲜明:递归摘要会导致性能逐步衰减(他们引用了OpenAI的一份内部研究),所以干脆不做。替代方案是/handoff——把当前线程的要点打包进一个新线程,用户可以在交接前审查和编辑带过去的内容。

线程在Amp里是一等公民:可以用@@引用其他线程、用threads: map可视化线程关系。理念是"一系列有焦点的短步骤,比一个逐渐退化的长对话好"。

不过2026年的Neo CLI更新里,Amp也加了90%窗口用量时的自动上下文管理——算是对纯手动路线的一个妥协。

3.6 MemGPT / Letta:上下文当RAM

学术派的代表。直接按操作系统的内存层次来建模:

层级 类比 容量 访问方式
Main Context RAM 模型上下文窗口大小 始终在prompt里
Recall Memory 交换分区 完整对话历史 conversation_search
Archival Memory 磁盘 无限(向量存储) archival_memory_search

关键区别是:换入换出由Agent自己决定(通过函数调用),不是被动截断。Letta是MemGPT的生产化框架,最新版本v0.16.7(2026年3月),GitHub 22.5k star,还在积极维护。

代价是架构复杂度高、需要外部向量存储、有检索延迟。适合需要跨会话长期记忆的场景,但对单会话内的压缩来说有点重。

3.x 第二代的实施陷阱:滑窗式stub替换 = 每步缓存失效

2026-05-13 补

第二代"分层渐进"如果实施得不对,会掉进另一个隐蔽的坑。

想象这样一种实现:你设了一个规则——"保留最近N条tool结果,更老的替换成stub"。听起来很合理对吧?问题在于如果这个判断在step-loop里每一步都重算,那么每完成一个step(新增2条消息),就有1条原本被保留的旧tool结果滑出窗口、被替换成stub。它的字节变了,从这个位置往后的整段prompt前缀对Prompt Cache就失效了,需要重新写入。

把压缩塞进每个step里跑、还用滑窗——结果就是每个step缓存前缀都会失效一次。

一个真实的Task:一个4轮、177 step、59分钟的会话,烧了$77.3,其中83%($64.8)全是cache_write。要知道cache_write的单价是cache_read的12.5倍。通过Langfuse中的step数据,一目了然:

stepnMsgs #stub lastStubIdx cacheReadcacheWrite
5 188 75165 175958 295← 命中
6 190 75165 176253 232← 命中
7 192 76167 10279574555← 炸了
8 194 77169 10279577238← 又炸
9 196 78171 10279577634← 还炸
...
58 294101273 102795117088← 一直炸

看规律:#stub每step加1,lastStubIdx每step加2,cacheRead永远卡在一个小数不动,cacheWrite一路涨到约12万/step。从step 7起前缀缓存就死了,剩下50多个step每一步都在为"窗口又挪了一格"买单。

stub决策必须单调推进——只能大跳,不能滑窗。一个part一旦被标成stub,后续所有turn、所有step里都保持stub不变,绝不因为"又老了一步"而反复触发。上面提到的cache_editsAPI(Claude Code走的那条路)天然满足这个性质——客户端字节不改,决策在服务端生效,缓存前缀永远稳定。

4. 从实践里提炼出的共识

方案放在一起看,细节差异很大,但有几条原则几乎人人认同:

  • 分层渐进,不一刀切。定义多个水平线,越接近上限手段越激进。系统永远在小幅维护,避免悬崖式塌方。
  • 成本严格递增。便宜的先做(字符串截断、placeholder替换),贵的最后做(LLM摘要)。能用零成本释放的空间,不花钱买。
  • 增量摘要优于全量摘要。老做法每次重新摘要全部历史,新做法保留一份活的摘要,每次只把新增部分合并进去。好处:单次输入更短更便宜也更准;同一段历史不会被反复重写,避免"摘要的摘要"导致语义漂移;合并时模型可以主动取舍。
  • 用真实token,别估算。LLM API每次都返回精确的usage.totalTokens,免费、精确、唯一可信。估算只在内部排序时凑合用,触发判断必须用真实值。
  • 用户消息有特权。用户的指令、问题、代码——这些是任务来源。Codex做到一字不动,OpenCode做到压缩后回放最后一条,其他方案至少保证用户纯文本不裁。
  • 保护近端。无论怎么压,最近几轮不能动。模型短期连贯性几乎全靠这几轮维持。常见做法是定义保护区——比如最近8000 token内的所有消息,任何级别都不参与压缩。
  • 单调边界,绝不滑窗。stub决策一旦做出,对应位置的字节就必须从此固定——下一turn、下一step再看到同一位置,永远是同一个stub,绝不因为"又老了一步"而重新触发替换。

5. 我们的方案:四级水平线

把上面的原则落地,我们选了四级水平线。理解方式很简单——想象一台电脑的内存压力监视器:

四个Tier不是互斥而是累积——Tier 3触发时会先做完Tier 1和Tier 2再做摘要。这意味着即使最坏情况,需要送给LLM的内容量也已经被前两步免费砍掉一大块。

Tier 0:什么都不做(< 60%)

上下文宽裕,模型注意力没散,最好的优化是不优化。

Tier 1:Snip——便宜的整理(60-80%)

到了60%开始预防性维护,没有LLM调用,纯字符串处理:

  • 截短老的工具输出。一次grep返回5000 token?保留前几行 + 工具名 + "还有X条结果被省略",剩下丢掉。
  • 截短用户消息里的代码块。200行代码保留文件名注释 + 前几行 + 总行数标注。

几个细节:保护区内的工具输出和代码块不动;某些工具享有豁免(比如Skill、Task这种返回结构化关键信息的);用户的纯文本指令永远不动,只压缩markdown代码块。

这一级成本是零,但能挡住相当一部分增长。

Tier 2:Prune——更狠的释压(80-95%)

预防性维护不够了:

  • Tier 1已经截短的工具输出进一步替换成占位符([Content compacted to sa ve space]
  • 裁掉assistant旧文本——保留前两句 +[truncated]
  • 截断阈值整体下调,能压的全压

依然零LLM成本,依然不动保护区,依然不动用户纯文本。

Tier 3:Summarize——兜底(≥ 95%)

只有Tier 1 + Tier 2都救不回来时才触发LLM摘要。做的是增量摘要:

  1. 找出"上次摘要之后 ~ 保护区之前"的消息作为delta
  2. LLM输入:上次摘要 + delta → 生成合并摘要
  3. 替换旧摘要,删除delta消息
  4. 保护区不动

第一次触发时"上次摘要"为空,相当于普通摘要;之后每次都是追加合并,避免反复重写历史导致语义漂移。

摘要prompt用结构化输出,四段:进展 / 文件 / 待办 / 上下文(用户偏好、错误、约束)。

6. 云端Agent还要多做三层

前面四级水平线解决的是"上下文里压什么、压多狠"。但MUR AI跑在云端、服务多用户——这意味着得处理几件CLI工具可以无视的事:

  • 用户关掉浏览器再回来,压缩状态不能丢
  • Pod重启、流量漂移,跨进程的压缩决策必须一致
  • 工具完整日志要支持事后审计和前端回取,不能为了省context就把日志丢了
  • sandbox里的工具输出动辄几十MB,完整性和模型注意力不能二选一

这些靠水平线解决不了。所以在四级水平线之上,我们叠了三层云端特化设计。

6.1 存储分离:完整日志落盘,对话里只留截断版

每次工具调用——bash、read、grep——都可能吐出几万token。直接塞进对话历史不行,全丢掉又损失调试和审计能力。

我们的做法:完整输出落盘,对话里只保留截断版 + 一条回取路径。

具体来说:engine层调用persistTruncatedOutput把完整内容写到沙箱的_internal/truncated-outputs/{callId}.log;沙箱写失败就降级到COS直传。截断版的metadata带上fullLogPath,模型看到的是"前几行内容 + [截断] + 完整日志路径"——它知道完整内容在哪,但没花token去读。前端展示时按需调sandbox-file API现取现读,完全绕开context约束。

本质上是把"模型的工作记忆"和"用户的审计需求"解耦了。CLI工具不用管这个——它们没有前端,工具输出要么留在context里,要么就没了。

6.2 工具差异化:不是所有工具都该同等对待

第5节提过"某些工具有豁免权",这里把完整分类讲清楚。我们把工具分四个梯度:

  • 完全保护PROTECTED_TOOLS = ['Skill', 'Task']):Skill关联知识库绑定,Task是会话级元任务。输出有状态意义,删了Agent就糊涂了。任何Tier都不动。
  • 微压缩豁免(Task / AskUserQuestion):被动小幅压缩也跳过。AskUserQuestion的输出是用户的回答,删了等于把用户回话抹掉。
  • 白名单内可压(bash / read / grep / websearch …):无状态读取类工具,压缩的主力。
  • 差异化存储预算:单次输出落盘上限按工具区分——Read 30KB、Bash 50KB、WebSearch 15KB。Bash经常吐大段构建日志,Read相对可控,WebSearch通常只要几条结果就够。

这里的考虑很直接:一段grep输出和一次Skill调用的信息密度不在一个量级,用统一阈值处理就是粗暴。

6.3 跨轮缓存:让压缩决策在重启后还算数

这是云端特有的问题。

场景:用户聊到第20轮,系统在第8轮、第14轮分别做过snip,截短了某些grep输出。然后实例重启了,或者下一个请求被路由到另一个Pod。第21轮触发新一轮压缩——如果什么都不记得,新进程会从头重新判断每个part该怎么压,结果可能跟之前不一样。

这会带来两个问题:

  • Prompt Cache全废——消息序列前缀变了,缓存命中率掉到零,每一轮都按新prompt计费
  • 模型困惑——同一段历史在不同轮次里"长得不一样",模型可能重新触发已经处理过的工具调用,甚至开始鬼打墙

我们的解法叫ReplacementCache——把每一轮的截断决策按part ID存进Redis(key形如msgOptCache:{sessionId},TTL 30分钟)。下一轮无论是同进程还是另一个Pod,先查缓存。已经决定过怎么压的part直接复用之前的结果,没决定过的才走新策略。

效果:同一个part在整个会话里始终长一样,消息前缀稳定,Prompt Cache友好;跨实例、跨重启无感;从缓存读出的数据先过isValidReplacementEntry()校验,损坏的直接丢弃重算。

6.4 多用户隔离

最后一层最朴素但绝对不能省。所有压缩状态按(userId, sessionId)二元组隔离:

  • 数据库写入强制带双条件WHERE userId = ? AND sessionId = ?,杜绝越权读写
  • COS路径形如user_sessions/{userId}/sessions/{sessionId}/_internal/...,天然按用户分区
  • SSE压缩事件按sessionId严格过滤订阅,一个用户收不到另一个用户的压缩通知

多租户场景下,"压错给谁"比"压错什么"严重得多。这一层没有为了性能而省的空间。

这四层——存储分离、工具差异化、跨轮缓存、多用户隔离——是CLI工具都不必处理的。它们是单机工具,重启意味着开新会话,压缩状态丢了就丢了。但云端Agent不一样:用户合上电脑去吃饭、明天回来,期待的是接着上次继续,而不是"对不起请重新介绍一下需求"。

7. 几个关键决策背后的原因

设计过程中有几个看似细节但影响很大的选择。

为什么阈值是60/80/95?

60%是预防性维护的甜点——再低会频繁触发没意义,再高模型注意力已经开始下降。80%是危险线,离溢出还有缓冲但已经不能等。95%是最后防线,再不调LLM就要爆了。

这三个值都是可配置的常量,接入远程配置后能热更新。出问题时把任意一个调到1.0就能禁用对应Tier。

为什么先Snip再Prune?

Snip保留更多元信息。一个被snip的工具结果还能让模型看到"我之前grep过这个文件";被prune替换成占位符后连这个都看不到了。Snip是轻量记号,Prune是彻底擦除——能用前者解决的就别上后者。

为什么要增量摘要?

打个比方:

  • 全量摘要 = 每周把过去三个月的工作重新写一份周报
  • 增量摘要 = 维护一份持续更新的项目状态,每周只追加和修订变化的部分

哪种更准更便宜?不言自明。增量摘要还有个隐藏好处:同一个文件被多次提及时,最新状态覆盖旧状态;全量摘要里这个文件可能被描述好几次而且互相矛盾。

为什么用真实token?

一开始也用text.length / 3估算。直到有一次发现估算值显示70%,实际LLM返回的usage已经92%——再来一轮直接溢出。原因是中文字符在BPE里通常占1.5-2 token,英文代码大量短token,混合内容的实际token数比字符估算高30-50%。

LLM API每次都返回精确的usage.totalTokens——免费、精确、唯一可信,没理由不用。但内部排序("先裁哪个工具输出")继续用估算就够了,需要的是相对大小不是绝对值,30%误差不影响排序。

为什么保留compactionProtected标记?

预留扩展。某些Part可能很重要但看起来很普通——用户上传的设计文档、关键错误堆栈、标记为"记住这个"的指令。给它们打标记,任何Tier都跳过。能力先建好,需要时直接用。

8. 红线:什么东西任何Tier都不能动

把红线列清楚很重要——压缩系统最大的事故不是压不够,而是压错东西。

内容 原因
保护区内的所有消息 模型短期连贯性的命脉
用户消息的纯文本部分 用户意图就是任务来源
PROTECTED_TOOLS的输出(Skill / Task) 高度结构化的关键信息或会话级状态
MICRO_COMPACTION_EXEMPT的工具(Task / AskUserQuestion) 保住对话流的关键节点
带compactionProtected标记的Part 业务侧明确指定的"必须保留"

这些规则在每一级压缩里严格执行。不存在"为了救场破例"的情况。如果保护区都救不下来,那就让上下文真的爆出来——比错误压缩造成模型行为漂移要好。

9. 可观测性:让压缩被看见

压缩发生在背后,没有可观测性的话调起来全靠玄学。在SSE事件里加了这些信息:

  • 当前触发的tier(0/1/2/3)
  • 当时的token使用率(来自LLM真实usage)
  • Snip截了几个part
  • Prune替换了几个part
  • Summarize是否真的调用了
  • 预估节省了多少token
  • 命中ReplacementCache的part数(衡量跨轮一致性收益)

前端可以据此渲染一个压缩面板:让用户看到"这一轮触发了Tier 1,节省了3000 token,其中6个part直接复用了上一轮决策"。这些数据接入trace系统后,可以基于真实生产数据调水平线,不再拍脑袋。

10. 我们没做的事

为了避免过度工程,有几条路暂时没走,但保留了可能性:

  • 主动cache-aware调度(Claude Code的做法):压缩时主动调整顺序来最大化Prompt Cache命中。目前用ReplacementCache实现了被动一致性,还没做按cache边界主动选择压谁。等cache命中率成为瓶颈再上。
  • 可逆隐藏(OpenCode的做法):用时间戳标记而非真删。等用户需要"回滚到某次压缩之前"时再上——目前_internal落盘已经满足审计需求。
  • 回放最后一条用户消息(OpenCode的做法):摘要后重新追加用户最近指令。Tier 3触发频率本来就低,优先级不高。
  • 用户消息一字不动(Codex的做法):已经不动用户纯文本,但还会截断用户贴的代码块。是否进一步走极端,看后续生产数据。
  • 分层长期记忆(MemGPT / Letta / Mem0):跨会话记忆是另一个产品方向,不是这一轮要解决的问题。

11. 说点感性的

聊了这么多技术细节,最后跳出来说一件事。

Context压缩的目标从来不是省token。省钱是顺带的。它要解决的问题是保护模型的注意力。

200K的上下文窗口听起来很大,但研究反复表明:上下文塞到70%以上,模型的中段失忆和指令漂移就会明显恶化。它不是真的"忘了",是注意力被稀释、信号被噪声淹没。这就是Context Rot。

所以一个合格的压缩系统应该是一个信号工程师——把无关紧要的工具输出降为占位符,让模型不用扫过它们;把老的assistant文本裁短,让最近的对话不被淹没;把历史合并成结构化摘要,让模型用事实思考而不是用文本回忆。

花力气做分级、做增量、做保护区,本质上是在回答一个问题:"这一轮对话里,模型应该把注意力放在什么上面?"

2026年做Agent工程,这个问题绕不开。

附录:参考资料

  • Anthropic — Effective context engineering for AI agents
  • Anthropic — Compaction (Claude API)
  • Justin3go — Shedding Hea vy Memories: Context Compaction in Codex, Claude Code, and OpenCode
  • badlogic — Context Compaction Research: Claude Code, Codex CLI, OpenCode, Amp
  • Letta — Agent Memory: How to Build Agents that Learn and Remember
  • Mem0 — LLM Chat History Summarization Guide
  • Microsoft — Compaction (Agent Framework)
  • MemGPT — Engineering Semantic Memory through Adaptive Retention
  • Cline / Cursor / Amp — 各家官方文档

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策