跳到主要内容

autoCompact.ts:何时自动触发压缩

🟡 进阶

自动压缩是 Claude Code 中一个"无感"的后台机制——用户在正常使用时不需要关心,但一旦触发,它能让对话在不中断的情况下突破 token 窗口的限制。autoCompact.ts 负责回答一个核心问题:什么时候该压缩,什么时候还不用?

有效上下文窗口的计算

并非整个 200K token 都可以用于对话历史。autoCompact.ts 首先计算"有效上下文窗口":

// source/src/services/compact/autoCompact.ts
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000

export function getEffectiveContextWindowSize(model: string): number {
// 为压缩摘要的输出预留空间
// (基于 p99.99 的压缩输出为 17,387 tokens)
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) {
const parsed = parseInt(autoCompactWindow, 10)
if (!isNaN(parsed) && parsed > 0) {
contextWindow = Math.min(contextWindow, parsed)
}
}

return contextWindow - reservedTokensForSummary
}

以 Claude 3.7 Sonnet(200K context,64K max output)为例:

有效上下文窗口 = 200,000 - min(64,000, 20,000) = 200,000 - 20,000 = 180,000 tokens

为什么要预留 20,000 tokens?因为压缩本身需要调用 Claude 生成摘要,这个输出也会消耗 token。如果不预留,压缩请求本身可能触发 prompt_too_long

自动压缩阈值

在有效窗口基础上,再减去一个缓冲区:

export const AUTOCOMPACT_BUFFER_TOKENS = 13_000

export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
const autocompactThreshold = effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS

// 支持通过百分比环境变量覆盖(调试用)
const envPercent = process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE
if (envPercent) {
const parsed = parseFloat(envPercent)
if (!isNaN(parsed) && parsed > 0 && parsed <= 100) {
const percentageThreshold = Math.floor(
effectiveContextWindow * (parsed / 100),
)
return Math.min(percentageThreshold, autocompactThreshold)
}
}

return autocompactThreshold
}

算下来:180,000 - 13,000 = 167,000 tokens 是自动压缩的触发阈值(约占总窗口的 83.5%)。

五档警告状态机

calculateTokenWarningState() 计算当前处于哪个警告级别:

export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
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: number,
model: string,
): {
percentLeft: number // 剩余百分比
isAboveWarningThreshold: boolean // 接近上限,显示黄色警告
isAboveErrorThreshold: boolean // 更接近,显示红色警告
isAboveAutoCompactThreshold: boolean // 触发自动压缩
isAtBlockingLimit: boolean // 完全阻塞,禁止继续对话
} {
const autoCompactThreshold = getAutoCompactThreshold(model)
const threshold = isAutoCompactEnabled()
? autoCompactThreshold
: getEffectiveContextWindowSize(model)

const percentLeft = Math.max(
0,
Math.round(((threshold - tokenUsage) / threshold) * 100),
)

// 各阈值计算
const warningThreshold = threshold - WARNING_THRESHOLD_BUFFER_TOKENS
const errorThreshold = threshold - ERROR_THRESHOLD_BUFFER_TOKENS
const blockingLimit = effectiveWindow - MANUAL_COMPACT_BUFFER_TOKENS

return {
percentLeft,
isAboveWarningThreshold: tokenUsage >= warningThreshold,
isAboveErrorThreshold: tokenUsage >= errorThreshold,
isAboveAutoCompactThreshold: isAutoCompactEnabled() && tokenUsage >= autoCompactThreshold,
isAtBlockingLimit: tokenUsage >= blockingLimit,
}
}

可视化这几个阈值:

0               100K             167K    177K  180K 183K
|───────────────|─────────────────|───────|─────|────|
↑ ↑ ↑ ↑
autoCompact warn err block
触发压缩 黄色 红色 禁止

shouldAutoCompact 的决策逻辑

每次对话轮次结束后,shouldAutoCompact() 被调用来决定是否启动压缩:

export async function shouldAutoCompact(
messages: Message[],
model: string,
querySource?: QuerySource,
snipTokensFreed = 0,
): Promise<boolean> {

// 防递归:session_memory 和 compact 本身是 forked agent,不能再自我压缩
if (querySource === 'session_memory' || querySource === 'compact') {
return false
}

// 防递归:context collapse 的内部 agent 不触发 autocompact
if (feature('CONTEXT_COLLAPSE')) {
if (querySource === 'marble_origami') {
return false
}
}

// 功能开关检查
if (!isAutoCompactEnabled()) return false

// 实验性 reactive compact 模式:关闭 proactive autocompact
if (feature('REACTIVE_COMPACT')) {
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) {
return false
}
}

// Context collapse 模式:折叠系统自己管理上下文,不需要 autocompact
if (feature('CONTEXT_COLLAPSE')) {
const { isContextCollapseEnabled } = require('../contextCollapse/index.js')
if (isContextCollapseEnabled()) return false
}

// 计算实际 token 数(扣除 snip 已释放的)
const tokenCount = tokenCountWithEstimation(messages) - snipTokensFreed
const { isAboveAutoCompactThreshold } = calculateTokenWarningState(tokenCount, model)

return isAboveAutoCompactThreshold
}

注意 snipTokensFreed 参数:snip 压缩(一种轻量级预压缩)可能已经释放了一些 token,需要从计数中扣除,避免触发不必要的全量压缩。

连续失败保护

如果自动压缩连续失败(例如因为上下文已经严重超限),继续重试会浪费大量 API 调用:

// source/src/services/compact/autoCompact.ts
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

// AutoCompactTrackingState 记录连续失败次数
export type AutoCompactTrackingState = {
compacted: boolean
turnCounter: number
turnId: string
consecutiveFailures?: number // 连续失败次数,达到 3 次后停止重试
}

源码注释记录了这个设计决策的数据背景:

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

也就是说,历史数据显示有些会话的 autocompact 会失败 3000+ 次,每天全球范围内浪费 25 万次 API 调用。引入 3 次熔断机制后,这个问题得以控制。

isAutoCompactEnabled 的多层检查

export function isAutoCompactEnabled(): boolean {
// 1. 环境变量完全禁用(包括手动 /compact)
if (isEnvTruthy(process.env.DISABLE_COMPACT)) {
return false
}

// 2. 仅禁用自动压缩,保留手动 /compact
if (isEnvTruthy(process.env.DISABLE_AUTO_COMPACT)) {
return false
}

// 3. 用户在全局配置中禁用
const userConfig = getGlobalConfig()
return userConfig.autoCompactEnabled
}

这提供了三个层次的控制:

  • DISABLE_COMPACT=1:完全禁用所有压缩
  • DISABLE_AUTO_COMPACT=1:只禁用自动触发,用户仍可手动 /compact
  • 用户配置 autoCompactEnabled: false:持久化的用户偏好

时序图:自动压缩的完整生命周期

用户发送消息

query.ts 主循环

Claude API 响应

runTools() 执行工具

tokenCountWithEstimation() ←── 估算当前 token 使用量

shouldAutoCompact() ←─────────── 判断是否需要压缩
↓ (是)
autoCompactIfNeeded()

trySessionMemoryCompaction() ←── 先尝试 session memory 压缩(更轻量)
↓ (不足以释放足够空间)
compactConversation() ←────────── 全量压缩

更新 messages ←────────────────── 用摘要替换历史

继续下一轮对话

环境变量配置

对于开发者或高级用户,autoCompact 提供了几个调试用的环境变量:

变量作用
DISABLE_COMPACT=1完全禁用压缩
DISABLE_AUTO_COMPACT=1禁用自动压缩
CLAUDE_CODE_AUTO_COMPACT_WINDOW=50000强制使用更小的上下文窗口(测试用)
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=80以百分比形式设置触发阈值
CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE=150000覆盖阻塞限制
📄source/src/services/compact/autoCompact.tsL28-238查看源码 →