type
Post
status
Published
date
Apr 1, 2026
slug
cc3
summary
基于 Claude Code 源码(2026-03-31 快照,512K 行 TypeScript)逆向分析
核心文件:src/services/compact/ 目录下 11 个文件
tags
人工智能
上下文工程
category
技术文档
icon
password
Description
1. 架构总览
Claude Code 的上下文压缩不是单一算法,而是一个 4层递进的压缩体系,每层解决不同层面的问题:
核心原则: 尽可能用廉价的规则操作延迟昂贵的 LLM 调用,只在不得已时丢弃信息。
涉及的源文件
文件 | 行数 | 职责 |
microCompact.ts | ~400 | 微压缩:规则清理旧工具结果 |
apiMicrocompact.ts | — | API 层缓存编辑集成 |
timeBasedMCConfig.ts | — | 时间触发微压缩配置 |
autoCompact.ts | ~350 | 自动压缩:阈值判断 + 断路器 |
compact.ts | ~600+ | 传统压缩:Fork Agent 摘要 |
prompt.ts | ~375 | 压缩提示词模板 |
sessionMemoryCompact.ts | ~630 | Session Memory 压缩路径 |
grouping.ts | ~63 | 消息按 API 轮次分组 |
postCompactCleanup.ts | — | 压缩后清理 |
compactWarningHook.ts | — | 压缩警告钩子 |
compactWarningState.ts | — | 压缩警告状态 |
2. 第1层:微压缩(Microcompact)
源文件:
microCompact.ts核心思想
不调用 LLM,纯规则操作——清理旧的、大块的工具输出结果,保留语义信息。这是每轮查询前都会执行的最轻量操作。
可压缩工具白名单
不在白名单中的工具(如 Agent、Skill、MCP 等)的结果不会被微压缩。
两个子路径
子路径 A:时间触发微压缩
子路径 B:缓存编辑微压缩(Cached MC)
这是更精巧的路径——在缓存仍然有效时工作:
缓存编辑的状态管理:
Token 估算辅助
3. 第2层:自动压缩(Auto-Compact)
源文件:
autoCompact.ts阈值计算
其他阈值
断路器机制
shouldAutoCompact 决策树
autoCompactIfNeeded 执行流程
4. 第3层:传统压缩(Full Compact)
源文件:
compact.ts + prompt.ts核心机制:Fork Agent
传统压缩使用一个 Fork Agent——创建当前会话的一个分支,让它生成摘要。关键优势是共享主会话的 prompt cache。
预处理管线
摘要输出格式
Fork Agent 被要求生成两个 XML 块:
防止工具调用的强力前导词
为什么需要这么强力?注释解释了:
后处理(formatCompactSummary)
压缩后消息序列
Prompt-Too-Long 重试机制
当压缩请求本身超过上下文限制时:
部分压缩(Partial Compact)
支持两个方向:
方向 | 提示词 | 用途 |
from(默认) | PARTIAL_COMPACT_PROMPT | 保留旧消息,仅摘要"最近的消息" |
up_to | PARTIAL_COMPACT_UP_TO_PROMPT | 摘要旧消息,保留新消息。摘要会放在开头,后续消息跟在后面 |
up_to 模式的摘要包含一个特殊的第 9 章节 "Context for Continuing Work",专门为后续消息提供上下文。5. 第4层:Session Memory 压缩
源文件:
sessionMemoryCompact.ts核心思想
不调用 LLM 生成新摘要,而是直接使用已经通过后台记忆提取(
extractMemories)积累的 Session Memory 作为"摘要"。优势
- 速度快:不需要 API 调用,<10ms
- 质量可预测:Session Memory 是在每轮查询后渐进更新的,不是一次性压缩
- 保留近期消息:不像传统压缩那样替换所有消息
配置参数
calculateMessagesToKeepIndex 算法
这是 Session Memory 压缩的核心算法——决定保留哪些最近的消息:
adjustIndexToPreserveAPIInvariants 算法
这个算法解决一个棘手的问题——流式传输时,一个 API 响应会产生多条消息(thinking、tool_use 等),它们共享同一个
message.id。如果在中间切断,normalizeMessagesForAPI 合并时会丢失 thinking 块。源码注释中的真实 bug 场景:
trySessionMemoryCompaction 完整流程
6. 消息分组算法
源文件:
grouping.ts设计细节:
- 同一 API 请求的流式块共享同一个
message.id
StreamingToolExecutor在流式输出期间交错插入tool_result,但它们属于同一轮次
- 只要
message.id不变,所有消息都在同一组内
- 不跟踪未解决的
tool_useID——让分组边界自然形成,由ensureToolResultPairing在 API 层修复残留的配对问题
7. Token 估算算法
贯穿所有压缩路径的核心辅助:
粗略估算
消息级估算
精确计算
8. 压缩提示词工程
源文件:
prompt.ts三种提示词模板
模板 | 变量名 | 用途 |
完整压缩 | BASE_COMPACT_PROMPT | 摘要整个对话 |
部分压缩(from) | PARTIAL_COMPACT_PROMPT | 只摘要最近的消息 |
部分压缩(up_to) | PARTIAL_COMPACT_UP_TO_PROMPT | 摘要旧消息,作为后续消息的前导 |
提示词结构
<analysis> 块的作用
压缩后的摘要消息
9. 5层错误恢复中的压缩角色
在
query.ts 的 5 层错误恢复机制中,压缩系统承担了前 2 层:响应式压缩 vs 主动压缩:
属性 | 主动压缩(Auto-Compact) | 响应式压缩(Reactive Compact) |
触发 | token 数超过阈值 | API 返回 prompt-too-long 错误 |
时机 | API 调用前 | API 调用失败后 |
路径 | autoCompact.ts → compact.ts | query.ts 错误恢复层 |
回退 | SM → 传统压缩 → 放弃 | 传统压缩 → 裁剪最旧消息 |
10. 各层对比与设计哲学
横向对比
属性 | 微压缩 | Session Memory | 传统压缩 | PTL Recovery |
调用 LLM | 否 | 否 | 是(Fork Agent) | 否 |
信息损失 | 最小(仅删工具输出) | 中等(靠已有摘要) | 中等(9章摘要) | 高(丢弃最旧消息) |
延迟 | <1ms | <10ms | 5-30s | ~0 |
触发 | 每轮自动 | 自动压缩优先路径 | 自动/手动 | 压缩自身超限 |
prompt cache | 保留(缓存编辑) | 破坏 | 共享(Fork) | 破坏 |
最大输出 | — | — | 20K tokens | — |
断路器 | 无 | 无 | 有(3次) | 有(3次) |
token 预算 | — | 40K 最大保留 | 50K 文件重注入 | 按组裁剪 |
设计哲学总结
- 渐进式降级:从无损操作(微压缩)到有损操作(传统压缩)到丢弃操作(PTL Recovery),每一层都比上一层代价更高
- 缓存优先:微压缩的缓存编辑路径专门设计为不破坏 prompt cache,传统压缩通过 Fork Agent 共享 cache
- 安全性保证:
- 永远不切断 tool_use/tool_result 配对
- 永远不分离共享 message.id 的消息块
- 断路器防止失败时无限重试
- 递归保护防止压缩代理触发自身压缩
- 可观测性:每个压缩操作都记录
logEvent(如tengu_compact,tengu_cached_microcompact),包含压缩前后的 token 数、触发原因、重试次数等
- 可配置性:几乎所有阈值都支持环境变量覆盖(用于测试)或 GrowthBook 远程配置(用于动态调整)
基于 Claude Code 源码逆向分析,2026-03-31