microcompact-deep-dive
Claude Code 上下文压缩(二):Microcompact 微压缩深度解析
这是 Claude Code 源码学习系列的第二篇。第一篇介绍了 Snip Compact(消息精简化删除),本文聚焦第二级压缩——Microcompact。
一、它在哪一层?
Claude Code 的上下文压缩是一个三级递进式防护体系。每轮对话开始前,按顺序执行:
Snip Compact(删除整条消息)
↓
Microcompact(清除工具结果内容) ← 本文
↓
Context Collapse(折叠历史上下文)
↓
AutoCompact(LLM 全文总结)
Microcompact 是最轻量、最高频的一级:不调用 LLM、每次 API 请求前自动执行。
二、解决什么问题?
在 AI 编程助手的对话中,大量 token 被工具调用的结果占据——Read 读出的文件内容、Bash 输出的命令结果、Grep 的搜索结果。这些结果在模型消费过后,原文就不再需要了。但如果不清理,它们会永久留在对话中,挤占宝贵的上下文窗口。
最直接的想法是直接把那些旧的 tool_result 内容替换成 "[已清除]"。但这有个严重问题:
Prompt Caching 与破坏缓存的矛盾
Anthropic API 支持 prompt caching:服务端把请求前缀的 KV cache 存下来。下次请求如果前缀的字节序列完全一致,服务端直接复用缓存,跳过 attention 重计算。
请求 N: [system][msg0][msg1(含 5000行 Read 结果)][msg2]
↑
cache_control 标记
请求 N+1: [system][msg0][msg1(含 5000行 Read 结果)][msg2][新 msg3]
↑ 这段字节和上次完全一样 → 缓存命中 ✓
如果你改了 msg1 的内容:
请求 N+1: [system][msg0][msg1(含 "[已清除]")][msg2][新 msg3]
↑ msg1 的字节变了 → hash 不匹配 → 缓存全部作废 ❌
Microcompact 要达成的目标:既清除旧的工具结果,又不破坏 prompt cache。
三、两条路径:聪明地分情况处理
Microcompact 的核心入口在 services/compact/microCompact.ts。它先判断缓存状态,选择不同策略:
microcompactMessages()
│
├─ 缓存是否超时(>60分钟无互动)?
│ YES → Time-Based Microcompact(直接内容清空)
│
└─ 缓存还热?
YES → Cached Microcompact(cache_edits 路径)
NO → 什么都不做
关键洞察:两条路径的设计不是随意的。缓存超时的时候,反正下次请求缓存也不会命中,可以直接改消息内容;缓存还热的时候,用更优雅的方式绕过。
四、路径一:Cached Microcompact——不碰消息,给服务端发"删除指令"
这是 Microcompact 最精巧的部分,也是代码中 feature('CACHED_MICROCOMPACT') 门控的 ant-only 实验特性。
4.1 核心机制:cache_edits
Anthropic API 的 cache editing 允许客户端在请求中附带一个 cache_edits block:
{
"type": "cache_edits",
"edits": [
{ "type": "delete", "cache_reference": "tool_abc123" }
]
}
这个 block 不修改消息本体,而是给服务端发一条指令:“把 cache_reference 为 tool_abc123 的 KV cache 条目从缓存页中删除。”
4.2 端到端流程
Step 1: 收集可压缩工具
遍历所有消息,找出工具调用 ID。白名单:
Read, Bash, Grep, Glob, WebSearch, WebFetch, Edit, Write
Step 2: 注册追踪(cachedMicrocompact.ts 中)
每遇到一个新工具结果 → 注册到全局 state 中
记录:tool_use_id + 所属哪个 user message(用于后续 pin 位置)
Step 3: 判断是否需删除
由 GrowthBook 远程配置控制:
- triggerThreshold: 累计达到多少条后触发删除
- keepRecent: 始终保留最近 N 条(给模型参考最近操作)
Step 4: 生成 cache_edits block
createCacheEditsBlock() → { type: "cache_edits", edits: [...] }
→ 存入 pendingCacheEdits(等待消费)
Step 5: API 层消费(claude.ts → addCacheBreakpoints)
consumePendingCacheEdits() → 读取
insertBlockAfterToolResults() → 注入到最后一个 user message 中
pinCacheEdits() → 记录位置
4.3 为什么需要 Pin
cache_edits 的效果不是永久的——它只在发送它的那一轮请求中告诉服务端删除缓存。下一轮如果不重发,被删除的条目会在服务端恢复。
所以系统用 pinCacheEdits() 记录每个 cache_edits block 在哪个消息位置。后续每轮请求都在相同位置重新注入相同指令:
请求 N: [msg0][msg1][msg2 ← cache_edits {del tool_A}]
请求 N+1: [msg0][msg1][msg2 ← 同样的 cache_edits {del tool_A}][msg3 ← 新的 cache_edits {del tool_B}]
请求 N+2: [msg0][msg1][msg2 ← 还在][msg3 ← 还在][msg4 ← 新注入]
位置必须固定——因为 prompt cache 是按字节位置匹配的。如果换位置,msg2 的字节就变了 → 缓存断裂。
4.4 主线程独享
Cached MC 的状态是模块级全局变量。主线程和子代理(forked agent)共享进程空间。如果子代理的工具结果也被注册到全局 state,主线程后续会尝试删除不存在于自己对话中的工具。所以 Cached MC 只对主线程生效:
function isMainThreadSource(querySource) {
return !querySource || querySource.startsWith('repl_main_thread')
}
五、路径二:Time-Based Microcompact——缓存已冷,直接清
当 (当前时间 - 最后一条 assistant 消息的时间) > 60分钟,服务端的 prompt cache(TTL 为 1 小时)几乎必然过期。既然缓存已经没了,不妨直接在客户端替换内容:
// 保留最近 N 条,清空其余
block.content = '[Old tool result content cleared]'
这样下一次请求的消息体本身就是缩小后的。同时调用 resetMicrocompactState() 重置 Cached MC 的状态——因为缓存已冷,之前的追踪状态和服务端不再一致。
六、可压缩工具的白名单
const COMPACTABLE_TOOLS = new Set([
'Read', // 文件读取 —— 一次性参考
'Bash', // 命令执行 —— 结果已消费
'Grep', // 代码搜索 —— 结果已消费
'Glob', // 文件匹配 —— 结果已消费
'WebSearch', // 网页搜索 —— 结果已消费
'WebFetch', // 网页抓取 —— 结果已消费
'Edit', // 文件编辑 —— 结果已消费
'Write', // 文件写入 —— 结果已消费
])
这些工具的共同特点:输出是一次性消费的上下文参考,后续对话只需要知道"做了什么",不需要原文。
不在白名单中的如 AgentTool、TaskTool、SkillTool——它们的结果可能需要在后续引用。
七、总结
Microcompact 是 Claude Code 上下文管理中最轻量的一层。它巧妙地利用了 Anthropic API 的 cache editing 机制,在不破坏 prompt cache、不修改消息体、不调用 LLM 的前提下,用零 API 成本实现了工具结果的有效清理。
它的设计体现了几个工程智慧:
- 分路径处理——根据缓存是否还热选择策略,不浪费已建立的缓存
- 消息不可变原则——Cached 路径完全不动消息体,所有操作在 API 请求构建层完成
- Pin 机制——保证 cache_edits 的持续生效和缓存位置稳定
- 主线程隔离——避免子代理污染全局压缩状态
下一篇将介绍第三级压缩:Context Collapse(上下文折叠)。本文全部来自博主学习和与AI的问答整理。
更多推荐

所有评论(0)