Claude code源码精读之 上下文压缩
主流程如下。每个步骤的输出是下一步的输入,形成串行管道。Snip 和 Microcompact 的释放 token 数会传递给 autocompact 的阈值计算(snipTokensFreed),避免重复压缩。

messagesForQuery(原始消息)`
` ↓ applyToolResultBudget() --- 工具结果预算截断(按 maxResultSizeChars)`
` ↓ snipCompactIfNeeded() --- 历史 Snip 压缩(HISTORY_SNIP feature)`
` ↓ microcompact() --- 微压缩(工具结果摘要)`
` ↓ applyCollapsesIfNeeded() --- 上下文折叠(CONTEXT_COLLAPSE feature)`
` ↓ autocompact() --- 自动压缩(超出阈值时触发)`
`messagesForQuery(处理后的消息)→ 发往 API`
`
关键算法步骤详解
候选收集与分组 (collectCandidatesByMessage)
分组逻辑:
- 边界条件:仅 user,assistant 消息创建分组边界
- 相同 ID 合并:跟踪 seenAsstIds,相同 ID 的助手消息不触发 flush()
- 忽略类型:progress/attachment/system 消息不创建边界
const flush` `=` `()` `=>` `{`
`if` `(current.length >` `0) groups.push(current)`
` current =` `[]`
`}`
` const seenAsstIds =` `new` `Set<string>()`
`for` `(const message of messages)` `{`
`if` `(message.type ===` `'user')` `{`
` current.push(...collectCandidatesFromMessage(message))`
`}` `else` `if` `(message.type ===` `'assistant')` `{`
`if` `(!seenAsstIds.has(message.message!.id ??` `''))` `{`
`flush()`
` seenAsstIds.add(message.message!.id ??` `'')`
`}`
`}`
`// progress / attachment / system are filtered or merged by`
`// normalizeMessagesForAPI --- they don't create wire boundaries.`
`}`
`flush()`
`return groups`
`
- 候选分区 (partitionByPriorDecision)
// toolResultStorage.tsL649-L667`
`function` `partitionByPriorDecision(candidates, state): CandidatePartition`
`return candidates.reduce<CandidatePartition>(`
`(acc, c)` `=>` `{`
`const replacement = state.replacements.get(c.toolUseId)`
`if` `(replacement !== undefined)` `{`
` acc.mustReapply.push({` `...c, replacement })`
`}` `else` `if` `(state.seenIds.has(c.toolUseId))` `{`
` acc.frozen.push(c)`
`}` `else` `{`
` acc.fresh.push(c)`
`}`
`return acc`
`},`
`{ mustReapply:` `[], frozen:` `[], fresh:` `[]` `},`
`)`
`
分区规则:
mustReapply:state.replacements 中存在 → 必须重新应用缓存替换
frozen:state.seenIds 中存在但无替换记录 → 已冻结,不可更改
fresh:既不在 seenIds 也不在 replacements → 新鲜,可决策
- 选择替换候选 (selectFreshToReplace)
选择策略:
按大小降序排序:优先替换最大的结果
贪心算法:从大到小选择,直到总大小 ≤ 限制
近似计算:使用原始大小而非替换后大小(预览约 2K,远小于原始大结果)
边界情况:
仅 fresh 候选可被选择
frozen 结果即使超预算也接受(由微压缩后续处理)
- 持久化工具结果persistToolResult
将内容持久化到硬盘上。并生成对应的预览(即截断)。
function` `generatePreview(`
`content:` `string,`
`maxBytes: number,`
`):` `{ preview:` `string; hasMore: boolean }` `{`
`if` `(content.length <= maxBytes)` `{`
`return` `{ preview: content,` `hasMore:` `false` `}`
`}`
`// Find the last newline within the limit to avoid cutting mid-line`
`const` `truncated` `= content.slice(0, maxBytes)`
`const` `lastNewline` `= truncated.lastIndexOf('\n')`
`// If we found a newline reasonably close to the limit, use it`
`// Otherwise fall back to the exact limit`
`const` `cutPoint` `= lastNewline > maxBytes *` `0.5` `? lastNewline : maxBytes`
`return` `{ preview: content.slice(0, cutPoint),` `hasMore:` `true` `}`
`}`
`
具体流程图如下:

历史 Snip 压缩( snipCompactIfNeeded)
扫描消息数组,查找最后一个 snip_boundary 系统消息,并根据其元数据移除指定的历史消息,实现选择性历史压缩。
实际应用场景
场景一:长时间会话内存控制
用户与 Claude 交互 50 轮后,上下文窗口接近饱和`
`→ 模型调用 snip 工具标记前 20 条消息为可移除`
`→ snipCompactIfNeeded 找到边界,移除指定消息`
`→ 释放约 8,000 令牌,上下文窗口恢复可用空间`
`
场景二:项目切换时的历史清理
用户完成模块A,开始处理模块B`
`→ 模型使用 /force-snip 压缩模块A相关历史`
`→ 保留关键决策和结果,移除详细实现讨论`
`→ 保持上下文相关性,避免无关信息干扰`
`
export` `function` `snipCompactIfNeeded(`
` messages: Message[],`
` _options?:` `{ force?:` `boolean` `},`
`):` `{`
` messages: Message[]`
` executed:` `boolean`
` tokensFreed:` `number`
` boundaryMessage?: Message`
`}` `{`
`// Find the last snip_boundary message`
`let boundaryIdx =` `-1`
`let removedUuids:` `string[]` `|` `undefined`
`for` `(let i = messages.length -` `1; i >=` `0; i--)` `{`
`const msg = messages[i]!`
`if` `(`
` msg.type ===` `'system'` `&&`
`(msg as Record<string,` `unknown>).subtype ===` `'snip_boundary'`
`)` `{`
` boundaryIdx = i`
`const meta =` `(msg as Record<string,` `unknown>).snipMetadata as`
`|` `{ removedUuids?:` `string[]` `}`
`|` `undefined`
` removedUuids = meta?.removedUuids`
`break`
`}`
`}`
`
- snip_boundary 是什么时候更新到历史消息里的?
snip_boundary 是 Claude Code 中历史消息压缩(Snip)功能的核心标记,它是一个特殊的系统消息,用于记录哪些历史消息应该被从模型上下文中移除。其插入时机主要有以下两种场景:
- 用户显式执行 /force-snip 命令时
触发时机:当用户在 REPL 中输入 /force-snip 命令时,系统会立即在消息数组末尾插入一个 snip_boundary 消息。
//force-snip.tsL30-L42`
`const boundaryMessage: Message = {`
` type: 'system',`
` subtype: 'snip_boundary',`
` content: '[snip] Conversation history before this point has been snipped.',`
` isMeta: true,`
` timestamp: new Date().toISOString(),`
` uuid: randomUUID(),`
` snipMetadata: {`
` removedUuids, // 所有当前消息的 UUID 数组`
` },`
`} as Message`
`setMessages(prev => [...prev, boundaryMessage])`
`
- 模型自主调用 Snip 工具
流程:
模型决策:模型认为某些历史消息不再需要完整保留
调用 Snip 工具:指定 message_ids 和可选的 reason
工具结果返回:Snip 工具返回执行结果(仅记录意图)
查询引擎拦截:在 query() 函数的下一个循环开始前,查询引擎拦截 Snip 工具结果
插入边界消息:系统创建 snip_boundary 消息,仅包含指定消息的 UUID
投影系统生效:snipCompactIfNeeded() 在查询前应用压缩
Snip 工具,相关提示词
Snip messages from your conversation history to free up context window space. Snipped messages are replaced with a compact summary so you retain awareness of what happened without the full content.`
`Use this when:`
`- Your context is getting full and you need to make room`
`- Earlier messages contain large tool outputs you no longer need in full`
`- You want to compact a long exploration sequence into a summary`
`Guidelines:`
`- Only snip messages you're confident you won't need verbatim again`
`- The summary replacement preserves key facts (file paths, decisions, errors found)`
`- You cannot un-snip --- the original content is gone from context`
`
微压缩( microcompact )
Claude Code 微压缩(Microcompact)系统的核心执行引擎,负责在查询前对工具结果进行智能压缩,以控制上下文窗口大小,同时最大化提升缓存命中率。
- 方法签名与定位
microCompact.ts L257-L297`
`export async function microcompactMessages(`
` messages: Message[],`
` toolUseContext?: ToolUseContext,`
` querySource?: QuerySource,`
`): Promise<MicrocompactResult> {`
`
- 核心算法分支详解
- 时间基微压缩 (maybeTimeBasedMicrocompact)
触发条件:
- 自上次助手消息的时间间隔 ≥ 配置阈值(默认 30 分钟)
- 查询源为主线程(repl_main_thread*),是主agent的才进行压缩,子agent的压缩不在这里处理
- 存在可压缩的工具结果
内容替换:将旧工具结果内容替换为 Old tool result content cleared可压缩工具集合:
const COMPACTABLE_TOOLS = new Set<string>([`
` FILE_READ_TOOL_NAME, // 文件读取`
` ...SHELL_TOOL_NAMES, // Shell 命令`
` GREP_TOOL_NAME, // 文本搜索`
` GLOB_TOOL_NAME, // 文件匹配`
` WEB_SEARCH_TOOL_NAME, // 网络搜索`
` WEB_FETCH_TOOL_NAME, // 网页抓取`
` FILE_EDIT_TOOL_NAME, // 文件编辑`
` FILE_WRITE_TOOL_NAME, // 文件写入`
`])`
`
- 缓存微压缩路径 (cachedMicrocompactPath)
负责通过 Anthropic API 的 cache_edits 功能智能删除服务器端缓存的旧工具结果,从而控制上下文窗口大小,同时最大化提示缓存命中率。
删除决策算法
基于工具结果数量触发,非时间或令牌阈值。
//cachedMicrocompact.tsL87-L94`
`export function getToolResultsToDelete(state: CachedMCState): string[] {`
` const { triggerThreshold, keepRecent } = getCachedMCConfig()`
` const active = state.toolOrder.filter(id => !state.deletedRefs.has(id))`
` if (active.length <= triggerThreshold) return []`
` const toDelete = active.slice(0, active.length - keepRecent)`
` return toDelete`
`}`
`
FIFO + 保尾策略:
- 先进先出:删除最早注册的工具结果
- 保留最近:始终保留最后 keepRecent 个结果
- 阈值触发:仅当活跃结果数 > triggerThreshold 时执行
作为 microcompactMessages 的高级路径,cachedMicrocompactPath 体现了 Claude Code 对提示缓存优化的极致追求:
- 精准外科手术:通过 cache_edits API 删除特定条目,而非整个前缀
- 状态感知路由:根据缓存冷暖自动选择最优压缩策略
- 零本地修改:保持消息内容不变,仅通过指令控制服务器行为
- 长期会话优化:FIFO+保尾策略平衡历史清理与上下文保持
"零本地修改" 指的是 cachedMicrocompactPath 方法在执行缓存微压缩时,不改变本地消息数组的内容,而是通过 pendingCacheEdits 将压缩指令传递给 API 层,由 Anthropic 服务器在缓存层面执行删除操作。不修改消息内容 → 缓存前缀保持不变;后续请求可以继续利用现有缓存;避免因内容修改导致的缓存完全失效。
服务端删除了部分工具结果后,会重新生成上下文,那么又如何保证模型的前缀缓存命中?
通过 cache_control 标记定义的精确边界内。每个 API 请求中只有一个 cache_control 标记,它定义了缓存前缀的终点。保持前缀哈希:缓存前缀的哈希值不变,因为:
- 边界标记位置不变
- 条目删除只是内容清空,不影响结构哈希
缓存命中的实际效益
性能提升
- 响应延迟:命中缓存时降低 200-500ms(跳过多层 Transformer 计算)
- 计算成本:减少服务器端 FLOPs,降低 API 成本
- 用户体验:长会话中保持流畅响应,避免"越用越慢"
原始缓存前缀:`
`[消息1][消息2][工具结果A][工具结果B][工具结果C][cache_control][当前消息]`
` ↑ ↑`
` 缓存起点 缓存终点(固定书签)`
`执行 cache_edits 删除工具结果B:`
`[消息1][消息2][工具结果A][空位][工具结果C][cache_control][当前消息]`
` ↑`
` 缓存终点不变,内部有空位`
`后续请求:`
`- 缓存前缀:消息1+消息2+工具结果A+空位+工具结果C,消息序列骨架相同,则结构哈希不变。就能命中模型的推理缓存。`
`- 有效长度:减少(空位不占令牌)`
`- 边界位置:完全不变`
`
这是 Anthropic(Claude 模型)的专有技术,主流大模型并不普遍支持。主流大模型支持的还是普通的前缀缓存匹配。
- 实际应用场景
- 场景一:长时间中断后恢复工作
用户上午与 Claude 交互,生成大量文件读取结果`
`→ 午休 2 小时后返回`
`→ 时间间隔 > 30 分钟阈值`
`→ 时间基微压缩触发`
`→ 保留最近 3 个文件读取结果,其余替换为清理消息`
`→ 减少重写冷缓存时的令牌消耗`
`
- 场景二:密集工具使用中的缓存管理
用户连续执行 10 个 grep 命令`
`→ 工具结果数量达到缓存微压缩触发阈值(如 8 个)`
`→ 缓存微压缩路径执行`
`→ 创建 cache_edits 块,标记前几个结果从缓存删除`
`→ 保持缓存前缀有效,仅删除旧条目`
`→ API 下次请求时应用删除指令`
`
- 场景三:子代理调用的安全跳过
会话内存代理调用 microcompactMessages`
`→ querySource =` `'session_memory'`
`→ isMainThreadSource 返回 false`
`→ 跳过缓存微压缩路径`
`→ 返回原始消息,避免状态污染`
`→ 子代理依赖 autocompact 处理上下文压力`
`
自动压缩( autocompact )
详细可以看往期文章。
Claude code源码精读-上下文管理-自动压缩-CSDN博客
总结
本次分析揭示了Claude上下文管理的三层防御体系:① 实时工具结果压缩(microcompact)② 智能对话摘要(compactConversation)③ 预防性容量控制(autocompact)。这种分层处理机制既确保了实时响应速度,又通过缓存前缀哈希保持等技术兼顾了长期会话的稳定性,其设计思路对大模型上下文优化具有显著参考价值。