Claude Code 源码分析(四):上下文窗口管理 —— 长对话场景下的 Token 预算与自动压缩
本系列文章基于 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 应用中,"知道何时停止重试"与"知道何时触发操作"同样重要。
更多推荐


所有评论(0)