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
2
3
4
5
6
7
8
9
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000

function getEffectiveContextWindowSize(model: string): number {
const reservedTokensForSummary = Math.min(
getMaxOutputTokensForModel(model),
MAX_OUTPUT_TOKENS_FOR_SUMMARY
)
return getContextWindowForModel(model) - reservedTokensForSummary // 200K - 20K = 180K
}

为什么要预留 20K?
想象书桌上堆满了资料,你想用一个收纳盒把它们整理归类——但收纳盒本身也需要空间。这 20K 就是收纳盒的空间:当需要压缩时,系统要在同一个 API 调用中塞下旧对话 + 压缩指令 + 模型输出的摘要。如果窗口全用完了,连压缩请求都发不出去。

3.2 输出 Token 的精打细算

调用 Claude API 时,有一个关键参数 max_tokens——告诉 API “这次回复最多允许输出多少 token“。当前 Claude 4 系列模型的最大输出能力已达 64K token,但 Claude Code 默认只请求 8K

1
2
const CAPPED_DEFAULT_MAX_TOKENS = 8_000   // 默认上限
const ESCALATED_MAX_TOKENS = 64_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
2
AUTOCOMPACT_BUFFER_TOKENS = 13_000
autoCompactThreshold = effectiveContextWindow - 13_000 // 180K - 13K = 167K
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 两种触发方式

  1. 基于时间:用户离开一段时间后回来,服务端 Prompt 缓存已失效,趁机清理旧结果——反正缓存都没了,不如顺手清理
  2. 基于缓存编辑:利用 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["模型在 &lt;analysis&gt; 标签内<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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<analysis>
用户的核心需求是将 React 项目从 Class 迁移到函数组件 + Hooks...
第 1 轮明确说了"别再写任何 Class 组件"...
第 12 轮补充了"命名用 PascalCase"...
第 20 轮遇到 useEffect 依赖数组导致死循环,已修复...
已完成文件:App.tsx、Header.tsx、UserList.tsx 等 15 个...
当前正在处理 Sidebar.tsx 的 componentDidMount 改写...
(自由梳理,不限格式,确保不遗漏任何要点)
</analysis>

1. Primary Request & Intent:
将整个 React 项目从 Class 组件迁移到函数组件 + Hooks,使用 TypeScript...

2. Key Technical Concepts:
React Hooks(useState、useEffect、useContext)、TypeScript...

3. Files & Code Sections:
已完成:App.tsx、Header.tsx、UserList.tsx...

...(按 9 段模板逐段结构化输出)

为什么要多一个”草稿区”?直接输出 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
2
3
4
// 压缩时模型只有 1 轮机会输出(maxTurns: 1)
// 如果模型尝试调用工具(被拒绝),就不会产生文本输出,压缩失败
// Sonnet 4.6 失败率 2.79%(4.5 只有 0.01%)
// 所以加了一段非常激进的"禁止调用工具"前言来降低失败率

模型升级不是免费午餐
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.tsxUserList.tsxHeader.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.tsxSettings.tsxProfile.tsx 未迁移
8. Current Work ⭐⭐⭐ “正在分析 Sidebar.tsxcomponentDidMount 逻辑,准备改写为 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
2
3
4
5
6
7
IMPORTANT: ensure that this step is DIRECTLY in line with
the user's most recent explicit requests.

Include direct quotes from the most recent conversation
showing exactly what task you were working on and where
you left off. This should be verbatim to ensure there's
no drift in task interpretation.

这个设计的精妙之处在于:用原始文本来”锚定”模型的理解。不管压缩了多少次,只要原文还在,模型就不会偏离。

类比:法律合同中的”原文引用”
法律文件经常引用合同原文而不是转述——因为转述会引入歧义。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
2
3
4
5
6
7
8
9
10
11
// 1. 重新添加通过 FileRead 查看的、还在缓存中的文件(带截断上限)
const fileAttachments = createPostCompactFileAttachments(preCompactReadFileState)

// 2. 重新加入仍在进行中的 Plan 和 Skill
const planAttachment = createPlanAttachmentIfNeeded(context.agentId)
const skillAttachment = createSkillAttachmentIfNeeded(context.agentId)

// 3. 把被干掉的 Deferred Delta 工具协议重新注入
for (const att of getDeferredToolsDeltaAttachment()) {
postCompactFileAttachments.push(createAttachmentMessage(att))
}

小明的重建
压缩完成后,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
2
3
4
5
// CC-1180: 若 compact request 本身也超限,则剥洋葱
// 每次剥掉 20% 旧分组进行重试
const truncated = ptlAttempts <= MAX_PTL_RETRIES
? truncateHeadForPTLRetry(messagesToSummarize, summaryResponse)
: null

有损但必要
每剥一层就永久丢失 20% 的旧上下文。但比起整个会话完全卡死、反复报错烧 API 额度,有损总比无法工作好。

10.2 熔断机制——防止无限重试

如果压缩连续失败(比如小明上传了一张超大的架构图),系统会触发熔断:

1
2
3
4
5
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

if (tracking.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false } // 熔断:放弃压缩
}

每天省下 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
2
3
const promptCacheSharingEnabled = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_compact_cache_prefix', true
)

这意味着压缩操作不需要重新”预热”缓存,能省掉可观的头部填充 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.tssrc/services/compact/compact.tssrc/services/compact/prompt.tssrc/utils/context.tssrc/services/compact/microCompact.ts
  • 《Claude Code 源码解析》花叔著,2026 年 4 月橙皮书系列
  • claude-code-analysis 第八章:Context 上下文管理机制