上下文压缩
压缩上下文有两个截然不同的动机:一是长度与成本约束,二是即使窗口没满,长上下文也会让检索精度下降(context rot)。压缩的第二个价值,是把需要思考才能得到的结论,变成可以直接检索的知识。
前面几节讨论了如何往上下文里放内容——提示工程决定写什么,Skills 决定按需加载什么,Agent 状态栏决定注入什么元信息。但随着多轮交互的深入,上下文会不断膨胀。本节讨论相反的方向:如何从上下文中减少内容——什么时候压缩、怎么压缩、为什么即使上下文没满也应该压缩。
为什么需要压缩:不只是长度问题
压缩上下文有两个截然不同的动机,理解这一点对设计压缩策略至关重要。
第一,解决长度约束和成本约束。 这是最直观的原因:上下文窗口有限,工具调用结果动辄数万字符,几轮交互就可能撑满窗口,任务被迫中断。同时 token 越多,API 成本越高,推理延迟也会急剧上升。
第二,提升思考质量——总结后的知识比原始形式更利于模型使用。 这个动机更深层,也更容易被忽视。即使上下文窗口足够大,把所有原始信息堆在上下文里也不是最优选择。考虑一个具体的例子:agent 在执行一个复杂任务的过程中,通过 10 次网页搜索积累了关于某个主题的信息。这些搜索结果以原始形式散落在上下文的各个位置。当 agent 需要基于所有这些信息做最终决策时,它必须在数万 token 中反复“检索”相关片段,注意力被分散,关键信息容易被遗漏。而如果在第 10 次搜索之后,先用一次 LLM 调用将已有信息做一次结构化总结,模型在后续思考时就可以直接使用这个精炼的知识表示。
这个现象的根源在于注意力机制的本质:上下文学习的内部机制更像检索而非推理。模型擅长从已有内容里“查找”,却不擅长在一次前向传播里主动“归纳统计”。因此压缩的第二个价值,是把需要思考才能得到的结论,变成可以直接检索的知识。更深层的问题在于,长上下文会导致检索精度的下降:明明上下文窗口还远没有满,但 agent 突然找不到关键信息,或者反复纠结于一个早已解决的问题——这种现象被称为上下文腐化(Context Rot)。它与上下文溢出(窗口用完)是不同的问题:溢出是“装不下了”,腐化是“装得下但找不到了”——后者更隐蔽,因为 agent 表面上还在正常工作,只是决策质量悄然下降。
压缩与 KV Cache:看似矛盾,实则互补
前面反复强调 KV Cache 要求上下文前缀保持不变,但压缩不就是要修改上下文中间的内容吗?关键在于理解压缩发生的时机和位置。压缩不是在单次 API 调用的过程中修改上下文,而是在两次 API 调用之间,由 agent 框架对消息列表进行预处理:
- System Prompt 和 Tool Definitions 永远不动——这是上下文最前面的“静态前缀”,KV Cache 持续缓存。
- 压缩的对象是对话历史中的 tool results——当框架用压缩后的摘要替换原始的工具输出时,替换位置之后的缓存会失效,但之前的缓存仍然有效。
- 这是一个有意识的权衡:不压缩,上下文膨胀到超出窗口限制,任务直接失败;压缩后,虽然损失了部分缓存,但上下文长度可控且信息密度更高。因此压缩的频次需要权衡——最好在上下文接近阈值时批量压缩,而不是每轮都压。
生产级的分层压缩机制
在生产环境中,成熟的 agent 系统通常不会只采用单一策略,而是将多种策略组合为分层的压缩机制——不同类型的信息有不同的保质期,压缩策略应当与信息的预期生命周期匹配。以 Claude Code 的做法为参照,一个成熟的上下文管理系统通常包含五个层次:
- 工具结果预算控制:大体积的工具输出存到磁盘,模型只看摘要预览。替换决策一旦做出就被冻结,以保证缓存的一致性。
- 噪声直接删除:低价值的内容直接移除,不做摘要——对噪声做摘要只是在浪费 token。
- API 层微压缩:通过 API 层的上下文编辑能力,指示服务端从前缀中移除指定的工具结果,本地消息保持不变。适合在上下文即将溢出、反正要付出这次重建代价时使用。
- 归档式摘要:逐轮做结构化摘要(像 git log 那样保留每轮的独立记录,而非像 git squash 那样合并成一条),保留对话的逻辑脉络。
- 全量压缩:由 LLM 驱动的完整压缩,作为最后手段,并配备连续失败的熔断器。
注意这五层的排列顺序:前三层实现成本最低、对缓存的扰动可控,应当优先使用;后两层成本较高但压缩效果更强,作为兜底手段。
压缩策略的设计原则
- 信息价值的非均匀分布:关键的决策点(如人员名单)的价值高于支撑性的证据,更高于冗余的噪声。
- 语义完整性:“Sutskever 于 2024 年 5 月离开 OpenAI”不能压缩成“Sutskever 离开”——时间和公司名是不可丢失的关键信息。
- 任务相关性:同样的内容在“查找创始人名单”和“了解个人背景”两个不同的任务下,应该产生不同的压缩结果。
- 压缩即理解:有效的压缩需要深层的语义理解能力,而且显式压缩的结果是可审查的、可跨会话复用的。
由此得到一份保留优先级:架构决策和关键约束不得摘要;已修改的文件列表、验证状态(pass/fail)、未解决的 TODO 与回滚笔记必须保留;工具输出可以删除,仅保留 pass/fail 结论。此外,UUID、hash、IP、端口、URL、文件名等标识符必须原样保留——一旦把 commit hash 改错一位,后续的工具调用就会直接失效。
隔离优于压缩:子 agent 上下文隔离
压缩是在信息已经进入上下文之后做减法,而一个更釜底抽薪的思路是:让大体积的中间信息根本不进入主上下文。这就是子 agent 上下文隔离——主 agent 把“读取大量文件”“在代码库中大范围搜索”这类会产生海量中间内容的任务,委派给一个独立的子 agent;子 agent 在自己的上下文中完成探索,只把几百 token 的结论性摘要回传给主 agent。这本质上是用隔离代替压缩:压缩是有损的、需要额外 LLM 调用的事后补救;隔离则让噪声从一开始就与主上下文绝缘,主 agent 的 KV Cache 前缀也完全不受影响。代价是任务描述必须自包含、目标明确。完整设计见子代理。
工程实践
Zapvol 把上述原理落成一个“每次模型调用都执行、建立在不可变记录之上”的压缩操作,与 book 的五层机制一一对得上。核心是一条物理事实:消息流绝不重写。一个任务之上,记录与投影并存——
- 记录(
task_message)只追加、绝不重写,既是用户看到的完整历史,也是每份投影的源; - 模型的输入是建立在记录之上的一份投影,按预算缩减,调用后即弃、绝不写回。
让记录不可变,是下游一切得以成立的原因:用户始终握有真实历史;被削出视图之外的内容仍在记录里;且投影是确定性的,恢复或重试会重推出逐字节一致的视图。每个压缩决策——还原什么、削减什么、边界落在哪——都在每次调用时从原始历史、内容寻址的快照存储与预算重新推导,决策本身从不持久化。这消除了陈旧决策类 bug,也使中断自愈:崩溃或 HITL 暂停仅损失进程内临时状态,下一次调用重新推导出同一视图。
削减走一条四层阶梯,核心是一个受硬保护的近窗(COMPACTION_PROTECT_RECENT_STEPS,默认 5 步)——常规压缩从不触碰最后 N 步,只有最后手段层会击穿;这正是 book “噪声删除 / 归档摘要 / 全量压缩”分层的具体形态:
| 层 | 动作 | 近窗 |
|---|---|---|
| ① | 削减近窗之外较早的工具(工具感知压缩,非粗放清除),由老到新趋向 stop | 受保护 |
| ② | 地板摘要近窗之外那段较早历史 | 受保护 |
| ③ | 击穿削减——①② 用尽仍超 trigger:削减整个数组,含近窗 | 被击穿 |
| ④ | 击穿地板——③ 仍无法容纳:摘要时不再保护近窗 | 被击穿 |
内容按地址而非 id 寻址(工具按 toolCallId、文本/reasoning 按内容 hash),这正是“同一操作跨预处理与 step 两个消息源”得以成立的根据。阈值锚定在 provider 的真实 prompt token 上、只估新增量(predictedTokens = prevRealTokens + increment),避免 chars/4 估算随对话累积误差。触发是有效窗口的比例:wEff = contextWindow − OUTPUT_RESERVED_TOKENS,trigger = wEff × 0.65、stop = wEff × 0.50——(stop, trigger] 是刻意保留的 hysteresis 死区,对应 book “最好在接近阈值时批量压缩、而非每轮都压”。地板摘要则是 book “保留优先级”那条原则的落地:过程被丢弃,承重的结论、用户意图、交付物、安全规则予以保留,渲染为序列首部的 ## Task Context 块并每步重发,受一个优先级地板(安全/请求绝不降级 → findings 最先降级)约束在 ROLLING_SUMMARY_BUDGET_TOKENS(2500)之内。
参考:数据模型与术语
三张持久表——一张不可变原始源,加两张内容寻址的投影存储:
| 表 | 职责 |
|---|---|
task_message | 真相之源 / 恢复源——纯原始历史,只追加 |
task_compaction_snapshot | 内容寻址存储——每个已存削减一行,按 key upsert(key 是 part 地址、摘要覆盖范围,或 meta:anchor) |
task_tool_compaction | 按 toolCallId 寻址的工具存储——工具完成时预写入,削减读取 |
| 术语 | 含义 |
|---|---|
| turn | 一对持久化在原始历史里的 (user, assistant) 消息 |
| step | 一次模型调用加其工具调用,处于一个 turn 之内 |
预处理每 turn 运行一次(不在热路径);压缩操作在每一步运行,含第一步。近窗保护按 step 计数。
相关阅读
- KV Cache 设计——压缩为何采用阈值触发而非即时触发:让前缀在一 turn 各步之间稳定、使缓存持续见效。
- 子代理——隔离优于压缩的实现。
- 缓存 vs 压缩——大小 ↔ 缓存这条坐标轴的框架无关综述。
来源
- Effective Context Engineering for AI Agents — Anthropic, 2026
- Compaction — Anthropic, Claude API 文档