本系列文章基于 Claude Code 2.1.88 版本的 TypeScript 源码进行分析。源码版权归 Anthropic 所有,本文仅用于技术研究。

引言

上下文窗口管理是 AI 应用工程中最核心的挑战之一。Claude Code 作为一个长时间运行的终端助手,单次会话可能持续数小时、涉及数百次工具调用,上下文 token 消耗极易超出模型限制。src/services/compact/ 目录下的 11 个文件实现了一套完整的上下文预算管理方案,涵盖自动压缩触发、多策略压缩、熔断保护、token 估算等环节。

涉及的核心源码文件:

  • src/services/compact/autoCompact.ts —— 自动压缩触发与熔断
  • src/services/compact/compact.ts —— 压缩策略实现
  • src/services/compact/microCompact.ts —— 缓存级微压缩
  • src/services/compact/sessionMemoryCompact.ts —— 会话记忆压缩
  • src/services/compact/prompt.ts —— 压缩 prompt 模板
  • src/services/tokenEstimation.ts —— 多策略 token 估算
  • src/cost-tracker.ts —— 成本追踪

一、自动压缩触发机制

1.1 阈值计算

系统根据模型的上下文窗口大小动态计算压缩阈值。计算过程分为两步:

第一步,计算有效上下文窗口大小(扣除输出预留):

const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000

export function getEffectiveContextWindowSize(model: string): number {
  const reservedTokensForSummary = Math.min(
    getMaxOutputTokensForModel(model),
    MAX_OUTPUT_TOKENS_FOR_SUMMARY
  )
  let contextWindow = getContextWindowForModel(model, getSdkBetas())
  
  // 支持环境变量覆盖(用于测试)
  const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW
  if (autoCompactWindow) {
    contextWindow = Math.min(contextWindow, parseInt(autoCompactWindow, 10))
  }
  
  return contextWindow - reservedTokensForSummary
}

20,000 token 的预留基于生产数据——源码注释指出"p99.99 of compact summary output being 17,387 tokens"。

第二步,在有效窗口基础上扣除缓冲区:

export const AUTOCOMPACT_BUFFER_TOKENS = 13_000

export function getAutoCompactThreshold(model: string): number {
  return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS
}

1.2 多级警告

系统设置了四个递进的警告级别:

export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
export const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000
export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000

export function calculateTokenWarningState(tokenUsage, model) {
  return {
    percentLeft,                    // 剩余百分比
    isAboveWarningThreshold,        // 距上限 20K tokens
    isAboveErrorThreshold,          // 距上限 20K tokens
    isAboveAutoCompactThreshold,    // 触发自动压缩
    isAtBlockingLimit,              // 距上限 3K tokens(阻止新请求)
  }
}

当 token 用量达到阻塞阈值(距上限仅 3,000 tokens)时,系统会阻止新的请求,强制用户手动压缩。

1.3 递归保护

自动压缩本身也是一次 API 调用,因此需要防止递归触发:

export async function shouldAutoCompact(messages, model, querySource) {
  // 会话记忆提取和压缩本身不触发自动压缩
  if (querySource === 'session_memory' || querySource === 'compact') {
    return false
  }
  // 上下文折叠代理不触发自动压缩
  if (feature('CONTEXT_COLLAPSE')) {
    if (querySource === 'marble_origami') return false
  }
}

源码注释解释了 marble_origami(上下文折叠代理)被排除的原因:如果它的上下文膨胀触发了自动压缩,runPostCompactCleanup 会调用 resetContextCollapse(),而这会销毁主线程的已提交日志(因为是模块级共享状态)。


二、熔断机制

这是整个压缩系统中最具工程价值的设计之一。

const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

export async function autoCompactIfNeeded(messages, toolUseContext, ...) {
  // 熔断器:连续失败 N 次后停止重试
  if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
    return { wasCompacted: false }
  }
  
  try {
    const compactionResult = await compactConversation(...)
    return { wasCompacted: true, consecutiveFailures: 0 }  // 成功则重置
  } catch (error) {
    const nextFailures = (tracking?.consecutiveFailures ?? 0) + 1
    if (nextFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
      logForDebugging(
        `autocompact: circuit breaker tripped after ${nextFailures} 
         consecutive failures — skipping future attempts this session`
      )
    }
    return { wasCompacted: false, consecutiveFailures: nextFailures }
  }
}

源码注释中记录了这一设计的数据驱动背景:

// 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.

在引入熔断器之前,当上下文不可恢复地超出限制时,系统会在每个 turn 都尝试压缩,最多一个会话内连续失败 3,272 次,全局每天浪费约 25 万次 API 调用。熔断器将最大重试次数限制为 3 次。


三、多策略压缩

3.1 压缩优先级

autoCompactIfNeeded 中的压缩策略有明确的优先级:

// 优先尝试会话记忆压缩
const sessionMemoryResult = await trySessionMemoryCompaction(
  messages, toolUseContext.agentId, threshold
)
if (sessionMemoryResult) {
  // 会话记忆压缩成功,跳过传统压缩
  return { wasCompacted: true, compactionResult: sessionMemoryResult }
}

// 回退到传统对话压缩
const compactionResult = await compactConversation(messages, ...)

会话记忆压缩优先于传统压缩,因为它基于记忆提取而非简单截断,能保留更多有价值的上下文信息。

3.2 传统压缩的预处理

compact.ts 中的传统压缩在调用 API 之前会进行多步预处理:

// 剥离消息中的图片内容
function stripImagesFromMessages(messages: Message[]): Message[] { ... }

// 清理重复注入的附件
function stripReinjectedAttachments(messages: Message[]): Message[] { ... }

// 针对 prompt-too-long 错误的紧急头部截断
function truncateHeadForPTLRetry(messages: Message[]): Message[] { ... }

3.3 微压缩

microCompact.ts 实现了缓存级别的细粒度压缩,维护了 pending 和 pinned 两个编辑队列:

function consumePendingCacheEdits(): CacheEdit[] { ... }
function getPinnedCacheEdits(): PinnedCacheEdits[] { ... }
function pinCacheEdits(edits: CacheEdit[]): void { ... }

微压缩在 API 请求层面操作,通过编辑缓存中的内容来减少 token 消耗,粒度比对话级压缩更细。

3.4 按轮次分组

grouping.ts 将消息按 API 轮次分组,确保压缩时不会在一个轮次的中间截断:

function groupMessagesByApiRound(messages: Message[]): Message[][] { ... }

四、Token 估算

tokenEstimation.ts 提供了多层次的 token 计数策略,在精度和性能之间做了梯度权衡。

4.1 API 精确计数

async function countTokensWithAPI(
  messages, model, systemPrompt
): Promise<number> { ... }

调用 Claude API 的 token counting 端点,精度最高但有网络延迟。

4.2 Haiku 回退

async function countTokensViaHaikuFallback(
  messages, systemPrompt
): Promise<number> { ... }

当主模型不可用时,使用 Haiku 模型估算。Haiku 的 tokenizer 与主模型相近,但调用成本更低。

4.3 粗略估算

function roughTokenCountEstimation(text: string): number {
  // 默认 4 bytes per token
}

function bytesPerTokenForFileType(fileExtension: string): number {
  // 不同文件类型的 bytes-per-token 比率不同
  // 代码文件通常比自然语言文本有更高的 bytes-per-token
}

粗略估算按字节数计算,并根据文件类型调整比率。这是最快的估算方式,用于不需要精确计数的场景。

4.4 Bedrock 计数

async function countTokensWithBedrock({ messages, model }): Promise<number> { ... }

针对 AWS Bedrock 部署的专用计数端点。


五、成本追踪

cost-tracker.ts 实现了按模型的 token 用量和费用追踪。

5.1 核心功能

function addToTotalModelUsage(model, inputTokens, outputTokens) { ... }
function addToTotalSessionCost(cost) { ... }
function formatModelUsage(): string { ... }   // 格式化各模型用量
function formatTotalCost(): string { ... }     // 格式化总费用
function formatCost(cost, maxDecimalPlaces): string { ... }

5.2 会话恢复

function getStoredSessionCosts(sessionId): StoredCosts | null { ... }
function restoreCostStateForSession(sessionId): boolean { ... }
function saveCurrentSessionCosts(fpsMetrics?): void { ... }

成本状态支持跨会话持久化和恢复,确保用户在恢复会话时能看到累计的费用统计。


六、Prompt Cache 感知

压缩系统与 prompt cache 机制紧密集成。每次压缩后都会通知 cache 检测系统:

if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
  notifyCompaction(querySource ?? 'compact', toolUseContext.agentId)
}

源码注释解释了这一设计的必要性:

// Reset cache read baseline so the post-compact drop isn't flagged 
// as a break. BQ 2026-03-01: missing this made 20% of 
// tengu_prompt_cache_break events false positives

压缩会导致 prompt cache 命中率下降,如果不重置基线,cache 监控系统会将这种正常下降误报为 cache 中断。在引入这一修复之前,20% 的 cache 中断告警是误报。


七、与其他系统的协同

7.1 与上下文折叠的互斥

当上下文折叠(Context Collapse)功能启用时,自动压缩会被禁用:

if (feature('CONTEXT_COLLAPSE')) {
  const { isContextCollapseEnabled } = require('../contextCollapse/index.js')
  if (isContextCollapseEnabled()) return false
}

源码注释详细解释了原因:上下文折叠在 90% 时提交、95% 时阻塞,而自动压缩在约 93% 时触发。两者会竞争,自动压缩通常先触发,会销毁折叠系统正在保存的细粒度上下文。

7.2 与会话记忆的协同

压缩完成后会重置会话记忆的状态:

// Reset lastSummarizedMessageId since compaction replaces all messages
// and the old message UUID will no longer exist
setLastSummarizedMessageId(undefined)

压缩会替换消息列表,旧的消息 UUID 不再存在,因此记忆系统的"上次总结位置"标记需要重置。


八、总结

Claude Code 的上下文窗口管理系统展示了一套经过生产验证的方案。其核心设计决策可以归纳为:

其一,数据驱动的参数选择。20,000 token 的输出预留基于 p99.99 统计,熔断阈值 3 次基于全局 API 调用浪费的量化分析。这些参数不是拍脑袋决定的,而是来自生产环境的持续监控。

其二,多策略梯度降级。从会话记忆压缩到传统压缩,从 API 精确计数到粗略估算,系统在每个环节都提供了降级方案,确保在各种条件下都能正常工作。

其三,系统间的协同设计。压缩系统与 prompt cache、上下文折叠、会话记忆等系统之间有明确的协同规则和互斥关系,避免了多个系统同时操作上下文导致的冲突。

其四,熔断保护防止级联故障。当压缩持续失败时,熔断器及时止损,避免了无效的 API 调用浪费。这种防御性编程在 AI 应用中尤为重要,因为 API 调用既有延迟又有成本。

对于正在构建长对话 AI 应用的团队,这套系统最值得借鉴的是熔断机制的设计——在 AI 应用中,"知道何时停止重试"与"知道何时触发操作"同样重要。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐