乐于分享
好东西不私藏

Claude Code 源码深度拆解④ | 三层压缩策略:把Context Window变成可管理资源

Claude Code 源码深度拆解④ | 三层压缩策略:把Context Window变成可管理资源

上下文不是越大越好,是越干净越好。3272次连续失败的熔断器故事,从这里开始。

一、引言:Context Window的本质焦虑

在上一期拆解QueryEngine时,我们留下了一个悬念:当TAOR循环不断累积消息,token数量逐渐逼近模型上限时,系统该怎么办?

这个问题背后是一个根本性的焦虑——Context Anxiety(上下文焦虑)

Anthropic的研究团队发现:当模型感知到上下文窗口即将耗尽时,它会倾向于“草草收尾”——跳过验证步骤、省略边缘情况、给出不完整的方案。就像一个知道自己只剩5分钟考试时间的学生,开始跳步骤、走捷径。

200,000 token的窗口看起来很宽裕,但在实际编程会话中消耗极快:

上下文消耗来源
估算token
占比
系统提示词 + CLAUDE.md
5,000 – 15,000
3-8%
读取的源代码文件
50,000 – 120,000
25-60%
对话历史
30,000 – 80,000
15-40%
模型生成的响应
20,000 – 50,000
10-25%

一个500行的TypeScript文件大约消耗3,000 token。一次典型的编程会话,读取10-20个文件、进行几轮工具调用、接收模型的长响应,很容易在30分钟内逼近上限。

核心矛盾

  • 必须保留历史——AI需要知道之前做了什么、用户的需求是什么
  • 必须压缩历史——token有硬上限,不可能无限增长
  • 压缩有代价——每次压缩都要调用AI写摘要,消耗时间和费用

Claude Code的答案是三层递进式压缩策略,从轻量到重量依次尝试,能不用AI就不用AI,能少处理就少处理。

二、三层压缩架构全景

2.1 压缩触发链路

┌─────────────────────────────────────────────────────────────────┐│                      QueryEngine 主循环                          ││                                                                 ││  每轮迭代前 → TokenBudgetManager.checkBudget()                   ││                     ↓                                           ││              ┌──────────────────┐                               ││              │ 预算状态判断      │                               ││              └──────────────────┘                               ││                /        |        \                              ││         <80%  /    80-95%|    >95% \                            ││              /            |          \                          ││        正常执行      预警标记    触发压缩流程                      ││                        |               ↓                        ││                        |      ┌────────────────┐                ││                        |      │ 三层压缩策略    │                ││                        |      │ 依次尝试        │                ││                        |      └────────────────┘                ││                        |               ↓                        ││                        └──────→ 压缩后继续循环                   │└─────────────────────────────────────────────────────────────────┘

对于200K上下文的模型,自动压缩在约167,000 token时触发(预留约13,000 token缓冲)。

2.2 三层策略对比

层级
名称
触发条件
是否调用AI
核心操作
第一层
MicroCompact
每轮自动检查/工具输出过大
❌ 无
清空旧工具结果内容
第二层
SessionMemory Compact
AutoCompact触发时优先尝试
❌ 无(读文件)
用Session Memory摘要替换历史
第三层
Full Compact
SessionMemory不可用/手动触发
✅ 调用
AI生成结构化摘要

设计哲学:能不调用AI就不调,能复用已有摘要就复用,最后迫不得已才让AI现场写摘要。

三、第一层:MicroCompact —— 零成本的“轻打扫”

3.1 触发机制与设计思路

MicroCompact是最轻量的一层,有两个触发场景:

场景一:时间触发当距离上次AI响应超过一定时间(源码中是5分钟左右),Anthropic服务端的Prompt Cache已经失效。继续保留那些旧的工具调用结果只会浪费空间,因为缓存失效后它们不再有任何复用价值。

场景二:缓存感知触发在正常响应过程中,如果检测到消息中有大量工具调用结果且token数较高,也会触发此层。

3.2 核心实现逻辑

// microCompact.ts 核心逻辑(基于源码推断)const COMPACTABLE_TOOLS = [  'FileRead',   // 文件读取结果(可能很长)  'Bash',       // 命令行输出(可能很长)  'Grep',       // 搜索结果  'Glob',       // 文件列表  'WebSearch',  // 网络搜索结果];function microCompact(messages: Message[]): Message[] {  const now = Date.now();  for (const msg of messages) {    // 只处理工具结果消息    if (msg.role !== 'tool') continue;    // 找到对应的工具调用    const toolCall = findToolCallByTimestamp(msg.tool_call_id);    if (!toolCall) continue;    // 判断是否应该清空    const age = now - toolCall.timestamp;    const shouldClear =       age > CACHE_EXPIRY_MS ||  // 时间过期(约5分钟)      isCompactableTool(toolCall.name);  // 或属于可压缩工具    if (shouldClear && msg.content.length > 0) {      // 关键:不删除消息,只替换内容      msg.content = '[Old tool result content cleared]';    }  }  return messages;}

3.3 为什么“不删除消息”?

这是一个精妙的设计细节:

// 消息结构的不变量interface MessagePair {  assistant: { role: 'assistant', tool_calls: [...] };  // 工具调用请求  tool: { role: 'tool', tool_call_id: string, content: string }; // 工具结果}// ❌ 错误做法:删除tool消息// API会报错:tool_use without corresponding tool_result// ✅ 正确做法:保留消息结构,只清空内容// API检查通过,但token消耗大幅降低

AI模型API要求每个tool_use(工具调用请求)必须有对应的tool_result(工具调用结果),它们必须成对出现。删除消息会破坏这个不变量。所以只能清空内容,保留消息骨架。

Java类比:类似于把Map<String, Object>中的value设为null,但保留key——否则外部代码遍历key时会出错。

3.4 设计评价

好在哪

  1. 零API成本
    :纯本地字符串操作,不消耗任何配额
  2. 结构安全
    :保留消息骨架,不破坏API协议
  3. 智能判断
    :结合时间和工具类型,不盲目清空
  4. 透明高效
    :用户无感知,自动在后台完成

四、第二层:SessionMemory Compact —— 复用已有摘要

4.1 设计动机

MicroCompact只能清理工具结果,无法压缩对话的核心内容(用户的指令、模型的决策、关键的错误信息)。当上下文真正逼近上限时,需要更彻底的压缩。

但直接调用AI生成摘要有一个问题:重复劳动

Session Memory是一个独立的后台服务,它在会话进行过程中持续提取关键信息,维护一份结构化的会话摘要。当需要压缩时,如果这份摘要已经存在,直接拿来用就行了——何必再调用一次AI?

4.2 SessionMemory Compact的执行流程

// sessionMemoryCompact.ts(基于源码推断)async function sessionMemoryCompact(  messages: Message[],  context: ExecutionContext): Promise<CompactResult | null> {  // 1. 检查Session Memory是否可用  const sessionMemory = await getSessionMemory();  if (!sessionMemory || !sessionMemory.hasContent()) {    return null;  // 不可用,让上层走Full Compact  }  // 2. 获取“上次总结到哪条消息”的标记  const lastSummarizedId = getLastSummarizedMessageId();  // 3. 计算需要保留的近期消息  const recentMessages = getMessagesAfter(lastSummarizedId, messages);  // 保护机制:至少保留5条消息、至少10,000 token  const minMessages = 5;  const minTokens = 10000;  let protectedMessages = recentMessages;  if (recentMessages.length < minMessages) {    protectedMessages = takeLast(messages, minMessages);  }  const protectedTokens = estimateTokens(protectedMessages);  if (protectedTokens < minTokens) {    // 往前多取一些消息直到满足最低token保护    protectedMessages = expandToMinTokens(messages, protectedMessages, minTokens);  }  // 4. 组装压缩后的消息列表  const compactedMessages: Message[] = [    // 首先:Session Memory摘要(作为系统消息或用户消息)    {      role: 'user',      content: `[Previous session summary]\n${sessionMemory.content}`,    },    // 然后:CompactBoundaryMessage(标记压缩边界)    {      role: 'system',      content: '--- Context compacted here ---',      isBoundary: true,    },    // 最后:保留的近期消息(原封不动)    ...protectedMessages,  ];  // 5. 重置位置标记(因为旧消息UUID已不存在)  setLastSummarizedMessageId(undefined);  return { messages: compactedMessages, method: 'sessionMemory' };}

4.3 关键设计:保护近期消息

// 保护机制的核心逻辑const PROTECTION_RULES = {  minMessages: 5,        // 至少保留5条消息  minTokens: 10000,      // 至少保留10,000 token  preserveUserMessages: true,  // 用户消息优先保留  preserveErrors: true,       // 错误消息优先保留};// 模型最终看到的是:// [Session Memory摘要] + [边界标记] + [保留的近期消息]// // 这意味着:最近的对话上下文没有丢失,// 模型既能看到“历史摘要”,又能看到“最近发生了什么”

这个设计好在哪里?

  1. 零额外API成本
    :复用Session Memory后台服务已经生成的摘要
  2. 保留连贯性
    :近期消息原封不动保留,模型不会感到“断片”
  3. 智能降级
    :如果Session Memory不可用,自动fallback到Full Compact
  4. 双重保护
    :minMessages + minTokens确保不会因压缩过度丢失上下文

4.4 与Session Memory后台服务的协同

// SessionMemory服务的独立工作流程class SessionMemoryService {  private lastSummarizedMessageId: string | undefined;  private extractionInProgress = false;  // 后台定期触发(由QueryEngine在空闲时调用)  async maybeExtract(messages: Message[]): Promise<void> {    // 触发条件判断    if (!this.shouldExtract(messages)) return;    // 并发控制:防止多个提取任务同时运行    if (this.extractionInProgress) {      await this.waitForExtraction();      return;    }    this.extractionInProgress = true;    try {      // Fork一个子Agent来提取记忆(不阻塞主循环)      const extracted = await this.runForkedAgent(messages);      // 更新记忆文件      await this.updateMemoryFile(extracted);      // 更新位置标记      this.lastSummarizedMessageId = getLastMessageId(messages);    } finally {      this.extractionInProgress = false;    }  }  private shouldExtract(messages: Message[]): boolean {    // 累计token超过10,000    // 且距上次提取新增token超过5,000    // 且距上次提取的工具调用超过3次    // 且最近一轮没有工具调用(说明对话进入总结阶段)    return checkConditions(messages, this.lastSummarizedMessageId);  }}

这个设计体现了关注点分离:压缩是压缩,记忆提取是记忆提取,两者独立运行,但在压缩时可以协同。

五、第三层:Full Compact —— 最后的防线

5.1 触发场景

当SessionMemory Compact不可用时(比如手动压缩时用户提供了自定义指令,或Session Memory文件损坏),系统走Full Compact路径。

// compact.ts 中的标准压缩(基于源码推断)async function fullCompact(  messages: Message[],  options: CompactOptions): Promise<CompactResult> {  // 1. 构建压缩提示词  const compactPrompt = buildCompactPrompt(options);  // 2. 调用AI生成摘要  const summary = await callModelForSummary(messages, compactPrompt);  // 3. 全部替换:只保留摘要,不保留近期消息  const compactedMessages: Message[] = [    {      role: 'user',      content: `[Conversation summary]\n${summary}`,    },    {      role: 'system',      content: '--- Context compacted here ---',      isBoundary: true,    },  ];  // 4. 重新注入关键信息  await reinjectContext(compactedMessages, {    recentlyAccessedFiles: getRecentlyAccessedFiles(5),  // 最近5个文件,每个最多5000 token    activePlans: getActivePlans(),    skillSchemas: getRelevantSkillSchemas(),  });  // 5. 重置工作预算  context.tokenBudgetManager.reset(50000);  // 压缩后预算重置为50,000 token  return { messages: compactedMessages, method: 'full' };}

5.2 结构化摘要的9段式模板

Full Compact生成的摘要是高度结构化的,确保关键信息不丢失:

## 核心请求用户要求实现用户认证模块,包括登录、注册、JWT验证## 关键概念JWT with refresh token rotationbcrypt for password hashingMiddleware pattern for route protection## 文件和代码src/auth/login.ts: 实现登录逻辑src/auth/register.ts: 实现注册逻辑  src/middleware/auth.ts: JWT验证中间件## 错误和修复TS2304: Cannot find name 'jwt' → 安装jsonwebtoken类型定义密码哈希问题 → 改用bcrypt.compare而非直接比较## 解决过程1. 创建auth模块结构2. 实现基础登录/注册3. 添加JWT中间件4. 修复类型错误5. 完成单元测试## 所有用户消息[完整保留所有用户原始消息——因为用户的每句话都可能包含隐性偏好]## 待办任务实现密码重置功能添加登录限流## 当前工作正在调试JWT刷新token逻辑## 下一步行动修复refresh token的过期时间设置

最关键的规则所有用户消息必须完整保留。AI可能在第3轮被用户纠正了某个做法,压缩时若丢弃这条纠正,后续就会重蹈覆辙。

5.3 与SessionMemory Compact的关键区别

对比维度
SessionMemory Compact
Full Compact
摘要来源
读取已有文件(零API成本)
调用AI现场生成
近期消息
保留

(至少5条/10k token)
不保留
模型看到的内容
摘要 + 边界 + 近期消息
只有摘要 + 重新注入的上下文
适用场景
自动压缩
手动压缩/SessionMemory不可用

设计意图:SessionMemory Compact更“温和”——模型不会感到上下文突然断裂。Full Compact更“彻底”——当需要完全重置上下文时使用。

六、熔断器:3272次失败教会我们的

6.1 问题的发现

2026年3月10日之前,Claude Code的自动压缩功能没有重试上限

源码中的BigQuery数据分析注释记录了这个惨痛的教训:

// BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272)// in a single session, wasting ~250K API calls/day globally.const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3;

翻译一下:

  • 1,279个会话出现了50次以上的连续压缩失败
  • 最严重的一个会话连续失败了3,272次
  • 全局每天浪费约25万次API调用

为什么会连续失败?因为上下文已经“坏掉了”——压缩操作本身需要消耗token来调用AI生成摘要,但如果上下文已经处于某种异常状态(比如消息结构损坏、token计数错误),压缩会一直失败,然后系统会一直重试,形成无限循环。

6.2 熔断器的实现

// compact.ts 中的熔断器(基于源码推断)class CompactionCircuitBreaker {  private failureCount: number = 0;  private readonly maxFailures: number = 3;  private lastFailureTime: number = 0;  private readonly cooldownMs: number = 60000; // 1分钟  async compact(    messages: Message[],    compactor: Compactor  ): Promise<Message[]> {    // 检查熔断状态    if (this.failureCount >= this.maxFailures) {      const timeSinceLastFailure = Date.now() - this.lastFailureTime;      if (timeSinceLastFailure < this.cooldownMs) {        // 熔断器打开,拒绝执行        throw new Error(          `Compaction circuit breaker is OPEN. ` +          `${this.failureCount} failures in a row. ` +          `Try again in ${Math.ceil((this.cooldownMs - timeSinceLastFailure) / 1000)}s.`        );      }      // 冷却期已过,进入半开状态      this.failureCount = 0;    }    try {      const result = await compactor.compact(messages);      // 成功,重置计数器      this.failureCount = 0;      return result;    } catch (error) {      // 失败,增加计数      this.failureCount++;      this.lastFailureTime = Date.now();      console.warn(        `[CircuitBreaker] Compaction failed (${this.failureCount}/${this.maxFailures}): ${error}`      );      throw error;    }  }}

6.3 这个设计教会我们什么?

第一,demo只需要跑通,生产系统需要知道失败时怎么止损。

很多Agent原型在处理上下文超限时,要么直接崩溃,要么简单粗暴地截断。但在生产环境中,你需要考虑:压缩失败了怎么办?连续失败怎么办?如何防止雪崩?

第二,用数据驱动设计决策。

熔断器的阈值3不是拍脑袋定的,而是基于BigQuery中真实的失败数据分析得出的。如果阈值设得太高(比如10次),仍会浪费大量API调用;设得太低(比如1次),可能因为偶发故障导致压缩过早放弃。

第三,失败是常态,优雅降级是能力。

熔断器打开后,系统不会崩溃——它只是告诉上层“压缩暂时不可用”,让上层决定是等待还是用其他方式继续。

七、完整压缩流程串联

将三层压缩策略串联起来,完整的自动压缩流程如下:

┌─────────────────────────────────────────────────────────────────┐│                    AutoCompact 完整流程                          │└─────────────────────────────────────────────────────────────────┘                              │                              ▼              ┌───────────────────────────────┐              │   Token预算检查:接近上限?      │              └───────────────────────────────┘                              │              ┌───────────────┴───────────────┐              │                               │              ▼                               ▼        未达上限                         达到上限(约167k token)        正常执行                              │                                            ▼                            ┌───────────────────────────────┐                            │   熔断器检查:允许压缩?         │                            └───────────────────────────────┘                                            │                            ┌───────────────┴───────────────┐                            │                               │                            ▼                               ▼                       熔断器打开                        熔断器关闭                       抛出异常                              │                                                            ▼                                            ┌───────────────────────────────┐                                            │   第一层:MicroCompact         │                                            │   (已在每轮自动执行)           │                                            └───────────────────────────────┘                                                            │                                                            ▼                                            ┌───────────────────────────────┐                                            │   第二层:SessionMemory Compact│                                            │   检查Session Memory是否可用    │                                            └───────────────────────────────┘                                                            │                                            ┌───────────────┴───────────────┐                                            │                               │                                            ▼                               ▼                                       可用                              不可用                                   使用已有摘要                        继续下一层                                            │                               │                                            │                               ▼                                            │               ┌───────────────────────────────┐                                            │               │   第三层:Full Compact         │                                            │               │   调用AI生成摘要                │                                            │               └───────────────────────────────┘                                            │                               │                                            └───────────────┬───────────────┘                                                            │                                                            ▼                                            ┌───────────────────────────────┐                                            │   更新熔断器状态                 │                                            │   - 成功:计数器归零             │                                            │   - 失败:计数器+1               │                                            └───────────────────────────────┘                                                            │                                                            ▼                                            ┌───────────────────────────────┐                                            │   重新注入关键上下文             │                                            │   - 最近访问文件                 │                                            │   - 活跃计划                    │                                            │   - 技能Schema                 │                                            └───────────────────────────────┘                                                            │                                                            ▼                                            ┌───────────────────────────────┐                                            │   重置Token预算为50,000         │                                            │   继续TAOR循环                  │                                            └───────────────────────────────┘

八、对Agent开发的核心启示

8.1 四条可迁移的设计原则

原则一:分层压缩,能省则省

// ❌ 错误做法:每次压缩都调用AIasync function compress(messages) {  return await ai.summarize(messages);  // 昂贵!}// ✅ 正确做法:分层尝试async function compress(messages) {  // 先尝试轻量级本地清理  const micro = microCompact(messages);  if (estimateTokens(micro) < threshold) return micro;  // 再尝试复用已有摘要  const cached = getCachedSummary();  if (cached) return buildWithCached(cached, messages);  // 最后才调用AI  return await ai.summarize(messages);}

原则二:保护近期上下文

// 压缩时永远不要丢弃最近N条消息const RECENT_PROTECTION = {  minMessages: 5,  minTokens: 10000,};// 模型应该看到“摘要 + 近期消息”,而非“只有摘要”

原则三:熔断器是生产系统标配

// 任何可能失败的操作都应该有熔断器class CircuitBreaker {  private failures = 0;  private readonly maxFailures = 3;  async execute<T>(fn: () => Promise<T>): Promise<T> {    if (this.failures >= this.maxFailures) {      throw new Error('Circuit breaker open');    }    try {      const result = await fn();      this.failures = 0;      return result;    } catch (e) {      this.failures++;      throw e;    }  }}

原则四:压缩后重新注入环境

// 压缩清空了上下文,但项目环境信息需要重新注入async function afterCompact(messages: Message[]): Promise<Message[]> {  // 重新告诉模型它在哪个项目、有哪些文件  await injectProjectContext(messages);  await injectRecentFiles(messages);  await injectActivePlans(messages);  return messages;}

8.2 一个简化的压缩实现

// 你可以立刻使用的简化压缩策略interface CompactStrategy {  name: string;  cost: 'free' | 'cheap' | 'expensive';  compact: (messages: Message[]) => Promise<Message[]>;}class ContextCompactor {  private strategies: CompactStrategy[] = [    {      name: 'micro',      cost: 'free',      compact: async (msgs) => this.microCompact(msgs),    },    {      name: 'cached',      cost: 'cheap',      compact: async (msgs) => this.cachedCompact(msgs),    },    {      name: 'full',      cost: 'expensive',      compact: async (msgs) => this.fullCompact(msgs),    },  ];  private circuitBreaker = new CircuitBreaker(3, 60000);  async compact(messages: Message[], threshold: number): Promise<Message[]> {    const currentTokens = estimateTokens(messages);    if (currentTokens < threshold) return messages;    for (const strategy of this.strategies) {      try {        const result = await this.circuitBreaker.execute(() =>           strategy.compact(messages)        );        const newTokens = estimateTokens(result);        if (newTokens < threshold) {          console.log(`Compact success via ${strategy.name}: ${currentTokens} → ${newTokens} tokens`);          return result;        }      } catch (e) {        console.warn(`Strategy ${strategy.name} failed:`, e);        // 继续尝试下一个策略      }    }    // 所有策略都失败了    throw new Error('All compaction strategies failed');  }  private microCompact(messages: Message[]): Message[] {    // 清空旧的工具结果    return messages.map(msg => {      if (msg.role === 'tool' && isOldToolResult(msg)) {        return { ...msg, content: '[Content cleared]' };      }      return msg;    });  }  private async cachedCompact(messages: Message[]): Promise<Message[]> {    const cached = await this.getCachedSummary();    if (!cached) throw new Error('No cached summary');    const recent = this.getRecentMessages(messages, 5);    return [      { role: 'user', content: `[Summary]\n${cached}` },      { role: 'system', content: '---' },      ...recent,    ];  }  private async fullCompact(messages: Message[]): Promise<Message[]> {    const summary = await this.callAIForSummary(messages);    return [      { role: 'user', content: `[Summary]\n${summary}` },    ];  }}

九、小结与下一期预告

三层压缩策略是Claude Code最具工程价值的模块之一。它告诉我们:

  1. Context Window是需要主动管理的稀缺资源
    ——不是越大越好,是越干净越好
  2. 分层策略是成本与效果的平衡
    ——能不用AI就不用,能复用就复用
  3. 熔断器是生产系统的标配
    ——3272次失败的教训值得铭记
  4. 压缩不是终点,压缩后要重建环境感知
    ——重新注入文件、计划、Schema

这套设计之所以值得学习,不是因为它用了多么复杂的算法,而是因为它对边界情况的处理——缓存失效了怎么办、压缩失败了怎么办、连续失败了怎么办。demo只需要跑通happy path,生产系统需要知道所有unhappy path怎么处理。

下一期,我们将进入多Agent编排的世界,看Claude Code如何用Coordinator-Worker架构+邮箱模式,让多个子Agent并行工作而不失控。那个验证Agent的自我欺骗检测列表(“reading is not verification”“the implementer is an LLM,verify independently”),将成为第五期的亮点。


上一篇回顾:Claude Code 源码深度拆解③ | QueryEngine:46000行的TAOR循环如何运转

下一篇预告:Claude Code 源码深度拆解⑤ | 多Agent编排:Coordinator-Worker+邮箱模式详解


延伸思考:如果你的Agent需要支持无限长的会话,除了压缩,还有什么其他策略?提示:想想Sub-Agent隔离、结果磁盘化、LRU缓存……欢迎在评论区分享你的思路。