花 77 美元买来的教训:为什么你的「分层渐进」压缩让缓存每步都失效?
一、六大产品速览
Agent 要做上下文压缩,如今几乎成为所有 Agent 必做的一环。但怎么做的分歧大得离谱——六家主流产品,六种完全不同的技术哲学
横向对比表
| 产品 | 核心策略 | 一句话概括 | 关键特色 |
|---|---|---|---|
| Claude Code(Anthropic) | 五段流水线,按成本递增排列 | 便宜的本地操作先上,LLM 摘要兜底 | 服务端 cache_edits API,缓存前缀稳定 |
| Codex CLI(OpenAI) | 保留近期用户消息原文,其余替换为 handoff 摘要 | 用户说的话最准确,模型说的可以重写 | 20k token 用户消息原样保留 |
| OpenCode | 时间戳标记隐藏 + 结构化摘要 + 回放最后一条用户消息 | 不真删,理论上可恢复 | 数据在数据库里,只是模型看不到 |
| Cline | /smol 手动 + Auto-Compact 自动双模式 | 生成摘要后在同一任务内接续 | Focus Chain 待办列表穿越压缩存活 |
| Cursor | 自动摘要 + 提示开新对话 + 历史可搜索 | 压缩后仍能回溯原始历史 | Dynamic Context Discovery 减少 46.9% token |
| Amp(Sourcegraph) | 不做递归压缩,用 /handoff 开新线程携带要点 | 长对话本身就是问题,换线程比压缩好 | 线程一等公民,@@ 引用,threads: map 可视化 |
| MemGPT / Letta | 上下文=RAM,历史=磁盘,Agent 自主换入换出 | 操作系统级的内存调度 | 三层架构:Main Context / Recall / Archival |
关键洞察
"六家产品,六种哲学。说明这件事没有显而易见的最优解,每一种选择背后都是取舍。"
文章结构:先横向看各家具体做法 → 提炼共识原则 → 介绍 MUR AI 最终落地方案
MUR AI 是腾讯面向用研场景的云端多用户 Agent,它比本地 CLI 工具多了好几层需要考虑的事
二、第一代方案的五大痛点
第一代压缩的逻辑一句话就能讲完:「等上下文窗口快撑不住了,把前面几十轮历史一口气扔给 LLM 生成一段摘要」。直观、好实现,但体验极差
痛点 1:悬崖式触发
症状:不溢出的时候一动不动,对话越来越胖、模型注意力越来越散但系统毫无反应;一旦溢出就全量出手,把前面几十轮一口气捏成一段摘要
后果:触发的那一刻质量已经塌了——模型刚因为信息过载走神,紧接着又被剥夺大半上下文。这种「零或全」的模式意味着用户感知到压缩的时候,往往已经是问题发生之后
痛点 2:全量摘要丢细节
症状:几十轮消息压成几百字,无论 prompt 写得多好都会丢失关键信息
丢失的内容包括:变量名、函数签名、错误堆栈、用户的具体措辞——偏偏这些是 Agent 继续干活最需要的东西
后果:模型「失忆」——不是忘记整体进展,而是忘记某个具体的变量名、某行关键的报错信息
痛点 3:Token 估算粗糙
症状:不少实现用
text.length / 3估算 token 数实际误差:中英混合场景下误差 30-50%
后果:以为安全实际已经溢出(估算 70%,实际 92%),或者以为该压了实际还早
根因:中文字符在 BPE 里通常占 1.5-2 token,英文代码有大量短 token(单个字符就是一个 token),混合内容的实际 token 数比字符数估算高 30-50%
痛点 4:不区分信息价值
症状:5000 行 grep 输出和 5000 token 的关键诊断被同等对待
对比:grep 输出裁了几乎不影响任务(只是少了一些搜索结果),关键诊断裁了 Agent 立刻不会干活
后果:在紧要信息和噪音之间,系统做了一视同仁的取舍
痛点 5:用户内容被一刀切
症状:用户贴的代码代表输入意图,和工具输出性质完全不同,但如果一视同仁地压,就会出现「用户贴的代码被压没了,模型忘了要改什么」
后果:Agent 丢失了任务来源信息,行为开始漂移
总结
第一代做法的根本问题:它把压缩当突发事件处理,而不是一种持续维护的能力
三、第二代方案逐家拆解
过去一年,几个主流 Agent 不约而同走向「分层 + 渐进」,但策略和哲学各不相同
3.1 Claude Code(Anthropic):五段流水线 + 结构化摘要
Claude Code 把上下文管理做成了一条严格按成本递增排列的流水线,共五段:
流水线五段
| 阶段 | 名称 | 操作 | 成本 |
|---|---|---|---|
| 1 | Budget Reduction | 调整工具输出的截断预算 | 零(本地) |
| 2 | Snip | 截短老的工具输出,留「做过什么」的摘要行 | 零(本地) |
| 3 | Microcompact | 对工具输出内容做局部内联压缩 | 零(本地) |
| 4 | Context Collapse | 对更久远的历史做粒度更细的折叠 | 零(本地) |
| 5 | Auto-Compact | 调 LLM 生成结构化摘要(兜底) | 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.2 Codex CLI(OpenAI):近期用户消息优先保护
Codex 的策略相对简单直接:
触发时机:约 95% 容量时触发
操作:生成一份 handoff 摘要替换掉旧历史
重建后的上下文结构:
所有 assistant 回复和工具结果被物理删除,由摘要替代
近期约20k token 内的用户消息原样保留
更早的用户消息则被蒸馏进摘要里——关键请求和约束会被保留下来
设计哲学:把压缩当作「同事间的工作交接」——进展、约束、剩余任务,足够下一个模型接手继续干
3.3 OpenCode:可逆隐藏 + 回放最后一条指令
OpenCode 的做法分两步,很有意思:
第一步:Prune(轻量,无 LLM 调用)
触发时机:每次成功响应后自动触发
操作逻辑:
往回遍历消息,跳过最近 2 轮用户对话
保护最近 40k token 的工具输出不动
把更老的工具输出用时间戳标记为「已压缩」
关键特性:数据还在数据库里,只是模型看到的变成了占位符
[Old tool result content cleared]本质:不真删,理论上可恢复
第二步:Summary(重量,调 LLM)
触发时机:只在 token 用量超过模型输入上限时触发
操作:生成一份五段式结构化摘要(目标 / 指令 / 发现 / 已完成 / 相关文件),然后自动回放用户最后一条消息——模型不从摘要继续,而是从用户最近的指令继续
一个有趣的细节
「它只有一份摘要同时服务于模型和界面展示,并没有做『给模型看的详细版』和『给用户看的精简版』的分离。」
3.4 Cline:自动 + 手动双模式
Cline 从 v3.25 开始支持两种压缩模式:
/smol(别名/compact):手动触发,生成摘要后在同一任务内接续。决策、代码变更、状态都保留在摘要里,不用切新会话Auto-Compact:接近上下文上限时自动触发,行为和
/smol一致
特色功能:如果开启了 Focus Chain(v3.25 默认开启),待办列表会穿越压缩存活下来,作为进度锚点。这意味着即使历史被压缩,Agent 仍然知道自己接下来要做什么。
3.5 Cursor:压缩 + 可回溯
基础能力:上下文超出模型窗口时自动压缩旧消息,同时提示用户「开一个带摘要的新对话」
2026 年新增——Dynamic Context Discovery:把聊天历史变成可搜索的文件,即使压缩后 Agent 也能回头检索原始细节
效果数据:A/B 测试里减少了46.9%的总 token 消耗
已知问题:社区反馈压缩后模型有时会「忘掉」刚才的编辑,Cursor 团队确认这是高优 bug 在修
3.6 Amp(Sourcegraph):不压缩,换线程
Amp 采取了最激进的异见立场:
核心论点:递归摘要会导致性能逐步衰减(他们引用了 OpenAI 的一份内部研究),所以干脆不做压缩
替代方案:
/handoff——把当前线程的要点打包进一个新线程,用户可以在交接前审查和编辑带过去的内容线程是 Amp 的一等公民:
可以用
@@引用其他线程用
threads: map可视化线程关系理念是「一系列有焦点的短步骤,比一个逐渐退化的长对话好」
妥协:2026 年的 Neo CLI 更新里,Amp 也加入了 90% 窗口用量时的自动上下文管理——算是对纯手动路线的一个妥协。
3.7 MemGPT / Letta:上下文当 RAM
学术派的代表,直接按操作系统的内存层次来建模:
| 层级 | 类比 | 容量 | 访问方式 |
|---|---|---|---|
| Main Context | RAM | 模型上下文窗口大小 | 始终在 prompt 里 |
| Recall Memory | 交换分区 | 完整对话历史 | conversation_search |
| Archival Memory | 磁盘 | 无限(向量存储) | archival_memory_search |
关键区别:换入换出由Agent 自己决定(通过函数调用),不是被动截断。Agent 主动判断什么时候需要从 Recall 或 Archival 里检索什么信息
Letta是 MemGPT 的生产化框架:
最新版本 v0.16.7(2026 年 3 月)
GitHub 22.5k star
还在积极维护
代价:架构复杂度高、需要外部向量存储、有检索延迟
适用场景:需要跨会话长期记忆,但对单会话内的压缩来说有点重
四、实施陷阱:滑窗式 Stub 替换 = 每步缓存失效
问题描述
第二代「分层渐进」如果实施不对,会掉进一个非常隐蔽的坑。想象一种实现:
「保留最近 N 条 tool 结果,更老的替换成 stub」
听起来很合理。但如果这个判断在 step-loop 里每一步都重算,那么每完成一个 step(新增 2 条消息),就有 1 条原本被保留的旧 tool 结果滑出窗口、被替换成 stub。它的字节变了,从这个位置往后的整段 prompt 前缀对 Prompt Cache 就失效了,需要重新写入
真实案例数据
一个 4 轮、177 step、59 分钟的会话,烧了$77.3,其中83%($64.8)全是 cache_write
cache_write 的单价是 cache_read 的12.5 倍
正确做法
stub 决策必须单调推进——只能大跳,不能滑窗
一个 part 一旦被标成 stub,后续所有 turn、所有 step 里都保持 stub 不变,绝不因为「又老了一步」而反复触发
实现方式
方案 A:把决策按 part ID 持久化(Redis 或内存映射),下次直接复用不重算
方案 B:交给服务端的
cache_edits/context_managementAPI——客户端字节零变化,缓存天然稳定(Claude Code 走的路)
五、行业六大共识
从各家方案的共同选择中,文章提炼了六条几乎人人认同的原则:
共识 1:分层渐进,不一刀切
定义多个水位线,越接近上限手段越激进。系统永远在小幅维护,避免悬崖式塌方。不再是「满到 100% 时一脚踩死」,而是「到了 60% 开始修剪,到了 80% 开始大修,到了 95% 才叫 LLM 救命」
共识 2:成本严格递增
便宜的先做(字符串截断、placeholder 替换),贵的最后做(LLM 摘要)。能用零成本释放的空间,不花钱买。这是 Claude Code 五段流水线的核心精神,也是 MUR AI 四级水位线的基础原则
共识 3:增量摘要优于全量摘要
老做法:每次重新摘要全部历史
新做法:保留一份活的摘要,每次只把新增部分合并进去
三大好处:
单次输入更短、更便宜、也更准(模型不用处理几百条消息)
同一段历史不会被反复重写,避免「摘要的摘要」导致语义漂移
合并时模型可以主动取舍(例:「这个文件改过了,旧描述更新一下」)
共识 4:用真实 Token,别估算
LLM API 每次都返回
usage.totalTokens——免费、精确、唯一可信text.length / 3只在内部排序时凑合用(「先裁哪个工具输出」),需要的是相对大小不是绝对值触发判断必须用真实值
共识 5:用户消息有特权
用户的指令、问题、代码——这些是任务来源:
Codex做到一字不动
OpenCode做到压缩后回放最后一条
其他方案至少保证用户纯文本不裁
共识 6:保护近端
无论怎么压,最近几轮不能动。模型短期连贯性几乎全靠这几轮维持。常见做法是定义保护区——比如最近 8000 token 内的所有消息,任何级别都不参与压缩
共识 7:单调边界,绝不滑窗
stub 决策一旦做出,对应位置的字节就必须从此固定。下一 turn、下一 step 再看到同一位置,永远是同一个 stub,绝不因为「又老了一步」而重新触发替换。这是第 4 节惨痛教训的直接产出
六、MUR AI 四级水位线方案
设计理念
「想象一台电脑的内存压力监视器。」
四个 Tier 不是互斥而是累积——Tier 3 触发时会先做完 Tier 1 和 Tier 2 再做摘要。这意味着即使最坏情况,需要送给 LLM 的内容量也已经被前两步免费砍掉了一大块。
水位线总览
| 级别 | 触发条件 | 操作性质 | LLM 成本 | 核心动作 |
|---|---|---|---|---|
| Tier 0 | < 60% 窗口用量 | 什么都不做 | 零 | — |
| Tier 1 Snip | 60-80% | 预防性维护 | 零 | 截短老工具输出、截短用户代码块 |
| Tier 2 Prune | 80-95% | 深度释压 | 零 | 旧输出 → 占位符、旧 assistant → 截断 |
| Tier 3 Summarize | ≥ 95% | LLM 摘要兜底 | 有调用 | 增量摘要 + 结构化输出 |
Tier 0:什么都不做(< 60%)
上下文宽裕,模型注意力没散,最好的优化是不优化。不必要的预防性操作本身也会引入干扰
Tier 1:Snip — 便宜的整理(60-80%)
到了 60% 开始预防性维护。没有 LLM 调用,纯字符串处理:
操作内容
截短老的工具输出:一次 grep 返回 5000 token?保留前几行 + 工具名 +「还有 X 条结果被省略」,剩下丢掉
截短用户消息里的代码块:200 行代码保留文件名注释 + 前几行 + 总行数标注
保护规则
保护区内的工具输出和代码块不动
某些工具享有豁免(比如 Skill、Task 这种返回结构化关键信息的)
用户的纯文本指令永远不动,只压缩 markdown 代码块
「这一级成本是零,但能挡住相当一部分增长。」
Tier 2:Prune — 更狠的释压(80-95%)
预防性维护不够了,需要更激进的手段(依然零 LLM 成本):
Tier 1 已经截短的工具输出进一步替换成占位符
[Content compacted to save space]裁掉 assistant 旧文本——保留前两句 +
[truncated]截断阈值整体下调,能压的全压
依然不变的:零 LLM 成本、不动保护区、不动用户纯文本
Tier 3:Summarize — 兜底(≥ 95%)
只有 Tier 1 + Tier 2 都救不回来时才触发 LLM 摘要
增量摘要流程
找出「上次摘要之后 ~ 保护区之前」的消息作为delta
LLM 输入:上次摘要 + delta→ 生成合并摘要
替换旧摘要,删除 delta 消息
保护区不动
第一次触发时「上次摘要」为空,相当于做一次普通摘要;之后每次都是追加合并,避免反复重写历史导致语义漂移
结构化输出
摘要 prompt 要求 LLM 输出四段结构化内容:
| 章节 | 内容 |
|---|---|
| 进展 | 已完成的工作、当前状态 |
| 文件 | 涉及的文件清单、修改状态 |
| 待办 | 剩余任务、未解决的问题 |
| 上下文 | 用户偏好、已知错误、约束条件 |
七、云端多用户额外三层设计
四级水位线解决的是「上下文里压什么、压多狠」。但 MUR AI 跑在云端、服务多用户,这意味着必须处理几件 CLI 工具可以无视的事:
用户关掉浏览器再回来,压缩状态不能丢
Pod 重启、流量漂移,跨进程的压缩决策必须一致
工具完整日志要支持事后审计和前端回取,不能为了省 context 就把日志丢了
sandbox 里的工具输出动辄几十 MB,完整性和模型注意力不能二选一
「这些靠水位线解决不了。」
7.1 存储分离:完整日志落盘,对话里只留截断版
每次工具调用(bash、read、grep)都可能吐出几万 token。直接塞进对话历史不行,全丢掉又损失调试和审计能力
实现方案
engine 层调用
persistTruncatedOutput把完整内容写到沙箱的_internal/truncated-outputs/{callId}.log沙箱写失败就降级到 COS 直传
截断版的 metadata 带上
fullLogPath,模型看到的是「前几行内容 + [截断] + 完整日志路径」——它知道完整内容在哪,但没花 token 去读前端展示时按需调 sandbox-file API 现取现读,完全绕开 context 约束
设计本质
「把『模型的工作记忆』和『用户的审计需求』解耦了。」
CLI 工具不用管这个——它们没有前端,工具输出要么留在 context 里,要么就没了
7.2 工具差异化:四个梯度
| 梯度 | 类别 | 包含工具 | 处理方式 |
|---|---|---|---|
| 完全保护 | PROTECTED_TOOLS | Skill、Task | 任何 Tier 都不动 |
| 微压缩豁免 | 被动压缩跳过 | Task、AskUserQuestion | 小幅压缩也跳过 |
| 白名单可压 | 压缩主力 | bash、read、grep、websearch… | 无状态读取类,承受压缩 |
| 参考建议 | 非压缩范围 | 其他 | — |
差异化存储预算
| 工具 | 单次输出落盘上限 |
|---|---|
| Read | 30KB |
| Bash | 50KB |
| WebSearch | 15KB |
这组数值的考量:Bash 经常吐大段构建日志,Read 相对可控,WebSearch 通常只要几条结果就够
「一段 grep 输出和一次 Skill 调用的信息密度不在一个量级,用统一阈值处理就是粗暴。」
特殊保护:AskUserQuestion 的输出是用户的回答,删了等于把用户回话抹掉,所以即使被动小幅压缩也跳过
7.3 跨轮缓存(ReplacementCache)
问题场景
用户聊到第 20 轮,系统在第 8 轮、第 14 轮分别做过 snip,截短了某些 grep 输出。然后实例重启了,或者下一个请求被路由到另一个 Pod。第 21 轮触发新一轮压缩——如果什么都不记得,新进程会从头重新判断每个 part 该怎么压,结果可能跟之前不一样
两个严重后果
Prompt Cache 全废:消息序列前缀变了,缓存命中率掉到零,每一轮都按新 prompt 计费
模型困惑:同一段历史在不同轮次里「长得不一样」,模型可能重新触发已经处理过的工具调用,甚至开始鬼打墙
解决方案
把每一轮的截断决策按 part ID 存进Redis(key 形如
msgOptCache:{sessionId},TTL 30 分钟)下一轮无论是同进程还是另一个 Pod,先查缓存
已经决定过怎么压的 part 直接复用之前的结果,没决定过的才走新策略
从缓存读出的数据先过
isValidReplacementEntry()校验,损坏的直接丢弃重算
效果
同一个 part 在整个会话里始终长一样,消息前缀稳定,Prompt Cache 友好
跨实例、跨重启无感
有数据校验兜底,不会因缓存损坏而崩溃
7.4 多用户隔离
最后一层最朴素但绝对不能省。所有压缩状态按(userId, sessionId)二元组隔离:
数据库写入强制带双条件
WHERE userId = ? AND sessionId = ?,杜绝越权读写COS 路径形如
user_sessions/{userId}/sessions/{sessionId}/_internal/...,天然按用户分区SSE 压缩事件按 sessionId 严格过滤订阅,一个用户收不到另一个用户的压缩通知
「压错给谁」比「压错什么」严重得多。这一层没有为了性能而省的空间
四层云端特化设计的关系
| 层次 | 解决的问题 | CLI 工具需要吗? |
|---|---|---|
| 存储分离 | 审计需求 vs 上下文限制 | 不需要 |
| 工具差异化 | 不同工具信息密度不同 | 部分需要 |
| 跨轮缓存 | 实例重启后一致性 | 不需要 |
| 多用户隔离 | 租户间安全 | 不需要 |
「它们是单机工具,重启意味着开新会话,压缩状态丢了就丢了。但云端 Agent 不一样:用户合上电脑去吃饭、明天回来,期待的是接着上次继续,而不是『对不起请重新介绍一下需求』。」
八、关键设计决策
8.1 为什么阈值是 60 / 80 / 95?
| 阈值 | 含义 |
|---|---|
| 60% | 预防性维护的甜点——再低会频繁触发没意义,再高模型注意力已经开始下降 |
| 80% | 危险线,离溢出还有缓冲但已经不能等 |
| 95% | 最后防线,再不调 LLM 就要爆了 |
三个值都是可配置的常量,接入远程配置后能热更新。出问题时把任意一个调到 1.0 就能禁用对应 Tier——这是生产环境必备的「保险丝」
8.2 为什么先 Snip 再 Prune?
一个关键区别:
| 操作 | 模型还能知道什么 |
|---|---|
| Snip | 「我之前 grep 过这个文件」——保留了工具名和部分元信息 |
| Prune | 只剩一个占位符[Content compacted to save space]——连执行的什么工具都看不到了 |
Snip 是轻量记号,Prune 是彻底擦除——能用前者解决的就别上后者
8.3 为什么要增量摘要?
打个比方:
| 做法 | 类比 |
|---|---|
| 全量摘要 | 每周把过去三个月的工作重新写一份周报 |
| 增量摘要 | 维护一份持续更新的项目状态,每周只追加和修订变化的部分 |
增量摘要的隐藏好处:同一个文件被多次提及时,最新状态覆盖旧状态;全量摘要里这个文件可能被描述好几次而且互相矛盾
8.4 为什么用真实 Token?
作者团队一开始也用text.length / 3估算,直到踩了坑:
「估算值显示 70%,实际 LLM 返回的 usage 已经 92%——再来一轮直接溢出。」
触发判断:必须用 API 真实值
内部排序(「先裁哪个工具输出」):用估算就够了,需要的是相对大小不是绝对值,30% 误差不影响排序
8.5 为什么保留 compactionProtected 标记?
预留扩展。某些 Part 可能很重要但看起来很普通——用户上传的设计文档、关键错误堆栈、标记为「记住这个」的指令。给它们打标记,任何 Tier 都跳过
「能力先建好,需要时直接用。」
九、红线清单
「压缩系统最大的事故不是压不够,而是压错东西。」
以下内容在任何 Tier 下都绝对不能动:
| 红线内容 | 原因 |
|---|---|
| 保护区内的所有消息 | 模型短期连贯性的命脉 |
| 用户消息的纯文本部分 | 用户意图就是任务来源 |
| PROTECTED_TOOLS 的输出(Skill / Task) | 高度结构化的关键信息或会话级状态 |
| MICRO_COMPACTION_EXEMPT 的工具输出(Task / AskUserQuestion) | 保住对话流的关键节点 |
| 带 compactionProtected 标记的 Part | 业务侧明确指定的「必须保留」 |
严格执行原则:这些规则在每一级压缩里严格执行。不存在「为了救场破例」的情况
「如果保护区都救不下来,那就让上下文真的爆出来——比错误压缩造成模型行为漂移要好。」
十、可观测性设计
「压缩发生在背后,没有可观测性的话调起来全靠玄学。」
SSE 事件包含的信息
| 指标 | 含义 |
|---|---|
| 当前触发的 tier | 0 / 1 / 2 / 3 |
| 当时的 token 使用率 | 来自 LLM 真实 usage |
| Snip 截了几个 part | Tier 1 的处理量 |
| Prune 替换了几个 part | Tier 2 的处理量 |
| Summarize 是否真的调用了 | Tier 3 是否触发 |
| 预估节省了多少 token | 压缩效果量化 |
| 命中 ReplacementCache 的 part 数 | 跨轮一致性收益 |
前端展示
前端渲染一个压缩面板,让用户看到类似:
「这一轮触发了 Tier 1,节省了 3000 token,其中 6 个 part 直接复用了上一轮决策。」
持续优化
这些数据接入 trace 系统后,可以基于真实生产数据调水位线,不再拍脑袋。比如:
如果 Tier 1 触发过于频繁但每次节省太少,调高阈值
如果 Tier 3 触发频率超出预期,说明前两级不够激进
如果 ReplacementCache 命中率低,缩短 TTL 或检查缓存策略
十一、刻意未做的事
为了避免过度工程,有几条路团队暂时没走,但保留了可能性:
1. 主动 cache-aware 调度(Claude Code 的做法)
是什么:压缩时主动调整顺序来最大化 Prompt Cache 命中
现状:目前用 ReplacementCache 实现了被动一致性
为什么不做:还没做按 cache 边界主动选择压谁
触发条件:等 cache 命中率成为瓶颈再上
2. 可逆隐藏(OpenCode 的做法)
是什么:用时间戳标记而非真删
现状:目前
_internal落盘已经满足审计需求为什么不做:等用户需要「回滚到某次压缩之前」时再上
3. 回放最后一条用户消息(OpenCode 的做法)
是什么:摘要后重新追加用户最近指令
为什么不做:Tier 3 触发频率本来就低,优先级不高
4. 用户消息一字不动(Codex 的做法)
是什么:用户消息完全不动,包括代码块
现状:已经不动用户纯文本,但还会截断用户贴的代码块
为什么不做:是否进一步走极端,看后续生产数据
5. 分层长期记忆(MemGPT / Letta / Mem0)
是什么:跨会话记忆系统
为什么不做:跨会话记忆是另一个产品方向,不是这一轮要解决的问题
十二、核心哲学
Context 压缩的目标从来不是省 Token
省钱是顺带的。它要解决的问题是保护模型的注意力
Context Rot 现象
200K 的上下文窗口听起来很大,但研究反复表明:
上下文塞到 70% 以上,模型的中段失忆和指令漂移就会明显恶化
它不是真的「忘了」——是注意力被稀释、信号被噪声淹没。这就是Context Rot
压缩系统 = 信号工程师
一个合格的压缩系统应该:
把无关紧要的工具输出降为占位符,让模型不用扫过它们
把老的 assistant 文本裁短,让最近的对话不被淹没
把历史合并成结构化摘要,让模型用事实思考而不是用文本回忆
终极问题
「这一轮对话里,模型应该把注意力放在什么上面?」
2026 年做 Agent 工程,这个问题绕不开
