上下文预处理
1. 文档目标
本文聚焦 Claude Code 在正式向模型发起一轮请求前,如何对会话上下文做“压缩前预处理”和“压缩执行”。这里的“上下文压缩”不是单一功能,而是几层机制叠加:
- 查询前的轻量裁剪
- 请求前的微压缩(microcompact)
- 超阈值后的自动压缩(autocompact)
- 基于 session memory 的替代压缩
- 手动
/compact与局部 partial compact - 压缩后关键上下文的重新注入
核心入口主要在:
src/query.tssrc/services/compact/autoCompact.tssrc/services/compact/microCompact.tssrc/services/compact/compact.tssrc/services/compact/sessionMemoryCompact.tssrc/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 类工具
GrepGlobWebSearchWebFetchFileEditFileWrite
关键点:
- 只对主线程查询源生效,避免污染子 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() 的顺序是:
- 先尝试
sessionMemoryCompact - 不满足再走传统
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_referenceplan_modeinvoked_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负责消息合法化和压缩边界切片
所以如果后续要继续分析,可以优先把问题拆成三类:
- 请求前到底裁掉了什么
- 真正送去摘要模型的输入长什么样
- 压缩后哪些信息被重新注入,哪些信息被永久丢弃