上下文预处理

1. 文档目标

本文聚焦 Claude Code 在正式向模型发起一轮请求前,如何对会话上下文做“压缩前预处理”和“压缩执行”。这里的“上下文压缩”不是单一功能,而是几层机制叠加:

  • 查询前的轻量裁剪
  • 请求前的微压缩(microcompact)
  • 超阈值后的自动压缩(autocompact)
  • 基于 session memory 的替代压缩
  • 手动 /compact 与局部 partial compact
  • 压缩后关键上下文的重新注入

核心入口主要在:

  • src/query.ts
  • src/services/compact/autoCompact.ts
  • src/services/compact/microCompact.ts
  • src/services/compact/compact.ts
  • src/services/compact/sessionMemoryCompact.ts
  • src/utils/messages.ts

2. 总体链路

主查询循环在 src/query.ts 中,每轮请求大致按下面顺序处理上下文:

messages
  -> getMessagesAfterCompactBoundary()
  -> applyToolResultBudget()
  -> HISTORY_SNIP(若开启)
  -> microcompactMessages()
  -> CONTEXT_COLLAPSE(若开启)
  -> autoCompactIfNeeded()
  -> 真正调用模型

这意味着“压缩”并不是在上下文爆掉时才一次性做,而是在每轮请求前就开始逐层减负。

3. 第一层:只保留最后一个 compact 边界之后的会话

getMessagesAfterCompactBoundary() 位于 src/utils/messages.ts。

它的作用是:

  • 找到最后一个 compact_boundary
  • 只保留这个边界之后的消息
  • 默认还会过滤掉已经被 snip 标记裁掉的消息

这一步非常关键,因为它保证后续压缩、摘要、恢复都只围绕“当前活跃上下文段”工作,而不是反复处理历史上已经被摘要过的旧段。

4. 第二层:请求前轻量预处理

4.1 tool result 预算裁剪

在 query.ts 中,applyToolResultBudget() 先对聚合后的工具结果做预算控制。这一层不是摘要,而是把超大的工具输出替换为更短的内容表示,避免单条工具结果过度吞掉上下文窗口。

4.2 HISTORY_SNIP

如果开启 HISTORY_SNIP,snipCompactIfNeeded() 会先把低价值历史消息从“模型可见视图”里投影掉,但 UI 仍可保留完整滚动历史。

特点:

  • 主要解决“模型要看的上下文”过长
  • 不直接改写全部会话存储
  • 会把节省出来的 token 数回传给 autocompact 阈值判断

5. 第三层:microcompact

src/services/compact/microCompact.ts 提供两种微压缩路径,本质都不做“语义摘要”,只清理高成本工具结果。

5.1 Cached microcompact

这是默认主路径之一,目标是:

  • 找出可压缩工具的 tool_use_id
  • 不直接改写本地消息
  • 而是在 API 层插入 cache_edits
  • 让旧工具结果从服务端 prompt cache 里被删除

可压缩工具包括:

  • FileRead
  • shell 类工具
  • Grep
  • Glob
  • WebSearch
  • WebFetch
  • FileEdit
  • FileWrite

关键点:

  • 只对主线程查询源生效,避免污染子 agent 的状态
  • 通过 pendingCacheEdits 延后到 API 调用层插入
  • API 返回后再根据真实的 cache_deleted_input_tokens 生成 microcompact_boundary

所以 cached microcompact 的特点不是“改消息”,而是“保缓存、删缓存里的旧工具结果”。

5.2 Time-based microcompact

evaluateTimeBasedTrigger() / maybeTimeBasedMicrocompact() 会在“距离上一次 assistant 消息时间过长”时触发。

默认思路是:

  • 如果服务端 prompt cache 大概率已经过期
  • 那就没必要继续保留很老的工具结果正文
  • 直接把旧 tool_result.content 替换成 [Old tool result content cleared]
  • 仅保留最近 keepRecent 个工具结果

这是一次真正的本地消息改写,但只改工具结果正文,不做语义摘要。

6. 第四层:何时触发完整压缩

src/services/compact/autoCompact.ts 负责自动压缩判定。

6.1 阈值计算

自动压缩阈值大致是:

effectiveContextWindow = 模型上下文窗口 - 预留摘要输出 token
autoCompactThreshold = effectiveContextWindow - 13000

其中:

  • 预留摘要输出 token 默认 20000
  • warning buffer 默认 20000
  • manual blocking buffer 默认 3000

因此压缩并不是“正好撞满窗口才触发”,而是提前留出安全余量。

6.2 自动压缩优先级

当 shouldAutoCompact() 为真时,autoCompactIfNeeded() 的顺序是:

  1. 先尝试 sessionMemoryCompact
  2. 不满足再走传统 compactConversation

这说明 session memory 压缩是优先分支,不是附属功能。

7. 第五层:传统完整压缩的预处理

传统完整压缩实现位于 src/services/compact/compact.ts 的 compactConversation()。

它在真正发出摘要请求前,会做几类重要预处理。

7.1 执行 PreCompact hooks

先跑 executePreCompactHooks(),允许外部 hook:

  • 注入额外的 compact 指令
  • 给用户显示附加提示

之后通过 mergeHookInstructions() 把用户传入的 compact 指令和 hook 生成的指令合并。

7.2 只摘要 compact 边界后的消息

streamCompactSummary() 内部会对输入执行:

  • getMessagesAfterCompactBoundary(messages)
  • 再把本轮 summaryRequest 追加进去

所以完整压缩也不会把更早的历史段再次送给摘要模型。

7.3 去除图片和文档

stripImagesFromMessages() 会把:

  • 用户消息中的 image
  • 用户消息中的 document
  • tool_result 里的嵌套图片/文档

统一替换成 [image] 或 [document]。

目的有两个:

  • 减少压缩请求自身的 token
  • 保留“这里曾经出现过图片/文档”这一语义痕迹

7.4 去除会在压缩后重新注入的附件

stripReinjectedAttachments() 会移除部分附件,例如技能发现类附件,因为这些内容会在压缩后重新挂回上下文,提前送去做摘要只会浪费 token,还会把过时建议污染到摘要里。

7.5 消息归一化,修正 API 合法性

发送给压缩模型前会经过 normalizeMessagesForAPI(),而这一步本身也是很重要的“上下文预处理”。

它会做的事包括:

  • 重排 attachment,尽量贴近相关 assistant/tool_result
  • 过滤 display-only 的消息
  • 合并连续 user 消息
  • 清理失效的 tool_reference
  • 处理过大的 image/pdf 错误后的残留 meta 消息
  • 统一 assistant tool input 结构
  • 通过 ensureToolResultPairing() 修复缺失、重复、孤儿化的 tool_use/tool_result

这里的意义是:压缩请求要先保证自己是一个 API 可接受的消息流,否则连“做摘要”都可能失败。

7.6 禁止压缩 agent 再去调用工具

压缩 prompt 在 src/services/compact/prompt.ts 里,明确要求:

  • 只能输出纯文本
  • 必须返回 <analysis> + <summary>
  • 不允许调用任何工具

在 forked-agent 路径里,还会通过 createCompactCanUseTool() 从权限层直接拒绝工具调用。

因此压缩模型的职责是“总结上下文”,不是继续执行任务。

8. 压缩请求失败时的兜底预处理

如果压缩请求本身也触发 prompt too long,系统不会直接失败,而是执行 truncateHeadForPTLRetry()。

它会:

  • 按 groupMessagesByApiRound() 对消息按 API 往返分组
  • 优先丢弃最老的若干组
  • 必要时插入一个 synthetic user marker
  • 保证切掉前缀后仍是合法的 API 消息序列

也就是说,传统 compact 还有一层“为让摘要请求本身先活下来”的二次裁剪。

9. 第六层:session memory 压缩

src/services/compact/sessionMemoryCompact.ts 提供另一条路径:不用现场重新总结整段会话,而是直接使用已经提炼好的 session memory。

9.1 触发前提

需要同时满足:

  • session memory 功能开启
  • compact 相关开关开启
  • session memory 文件存在且不为空

9.2 保留尾部消息的策略

它不会简单地“只留下最后几条消息”,而是通过 calculateMessagesToKeepIndex() 计算保留起点,约束包括:

  • 至少保留一定 token 数
  • 至少保留一定数量的 text-block 消息
  • 不能超过 maxTokens
  • 不能拆断 tool_use/tool_result
  • 不能丢失与同一 message.id 相关的 thinking 块

这个保留策略由 adjustIndexToPreserveAPIInvariants() 保证 API 结构完整。

9.3 session memory 也会做长度截断

truncateSessionMemoryForCompact() 会控制 session memory 的 section 大小,避免“压缩摘要本身”过长,反而又把上下文塞满。

10. 第七层:压缩后的上下文恢复

压缩不是“生成一段摘要后就完了”。compactConversation() 和 partial compact 在摘要完成后,会主动恢复若干关键上下文。

10.1 恢复最近读取过的文件

createPostCompactFileAttachments() 会:

  • 从 readFileState 里找最近读过的文件
  • 重新读取真实文件内容
  • 按最近访问时间排序
  • 最多恢复 5 个文件
  • 单文件最多约 5000 token
  • 总预算最多约 50000 token

并且会跳过:

  • plan 文件
  • memory 文件
  • 已经在 preserved tail 中可见的 Read 结果

10.2 恢复计划、计划模式、技能、异步 agent 状态

压缩后可能附带恢复:

  • plan_file_reference
  • plan_mode
  • invoked_skills
  • 后台 agent 的 task_status

其中技能内容还会做截断,优先保留文件头部,避免每次 compact 都重新灌入完整技能文本。

10.3 恢复工具、agent 列表和 MCP 指令差量

因为 compact 会吃掉之前的 delta attachment,所以系统会重新生成:

  • deferred tools delta
  • agent listing delta
  • MCP instructions delta

这样下一轮对话仍然知道当前可用的工具、agent 和 MCP 说明。

10.4 再跑 SessionStart / PostCompact hooks

压缩成功后还会执行:

  • processSessionStartHooks('compact')
  • executePostCompactHooks()

这保证 CLAUDE.md、hook 指令和会话状态在 compact 后重新建立。 CLAUDE.md 的发现、注入与重载细节,可进一步参见《CLAUDE.md 工作机制》。

11. partial compact 的特殊点

partialCompactConversation() 支持围绕某个 pivot 做局部摘要,有两种方向:

  • from:摘要 pivot 之后的消息,保留更早的上下文
  • up_to:摘要 pivot 之前的消息,保留更新的上下文

它的本质不是单独一套实现,而是复用完整 compact 逻辑,只是:

  • messagesToSummarize 与 messagesToKeep 的选取不同
  • up_to 会主动去掉保留段里的旧 compact boundary / compact summary
  • 后续恢复 attachment 时会只补“被摘要段失去的那部分信息”

12. 结论

这个项目里的“上下文压缩机制”可以概括为一句话:

先用轻量规则尽量减少无价值 token,再在必要时把旧会话折叠成摘要,并把继续工作所需的文件、计划、技能和运行态重新挂回上下文。

从实现上看,它不是一个单点函数,而是一条完整流水线:

  • query.ts 负责压缩编排
  • microCompact.ts 负责低成本削减工具结果
  • autoCompact.ts 负责阈值与触发
  • sessionMemoryCompact.ts 负责优先走 session memory 摘要
  • compact.ts 负责传统摘要压缩与压缩后恢复
  • messages.ts 负责消息合法化和压缩边界切片

所以如果后续要继续分析,可以优先把问题拆成三类:

  1. 请求前到底裁掉了什么
  2. 真正送去摘要模型的输入长什么样
  3. 压缩后哪些信息被重新注入,哪些信息被永久丢弃