Claude Code 上下文管理与压缩策略深度解析
Claude Code 是 Anthropic 出品的 AI 编程 CLI 工具。本文基于 2026 年 3 月泄露的 Claude Code v2.1.88 源码(51 万行 TypeScript),通过一个完整的真实场景——“小明用 Claude Code 重构 React 项目”——带你从第 1 轮对话走到第 50 轮,亲历上下文膨胀、触发压缩、信息取舍的全过程。即使你没看过一行源码,也能彻底理解 Claude Code 的上下文管理体系。
一、故事的起点:一次令人困惑的”失忆”
小明是一名前端开发者,正在用 Claude Code 重构一个老旧的 React 项目——把 Class 组件全部迁移到函数组件 + Hooks。
第 1 轮,小明说:”帮我把这个项目的 Class 组件全部改成函数组件 + Hooks,用 TypeScript,别再写任何 Class 组件了。”
Claude Code 表现完美。接下来的 40 多轮对话中,它读文件、改代码、跑测试,一个接一个地完成了组件迁移。
然后,第 45 轮,小明让它重构 Dashboard.tsx。Claude Code 交出的代码里赫然写着:
1 | class Dashboard extends React.Component { ... } |
小明愣住了——“我第 1 轮就说了别用 Class 组件,你是不是没在听?”
Claude Code 不是没听,而是”忘了”。 准确地说,它在第 38 轮时经历了一次自动上下文压缩,而那条 “别再写任何 Class 组件” 的指令,在压缩中被意外丢失了。
这个故事会贯穿全文。我们将一步步拆解:上下文为什么会膨胀、系统如何感知危险、压缩时怎么选择保留什么、为什么有些信息会丢失、以及你该如何应对。
二、每一轮都是全量发送——TAOR 循环
要理解”失忆”,先要理解 Claude Code 的记忆机制。
Claude Code 的核心是一个 TAOR 循环——Think(思考)→ Act(行动)→ Observe(观察)→ Repeat(重复):
graph LR
A["🧠 Think<br/>分析当前状态"] --> B["⚡ Act<br/>选择并调用工具"]
B --> C["👁️ Observe<br/>检查工具返回结果"]
C --> D{"🔄 完成了?"}
D -->|"没有"| A
D -->|"完成"| E["✅ 输出回答"]
关键点:每一次循环都是一次完整的 API 调用,需要把整个对话历史发送给 Claude API。
用小明的例子感受一下这意味着什么:
| 轮次 | 发生了什么 | 新增 Token | 累计 Token |
|---|---|---|---|
| 第 1 轮 | 小明描述需求 | ~200 | ~200 |
| 第 3 轮 | 读取 App.tsx(500 行) |
~8,000 | ~12,000 |
| 第 5 轮 | grep 搜索所有 Class 组件 | ~6,000 | ~25,000 |
| 第 10 轮 | 读取 + 重构 3 个文件 | ~15,000 | ~55,000 |
| 第 20 轮 | 累计读了 12 个文件,改了 8 个 | ~40,000 | ~110,000 |
| 第 30 轮 | 累计工具调用 25 次 | ~30,000 | ~155,000 |
| 第 35 轮 | 继续工作… | ~12,000 | ~167,000 |
第 35 轮时,上下文已经逼近了一个关键阈值。 接下来会发生什么?这取决于 Claude Code 的”书桌”到底有多大。
核心比喻
把上下文窗口想象成一张固定大小的书桌。每次对话都要把所有参考资料摊在桌上——旧的不能拿走,新的还得加上。Claude Code 的上下文管理,就是一套精密的书桌整理术:什么时候该清理、清掉什么、保留什么。
三、200K 的书桌,没你想的那么大
Claude 模型的上下文窗口是 200K token(约 15 万字),但 Claude Code 不会把全部空间都给你用。
3.1 有效上下文窗口
系统会做一次预留扣减——给压缩操作留出输出空间:
graph TB
subgraph "模型上下文窗口 (200K tokens)"
A["🟦 有效可用空间<br/>= 200K - 20K = 180K tokens"]
B["🟧 压缩预留空间<br/>MAX_OUTPUT_TOKENS_FOR_SUMMARY<br/>= 20K tokens"]
end
style A fill:#4a9eff,color:#fff
style B fill:#ff9f43,color:#fff
1 | const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 |
为什么要预留 20K?
想象书桌上堆满了资料,你想用一个收纳盒把它们整理归类——但收纳盒本身也需要空间。这 20K 就是收纳盒的空间:当需要压缩时,系统要在同一个 API 调用中塞下旧对话 + 压缩指令 + 模型输出的摘要。如果窗口全用完了,连压缩请求都发不出去。
3.2 输出 Token 的精打细算
调用 Claude API 时,有一个关键参数 max_tokens——告诉 API “这次回复最多允许输出多少 token“。当前 Claude 4 系列模型的最大输出能力已达 64K token,但 Claude Code 默认只请求 8K:
1 | const CAPPED_DEFAULT_MAX_TOKENS = 8_000 // 默认上限 |
为什么不直接请求模型最大的 64K?
因为max_tokens不只是一个数字——它决定了 API 服务器要预留多少 GPU 资源给你。这个机制叫 Slot Reservation(座位预留)。打个比方:去餐厅吃饭,服务员问”几位?”你说 64 位,餐厅就得给你空出一张超大桌子。但实际上你 99% 的情况只来了 5 个人。桌子空着,别的客人也没法坐——整个餐厅的接待能力(吞吐量)因此下降。
统计数据印证了这一点:Claude Code 的 P99 输出只有 4,911 token——也就是说 99% 的回复在 5K 以内,8K 绑绑有余。先要小桌子,对 API 集群的整体吞吐量更友好。
那问题来了:如果某次回复确实需要超过 8K 怎么办?
截断 → 检测 → 升级重试
当模型输出到第 8,000 个 token 时,API 会直接截断——回复被强制中止,后面的内容不会生成。但 API 会通过响应中的 stop_reason 字段告诉你发生了什么:
| stop_reason | 含义 |
|---|---|
end_turn |
模型自然说完了,一切正常 |
max_tokens |
被截断了——模型还没说完,但到了 8K 上限被强制打断 |
Claude Code 检测到 stop_reason = max_tokens 后,会丢弃这次被截断的回复,用 64K 的上限重新请求同一轮对话:
graph TD
A["发起 API 请求<br/>max_tokens = 8,000"] --> B{"模型输出完成"}
B -->|"stop_reason = end_turn<br/>自然说完了"| C["✅ 正常使用回复"]
B -->|"stop_reason = max_tokens<br/>被截断了!"| D["❌ 丢弃截断的回复"]
D --> E["重新请求同一轮<br/>max_tokens = 64,000"]
E --> F["✅ 模型完整输出"]
style D fill:#e74c3c,color:#fff
style C fill:#2ecc71,color:#fff
style F fill:#2ecc71,color:#fff
关键细节:是”重来”不是”接着说”
被截断后不是让模型从断点继续输出,而是整轮重新生成。因为截断可能发生在一句话的中间、一段代码的中间——接着说很难保证连贯性,还不如从头来一次完整的。回到餐厅比喻:小碗装不下汤溢出来了,不是往旁边再加一个碗接着,而是换一个大碗重新盛一碗完整的汤。
为什么这个策略整体更优?
表面看,截断重试浪费了第一次的 8K 计算量。但算总账:
- 99% 的请求:8K 就够了,省下了大量 Slot 预留资源
- < 1% 的请求:多花一次重试的成本
相比每次都请求 64K 导致的全局资源浪费,偶尔重试一次的代价微乎其微。这是一个典型的乐观策略——先假设常见情况(8K 够用),遇到例外再兜底。
3.3 关键阈值:167K
理解了有效窗口,就能算出自动压缩的触发点:
1 | AUTOCOMPACT_BUFFER_TOKENS = 13_000 |
graph LR
subgraph "200K 窗口的空间分配"
direction TB
A["已使用 token"] --> B{"≥ 167K?"}
B -->|"是"| C["🔴 触发自动压缩"]
B -->|"否"| D["🟢 继续正常对话"]
end
回到小明的故事:他在第 35 轮时累计了 ~167K token,正好触碰了这条红线。Claude Code 开始行动了。
但它不会立刻做”大手术”。和现实中的医疗一样,先试微创手术,不行再升级。
四、第一道防线——Micro Compact 微手术
在触发正式压缩之前,Claude Code 会先尝试最轻量的方式:清理旧的工具返回结果。
4.1 工作原理
回想小明的对话:第 3 轮 grep 搜索所有 Class 组件返回了 500 行结果,第 5 轮 FileRead 读取了 App.tsx 的 500 行代码。到了第 35 轮,这些原始结果早已被模型”消化”——搜索结果中有用的文件名已经记住了,App.tsx 也已经改完了。
Micro Compact 做的事很简单:把这些已经被消化过的工具结果,替换成一个占位符。
graph TD
subgraph "小明第 3 轮的 grep 结果"
A["返回 500 行代码<br/>占 ~6,000 tokens"]
end
subgraph "第 35 轮时"
B{"这些结果还有用吗?"}
B -->|"核心信息已被消化"| C["替换为:<br/>[Old tool result content cleared]<br/>占 ~10 tokens"]
B -->|"最近还在用"| D["保留原文"]
end
A --> B
style C fill:#e67e22,color:#fff
style D fill:#2ecc71,color:#fff
4.2 哪些工具会被微压缩?
源码定义了一个白名单,共同特点是返回结果可能很大,但信息在被使用后就可以释放:
| 工具 | 典型场景 | 为什么可以清理 |
|---|---|---|
| FileRead | 读取 500 行源码 | 模型已分析完内容 |
| Shell (Bash) | 执行命令输出 200 行 | 结果已被处理 |
| Grep | 搜索返回 300 个匹配 | 匹配项已被筛选 |
| Glob | 文件列表 100 条 | 目标文件已确定 |
| WebSearch / WebFetch | 网页全文 | 关键信息已提取 |
| FileEdit / FileWrite | 操作确认 | 修改已完成 |
4.3 两种触发方式
- 基于时间:用户离开一段时间后回来,服务端 Prompt 缓存已失效,趁机清理旧结果——反正缓存都没了,不如顺手清理
- 基于缓存编辑:利用 API 的
cache_edits功能直接在服务端删除工具结果,既省 token 又不破坏缓存
类比:增量垃圾回收
Micro Compact 就像编程语言中的增量 GC(Garbage Collection)——不等内存快满了再做一次大扫除,而是持续做小清理。在小明的场景中,每清理一批旧工具结果,可能就能释放 5K-15K token,推迟正式压缩的触发。
在小明的案例中,Micro Compact 清理掉了第 3-10 轮的旧 grep 和 FileRead 结果,释放了约 20K token。但小明继续工作,到第 38 轮,token 再次逼近 167K。这次微手术已经不够了——是时候做正式压缩了。
五、正式压缩——两阶段流程
当 Micro Compact 无法释放足够空间时,系统触发正式的自动压缩(Auto Compact)。这个过程分为严格的两个阶段:
graph LR
subgraph "阶段 1: Analysis(自由分析)"
A1["模型在 <analysis> 标签内<br/>自由梳理所有要点<br/>不遗漏任何关键信息"]
end
subgraph "阶段 2: Summary(结构化输出)"
A2["基于分析结果<br/>按严格的 9 段模板<br/>输出结构化摘要"]
end
A1 -->|"确保无遗漏"| A2
A2 --> R["📋 最终摘要<br/>替换原有对话历史"]
style A1 fill:#9b59b6,color:#fff
style A2 fill:#2980b9,color:#fff
style R fill:#27ae60,color:#fff
5.1 为什么需要两个阶段?
需要澄清一个关键点:这两个阶段发生在同一次 API 调用中。系统把全部对话历史 + 压缩指令一起发给模型,模型在一次输出中依次完成两个阶段。<analysis> 标签不是一个独立的步骤,而是模型输出中的”草稿区”。
模型的实际输出大致长这样:
1 | <analysis> |
为什么要多一个”草稿区”?直接输出 9 段格式不行吗?
这就像考试答题——直接在答题卡上写,容易边想边写、顾此失彼,写到第 5 段时忘了前面遗漏了什么。先在草稿纸上(<analysis>)把所有要点不限格式地全部列出来,确保无遗漏,再往答题卡(9 段模板)上誊抄,最终输出的质量会高得多。
简单说:
- Analysis 阶段(草稿):模型在
<analysis>标签内自由思考,不受格式约束,把对话中的所有关键信息全部梳理一遍 - Summary 阶段(答卷):基于刚才的梳理结果,按照固定的 9 段模板逐段输出结构化摘要
5.2 压缩前的”脱水”处理
在交给模型做总结之前,系统会先剥离非文本内容,防止总结 API 自己也超限:
graph LR
A["原始对话历史<br/>(含图片/附件)"] -->|"stripImagesFromMessages()"| B["图片→[image]占位符"]
B -->|"stripReinjectedAttachments()"| C["剔除 skill 等附件"]
C -->|"交给模型"| D["两阶段压缩"]
5.3 一个意外的工程发现
源码注释揭示了一个有趣的问题:
1 | // 压缩时模型只有 1 轮机会输出(maxTurns: 1) |
模型升级不是免费午餐
4.5 → 4.6 的升级让压缩失败率从 0.01% 暴涨到 2.79%,增长了 279 倍。不同模型版本有不同的行为倾向,需要持续监控和适配 prompt 策略。
现在我们知道了压缩的触发时机和执行流程。但最核心的问题还没回答:50 轮对话中那么多信息,模型怎么决定保留什么、丢弃什么? 这就是 Claude Code 最精妙的设计——9 段式结构化压缩。
六、9 段式结构化摘要——压缩的核心算法
Claude Code 不会简单地说”总结一下”——那样太不可控了。它定义了一个严格的 9 段结构化提取模板,像一个清单一样,确保关键信息不被遗漏。
6.1 九段结构一览
graph TD
subgraph "9 段式结构化压缩模板"
S1["1️⃣ Primary Request & Intent<br/>用户的所有显式请求和意图"]
S2["2️⃣ Key Technical Concepts<br/>讨论过的技术概念、框架"]
S3["3️⃣ Files & Code Sections<br/>具体文件名、代码片段、修改记录"]
S4["4️⃣ Errors & Fixes<br/>遇到的错误和修复方法"]
S5["5️⃣ Problem Solving<br/>解决问题的过程和排查记录"]
S6["6️⃣ All User Messages<br/>所有用户消息(非工具结果)"]
S7["7️⃣ Pending Tasks<br/>被要求但尚未完成的任务"]
S8["8️⃣ Current Work<br/>压缩前正在做的工作详情"]
S9["9️⃣ Optional Next Step<br/>下一步计划"]
end
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9
style S1 fill:#e74c3c,color:#fff
style S6 fill:#e74c3c,color:#fff
style S8 fill:#e74c3c,color:#fff
style S2 fill:#3498db,color:#fff
style S3 fill:#3498db,color:#fff
style S4 fill:#2ecc71,color:#fff
style S5 fill:#2ecc71,color:#fff
style S7 fill:#3498db,color:#fff
style S9 fill:#2ecc71,color:#fff
6.2 用小明的对话理解每一段
让我们看看小明 38 轮对话被压缩后,每段保留了什么:
| 段落 | 优先级 | 小明案例中的内容 |
|---|---|---|
| 1. Primary Request | ⭐⭐⭐ | “将整个 React 项目从 Class 组件迁移到函数组件 + Hooks,使用 TypeScript” |
| 2. Key Concepts | ⭐⭐ | React Hooks、TypeScript、useState/useEffect 模式、组件生命周期映射 |
| 3. Files & Code | ⭐⭐ | 已完成:App.tsx、UserList.tsx、Header.tsx 等 15 个文件的迁移记录 |
| 4. Errors & Fixes | ⭐ | useEffect 依赖数组问题导致死循环,修复方案:添加 useMemo |
| 5. Problem Solving | ⭐ | UserContext 从 Class 迁移到 createContext + useContext 的完整排查 |
| 6. All User Messages | ⭐⭐⭐ | 第 1 轮:”别再写任何 Class 组件”;第 12 轮:”命名用 PascalCase”;… |
| 7. Pending Tasks | ⭐⭐ | 还有 Dashboard.tsx、Settings.tsx、Profile.tsx 未迁移 |
| 8. Current Work | ⭐⭐⭐ | “正在分析 Sidebar.tsx 的 componentDidMount 逻辑,准备改写为 useEffect“ |
| 9. Next Step | ⭐ | “完成 Sidebar.tsx 后,继续处理 Dashboard.tsx“ |
注意到了吗?第 6 段”All User Messages”是 ⭐⭐⭐ 最高优先级。
6.3 为什么用户消息必须完整保留?
源码 prompt 明确要求:
“List ALL user messages that are not tool results. These are critical for understanding the users’ feedback and changing intent.”
这是整个压缩系统最关键的设计决策。用户的消息不只是信息,更是约束。
小明的”别用 Class 组件”
这句话在第 1 轮说出,经过了 37 轮的”稀释”。如果压缩时只保留了技术概念(”迁移到 Hooks”),却丢失了用户的原始措辞(”别再写任何 Class 组件了”),模型可能会认为”偶尔用一下 Class 组件也行”。丢失用户消息 = 丢失对齐。 工具结果可以重新读取,代码可以重新查看,但用户说过的话代表了意图和约束——这是无法从其他地方恢复的。
然而,即便有 ⭐⭐⭐ 的保护,小明的指令仍然可能丢失。为什么?因为压缩本质上是一次翻译——把长文本翻译成短摘要。这引出了下一个关键问题:如何防止翻译走样?
七、防漂移设计——为什么要逐字引用原文
7.1 什么是任务漂移(Task Drift)?
每次压缩都像一次翻译。翻译就会丢失细微含义。如果对话经历了多次压缩,就像翻译的翻译的翻译——每次偏差一点点,几次后就完全走样了。
一个令人后怕的例子
- 用户原始说法:”把按钮改小一点”
- 第 1 次压缩后:”调整按钮样式”(”小一点”被泛化成了”样式”)
- 第 2 次压缩后:”优化 UI 组件”(具体的按钮变成了泛化的组件)
- 模型最终理解:改颜色?改布局?改字体?——已经完全偏离了原意
这就是任务漂移:每次压缩引入微小偏差,多次累积后彻底跑偏。
7.2 Claude Code 的解法:要求逐字引用
第 8 段(Current Work)和第 9 段(Next Step)要求 verbatim quotes——逐字照抄原文:
1 | IMPORTANT: ensure that this step is DIRECTLY in line with |
这个设计的精妙之处在于:用原始文本来”锚定”模型的理解。不管压缩了多少次,只要原文还在,模型就不会偏离。
类比:法律合同中的”原文引用”
法律文件经常引用合同原文而不是转述——因为转述会引入歧义。Claude Code 的 verbatim quotes 设计遵循同样的原则:在需要精确的地方,引用比总结更可靠。
7.3 回到小明的故事
理想情况下,小明第 1 轮说的”别再写任何 Class 组件”应该被完整保留在第 6 段(All User Messages)。但如果这条消息刚好处于一个被 Partial Compact 压缩的区间呢?如果压缩时模型将其泛化为”迁移到函数组件”呢?
这就是信息丢失的真实风险。 我们在最后一章会回到这个问题,给出具体的应对策略。在那之前,让我们继续深入压缩系统的另一个维度:不同场景该用哪种压缩方式?
八、两种手术刀——Full Compact 与 Partial Compact
Micro Compact 只是清理旧工具结果的”预处理”,真正的压缩手术刀有两把:Full Compact 和 Partial Compact。
8.1 三种模式的工作方式
graph TB
subgraph FC["Full Compact(全量压缩)"]
direction LR
F1["📜 全部 38 轮对话"] -->|"整体压缩"| F2["📋 一条结构化摘要"]
end
subgraph PC_FROM["Partial Compact (from 方向)"]
direction LR
P1["📜 旧消息 1-25 轮<br/>✅ 保留原文"] --> P2["📋 新消息 26-38 轮<br/>压缩为摘要"]
end
subgraph PC_UPTO["Partial Compact (up_to 方向)"]
direction LR
P3["📋 旧消息 1-25 轮<br/>压缩为摘要"] --> P4["📜 新消息 26-38 轮<br/>✅ 保留原文"]
end
style FC fill:#e74c3c,color:#fff
style PC_FROM fill:#3498db,color:#fff
style PC_UPTO fill:#2ecc71,color:#fff
8.2 系统如何选择压缩模式?
这是一个关键问题:面对三把手术刀,系统怎么决定用哪把?
答案是:自动压缩(Auto-Compact)默认使用 Full Compact,Partial Compact 主要在手动 /compact 或特定优化场景下使用。
为什么 Auto-Compact 默认选 Full Compact 而不是 Partial?
这看起来不太直觉——Partial 压缩损失更小、还能保留缓存,为什么不优先用?原因在于自动压缩面对的是紧急情况:上下文已经逼近 167K 的硬阈值,系统需要最大限度地释放空间。Full Compact 把所有对话压缩成约 20K 的摘要,一次性释放约 147K token。而 Partial 只压缩一半,释放的空间可能不够,导致很快再次触发压缩——反而浪费更多 API 调用。
打个比方:自动压缩像急诊手术,需要一步到位;Partial 更像门诊微创,适合有余量时精细操作。
那 Partial Compact 什么时候登场? 主要在以下场景:
| 场景 | 使用的模式 | 说明 |
|---|---|---|
| 自动压缩 | Full Compact | 上下文紧急,需最大释放空间 |
手动 /compact |
Full Compact(默认) | 用户主动触发,可附加自定义指令 |
手动 /compact + 指定范围 |
Partial Compact | 用户明确指定只压缩部分对话 |
| 缓存优化场景 | Partial (from) | 旧消息已有缓存命中,只压缩新消息以保留缓存 |
| 保留最新工作 | Partial (up_to) | 最近的代码修改很关键,只压缩已消化的旧消息 |
8.3 三种模式的对比
| 维度 | Full Compact | Partial (from) | Partial (up_to) |
|---|---|---|---|
| 压缩范围 | 整个对话 | 只压缩新消息 | 只压缩旧消息 |
| Prompt 缓存 | ❌ 完全失效 | ✅ 旧消息缓存命中 | ❌ 缓存失效 |
| 信息损失 | 较大 | 较小 | 较小 |
| 空间释放 | 最多(~147K) | 较少 | 较少 |
| 适用场景 | 上下文极度紧张 | 旧消息已缓存、新消息冗余 | 旧消息冗余、新消息重要 |
小明的场景
假设小明的前 25 轮主要是探索和读文件(大量工具结果),后 13 轮是实际的重构工作。
- 自动触发(最常见):系统使用 Full Compact,把 38 轮整体压缩成一条摘要。简单粗暴但有效。
- 手动微调:如果小明觉得最近 13 轮的代码修改很关键,不想被压缩,他可以手动执行
/compact并指定 Partial (up_to),只压缩前 25 轮的探索记录,保留后 13 轮的原文。- 缓存友好:如果旧消息已经建立了良好的 Prompt Cache,系统可能选择 Partial (from),只压缩新消息,让旧消息的缓存继续命中,省钱又省时。
8.4 Partial Compact 的 Prompt 差异
这不是简单的”压缩一半”。Prompt 的措辞也不同:
- Full Compact 说:“create a detailed summary of the conversation“
- Partial Compact 说:“summary of the RECENT portion of the conversation — the messages that follow earlier retained context”
这告诉模型:前面的消息还在,你只需要总结新的部分,不要重复已有的上下文。一个细微的 prompt 差异,避免了大量的信息冗余。
既然了解了三种压缩模式,下一个问题自然是:压缩完成后,Agent 怎么继续工作? 毕竟,压缩掉了对话历史,但小明正在进行的重构任务不能断。
九、压缩后的状态重建——无缝衔接
压缩后,原本 38 轮的对话历史被浓缩成了一条摘要消息。但 Agent 的工作不能断裂——小明正在看的文件、正在执行的计划、正在使用的工具,都需要恢复。
9.1 重建流程
graph TD
subgraph "压缩后的上下文重建"
SYS["⚙️ System Prompt<br/>身份 + 安全 + 工具说明"]
S["📋 压缩摘要<br/>(9 段结构化内容)"]
F["📁 正在查看的文件<br/>(从 FileRead 缓存截取)"]
P["📝 正在执行的 Plan"]
SK["🔧 激活的 Skill"]
T["🛠️ MCP Server & Tools 声明"]
end
SYS --> S --> F --> P --> SK --> T
T --> R["✅ 重建完成,继续工作"]
style S fill:#3498db,color:#fff
style F fill:#2ecc71,color:#fff
style P fill:#e67e22,color:#fff
style SK fill:#9b59b6,color:#fff
style SYS fill:#34495e,color:#fff
9.2 源码中的重建逻辑
1 | // 1. 重新添加通过 FileRead 查看的、还在缓存中的文件(带截断上限) |
小明的重建
压缩完成后,Claude Code 的上下文变成了:
[System Prompt]+[38 轮对话的结构化摘要]+[小明刚才正在看的 Sidebar.tsx 文件内容]+[重构计划:剩余 Dashboard/Settings/Profile 三个组件]+[所有可用工具的声明]对模型来说,就像刚从午休回来,桌上已经摆好了所有需要的资料——它可以无缝继续
Sidebar.tsx的重构工作。
压缩后的重建确保了 Agent 能继续工作。但如果出现极端情况——连压缩本身都无法完成呢?
十、极端防御——当压缩自身也失败
10.1 剥洋葱策略(PTL Defense)
如果对话历史实在太大,连压缩请求本身都触发了 Prompt Too Long 错误(压缩请求 = 全部旧消息 + 压缩指令,本身也可能超限),怎么办?
Claude Code 启动最后的救命策略——剥洋葱:
graph TD
A["触发压缩"] --> B{"压缩请求自身<br/>报 Prompt Too Long?"}
B -->|"否"| C["✅ 正常完成"]
B -->|"是"| D["🧅 剥洋葱<br/>砍掉最旧的 20% 消息"]
D --> E{"重试次数<br/>< MAX_PTL_RETRIES?"}
E -->|"是"| F["用更少消息重试"]
F --> B
E -->|"否"| G["🔴 熔断停止"]
style D fill:#e67e22,color:#fff
style G fill:#e74c3c,color:#fff
style C fill:#2ecc71,color:#fff
1 | // CC-1180: 若 compact request 本身也超限,则剥洋葱 |
有损但必要
每剥一层就永久丢失 20% 的旧上下文。但比起整个会话完全卡死、反复报错烧 API 额度,有损总比无法工作好。
10.2 熔断机制——防止无限重试
如果压缩连续失败(比如小明上传了一张超大的架构图),系统会触发熔断:
1 | const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 |
每天省下 250K 次死锁调用
源码注释提到,这个简单的数字”3”,每天为全网省下约 25 万次死锁 API 调用。没有它,极端会话会陷入”压缩→失败→重试→失败”的无限循环。
到目前为止,我们已经走完了压缩系统的完整链路。但还有一个横切所有环节的主题——成本。每次 API 调用都是真金白银,Claude Code 是怎么优化的?
十一、Prompt 缓存——贯穿全程的成本优化
11.1 三段式缓存结构
Claude Code 把 System Prompt 用一条分界线一刀切成三段,各有不同的缓存策略:
graph TB
subgraph "System Prompt 的物理结构"
direction TB
S["🔵 静态段(全局缓存)<br/>身份定义 / 安全准则 / 工具说明<br/>所有用户共享"]
B["───── DYNAMIC_BOUNDARY ─────"]
D["🟡 动态段(按内容 hash 缓存)<br/>CLAUDE.md / 目录信息 / 记忆文件<br/>每个用户独立"]
U["🔴 不缓存段<br/>MCP 服务器指令<br/>(随时可能变化)"]
end
S --> B --> D --> U
style S fill:#3498db,color:#fff
style D fill:#f39c12,color:#fff
style U fill:#e74c3c,color:#fff
为什么这样设计?
- 静态段:所有用户的 System Prompt 开头都一样(身份定义、安全规则),可以全局共享缓存——只处理一次,全球所有用户受益
- 动态段:每个用户的
CLAUDE.md、项目目录不同,但同一个用户的多轮对话中这些内容稳定不变,按 hash 缓存- 不缓存段:MCP 服务器配置随时可能变,缓存了也白缓存
11.2 缓存的脆弱性
真实案例
社区发现过 Claude Code v2.1.76 前后的 Prompt Cache 回归 bug:正常情况下第 4 轮对话成本降到第 1 轮的 13%(缓存命中),但 bug 版本中成本从 $0.04 膨胀到 $0.40——缓存前缀中任何微小变化都会导致整个缓存失效,成本膨胀 10-20 倍。
11.3 压缩与缓存的关系
这里有一个微妙的联系:Full Compact 会让缓存完全失效——因为压缩后的摘要替换了原来的对话历史,缓存前缀彻底变了。而 Partial Compact (from) 能保留旧消息的缓存——这也是系统优先选择 Partial 模式的原因之一。
压缩本身也在一个 Forked Agent(分叉代理)中执行,但它会借用主对话的 Prompt Cache:
1 | const promptCacheSharingEnabled = getFeatureValue_CACHED_MAY_BE_STALE( |
这意味着压缩操作不需要重新”预热”缓存,能省掉可观的头部填充 token 开销。
十二、回到小明——信息丢失与实战法则
12.1 小明的”失忆”到底是怎么发生的?
现在我们有了完整的知识来回答开头的问题。让我们复盘:
graph TD
A["第 1 轮:小明说<br/>'别再写任何 Class 组件了'"]
A --> B["第 2-35 轮:正常工作<br/>AI 一直遵守这条规则"]
B --> C["第 35 轮:token ≈ 167K<br/>触发 Micro Compact<br/>清理旧工具结果,释放空间"]
C --> D["第 36-37 轮:继续工作<br/>token 再次逼近 167K"]
D --> E1["第 38 轮:再次触发压缩<br/>Micro Compact 先尝试"]
E1 --> E2{"旧工具结果<br/>已在第 35 轮清过<br/>还能释放足够空间吗?"}
E2 -->|"不够"| E["升级为 Full Compact<br/>38 轮对话被压缩为摘要"]
E --> F{"第 6 段保留了<br/>'别用 Class 组件' 吗?"}
F -->|"保留了"| G["✅ AI 继续正确工作"]
F -->|"被泛化为'迁移到 Hooks'"| H["❌ 约束丢失"]
H --> I["第 45 轮:AI 写出了<br/>class Dashboard extends React.Component"]
style E1 fill:#f39c12,color:#fff
style E2 fill:#f39c12,color:#fff
style E fill:#e74c3c,color:#fff
style H fill:#e74c3c,color:#fff
style I fill:#e74c3c,color:#fff
style G fill:#2ecc71,color:#fff
根本原因:压缩是一次有损操作。虽然第 6 段(All User Messages)要求保留所有用户消息,但 38 轮对话中可能有十几条用户消息——在有限的摘要空间内,模型可能将”别再写任何 Class 组件了”泛化为”将项目迁移到函数组件 + Hooks”。这个泛化看起来语义等价,但丢失了”禁止”这个约束的强度。
12.2 各类信息的丢失风险
| 信息类型 | 风险 | 原因 | 小明案例 |
|---|---|---|---|
| 隐含偏好 | 🔴 高 | one-liner 容易被泛化 | “别用 var” 被压成”使用现代 JS” |
| 失败方案细节 | 🔴 高 | 只保留结论不保留过程 | 试了 3 种 Hook 写法只记住选了哪种 |
| 中间讨论 | 🔴 高 | 讨论过程被压缩为结论 | 讨论了 Context vs Redux,只剩”选了 Context” |
| 文件修改细节 | 🟡 中 | 第 3 段保留,但早期修改可能被覆盖 | 前 10 个文件的修改记录被简化 |
| 用户直接请求 | 🟢 低 | 第 6 段最高优先级保留 | “迁移到函数组件”被保留 |
| 当前工作状态 | 🟢 低 | 第 8 段逐字引用 | “正在重构 Sidebar.tsx”被精确保留 |
12.3 五条实战法则
与压缩共存的黄金法则
法则 1:把关键约束写进 CLAUDE.md
CLAUDE.md 在 System Prompt 中,压缩根本不会碰它。小明如果在项目的 CLAUDE.md 里写了”所有组件必须使用函数组件 + Hooks,禁止 Class 组件”,不管经历多少次压缩,这条规则都不会丢失。
法则 2:长会话中定期重申关键要求
如果你发现 AI 开始”忘记”某个规则,不要生气——它可能刚经历了一次自动压缩。重新说一遍就好,这条新消息会成为下次压缩时的”新鲜内容”,优先级更高。
法则 3:主动使用 /compact 而不是等自动触发
手动 /compact 时你可以附加自定义指令:”特别保留关于组件类型的所有约束”。自动压缩不给你这个机会。
法则 4:大任务拆成多个会话
如果任务复杂到需要多次压缩,不如拆成多个会话。每个会话的 CLAUDE.md 写清楚上下文。摘要的摘要远不如原始上下文可靠——就像翻译的翻译,每多一层就多一层失真。
法则 5:关注压缩后的第一轮回复
压缩后的第一轮回复最容易出问题。如果发现 AI 理解偏了,立刻纠正。这一轮纠正会成为新上下文的一部分,影响后续所有对话。越早纠正,偏差越小。
十三、全景图:上下文管理的完整生命周期
最后,把全文所有机制串成一张完整的流程图。这不是”最后才串起来”——而是你现在已经理解了每个环节,这张图只是将它们可视化连接:
graph TD
U["👤 用户输入消息"] --> SP["⚙️ 拼装 System Prompt<br/>(静态段 + 动态段 + MCP)"]
SP --> API["📡 API 调用<br/>(发送全部上下文)"]
API --> TAOR["🔄 TAOR 循环<br/>(Think→Act→Observe→Repeat)"]
TAOR --> TOOL["🛠️ 工具调用产生结果"]
TOOL --> CHECK{"📊 Token ≥ 167K?"}
CHECK -->|"否"| NEXT["继续下一轮"]
CHECK -->|"是"| MC["🔬 先试 Micro Compact<br/>清理旧工具结果"]
MC --> CHECK2{"还超吗?"}
CHECK2 -->|"否"| NEXT
CHECK2 -->|"是"| STRIP["🧹 脱水:剥离图片/附件"]
STRIP --> ANALYSIS["🧠 阶段 1: Analysis 自由分析"]
ANALYSIS --> SUMMARY["📋 阶段 2: 9 段式结构化摘要"]
SUMMARY --> PTL{"压缩请求自身<br/>也超限了?"}
PTL -->|"否"| REBUILD["🔧 状态重注入<br/>文件 + Plan + Skill + Tools"]
PTL -->|"是"| PEEL["🧅 剥洋葱:砍掉 20% 重试"]
PEEL -->|"重试次数未超"| SUMMARY
PEEL -->|"超过重试上限"| FUSE["🔴 熔断停止"]
REBUILD --> NEXT
style U fill:#3498db,color:#fff
style MC fill:#2ecc71,color:#fff
style SUMMARY fill:#e67e22,color:#fff
style REBUILD fill:#2ecc71,color:#fff
style PEEL fill:#e74c3c,color:#fff
style FUSE fill:#e74c3c,color:#fff
总结:三个核心原则
Claude Code 上下文管理的设计哲学
1. 结构化优于自由总结。 给模型一个严格的 9 段模板,比说”总结一下”可靠得多。就像法律文书有固定格式一样——格式本身就是质量保证。
2. 用户消息是最不能丢的信息。 工具结果可以重新读取,代码可以重新查看,但用户说过的话代表了意图和约束——这是不可重建的。
3. 防漂移需要主动设计。 不能指望模型自动保持一致性。逐字引用(verbatim quotes)、分段优先级、两阶段流程——都是主动的防漂移措施。每一个看似多余的设计,背后都有血泪教训。
参考资料
- Claude Code v2.1.88 源码(2026 年 3 月泄露版本),核心文件:
src/services/compact/autoCompact.ts、src/services/compact/compact.ts、src/services/compact/prompt.ts、src/utils/context.ts、src/services/compact/microCompact.ts- 《Claude Code 源码解析》花叔著,2026 年 4 月橙皮书系列
- claude-code-analysis 第八章:Context 上下文管理机制





