3.5 Token 预算管理
Claude Code 维护精细的 Token 预算追踪:
输出 Token 预留(按模型)
getModelMaxOutputTokens()(src/utils/context.ts)按模型返回默认值和上限:
| 模型 | 默认 max_output_tokens | 上限(upperLimit) |
|---|---|---|
| Opus 4.6 | 64,000 | 128,000 |
| Sonnet 4.6 | 32,000 | 128,000 |
| Opus 4.5 / Sonnet 4.x / Haiku 4 | 32,000 | 64,000 |
| Opus 4 / 4.1 | 32,000 | 32,000 |
| Claude 3.7 Sonnet | 32,000 | 64,000 |
| Claude 3 Sonnet、3.5 Sonnet/Haiku | 8,192 | 8,192 |
| Claude 3 Opus / Haiku | 4,096 | 4,096 |
未命中的模型走兜底 MAX_OUTPUT_TOKENS_DEFAULT;可通过 CLAUDE_CODE_MAX_OUTPUT_TOKENS 环境变量覆盖。思考 Token 预算不再是一张固定表:getMaxThinkingTokensForModel() 已标记 deprecated,实现只是返回 upperLimit - 1,新模型改用自适应思考(adaptive thinking)而非严格的思考预算。
Token 估算算法
tokenCountWithEstimation()(src/utils/tokens.ts)是上下文大小估算的核心函数。它的设计原则是从不调用 API——避免网络延迟对压缩决策的影响。
核心思路可以用一个类比来理解:假设你今早称了体重是 75 公斤,此后吃了一顿午饭。你不需要再次上称——估计 75.5 公斤就足够好了。 tokenCountWithEstimation() 的"体重秤"是 API 返回的 usage 数据(服务端精确计算的 Token 数),"午饭"是此后新增的少量消息。
算法逻辑:
export function tokenCountWithEstimation(messages: readonly Message[]): number {
// 1. 从消息末尾向前查找,找到最近一条有 API usage 数据的消息
let i = messages.length - 1
while (i >= 0) {
const usage = getTokenUsage(messages[i])
if (usage) {
// 2. 向前跳过同一 API 响应的分裂记录(相同 message.id)
// 并行工具调用可能将一个响应拆成多条消息
const responseId = getAssistantMessageId(messages[i])
if (responseId) {
let j = i - 1
while (j >= 0) {
if (getAssistantMessageId(messages[j]) === responseId) {
i = j // 锚定到同一响应的最早记录
} else if (getAssistantMessageId(messages[j]) !== undefined) {
break // 遇到不同的 API 响应,停止
}
j--
}
}
// 3. 用 server 报告的 token 数作为锚点,加上后续消息的粗略估算
return getTokenCountFromUsage(usage) +
roughTokenCountEstimationForMessages(messages.slice(i + 1))
}
i--
}
// 4. 如果没有任何 usage 数据(如会话刚开始),完全靠字符串长度估算
return roughTokenCountEstimationForMessages(messages)
}关键洞察:每次 API 响应都自带 usage 数据(包含 input_tokens、output_tokens、cache tokens),这是 server 端精确计算的结果。tokenCountWithEstimation() 把这个精确值作为锚点,只对锚点之后的新消息(通常只有几条工具结果)做粗略估算。估算按字符数 ÷ 4,即每 token 约 4 字节;密集 JSON 里单字符 token 多,改用 ÷ 2 的更保守比率。
这比完全靠客户端估算精确得多(误差从可能的 30%+ 降到通常 <5%),同时又不需要额外的 API 调用。
Task Budget 跨压缩结转
每次压缩前捕获 finalContextTokensFromLastResponse(),压缩后从剩余量中扣除。这确保跨压缩的 Token 预算连续性——压缩会"替换"消息,但 server 看到的只是压缩后的摘要,不知道压缩前的上下文有多大。taskBudgetRemaining 告诉 server:这些 Token 已经被"花掉"了。
// query.ts — 循环级别的 remaining 追踪
let taskBudgetRemaining: number | undefined = undefined
// 每次 compact 时:
// taskBudgetRemaining -= finalContextTokensFromLastResponse(messages)