Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

上下文预算与压缩

Agent 的上下文会随工具回合不断增长。如果只在到达模型硬上限后做一次全文总结,既浪费已经可以清理的大块工具结果,又可能因为 Prompt 已经太大而连总结请求都无法发出。

Claude Code 因此使用一套从便宜、细粒度到昂贵、高粒度的分层上下文管理。

请求前的固定顺序

flowchart LR
    A["工具结果大小预算"] --> B["历史截剪"]
    B --> C["微压缩"]
    C --> D["细粒度上下文折叠"]
    D --> E["自动完整压缩"]
    E --> F["请求模型"]
    F --> G["响应式压缩<br/>只在真实过长后恢复"]

顺序很重要:先让丢失最小的机制尝试释放空间,如果已经足够,就不必过早把整段对话替换成摘要。

第一层:限制工具结果的成本

文件读取、搜索、Shell 和网页结果往往是上下文中增长最快的部分。它们的原始内容对刚完成的决策很重要,但不一定需要永久保留。

因此运行时可以对工具结果设置预算,把过大内容截断、清理或替换为稳定占位说明。目标是保留“曾经执行过并得到某类结果”的协议骨架,而不是永久重复全量数据。

第二层:历史截剪与微压缩

微压缩不是完整对话摘要,而是对可压缩工具结果和思考数据做局部处理。当前源码快照可确认三类边界:

按时间清理旧结果

条件启用时,当主线会话经历长时间空闲且 Prompt cache 已经变冷,可以清理更旧的可压缩工具结果,保留最近若干个。

这个设计的精妙点是利用了“缓存已经冷却”这个时机:既然下一次本来就很难命中旧前缀,便可用更小历史重新开始。

使用 API 缓存编辑

在支持的模型与主线路径中,微压缩可以不改写本地 transcript,而是在下一次 API 请求中要求删除缓存里某些工具结果。只有 API 确认实际删除量后,本地才记录压缩边界。

它把“审计 transcript”和“本次模型实际需要的缓存内容”进一步分开。

API 端上下文管理

条件启用时,请求可携带清理旧思考或工具结果的策略。这与本地微压缩是不同层次:一个决定本地如何投影历史,另一个请求服务器管理已缓存上下文。

某些缓存微压缩与上下文折叠的内部模块不在当前源码快照中,因此本教程只陈述可验证的调用条件、输出契约和状态转移,不推测其内部删取算法。

第三层:自动完整压缩

当便宜清理仍不足以释放空间时,主线可以在达到硬上限之前主动进行完整压缩。

压缩阈值不能等于模型全部上下文窗口。它还要为压缩摘要本身、下一次输出和安全缓冲预留空间。

自动完整压缩还有多个保护:

  • 用户可以关闭自动压缩;
  • 专用压缩或 Session Memory 查询自身不再递归自动压缩;
  • 响应式专用模式或上下文折叠可以抑制主动完整压缩;
  • 连续压缩失败会触发断路器,避免每次迭代反复消耗注定失败的 API 请求。

压缩的输出不只是一段摘要

完整压缩后的上下文大致由下列部分重建:

压缩边界 → 摘要消息 → 必须保留的近期消息 → 恢复的附件 → Hook 结果

需要恢复的不只是文件路径,还包括:

  • 最近的关键文件上下文;
  • 当前 Plan 与 Plan Mode;
  • 已调用且仍有效的 Skills;
  • 异步 Agent 与任务状态;
  • 动态工具、Agent 列表和 MCP 变化。

压缩因此不是“把聊天记录换成一段总结”,而是一次运行状态重建

压缩摘要也尽量复用主线 Prompt cache

条件允许时,压缩摘要会使用一个受限 Fork,复用主线已渲染的 system、工具和历史前缀。该 Fork 不执行工具,只运行一轮摘要,并且避免为它的私有尾巴写入新缓存。

这里甚至要避免随意改变最大输出,因为输出上限可能间接改变思考预算,从而让本来相同的前缀无法命中缓存。

第四层:真实过长之后的响应式压缩

主动阈值是根据本地使用量预估,可能与提供方的真实接受边界有差异。因此当 API 实际返回 prompt too long 时,条件启用的响应式压缩作为最后恢复层。

它只在真实错误后运行,并且同一用户回合仅尝试一次,防止“压缩 → 仍然过长 → 再压缩”的无限螺旋。

当前源码快照没有包含该模块的内部删取算法,因此只能确认其进入条件、结果形状、单次保护和失败类型。

Prompt cache 与压缩是同一设计问题

压缩会改变历史,而 Prompt cache 依赖字节前缀稳定。因此上下文管理必须同时考虑:

  • system 稳定段与动态段的缓存作用域;
  • 消息级只保留一个有效 cache marker;
  • 普通请求把 marker 放在最后消息,不写入缓存的 Fork 放在最后一个共享前缀点;
  • 某些 beta、快速模式和缓存策略在会话内使用粘性快照,避免中途翻转破坏 cache key;
  • 动态 Agent 列表、日期变化和 MCP 变化尽量以尾部 delta 表达,而不回头改写旧前缀。

这是整个压缩架构最精妙的地方:它不是单纯追求“Token 更少”,而是在语义连续、协议合法、缓存稳定和恢复成本之间取得平衡。

条件启用的任务 Token 预算如何穿过压缩

当调用方提供 API 任务 Token 预算时,压缩摘要会把大量旧历史隐藏在更小文本后面。如果任务预算只根据压缩后可见 Token 重新计算,就会“忘记”压缩前已经消耗的预算。

因此运行时会在压缩时记住压缩前最后一次 API 给出的权威上下文使用量,从剩余预算中扣除,再在后续请求中显式携带。多次压缩会继续累计,不会重置任务消耗。

压缩后的连续性是重建出来的

自动压缩后仍能继续 Plan Mode、Skills 和异步 Agent,不是因为它们的所有原始文本都还在模型窗口。而是因为运行时在压缩后重新附加仍有效的状态和指令。

这是理解“长会话连续性”的关键:连续性来自被精心选择并重建的状态,不是无限上下文。

源码定位

  • 压缩调度顺序:src/query.ts
  • 微压缩边界:src/services/compact/microCompact.tssrc/services/compact/apiMicrocompact.ts
  • 自动完整压缩:src/services/compact/autoCompact.tssrc/services/compact/compact.ts
  • 压缩后消息重建:src/services/compact/compact.ts
  • Prompt cache 与缓存编辑:src/services/api/claude.tssrc/utils/api.ts