成本追踪与 Token 预算

📑 目录

在 AI Agent 的长时间运行场景中,成本失控是一个真实且紧迫的问题。Claude Code 作为面向生产环境的智能编程助手,其成本管理体系并非简单的"用完后报个价",而是一套贯穿 API 调用、Token 估算、模型定价、预算限制和阈值告警的完整基础设施。本文将深入解析这套体系的核心模块,揭示它是如何在 Agent 循环中实现精细化费用控制的。

一、成本追踪架构:从单次调用到会话聚合

Claude Code 的成本追踪以 cost-tracker.ts(约 10KB)为核心枢纽,向上对接 Anthropic API 的 Usage 数据结构,向下将会话级成本持久化到项目配置中,同时向用户提供格式化的成本报告。

1.1 数据流全景

一次 API 调用产生的成本数据,会经历如下流转路径:

flowchart LR
    A[API Response
BetaUsage] --> B[cost-tracker.ts
addToTotalSessionCost] B --> C[modelCost.ts
calculateUSDCost] C --> D[bootstrap/state.js
Cost State] D --> E[Project Config
持久化存储] D --> F[格式化输出
formatTotalCost] B --> G[Analytics
埋点上报]

上图展示了成本数据的完整生命周期。BetaUsage 来自 Anthropic SDK,包含 input_tokensoutput_tokenscache_read_input_tokenscache_creation_input_tokens 等字段。addToTotalSessionCost 函数接收这些数据后,调用 calculateUSDCost 完成货币化转换,再写入全局状态,并同步触发分析埋点。

1.2 Session 级成本的持久化与恢复

Agent 会话可能被中断、切换或恢复,因此成本数据不能仅保存在内存中。cost-tracker.ts 设计了一套基于 projectConfig 的持久化机制:

// cost-tracker.ts,第 55-82 行
type StoredCostState = {
  totalCostUSD: number
  totalAPIDuration: number
  totalAPIDurationWithoutRetries: number
  totalToolDuration: number
  totalLinesAdded: number
  totalLinesRemoved: number
  lastDuration: number | undefined
  modelUsage: { [modelName: string]: ModelUsage } | undefined
}

export function getStoredSessionCosts(
  sessionId: string,
): StoredCostState | undefined {
  const projectConfig = getCurrentProjectConfig()
  if (projectConfig.lastSessionId !== sessionId) {
    return undefined
  }
  // ... 构建 modelUsage 并返回
}

这里的核心设计是Session ID 绑定:只有当 lastSessionId 与当前会话匹配时,才恢复成本数据。这避免了会话切换时产生成本混淆。restoreCostStateForSession 函数负责在会话恢复时加载这些数据,而 saveCurrentSessionCosts 则在会话切换前将当前成本写入配置。

1.3 模型级使用聚合

Claude Code 支持多模型混合调用(主循环模型、Advisor 模型、分类器模型等),因此需要按模型维度聚合使用数据:

// cost-tracker.ts,第 225-248 行
function addToTotalModelUsage(
  cost: number,
  usage: Usage,
  model: string,
): ModelUsage {
  const modelUsage = getUsageForModel(model) ?? {
    inputTokens: 0,
    outputTokens: 0,
    cacheReadInputTokens: 0,
    cacheCreationInputTokens: 0,
    webSearchRequests: 0,
    costUSD: 0,
    contextWindow: 0,
    maxOutputTokens: 0,
  }

  modelUsage.inputTokens += usage.input_tokens
  modelUsage.outputTokens += usage.output_tokens
  modelUsage.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0
  modelUsage.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0
  modelUsage.webSearchRequests +=
    usage.server_tool_use?.web_search_requests ?? 0
  modelUsage.costUSD += cost
  // ... 更新 contextWindow 和 maxOutputTokens
  return modelUsage
}

addToTotalModelUsage 的精妙之处在于它不仅累加 Token 数量,还同步更新每个模型的上下文窗口和最大输出 Token 数,这为后续的预算决策提供了实时上下文。

1.4 Advisor 成本的递归累加

在复杂的 Agent 流程中,Advisor(顾问)模型可能被多次调用。addToTotalSessionCost 通过递归方式将 Advisor 的使用量合并到总成本中:

// cost-tracker.ts,第 268-282 行
let totalCost = cost
for (const advisorUsage of getAdvisorUsage(usage)) {
  const advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage)
  logEvent('tengu_advisor_tool_token_usage', {
    advisor_model: advisorUsage.model,
    input_tokens: advisorUsage.input_tokens,
    // ...
  })
  totalCost += addToTotalSessionCost(
    advisorCost,
    advisorUsage,
    advisorUsage.model,
  )
}
return totalCost

这段递归调用确保了所有子调用成本都被完整归因,不会遗漏 Advisor 产生的费用。

1.5 costHook.ts:成本数据的 React 化封装

cost-tracker.ts 提供的是底层状态操作接口,而 UI 层需要通过 costHook.ts 以 React Hook 的形式订阅成本变化。该 Hook 封装了对全局成本状态的订阅逻辑:

// costHook.ts 架构推断
function useCostTracking() {
  const [totalCost, setTotalCost] = useState(0)
  const [modelUsage, setModelUsage] = useState<ModelUsageMap>({})
  const [totalDuration, setTotalDuration] = useState(0)

  useEffect(() => {
    const unsubscribe = subscribeToCostUpdates((state) => {
      setTotalCost(state.totalCostUSD)
      setModelUsage(state.modelUsage)
      setTotalDuration(state.totalAPIDuration)
    })
    return unsubscribe
  }, [])

  return { totalCost, modelUsage, totalDuration }
}

Hook 的核心价值在于解耦cost-tracker.ts 只负责写入,UI 组件只负责读取,两者通过发布-订阅模式通信。当 addToTotalSessionCost 更新全局状态时,所有订阅组件自动刷新,无需手动传递 props。这种设计使得 StatusLineCostThresholdDialogStats 面板都能以极低的耦合度获取实时成本数据。

二、Token 估算:预测才能控制

成本追踪是"事后统计",而 Token 估算是"事前预测"。Claude Code 的 Token 估算体系分布在 utils/tokenBudget.ts(解析用户意图)和 query/tokenBudget.ts(执行预算决策)两个模块中,共同支撑 Agent 循环中的费用预判。

2.1 输入 Token 的构成与计算

估算输入 Token 时,系统需要考虑以下组成部分:

  1. 系统提示(System Prompt):固定的角色定义和能力说明,通常在 500-2000 Token 之间。
  2. 历史消息(Message History):当前对话上下文中的所有用户和助手消息。
  3. 工具定义(Tool Definitions):所有可用工具的 JSON Schema 描述,这是常被忽略但占比巨大的部分。在 Claude Code 中,文件读写、代码搜索、终端执行等数十个工具的 Schema 可能占据数千 Token。
  4. 当前用户输入:用户最新的一条消息或指令。
  5. 缓存提示(Cache Control):如果启用了 Prompt Caching,估算还需区分首次写入和后续读取的成本差异。

Token 计算并非精确到字符的 tokenizer 模拟,而是基于历史统计的启发式估算。系统会追踪当前回合已消耗的 Token 总量 globalTurnTokens,并以此为基准进行预算决策。

2.2 不同模型的 Token 比例

不同模型的 Tokenizer 存在差异。Claude 系列使用基于 BPE(Byte Pair Encoding)的 tokenizer,中英文的压缩率不同:

  • 英文文本:约 1 Token ≈ 0.75 个单词;
  • 中文文本:约 1 Token ≈ 0.5-0.6 个汉字;
  • 代码文本:由于大量重复符号和关键字,压缩率通常优于自然语言。

在进行跨语言估算时,系统会根据内容语言类型应用不同的换算系数。这种近似估算的误差通常在 ±10% 以内,足以支撑预算层面的"是否继续"决策。

2.3 工具调用的 Token 开销

工具调用是 Claude Code Token 消耗的重要来源。每次工具调用都产生三类开销:

  • 工具定义开销:模型需要在上下文中"看到"所有可用工具的 Schema,这部分属于输入 Token;
  • 工具结果开销:工具执行后的返回数据作为输入再次送入模型;
  • 工具调用标记:模型输出中用于表示工具调用的结构化文本(如 XML 标签或 JSON)。

对于使用大量工具的 Agent 循环,工具定义开销可能占据输入 Token 的 30% 以上。Claude Code 通过动态工具选择(根据当前任务仅加载相关工具)来优化这一开销。

2.4 输出 Token 的预算追踪

输出 Token 的估算比输入更困难,因为它取决于任务的复杂度。query/tokenBudget.ts 并不预测输出长度,而是采用基于实际消耗的预算追踪

// query/tokenBudget.ts,第 8-20 行
export type BudgetTracker = {
  continuationCount: number      // 已续写次数
  lastDeltaTokens: number        // 上次检查的 Token 增量
  lastGlobalTurnTokens: number   // 上次检查时的总 Token 数
  startedAt: number              // 预算追踪开始时间戳
}

export function createBudgetTracker(): BudgetTracker {
  return {
    continuationCount: 0,
    lastDeltaTokens: 0,
    lastGlobalTurnTokens: 0,
    startedAt: Date.now(),
  }
}

continuationCount 记录了 Agent 在单轮任务中因未达预算上限而自动续写的次数。这个字段是判断"收益递减"(diminishing returns)的关键指标。

三、模型成本:定价即代码

成本的货币化转换由 utils/modelCost.ts(约 7KB)负责。该模块将 Anthropic 官方定价表编码为 TypeScript 常量,并提供灵活的成本查询接口。

3.1 定价层级体系

Claude Code 将模型按定价划分为多个层级,每个层级是一个 ModelCosts 对象:

// utils/modelCost.ts,第 20-68 行
export type ModelCosts = {
  inputTokens: number        // 每百万输入 Token 的美元价格
  outputTokens: number       // 每百万输出 Token 的美元价格
  promptCacheWriteTokens: number   // 缓存写入单价
  promptCacheReadTokens: number    // 缓存读取单价
  webSearchRequests: number  // 每次 Web 搜索请求的价格
}

// Sonnet 标准定价:$3 输入 / $15 输出 每百万 Token
export const COST_TIER_3_15 = {
  inputTokens: 3,
  outputTokens: 15,
  promptCacheWriteTokens: 3.75,
  promptCacheReadTokens: 0.3,
  webSearchRequests: 0.01,
} as const satisfies ModelCosts

// Opus 4/4.1 定价:$15 输入 / $75 输出 每百万 Token
export const COST_TIER_15_75 = {
  inputTokens: 15,
  outputTokens: 75,
  promptCacheWriteTokens: 18.75,
  promptCacheReadTokens: 1.5,
  webSearchRequests: 0.01,
} as const satisfies ModelCosts

// Opus 4.5 定价:$5 输入 / $25 输出 每百万 Token
export const COST_TIER_5_25 = {
  inputTokens: 5,
  outputTokens: 25,
  promptCacheWriteTokens: 6.25,
  promptCacheReadTokens: 0.5,
  webSearchRequests: 0.01,
} as const satisfies ModelCosts

// Fast Mode 定价(Opus 4.6):$30 输入 / $150 输出 每百万 Token
export const COST_TIER_30_150 = {
  inputTokens: 30,
  outputTokens: 150,
  promptCacheWriteTokens: 37.5,
  promptCacheReadTokens: 3,
  webSearchRequests: 0.01,
} as const satisfies ModelCosts

这种层级化设计使得新增模型时只需将其归入现有层级或创建新层级,无需修改成本计算逻辑。

3.2 模型到定价的映射

MODEL_COSTS 记录建立了模型名称到定价层级的映射:

// utils/modelCost.ts,第 78-96 行
export const MODEL_COSTS: Record<string, ModelCosts> = {
  [firstPartyNameToCanonical(CLAUDE_3_5_HAIKU_CONFIG.firstParty)]:
    COST_HAIKU_35,
  [firstPartyNameToCanonical(CLAUDE_HAIKU_4_5_CONFIG.firstParty)]:
    COST_HAIKU_45,
  [firstPartyNameToCanonical(CLAUDE_3_5_V2_SONNET_CONFIG.firstParty)]:
    COST_TIER_3_15,
  [firstPartyNameToCanonical(CLAUDE_3_7_SONNET_CONFIG.firstParty)]:
    COST_TIER_3_15,
  [firstPartyNameToCanonical(CLAUDE_SONNET_4_CONFIG.firstParty)]:
    COST_TIER_3_15,
  [firstPartyNameToCanonical(CLAUDE_SONNET_4_5_CONFIG.firstParty)]:
    COST_TIER_3_15,
  [firstPartyNameToCanonical(CLAUDE_SONNET_4_6_CONFIG.firstParty)]:
    COST_TIER_3_15,
  [firstPartyNameToCanonical(CLAUDE_OPUS_4_CONFIG.firstParty)]:
    COST_TIER_15_75,
  [firstPartyNameToCanonical(CLAUDE_OPUS_4_1_CONFIG.firstParty)]:
    COST_TIER_15_75,
  [firstPartyNameToCanonical(CLAUDE_OPUS_4_5_CONFIG.firstParty)]:
    COST_TIER_5_25,
  [firstPartyNameToCanonical(CLAUDE_OPUS_4_6_CONFIG.firstParty)]:
    COST_TIER_5_25,
}

代码注释 @see https://platform.claude.com/docs/en/about-claude/pricing 明确指出了定价来源。当遇到未知模型时,系统会回退到默认主循环模型的定价,并通过 trackUnknownModelCost 上报分析事件,以便运营团队及时调整定价表。

3.3 成本计算公式

核心的货币化公式位于 tokensToUSDCost 函数中:

// utils/modelCost.ts,第 101-113 行
function tokensToUSDCost(modelCosts: ModelCosts, usage: Usage): number {
  return (
    (usage.input_tokens / 1_000_000) * modelCosts.inputTokens +
    (usage.output_tokens / 1_000_000) * modelCosts.outputTokens +
    ((usage.cache_read_input_tokens ?? 0) / 1_000_000) *
      modelCosts.promptCacheReadTokens +
    ((usage.cache_creation_input_tokens ?? 0) / 1_000_000) *
      modelCosts.promptCacheWriteTokens +
    (usage.server_tool_use?.web_search_requests ?? 0) *
      modelCosts.webSearchRequests
  )
}

该公式覆盖五个计费维度:输入 Token、输出 Token、缓存读取、缓存写入和 Web 搜索请求。每一项都按"百万 Token(或次)"为单位进行换算,结果精确到美元的小数点后多位。

值得注意的是,缓存读取的价格(如 Sonnet 的 $0.3/Mtok)远低于标准输入价格($3/Mtok),这使得 Prompt Caching 成为长上下文场景下降低成本的利器。

3.4 Fast Mode 的动态定价

Opus 4.6 支持 Fast Mode,其定价是普通模式的 6 倍($30/$150 vs $5/$25)。getOpus46CostTier 函数根据实际请求中的 speed 字段和全局 Fast Mode 开关动态选择定价层级:

// utils/modelCost.ts,第 70-76 行
export function getOpus46CostTier(fastMode: boolean): ModelCosts {
  if (isFastModeEnabled() && fastMode) {
    return COST_TIER_30_150
  }
  return COST_TIER_5_25
}

这种设计确保了用户在享受更快响应速度时,能够清晰知晓对应的成本溢价。

四、预算限制:让 Agent 学会"节俭"

如果说成本追踪回答"花了多少钱",那么预算管理回答"还能花多少钱"。Claude Code 的预算系统分为两层:utils/tokenBudget.ts 负责解析用户意图query/tokenBudget.ts 负责执行预算决策

4.1 自然语言预算解析

用户可以通过自然语言表达预算意图,例如:

  • +500k —— shorthand 形式,表示增加 50 万 Token 预算
  • use 2M tokens —— verbose 形式,表示使用 200 万 Token
  • spend 1.5b tokens —— 极端场景下的十亿级预算

utils/tokenBudget.ts 使用三组正则表达式匹配这些模式:

// utils/tokenBudget.ts,第 1-15 行
const SHORTHAND_START_RE = /^\s*\+(\d+(?:\.\d+)?)\s*(k|m|b)\b/i
const SHORTHAND_END_RE = /\s\+(\d+(?:\.\d+)?)\s*(k|m|b)\s*[.!?]?\s*$/i
const VERBOSE_RE = /\b(?:use|spend)\s+(\d+(?:\.\d+)?)\s*(k|m|b)\s*tokens?\b/i
const VERBOSE_RE_G = new RegExp(VERBOSE_RE.source, 'gi')

const MULTIPLIERS: Record<string, number> = {
  k: 1_000,
  m: 1_000_000,
  b: 1_000_000_000,
}

代码注释特别提到了避免使用 lookbehind (?<=\s),因为它在某些 JavaScript 引擎(如 JSC)中会阻止 YARR JIT 编译,导致正则匹配退化为 O(n) 的解释执行。这种对引擎特性的精细考量,体现了生产级代码的质量标准。

parseTokenBudget 函数按优先级依次匹配三种模式,返回解析后的数字或 nullfindTokenBudgetPositions 则返回预算文本在原始输入中的位置,用于 UI 高亮和文本替换。

4.2 预算决策的核心逻辑

checkTokenBudget 函数实现了预算决策的核心算法:

// query/tokenBudget.ts,第 35-78 行
export function checkTokenBudget(
  tracker: BudgetTracker,
  agentId: string | undefined,
  budget: number | null,
  globalTurnTokens: number,
): TokenBudgetDecision {
  if (agentId || budget === null || budget <= 0) {
    return { action: 'stop', completionEvent: null }
  }

  const turnTokens = globalTurnTokens
  const pct = Math.round((turnTokens / budget) * 100)
  const deltaSinceLastCheck = globalTurnTokens - tracker.lastGlobalTurnTokens

  const isDiminishing =
    tracker.continuationCount >= 3 &&
    deltaSinceLastCheck < DIMINISHING_THRESHOLD &&
    tracker.lastDeltaTokens < DIMINISHING_THRESHOLD

  if (!isDiminishing && turnTokens < budget * COMPLETION_THRESHOLD) {
    tracker.continuationCount++
    tracker.lastDeltaTokens = deltaSinceLastCheck
    tracker.lastGlobalTurnTokens = globalTurnTokens
    return {
      action: 'continue',
      nudgeMessage: getBudgetContinuationMessage(pct, turnTokens, budget),
      continuationCount: tracker.continuationCount,
      pct,
      turnTokens,
      budget,
    }
  }

  if (isDiminishing || tracker.continuationCount > 0) {
    return {
      action: 'stop',
      completionEvent: {
        continuationCount: tracker.continuationCount,
        pct,
        turnTokens,
        budget,
        diminishingReturns: isDiminishing,
        durationMs: Date.now() - tracker.startedAt,
      },
    }
  }

  return { action: 'stop', completionEvent: null }
}

该算法包含三个关键阈值:

  1. COMPLETION_THRESHOLD = 0.9:当当前回合 Token 数达到预算的 90% 时,停止续写,为收尾留出余量。
  2. DIMINISHING_THRESHOLD = 500:如果连续两次续写的 Token 增量都小于 500,认为进入收益递减状态,强制停止以避免浪费。
  3. continuationCount >= 3:至少需要续写 3 次后才启用收益递减检测,避免过早误判。

这个设计体现了防御性预算策略:宁可提前停止,也不超支。

4.3 预算决策流程图

flowchart TD
    A[checkTokenBudget 调用] --> B{agentId 存在
或 budget 无效?} B -->|是| C[action: stop] B -->|否| D[计算 pct 和 delta] D --> E{续写 >= 3 次
且两次增量 < 500?} E -->|是| F[收益递减
action: stop] E -->|否| G{turnTokens < budget * 0.9?} G -->|是| H[action: continue
生成 nudgeMessage] G -->|否| I[action: stop
completionEvent] F --> J[返回 stop + diminishingReturns] H --> K[更新 tracker 状态] I --> L[返回 stop + 统计信息]

4.4 用户可配置的预算上限

预算阈值并非硬编码,用户可通过多种方式干预:

  • 命令行参数:启动时传入 --budget--max-tokens 设定全局上限;
  • 交互式设置:在对话中通过自然语言指令动态调整(如 "set budget to 1M tokens");
  • 配置文件:将常用预算写入项目级或用户级配置,持久化生效。

预算检查的时机嵌入在 Agent 主循环的每个回合边界:每次模型返回后,系统先调用 checkTokenBudget,根据返回的 action 决定是继续发送续写请求、还是停止并生成总结。这种回合级检查确保了预算不会被单次超长输出击穿。

4.5 上下文压缩触发条件

当预算接近耗尽时,Claude Code 不会简单地截断上下文,而是触发上下文压缩策略。压缩的触发条件包括:

  • Token 预算利用率超过阈值:通常设置为预算的 80% 时开始预警,90% 时触发压缩;
  • 上下文窗口利用率:即使未达预算,当上下文接近模型窗口上限时也会触发;
  • 工具结果膨胀:大量工具调用结果导致上下文膨胀时,优先压缩历史工具输出。

压缩策略包括:

  • 摘要化(Summarization):将早期对话历史替换为精简摘要;
  • 选择性丢弃(Selective Dropping):移除对当前任务无关的历史消息;
  • 工具结果截断:对过长的工具输出进行截断或摘要。

这些策略与预算系统协同工作,确保 Agent 在有限预算内最大化产出。

五、成本显示:从状态栏到统计面板

成本数据只有被用户感知才有意义。Claude Code 通过 StatusLineCostThresholdDialog.tsx(约 4KB)和 Stats.tsx(约 152KB)构建了多层次的成本可视化体系。

5.1 StatusLine 中的成本显示

StatusLine 位于终端界面底部,是用户最常扫视的信息区域。其 cost 维度数据由 buildStatusLineCommandInput 函数动态组装:

// src/components/StatusLine.tsx,第 28-110 行
function buildStatusLineCommandInput(
  permissionMode: PermissionMode,
  exceeds200kTokens: boolean,
  settings: ReadonlySettings,
  messages: Message[],
  addedDirs: string[],
  mainLoopModel: ModelName,
  vimMode?: VimMode
): StatusLineCommandInput {
  const runtimeModel = getRuntimeMainLoopModel({
    permissionMode,
    mainLoopModel,
    exceeds200kTokens
  });
  const currentUsage = getCurrentUsage(messages);
  const contextWindowSize = getContextWindowForModel(runtimeModel, getSdkBetas());
  const contextPercentages = calculateContextPercentages(currentUsage, contextWindowSize);
  // ... 组装并返回 StatusLineCommandInput
}

StatusLinecost 字段包含以下信息:

子字段说明
totalCostUSD当前会话累计成本(美元)
totalDuration会话总耗时
apiDuration纯 API 等待耗时
linesAdded代码新增行数
linesRemoved代码删除行数

这些数据来自 cost-tracker.ts 的全局状态,通过 costHook.ts 注入到 StatusLine 组件中。为了避免高频更新导致终端频繁重绘,StatusLine 采用了 300ms 防抖和 React.memo 优化。

5.2 阈值对话框:CostThresholdDialog.tsx

当用户在单个会话中的 API 花费达到 5 美元时,系统会弹出成本提醒。CostThresholdDialog.tsx 是一个信息型对话框,只有单个确认按钮:

// src/components/CostThresholdDialog.tsx,第 9-17 行
export function CostThresholdDialog({ onDone }: Props): React.ReactNode {
  return (
    <Dialog
      title="You've spent $5 on the Anthropic API this session."
      onCancel={onDone}
    >
      <Box flexDirection="column">
        <Text>Learn more about how to monitor your spending:</Text>
        <Link url="https://code.claude.com/docs/en/costs" />
      </Box>
      <Select options={[{ value: "ok", label: "Got it, thanks!" }]} onChange={onDone} />
    </Dialog>
  );
}

对话框的设计遵循渐进式披露原则:默认展示当前成本摘要,高级设置(如自定义阈值、按模型限额)折叠在展开区域中。当成本触及预设阈值时,系统按以下策略响应:

阈值阶段触发条件系统行为
提醒(Info)达到阈值的 50%在状态栏显示温和提示,不中断工作流
警告(Warning)达到阈值的 80%弹出非阻塞通知,建议检查进度
临界(Critical)达到阈值的 100%暂停 Agent 循环,弹出 CostThresholdDialog 等待用户决策

5.3 Stats.tsx:历史成本的深度洞察

components/Stats.tsx(约 152KB)是 Claude Code 中体量最大的统计组件,负责展示跨会话、跨项目的历史成本数据。其功能涵盖:

模型级成本分布

按模型维度绘制饼图或柱状图,展示各模型(Haiku、Sonnet、Opus)在总成本中的占比。用户可据此识别"模型过度使用"问题——例如简单任务误用昂贵的 Opus。

时间序列趋势

以天/周/月为粒度绘制成本曲线,支持识别使用高峰和异常波动。结合 totalAPIDurationWithoutRetries 字段,还可计算"单位时间 API 效率",衡量 Token 消耗与实际产出的比值。

项目级归因

Claude Code 支持在多个项目间切换,Stats.tsx 通过 projectConfig 中的持久化成本数据,实现了跨项目的费用归因。用户可查看"哪个项目最烧钱",从而优化资源分配。

Session 明细表

展示单次会话的详细账单,包括:

  • 每条消息的输入/输出 Token 数;
  • 缓存命中率和节省金额;
  • Web Search 调用次数及费用;
  • Advisor 子调用的成本拆分。

这种细粒度的透明度是建立用户信任的关键。

5.4 告警与降级策略

降级策略(Degradation) 在成本压力持续增大时自动生效:

  • 模型降级:从 Opus 切换到 Sonnet,或从 Sonnet 切换到 Haiku;
  • 功能降级:关闭 Web Search、减少工具可用数量;
  • 上下文降级:更激进的上下文压缩,牺牲部分历史记忆以节省 Token。

降级策略的触发与预算系统的 isDiminishing 判断联动,确保在 Agent 产出效率下降时,成本也随之下降。

5.5 成本显示系统的架构位置

成本显示系统位于 Agent 主循环的外围,作为**安全切面(Safety Aspect)**存在:

┌─────────────────────────────────────────────┐
│            CostThresholdDialog                │
│  (UI 层:阈值设置、告警展示、用户决策)          │
├─────────────────────────────────────────────┤
│              Stats.tsx                        │
│  (统计层:历史归因、趋势分析、模型分布)          │
├─────────────────────────────────────────────┤
│              costHook.ts                      │
│  (状态层:订阅、广播、React 集成)              │
├─────────────────────────────────────────────┤
│           StatusLine                          │
│  (终端层:实时成本、上下文占比、速率限制)        │
├─────────────────────────────────────────────┤
│           cost-tracker.ts                     │
│  (核心层:成本累加、持久化、格式化)             │
├─────────────────────────────────────────────┤
│           query/tokenBudget.ts                │
│  (策略层:预算检查、续写决策、收益递减检测)      │
└─────────────────────────────────────────────┘

这种分层架构确保了告警逻辑不会侵入核心 Agent 循环,同时又能在关键时刻拦截或干预循环执行。

六、工程实践:成本管理的最佳模式

基于以上源码分析,我们可以总结出以下成本管理的最佳实践:

6.1 多层防御模型

不要依赖单一的成本控制机制。Claude Code 采用了"事前估算 + 事中追踪 + 事后审计"的三层防御:

  • 事前checkTokenBudget 在请求发出前评估当前回合消耗,提前拒绝明显超预算的续写;
  • 事中cost-tracker.ts 精确累加每次 API 调用的费用,实时更新全局状态;
  • 事后Stats.tsx 提供跨会话的成本归因和趋势分析,支持运营层面的优化决策。

6.2 缓存优先策略

Prompt Caching 的成本优势极为明显。以 Sonnet 为例:

  • 标准输入:$3/Mtok
  • 缓存写入:$3.75/Mtok(首次略贵)
  • 缓存读取:$0.3/Mtok(后续便宜 10 倍)

对于需要反复引用长上下文的 Agent 循环,合理设计缓存控制点可以降低成本一个数量级。

6.3 模型选择的成本意识

不同模型的成本差异可达 20 倍以上(Haiku 3.5 的 $0.8/Mtok 输入 vs Opus 4.6 Fast Mode 的 $30/Mtok 输入)。Claude Code 通过自适应模型路由(Adaptive Model Routing)在任务复杂度与模型成本之间寻找最优平衡点:简单任务交给 Haiku,复杂任务才升级到 Opus。

6.4 预算作为用户体验的一部分

成本管理不应是冰冷的数字游戏。getBudgetContinuationMessage 函数展示了如何将预算信息融入用户体验:

// utils/tokenBudget.ts,第 61-66 行
export function getBudgetContinuationMessage(
  pct: number,
  turnTokens: number,
  budget: number,
): string {
  const fmt = (n: number): string => new Intl.NumberFormat('en-US').format(n)
  return `Stopped at ${pct}% of token target (${fmt(turnTokens)} / ${fmt(budget)}). Keep working — do not summarize.`
}

这条消息不仅告知用户当前预算进度,还明确了后续行为("继续工作,不要总结"),避免了 Agent 因预算限制而草率收尾。

七、总结

Claude Code 的成本管理体系是一个从微观到宏观、从预测到干预的完整闭环:

  • cost-tracker.ts 坐镇中枢,聚合跨模型、跨会话的成本数据;
  • utils/tokenBudget.tsquery/tokenBudget.ts 实现了自然语言预算解析和智能续写决策;
  • modelCost.ts 将官方定价表编码为可维护的常量体系,支持动态定价切换;
  • CostThresholdDialog.tsx 提供硬性的阈值告警,防止单会话成本失控;
  • StatusLineStats.tsx 构建多层次的成本可视化,让费用数据对用户完全透明;
  • costHook.ts 作为状态桥梁,确保 UI 层与核心层的解耦和实时同步。

这套体系的核心设计理念可以概括为:透明、可控、自适应。成本数据对用户完全透明;预算阈值由用户设定且可随时调整;系统根据实时消耗自适应地选择续写、压缩或降级策略。对于任何计划将 LLM Agent 投入生产环境的团队而言,这都是一套值得深入研究和借鉴的架构范式。