上下文压缩
《深入理解 AI Agent》2.7「上下文压缩策略」学习笔记与架构设计延伸。本文先讨论上下文为什么需要压缩、压缩如何影响 KV Cache,以及历史决策怎样保留和回溯;最后提出一个完整的长时程 Agent 上下文管理方案。文中的 TRACE 是基于原书思路提出的概念架构,尚无实现与性能评测,不能将它当成已验证的研究成果。
图示兼容性:本文全部图示直接以内嵌 HTML / SVG 写入 Markdown,采用浅色背景,无需图床、图片路径或 JavaScript。流程图箭头使用显式 SVG 多边形绘制,不依赖
marker-end外部定义或重复的 SVG ID。需要在 Halo 中允许文章渲染原生 HTML(包括<svg>、<path>和<polygon>);如果主题或安全过滤器整体禁用了 SVG,任何纯内嵌 SVG 图形仍可能被过滤。
一个 Coding Agent 可能在数十轮工具调用后逐渐出现这样的情况:日志越查越多,旧代码被重复读取,早已排除的故障假设重新出现;与此同时,模型的上下文窗口还远远没有用满。
这引出了一个比「如何节省 Token」更重要的问题:一个长时间运行的 Agent,如何以有限的上下文持续利用已经获得的知识,同时保留修正错误判断的能力?
1. 上下文没有满,为什么仍然值得压缩?
假设我们正在排查一个 Paxos 集群频繁切换 Leader 的故障。Agent 已经执行 20 轮工具调用,读取了大量网络日志、调度日志、源码以及测试结果。以 128K Token 的窗口为例,当前使用了 85K。
| ■ 系统指令 10K | ■ 工具结果 60K |
| ■ 对话历史 15K | ■ 未使用 43K |
直觉上,剩余 43K 已经很多,可以暂时不压缩。这个判断有合理性:若当前信息都很重要,过早压缩确实会制造不必要的信息损失和缓存重建开销。但「窗口尚未用满」不足以证明上下文质量足够好。
考虑两个已使用 85K Token 的 Agent:A 的历史几乎全部与当前故障相关;B 的 60K 工具结果包含大量重复日志和已经失去价值的搜索输出。两者占用率相同,信息有效密度却可能相差很大。
这正是 Context Rot(上下文腐化) 的含义:模型虽然还能接收更多 Token,却可能越来越难正确利用已有的关键事实。它与上下文溢出不同。溢出是容量硬约束,腐化表现为信息定位、区分和使用质量的下降;其严重程度依赖具体模型、任务和信息位置,不能简单归因于注意力「平均分散」。
压缩因此具有两个目标:降低长度、成本及延迟;把分散的观察和反复计算过的结论整理成易于后续调用的知识。例如,与其让模型每轮在十万行日志中重新判断心跳超时次数,不如把统计结果、时间范围和证据引用直接交给它。
这里要警惕一个风险:摘要是压缩器形成的判断,可能包含错误。 如果早期将错误的网络故障假设当成事实,随后删除所有原始日志,后面越依赖摘要,就可能越偏离真实故障原因。
2. 压缩策略:什么时候压、压到什么程度?
原书 2.7 节比较了六种策略:不压缩、个体摘要、组合摘要、任务感知压缩、带引用的上下文感知压缩、自适应窗口化。其关键启发在于,压缩是否有效主要取决于当前任务需要保留哪些信息,而非压缩率有多高。
以下数据来自原书实验 2-10,测试任务为追踪 OpenAI 联合创始人的职业状态,实验将上下文预算限制在 128K Token;不同策略完成路径也可能不同,不能简单将累计 Token 当作单次压缩的成本。
| 个体摘要 | 276,608 | |
| 带引用的上下文感知 | 222,992 | |
| 自适应窗口化 | 174,601 | |
| 组合摘要 | 93,449 | |
| 上下文感知压缩 | 40,157 |
这组实验不能直接推导出「任务感知压缩总是最好」。一份摘要可能完美保留当前问题所需事实,却因为调查方向后来发生改变而丢失新的重要线索。带引用策略也展示了另一种权衡:增加一些 Token,可以换来更强的证据可追溯性。
原书的自适应窗口化采用 80% 的触发阈值、批量压缩未处理工具结果,以及避免重复压缩的标记。对于 128K 窗口,阈值对应 102.4K Token。这只是实验中的启发式配置;生产系统还应考虑下一次工具结果的预测大小、输出预留空间,避免在判断达到阈值之前就被突发结果冲垮。
实务上可以将压缩分成三个层次:
- 工具入口处的预算控制:原始输出先落盘,再给主 Agent 必要的片段、摘要和引用;避免数万 Token 从一开始进入主上下文。
- 运行中的批量清理:当窗口趋近阈值或噪声明显影响决策时,统一处理过期结果,避免每一轮改写历史。
- 必要时全量重建:保留原始证据与结构化任务状态,开启一个更短的新上下文。
原书还讨论了噪声直接删除、API 层微压缩、归档式摘要、全量压缩及失败熔断。不同内容有不同的有效生命周期,确实没有一种摘要方式适合所有任务。
3. 压缩和 KV Cache 之间究竟如何权衡?
这是 2.7 节与前面「KV Cache 友好的上下文设计」之间的连接。
在因果 Transformer 中,位置 i 的高层 KV 状态通常依赖它之前的 Token。若中间一段消息被摘要替换,后面即使文本完全不变,其原有 KV 状态也通常无法被精确复用。简单表示为:
改变早期的 x_j,可能使所有 i\ge j 的依赖状态发生变化。标准 Prefix Caching 通常仅能复用仍然相同的前缀。
假设历史已经有 85K Token,有两种常见方案。
|
方案 A · 局部压缩
压缩前:
压缩后:
蓝:修改点之前的缓存可复用;绿:新摘要需重算;橙:后续文本虽相同,也需要重新计算其依赖状态。
|
方案 B · 全量压缩 / 新上下文
压缩前:
新上下文:
相同的静态前缀可以继续复用;新任务摘要必须 Prefill,但总长度可能远低于方案 A 的剩余后缀。
|
于是可以看到,复用更多旧 KV 并不必然意味着总成本更低。A 的优势是保留部分原始后续历史,对既有推理轨迹的编辑较少;B 可以显著缩短后续上下文,代价是对新摘要更依赖。推理连续性由信息是否充分、消息协议是否有效决定,不能仅凭是否沿用同一对话 ID 判断。
更稳妥的方式是在多个 API 请求之间执行压缩,尽可能固定系统指令与工具定义等静态前缀,并对动态部分采取批量而非逐轮重写的策略。
4. 任务感知压缩的致命弱点:早期方向可能是错的
考虑一个具体的故障调查过程:
- 第 10 轮,Agent 观察到 Leader 切换前多次心跳超时,提出 H1:网络故障。
- 第 35 轮,网络测试未发现异常,Agent 暂时排除 H1,转向 H2:CPU 调度延迟。
- 压缩器认为网络日志不再重要,于是只保留「网络正常,已排除 H1」。
- 第 80 轮,新的高频采样发现网络连接每隔约 30 秒中断 200ms,之前的低频测试可能根本无法捕获。
如果压缩器早就把「未发现网络异常」提升为「网络一定正常」,现在就会遇到两重困难:原始证据找不到,错误结论还在引导后续决策。
因此,需要保存的不只是结论,还包括结论成立的条件、支持它的证据,以及什么情况会使它失效。
这与普通 Chain of Thought(CoT)不完全相同。CoT 是模型生成的推理文本,可能很长,也未必完整或可靠。工程上更适合维护结构化的 Decision Trace(决策轨迹),记录「假设—证据—决策—前提—失效条件」。
例如:
hypothesis_id: H1
claim: 网络异常导致 Leader 切换
status: provisionally_rejected
supporting_evidence:
- E001 # 网络测试未见异常
assumptions:
- 测试覆盖能力足以识别相关异常
invalidation_conditions:
- 出现新的网络异常证据
- 发现原采样频率不足
trace_ref: run-035
失效条件是关键。 即使 H1 的原始拒绝理由没有改变,只要其依据受到新证据挑战,就应该允许重新评估。反过来,如果新证据没有影响原结论的适用条件,就没有必要重新走一遍失败路径。
这里已经出现了一个超出简单摘要的要求:记录历史,并在必要时判断哪些历史结论需要撤回或复核。 如果将结构化轨迹与证据依赖组合起来,就能构建一种可回溯的 Agent 决策状态。
5. 失败记录会无限增长,怎么办?
记录所有失败分支的确能减少重复探索,但若所有失败记录都永久塞进当前 Context,迟早又会撑满窗口。这个矛盾的出路是:历史保存与当前可见信息采用不同生命周期。
例如在调查 H2 时,当前 Context 只需要一条简短记录:
H1 曾被暂时排除(依据 E001,见 trace H1-v1);若出现新网络异常或测试覆盖不足,请重新评估。
完整的 H1 调查过程则写入持久化任务状态,几十万行原始日志进一步写入对象存储或文件系统。等新证据出现时,再通过引用读取相关片段。
这也回答了一个常见判断题:将 50K Token 的工具结果压缩为 2K Token,并把原文完整写入磁盘,是否是无损压缩?
要区分三个意义:
- 存储层面可无损:只要原文完整、校验成功、版本和引用正确,就可以恢复原始数据。
- 当前上下文仍有损:模型眼前只有 2K 摘要,不能直接利用那 50K 中未经加载的信息。
- 决策层面无法自动保证无损:Agent 可能不知道何时应当检索,也可能遗漏真正相关的历史证据。
所以准确的表述是:原始数据无损归档、工作上下文有损压缩、历史信息按需恢复。
6. 子 Agent 隔离:从源头减少主上下文中的噪声
上述方法主要处理已经产生的信息。对于大规模代码库搜索、海量日志分析等任务,还有一种更经济的可能:直接把信息产生过程放进隔离的子 Agent,让主 Agent 只接收重要结果。
例如主 Agent 希望定位 CacheManager::release() 一类函数是否导致内存泄漏,可以给子 Agent 明确的调查任务、已知约束、排除事项和结果格式。子 Agent 独立检索 10 万个文件,生成 50K Token 的中间内容,但向主 Agent 只回传 500 Token 左右的结论与引用。
问题随之而来:如果结论后来被推翻,子 Agent 的中间调查过程是否就消失了?
如果子 Agent 的上下文在结束后直接丢弃,确实会有这个风险。但可以在子 Agent 内部同样维护证据档案与结构化轨迹,将完整中间记录保存在外部,再向主 Agent 发送可解析的 trace_ref。
|
Main Agent 目标:定位内存泄漏
背景:Leader 切换时发生 约束:只读调查 |
任务契约 → ← 结果 |
Sub-Agent 检索 10 万文件
分析 ≈ 50K Token 返回 ≈ 500 Token |
Trace / Artifact Store 原始搜索结果
排除过的路径 关键测试 / 日志 |
suspect: CacheManager::release() · status: suspectedevidence_refs: [E042, E045] · open_question: 并发取消路径trace_ref: child-run-042 → 后续结论受到质疑时重新读取相关调查片段
隔离策略的主要困难不是几百 Token 的摘要如何写,而是子 Agent 可能不知道主 Agent 已经发现、排除或者禁止了什么。因此委派任务必须具有明确目标、完整必要约束和可验证的输出契约。对涉及外部副作用的子任务,还需要独立处理权限、幂等与恢复。
7. 思考题:运行 100 轮的 Agent,能否兼顾压缩、可追溯和 KV Cache?
到这里,可以把整节学习归结为一个更完整的系统设计题:
假设一个 Agent 已经运行了 100 轮,我们希望同时满足三个目标:上下文长度始终受到控制;不丢失关键决策、约束和失败路径;尽量不破坏 KV Cache 的稳定前缀。
你能否结合分层存储、决策轨迹和子 Agent 隔离,设计一种完整的上下文管理策略?尤其要回答:历史信息什么时候进入 LLM Context,什么时候只保存在外部存储?
这个问题无法仅靠「把 80% 以前的消息总结一下」回答,因为它同时涉及模型推理、状态存储、数据一致性、缓存重用与故障恢复。
下面给出一套完整的工程设计。
8. TRACE:可追溯、可恢复、KV Cache 感知的上下文管理架构
我将这套方案称为 TRACE(Traceable, Recoverable and Adaptive Context Engine)。它把 LLM Context 看作面向当前决策、可以重新构建的工作视图,把完整任务历史交给持久化状态系统。
TRACE 仍然是设计提案:它组合了原书压缩策略、Event Sourcing、检索与状态恢复等思想;其中决策依赖失效传播、Context Epoch 与子 Agent 轨迹恢复的集成方式,是进一步的工程扩展,尚待性能验证。
8.1 总览:三个存储层、一个上下文控制器、一条执行闭环
可以形式化表示为:
这里 S_t 是持久化任务状态,包括事件 E_t、决策依赖图 G_t、原始工件 A_t 和快照 Q_t。C_t 是第 t 轮提供给 LLM 的当前上下文,R 负责根据任务查询 q_t 检索必要历史,H_t 是近期消息。
其基本预算约束是:
W 为模型窗口上限。输入本身不能耗尽全部容量,还需要保留预期生成空间和安全余量。
8.2 写路径:每次工具调用完成时,先持久化再摘要
工具执行结束,Harness 先给原始结果生成 artifact_id、来源版本和内容哈希,并将其写入 L3。校验对象可读取后,再在 L2 提交观察事件和 evidence_id;最后才将简短消息加入主 Context。
日志、参数、工具调用 ID、源码版本
哈希、来源、不可变版本;失败则禁止不可恢复地清理原文
记录事实、时间范围、状态版本、引用完整性
保留结论、适用条件、未解决问题和 E-001 引用
artifact_id 和 evidence_id 不必是一一对应关系:一个大型日志文件可能支持多个不同时间范围的事实。对原始证据要尽量保存不可变版本,避免未来查询到经过轮转或修改的文件。
8.3 读路径:先加载强约束,再用预算选择证据
从 L2 / L3 检索历史时,我会分为三个阶段:
第一阶段:强制加载。 当前目标、不能违反的约束、活跃假设、未完成的高风险工具动作,以及它们依赖的关键事实,必须出现在当前决策状态中。
第二阶段:多路召回。 对其他可能有用的信息,结合关键词、语义检索、明确的 Evidence ID、依赖图和失效条件检索。只用语义相似度会漏掉文字上不相似却能推翻旧决策的反证。
第三阶段:预算装配和校验。 对候选证据依据相关度、依赖重要度、遗漏风险和 Token 成本排序;在预算内选择,并检查引用和来源版本。如果关键证据装不下,应当压缩低优先级内容、缩小任务范围或延迟决策,不能静默删除关键信息。
| 1 · 必选 权限与约束 当前目标 未完成动作 |
2 · 候选 语义检索 Evidence ID 依赖与反证 |
3 · 预算选择 相关性 / 风险 冗余度 / Token 重要证据优先 |
4 · 校验 引用可解析 时间版本一致 加入 Context |
这些配额为示例,实际系统需要根据模型、任务和工具调用模式调整。当前状态中的“待验证假设”尤其不能在摘要中悄然提升为“已证实事实”。
8.4 决策轨迹:Event Log、版本化假设与依赖失效
L2 使用追加事件的方式保存任务变化,例如:
Event 035 H1 PROVISIONALLY_REJECTED evidence=E001
Event 080 E087 OBSERVED artifact=A087
Event 081 H1 REOPENED invalidates_assumption=coverage
Event 081 D3 REVALIDATION_REQUIRED depends_on=H1
当前的假设状态可以从事件日志重建,并定期生成 Snapshot。相较于直接覆盖旧字段,追加历史事件更容易解释「当时为什么这样判断」。
依赖图包括三类典型边:supports、contradicts 和 depends_on。当新证据挑战一条重要前提时,Harness 将该假设和直接依赖它的下游决策标记为 needs_revalidation,再由模型或验证工具决定下一步要补充什么证据。
尤其要避免简单的「一处被推翻,全部下游结论都算错」。旧 CPU 调度日志仍然可能是真实有效的;需要撤回的是它们依赖的某个不再成立的判断前提。
8.5 自适应压缩控制器:什么时候压缩,如何避免频繁重算
原书的压缩策略需要落实到运行时的两个不同阶段。工具结果入口过滤发生在大结果进入主上下文之前,目的是从产生端控制信息规模;Context Epoch 压缩针对已经积累的历史消息,需要同时评估 Token 预算、信息完整性和缓存重建成本。
压缩检查不能只看 80% 的窗口占用率:还要预留输出空间、处理未来的工具结果膨胀,并在摘要丢失关键约束或证据引用时拒绝提交新快照。为了降低前缀失效的频率,单个 Epoch 内优先采用 Append-only,集中进行批量压缩。
8.6 Context Epoch:有意识地控制 KV Cache 重建频率
在一个 Epoch 中,主 Agent 尽量以 Append-only 方式更新历史。达到压缩阈值或出现严重冗余时,生成新 Snapshot 并开始新的 Epoch。
对于每个 Epoch,Context Manager 可以估算继续使用较长历史的未来成本,与「压缩开销 + 新上下文重算开销 + 未来使用成本」比较。但这是近似决策模型,不能只看缓存命中率:长上下文的 Decode 注意力成本、压缩可能造成的错误以及摘要调用本身的费用都要计入。