职责概述

解决的问题:用大模型烧钱很快——一个复杂任务可能消耗数美元的 token。用户和组织需要知道花了多少钱、还剩多少预算,以及在预算快耗尽时优雅降级而不是直接报错。

应用场景:① 实时显示当前对话已消耗的 token 数和费用 ② Pro 用户达到每日限额时提示升级而不是直接中断 ③ 组织管理员设置月度预算上限 ④ 自动压缩上下文以减少 token 消耗 ⑤ 按模型分类统计成本,帮助用户选择性价比更高的模型。

一句话理解:就像手机的流量管理——实时显示用了多少流量、还剩多少、快超了就提醒你,超了就限速但不断网。

架构设计

成本采集层
cost-tracker.ts
costHook.ts
addToTotalSessionCost()
用量计算层
calculateUSDCost()
formatTotalCost()
getModelUsage()
阈值管理层
calculateTokenWarningState()
getAutoCompactThreshold()
getEffectiveContextWindowSize()
策略执行层
services/policyLimits/
isPolicyAllowed()
loadPolicyLimits()
持久化层
saveCurrentSessionCosts()
restoreCostStateForSession()
getStoredSessionCosts()

核心数据流

成本追踪流程

API 响应
queryModelWithStreaming() 返回包含 usage 字段的 AssistantMessage
addToTotalSessionCost()
计算 USD 成本(calculateUSDCost),累加到 bootstrap/state.js 的全局状态
按模型分类
addToTotalModelUsage() 维护 per-model 的 input/output/cache token 和 cost 统计
Advisor 用量
getAdvisorUsage() 提取嵌入在主响应中的 advisor(小模型)token 用量
Counter 上报
getCostCounter().add() 和 getTokenCounter().add() 写入 OpenTelemetry metrics

策略限制执行流程

初始化:loadPolicyLimits()
启动时获取策略限制,创建 loadingCompletePromise 供其他系统等待
fetchWithRetry()
带指数退避的 5 次重试,支持 ETag 条件请求(304 = 缓存有效)
缓存策略
成功获取:写入 policy-limits.json;失败且有缓存:使用过期缓存;失败且无缓存:fail open
isPolicyAllowed() 查询
各功能模块在运行时同步查询,返回 boolean;HIPAA 场景 fail closed
后台轮询(1h 间隔)
startBackgroundPolling() 定期刷新策略,.unref() 不阻止进程退出

Token 预算多级阈值

安全区
token < threshold - 20K
警告区
threshold - 20K ~ threshold - 20K
错误区
threshold - 20K ~ threshold
自动压缩
threshold = effectiveWindow - 13K
阻塞
token ≥ effectiveWindow - 3K

关键类型与接口

Token 警告状态

// services/compact/autoCompact.ts:93-145
export function calculateTokenWarningState(
  tokenUsage: number,
  model: string,
): {
  percentLeft: number                   // 剩余百分比
  isAboveWarningThreshold: boolean      // 超过警告阈值 (threshold - 20K)
  isAboveErrorThreshold: boolean        // 超过错误阈值 (threshold - 20K)
  isAboveAutoCompactThreshold: boolean  // 超过自动压缩阈值
  isAtBlockingLimit: boolean            // 达到阻塞限制
}

成本累加核心函数

// cost-tracker.ts:278-323
export function addToTotalSessionCost(
  cost: number,
  usage: Usage,     // BetaUsage from Anthropic SDK
  model: string,
): number {
  const modelUsage = addToTotalModelUsage(cost, usage, model)
  addToTotalCostState(cost, modelUsage, model)

  // OpenTelemetry counters
  getCostCounter()?.add(cost, { model })
  getTokenCounter()?.add(usage.input_tokens, { model, type: 'input' })
  getTokenCounter()?.add(usage.output_tokens, { model, type: 'output' })
  getTokenCounter()?.add(usage.cache_read_input_tokens ?? 0,
    { model, type: 'cacheRead' })
  getTokenCounter()?.add(usage.cache_creation_input_tokens ?? 0,
    { model, type: 'cacheCreation' })

  // Advisor(嵌入式小模型)成本
  for (const advisorUsage of getAdvisorUsage(usage)) {
    const advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage)
    totalCost += addToTotalSessionCost(advisorCost, advisorUsage, advisorUsage.model)
  }
  return totalCost
}

存储的成本状态

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

策略限制响应 Schema

// services/policyLimits/types.ts:8-16
export const PolicyLimitsResponseSchema = lazySchema(() =>
  z.object({
    restrictions: z.record(
      z.string(),
      z.object({ allowed: z.boolean() })
    ),
  })
)

export type PolicyLimitsFetchResult = {
  success: boolean
  restrictions?: PolicyLimitsResponse['restrictions'] | null
  etag?: string
  error?: string
  skipRetry?: boolean   // true = 不重试(如认证错误)
}

有效上下文窗口计算

// services/compact/autoCompact.ts:33-49
export function getEffectiveContextWindowSize(model: string): number {
  const reservedTokensForSummary = Math.min(
    getMaxOutputTokensForModel(model),
    MAX_OUTPUT_TOKENS_FOR_SUMMARY   // 20,000
  )
  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
}

设计模式与亮点

1. 递归成本计算(Advisor 嵌套)

addToTotalSessionCost() 中处理 advisor 用量时递归调用自身。一次 API 响应可能包含主模型 + advisor 模型的混合用量,递归确保每层模型的成本都被正确计算和归属。这种设计支持无限嵌套的 advisor 链,虽然实践中通常只有一层。

2. 成本持久化与 Session 恢复

成本状态通过 saveCurrentSessionCosts() 写入项目配置文件(.claude/settings.json 的 lastCost/lastModelUsage 字段),在 --resume 恢复会话时通过 restoreCostStateForSession() 读回。Session ID 校验确保不会加载过期会话的成本数据。

// cost-tracker.ts:130-137
export function restoreCostStateForSession(sessionId: string): boolean {
  const data = getStoredSessionCosts(sessionId)
  if (!data) return false  // session ID 不匹配
  setCostStateForRestore(data)
  return true
}

3. 多级缓冲区设计

系统使用三个不同大小的缓冲区控制上下文使用率:

这种分层设计确保自动压缩在 97% 使用率时触发,而手动压缩(用户显式 /compact)可以利用更多空间。

4. 策略限制的 Checksum 缓存

Policy Limits 使用 SHA-256 checksum 而非简单 ETag 做缓存验证:computeChecksum() 递归排序所有 key 后计算哈希,确保不同请求顺序产生相同的 checksum。这避免了服务端 ETag 实现差异导致的缓存失效。

// services/policyLimits/index.ts:133-159
function sortKeysDeep(obj: unknown): unknown {
  if (Array.isArray(obj)) return obj.map(sortKeysDeep)
  if (obj !== null && typeof obj === 'object') {
    const sorted: Record<string, unknown> = {}
    for (const [key, value] of Object.entries(obj).sort(([a], [b]) =>
      a.localeCompare(b)
    )) sorted[key] = sortKeysDeep(value)
    return sorted
  }
  return obj
}

function computeChecksum(restrictions): string {
  const sorted = sortKeysDeep(restrictions)
  const hash = createHash('sha256').update(jsonStringify(sorted)).digest('hex')
  return `sha256:${hash}`
}

5. HIPAA 场景的 Fail Closed

一般策略限制遵循 fail open 原则,但 ESSENTIAL_TRAFFIC_DENY_ON_MISS 集合中的策略(如 allow_product_feedback)在 HIPAA 模式下 fail closed——当缓存不可用时直接拒绝。这是合规要求的硬性约束,优先级高于可用性。

6. 进程退出时的成本快照

useCostSummary() Hook 在 process.on('exit') 时同时做两件事:输出格式化的成本摘要到 stdout,以及将当前成本状态持久化到项目配置。这确保即使异常退出,下次 session 也能看到准确的累计成本。

开发者实践指南

调试成本问题时,检查 .claude/settings.json 中的 lastCost 和 lastModelUsage 字段——这是 session 间成本传递的桥梁。

如何查看当前 session 成本

formatTotalCost() 生成完整的成本报告:

// cost-tracker.ts:228-244
export function formatTotalCost(): string {
  return chalk.dim(
    `Total cost:            $${getTotalCostUSD()}\n` +
    `Total duration (API):  ${formatDuration(getTotalAPIDuration())}\n` +
    `Total duration (wall): ${formatDuration(getTotalDuration())}\n` +
    `Total code changes:    ${getTotalLinesAdded()} lines added, ` +
    `${getTotalLinesRemoved()} lines removed\n` +
    `${formatModelUsage()}`
  )
}

成本覆盖环境变量

变量作用默认值
CLAUDE_CODE_AUTO_COMPACT_WINDOW覆盖上下文窗口大小(token)模型默认值
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE百分比设置自动压缩触发点~97%
CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE覆盖阻塞限制(token)effectiveWindow - 3K
DISABLE_COMPACT完全禁用压缩false
DISABLE_AUTO_COMPACT仅禁用自动压缩(保留手动 /compact)false

添加新的策略限制检查

在需要检查的地方调用 isPolicyAllowed('policy_name')

import { isPolicyAllowed } from '../services/policyLimits/index.js'

if (!isPolicyAllowed('allow_remote_sessions')) {
  // 功能被组织策略禁止
  return { error: 'Remote sessions are disabled by your organization' }
}

Model Usage 数据结构

每个模型的用量通过 getUsageForModel(model) 获取,包含完整的 token 分类:

type ModelUsage = {
  inputTokens: number
  outputTokens: number
  cacheReadInputTokens: number
  cacheCreationInputTokens: number
  webSearchRequests: number
  costUSD: number
  contextWindow: number
  maxOutputTokens: number
}

架构师决策指南

成本追踪的精度权衡

系统使用两种 token 计数方式:tokenCountFromLastAPIResponse() 使用 API 返回的精确 usage,tokenCountWithEstimation() 在 API 响应不可用时使用客户端估算(roughTokenCountEstimation,字符数/4 再乘 4/3 的保守系数)。这意味着压缩前的 token 计数是精确的,而压缩后的 token 估算是保守偏高的。

Policy Limits 的扩展性

策略限制采用 z.record(string, { allowed: boolean }) 的开放 schema——服务端可以添加新策略而不需要客户端更新。客户端通过 isPolicyAllowed() 查询,未知策略默认允许(fail open)。这种设计意味着添加新策略只需要服务端部署,但必须确保 fail open 对新策略是安全的。

上下文窗口的预留空间设计

有效上下文窗口从模型最大窗口中预留了 20,000 tokens 给摘要输出(MAX_OUTPUT_TOKENS_FOR_SUMMARY),基于 p99.99 的摘要输出统计。这个值通过 Math.min(maxOutputTokens, 20000) 确保:如果模型的 maxOutputTokens 更小,使用更小的值。这意味着在使用小输出模型时,自动压缩会提前触发。

策略限制的认证路径选择

Policy Limits 的认证同时支持 API Key 和 OAuth。优先使用 API Key(Console 用户),回退到 OAuth token(Claude.ai 用户)。对于 OAuth 用户,只有 Team 和 Enterprise 订阅者才被视为 eligible——个人用户不查策略限制。这种分层确保了策略检查的合理范围。

策略限制的后台轮询使用 setInterval + .unref()。在 CCR/GHA 等短生命周期环境中,进程可能在首次轮询前就退出。设计上这是可接受的——策略限制在进程启动时已加载,后台轮询只捕获中途变更。

成本数据的 Session 隔离

saveCurrentSessionCosts() 将 session ID 与成本数据一起写入项目配置。恢复时通过 projectConfig.lastSessionId !== sessionId 校验,防止读取错误 session 的成本。这在多 session 并行场景(多个 Claude Code 实例同时运行)中特别重要。

可视化处理拓扑图

Phase 1 — 成本追踪管线

每次 API 响应到达后,成本追踪管线按模型分类累计 token 用量和美元成本,同时递归处理嵌套的 advisor 调用。

API 响应到达queryModelWithStreaming() 返回
addToTotalSessionCost()cost-tracker.ts:278-323 — 成本追踪唯一入口
按模型分类累计addToTotalModelUsage() — per-model input/output/cache + costUSDcost-tracker.ts:250-276
OTel CountercostCounter.add()
Token Counterinput/output/cache
fast modespeed:'fast' 标记
Advisor 递归计费getAdvisorUsage(usage) → 提取嵌套 advisor 用量cost-tracker.ts:312-322
有 advisor → 递归调用 addToTotalSessionCost()支持多级嵌套
无 advisor → 继续
会话成本汇总totalCostUSD + per-model ModelUsage + 持久化

Phase 2 — 策略限制:资格检查 + 三级降级

loadPolicyLimits()policyLimits/index.ts — 启动时入口
资格检查 — isPolicyLimitsEligible()第三方 provider? → 自定义 base URL? → API Key / OAuth 分流policyLimits/index.ts:167-211
API Key 用户直接通过
OAuth 用户需 Claude.ai scope
Team/Enterpriseeligible
个人用户不查策略
eligible ↓
fetchWithRetry() — 带退避获取3 次重试 + If-None-Match (SHA-256 checksum)policyLimits/index.ts:267-295
三级降级策略服务端最新限制 → 本地文件缓存(即使过期)→ null(不施加限制)policyLimits/index.ts:432-495
获取成功保存到 policy-limits.json
失败 + 有缓存使用过期缓存
失败 + 无缓存fail open — 不限制
isPolicyAllowed() 运行时查询各功能模块同步查询 boolean — 默认允许HIPAA 场景 fail closed

Phase 3 — Token 预算与自动压缩多层守卫

Token 用量检查Query 循环每次迭代后
calculateTokenWarningState()effectiveWindow = contextWindow - maxOutputTokensautoCompact.ts:93-145
1
安全区 — token < threshold - 20K
正常工作,无任何警告或操作
正常
超过 warning threshold ↓
2
警告区 — threshold - 20K ~ threshold - 20K
UI 显示用量警告,提示用户手动 /compact
警告
超过 auto-compact threshold ↓
3
自动压缩 — threshold = effectiveWindow - 13K
shouldAutoCompact() 执行多层守卫检查
shouldAutoCompact() 四层守卫
autoCompact.ts:160-239
守卫1: session_memory/compact 来源 → 跳过(防止死锁)
守卫2: context-collapse 模式 → 跳过(collapse 自管上下文)
守卫3: reactive-only 模式 → 跳过(等 API 报错再处理)
守卫4: token 用量 > threshold → 触发压缩
4
阻塞限制 — token ≥ effectiveWindow - 3K
强制阻止后续 API 调用,必须压缩或停止
阻塞
拓扑总结:成本追踪管线在每次 API 调用后递归累计 token 和美元成本;策略限制通过资格分流和三级降级确保"可用性优先";自动压缩四层守卫防止在不当上下文中触发压缩。三者协同构成"花了多少→能用多少→还剩多少"的成本控制闭环。

核心处理流程详解

成本与限流系统由三条独立但协同的流程组成:成本追踪(每次 API 调用后累计)、策略限制(启动时加载 + 后台轮询)、自动压缩(token 用量达到阈值时触发)。三条流程共同确保 Claude Code 在成本可控的前提下持续运行。

1. API 响应到达——成本入口
每次 API 调用返回后,addToTotalSessionCost()(cost-tracker.ts:278-323)被调用,接收本次调用的美元成本、token 用量(Usage)和模型名。这是成本追踪的唯一入口——所有 API 调用(主模型、advisor、fast mode)都汇入此函数 — cost-tracker.ts:278-323
2. 按模型分类累计
addToTotalModelUsage()(cost-tracker.ts:250-276)按模型名查找或创建 ModelUsage 记录,累加 inputTokens、outputTokens、cacheReadInputTokens、cacheCreationInputTokens、webSearchRequests 和 costUSD。每个模型的 contextWindow 和 maxOutputTokens 也在此处记录 — cost-tracker.ts:250-276
3. OTel 指标上报
累计完成后,通过 OpenTelemetry Counter 上报:getCostCounter()?.add(cost, attrs) 上报美元成本,getTokenCounter()?.add(tokens, {type: 'input'|'output'|'cacheRead'|'cacheCreation'}) 上报各类 token 用量。fast mode 的请求额外标记 speed: 'fast'cost-tracker.ts:290-302
4. Advisor 递归计费
如果本次 API 调用涉及 advisor 模型(通过 getAdvisorUsage(usage) 提取),对每个 advisor 的用量递归调用 addToTotalSessionCost()(L315-322),将 advisor 的成本也计入总成本。递归确保了多级 advisor 嵌套场景的完整计费 — cost-tracker.ts:312-322
5. 策略限制——资格检查
isPolicyLimitsEligible()(policyLimits/index.ts:167-211)按顺序检查:第三方 provider 不适用 → 自定义 base URL 不适用 → API Key 用户直接通过 → OAuth 用户需要 Claude.ai inference scope + Team/Enterprise 订阅。个人 OAuth 用户不查策略限制 — services/policyLimits/index.ts:167-211
6. 策略限制——带重试的获取
fetchWithRetry()(policyLimits/index.ts:267-295)最多重试 3 次,指数退避。fetchPolicyLimits()(L300-386)发送 HTTP 请求,携带 If-None-Match 头(基于缓存 checksum)。304 响应表示缓存仍有效,直接复用。失败时使用文件缓存兜底(L483-485) — services/policyLimits/index.ts:267-495
7. 自动压缩阈值计算
calculateTokenWarningState()(autoCompact.ts:93-145)计算四个阈值状态:warning(阈值 - 缓冲)、error(更接近阈值)、autoCompact(达到自动压缩触发点)、blocking(达到阻塞限制)。effectiveWindow = contextWindow - maxOutputTokens,为模型输出预留空间 — services/compact/autoCompact.ts:93-145
8. 压缩决策——多层防护
shouldAutoCompact()(autoCompact.ts:160-239)执行多层守卫检查:session_memory/compact 来源不压缩(防止死锁)→ context-collapse 模式下不压缩(collapse 自己管理上下文)→ reactive-only 模式下不主动压缩 → 最终检查 token 用量是否超过阈值。每层守卫都有独立的 feature flag 保护 — services/compact/autoCompact.ts:160-239
三条流程的协同点在于 Query 循环的每次迭代:API 响应触发成本累计(流程1-4),策略限制的后台轮询在首次加载后以 setInterval 运行(流程5-6),token 用量检查决定是否需要自动压缩(流程7-8)。策略限制控制"哪些功能可用",成本追踪控制"花了多少钱",自动压缩控制"还能花多少"——三者共同构成了成本控制的闭环。

设计精华

1. Advisor 递归计费——透明成本归属

API 响应中的 usage 字段可能包含主模型和 advisor 模型的混合用量。getAdvisorUsage() 将 advisor 用量从主用量中提取出来,然后递归调用 addToTotalSessionCost() 计费。这意味着 advisor 的成本会出现在模型分类统计和 OTel 指标中,用户可以在 /cost 命令中看到每个 advisor 模型的独立成本。

递归 advisor 计费
将 advisor 的 token 用量从主调用中拆出,递归计入独立模型统计
// cost-tracker.ts:312-322
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,
    output_tokens: advisorUsage.output_tokens,
    cost_usd_micros: Math.round(advisorCost * 1_000_000),
  })
  totalCost += addToTotalSessionCost(advisorCost, advisorUsage, advisorUsage.model)
}
递归计费的设计巧妙之处在于它不需要调用者知道 advisor 的存在——调用者只关心总成本,advisor 的拆分在追踪层自动完成。这使得未来新增 advisor 类型时不需要修改任何调用代码。递归而非循环的处理也自然支持了嵌套 advisor 的场景。

2. 策略限制的三级降级——失败开放

策略限制的获取采用 "fails open" 策略:fetchAndLoadPolicyLimits()(policyLimits/index.ts:432-495)在获取失败时不会阻止用户使用 CLI。三级降级路径:服务端返回最新限制 → 使用本地文件缓存(即使过期)→ 返回 null(不施加任何限制)。304 Not Modified 复用缓存,404 删除缓存文件。

三级降级策略
网络失败时依次降级到文件缓存和无限制,确保 CLI 始终可用
// services/policyLimits/index.ts:432-495
async function fetchAndLoadPolicyLimits() {
  const cachedRestrictions = loadCachedRestrictions()
  const cachedChecksum = cachedRestrictions ? computeChecksum(cachedRestrictions) : undefined
  try {
    const result = await fetchWithRetry(cachedChecksum)
    if (!result.success) {
      if (cachedRestrictions) { return cachedRestrictions }  // 降级到缓存
      return null  // 无限制
    }
    if (result.restrictions === null && cachedRestrictions) {
      return cachedRestrictions  // 304: 缓存仍有效
    }
    // 成功: 保存并使用新限制
    await saveCachedRestrictions(newRestrictions)
    return newRestrictions
  } catch {
    return cachedRestrictions ?? null  // 异常降级
  }
}
"Fails open" 是企业级 CLI 工具的关键设计决策。想象一个场景:组织管理员配置了"禁止远程会话"策略,但 Anthropic 的策略服务临时不可用。如果 CLI fails closed(失败时施加最严格限制),用户完全无法使用 CLI,这比"临时绕过策略"的后果更严重。三级降级确保了可用性优先,同时通过后台轮询(startBackgroundPolling L635)尽快恢复策略执行。

3. 自动压缩的多层守卫——防止自毁式压缩

shouldAutoCompact()(autoCompact.ts:160-239)的设计目标是防止压缩在不恰当的上下文中触发。四层守卫:(1) session_memory/compact 来源不压缩(否则会死锁——压缩本身就是一次 API 调用);(2) context-collapse 模式下不压缩(collapse 有自己的 90%/95% 阈值);(3) reactive-only 模式下不主动压缩(等 API 返回 prompt-too-long 再处理);(4) 只有 token 用量真正超过阈值才触发。

多层守卫压缩决策
每层守卫防止一类不恰当的压缩触发,最终只有通过所有守卫才真正执行压缩
// services/compact/autoCompact.ts:160-239
export async function shouldAutoCompact(messages, model, querySource?, snipTokensFreed = 0) {
  // 守卫1: 压缩来源不触发压缩(防止死锁)
  if (querySource === 'session_memory' || querySource === 'compact') return false
  // 守卫2: context-collapse 模式自己管理上下文
  if (feature('CONTEXT_COLLAPSE') && isContextCollapseEnabled()) return false
  // 守卫3: reactive-only 模式等 API 报错再处理
  if (feature('REACTIVE_COMPACT') && getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) return false
  // 守卫4: 真正检查 token 用量
  const { isAboveAutoCompactThreshold } = calculateTokenWarningState(tokenCount, model)
  return isAboveAutoCompactThreshold
}
多层守卫模式的核心洞察是:压缩是一个"破坏性"操作(丢弃历史上下文以换取空间),必须比"放任"更谨慎。每层守卫都对应一个真实的生产事故场景:守卫1 防止 fork-agent 递归,守卫2 防止与 context-collapse 竞争,守卫3 将压缩延迟到绝对必要时。这种"宁可多占 token 也不误压缩"的哲学,在 AI 助手场景中尤为重要——丢失的关键上下文比多花几美分严重得多。

4. Session 隔离的成本持久化——多实例安全

saveCurrentSessionCosts()(cost-tracker.ts:143-175)将 session ID 与成本数据一起写入项目配置。恢复时通过 projectConfig.lastSessionId !== sessionId 校验(在 restoreCostStateForSession() L130-137 中),防止读取错误 session 的成本。这在多 session 并行场景(多个 Claude Code 实例同时运行)中确保了成本数据的隔离性。

Session 隔离持久化
用 sessionId 作为成本数据的命名空间,防止多实例场景下的成本数据串扰
// cost-tracker.ts:143-175 + 130-137
export function saveCurrentSessionCosts(fpsMetrics?) {
  const sessionId = getSessionId()
  // 将 sessionId + 成本数据 + 模型用量写入 projectConfig
  updateProjectConfig({ lastSessionId: sessionId, costs: ... })
}

export function restoreCostStateForSession(sessionId: string): boolean {
  const stored = getStoredSessionCosts(sessionId)
  if (!stored) return false
  // 仅当 sessionId 匹配时才恢复——防止串扰
  setCostStateForRestore(stored)
  return true
}
多实例并发是 CLI 工具中容易被忽视的场景。开发者经常在多个终端窗口同时运行 Claude Code——一个做前端开发,一个做后端调试。如果不做 session 隔离,实例 A 的成本可能被实例 B 覆盖或累加,导致 /cost 显示的成本完全不准确。session ID 作为命名空间是最简单有效的隔离方案。

Agent 实践借鉴 — 客服 Agent 成本控制设计

一、场景映射:客服 agent 的成本治理挑战

每个 LLM 调用都要花钱。假设一个客服平台有 10 万个客户同时在线,每个客户平均 5 轮对话,每轮消耗 2000 token,使用 GPT-4o 每百万 token 约 $5,一天的光 LLM 费用就是 100000 x 5 x 2000 / 1000000 x $5 = $5000。一个月就是 $150,000。不同租户(企业客户)的预算不同:大客户每月 10 万额度,小客户每月 5000。单个会话如果客户聊了 50 轮,光 LLM 费用可能就要几十块。你需要实时知道"这个租户本月还剩多少额度"——这就是 CC 的实时成本追踪在客服场景的直接映射。

二、借鉴 CC + 客服改造

实践 1:按租户 + 按会话的成本追踪(借鉴 CC 的 addToTotalSessionCost)

CC 用 addToTotalSessionCost() 在每次 API 调用后累计成本。客服场景需要双维度追踪:既要按租户累计(月底计费),又要按会话累计(防止单会话成本爆炸)。

// 借鉴 cost-tracker.ts:278-323 的单入口累计模式
interface TenantCostRecord {
  tenantId: string
  monthYear: string            // '2026-04'
  totalCostMicros: number      // 微美分整数,避免浮点精度问题
  totalInputTokens: number
  totalOutputTokens: number
  sessionCount: number
}

interface SessionCostRecord {
  sessionId: string
  tenantId: string
  turnCount: number
  totalCostMicros: number
  modelUsed: string
}

class CustomerServiceCostTracker {
  // 按租户按月累计(计费用)
  private tenantMonthly = new Map<string, TenantCostRecord>()
  // 按会话累计(实时控制用)
  private sessionCosts = new Map<string, SessionCostRecord>()

  // 唯一入口:每次 LLM 调用后调用(借鉴 CC 的 addToTotalSessionCost)
  recordLLMCall(params: {
    tenantId: string; sessionId: string; model: string
    inputTokens: number; outputTokens: number; costUSD: number
  }): void {
    const costMicros = Math.round(params.costUSD * 1_000_000)  // 整数累加

    // 维度 1:按租户按月累计
    const monthKey = `${params.tenantId}:${new Date().toISOString().slice(0, 7)}`
    const tenantRecord = this.getOrCreateTenantRecord(monthKey, params.tenantId)
    tenantRecord.totalCostMicros += costMicros
    tenantRecord.totalInputTokens += params.inputTokens
    tenantRecord.totalOutputTokens += params.outputTokens

    // 维度 2:按会话累计
    const sessionRecord = this.getOrCreateSessionRecord(
      params.sessionId, params.tenantId, params.model
    )
    sessionRecord.turnCount += 1
    sessionRecord.totalCostMicros += costMicros

    // 上报遥测(借鉴 CC 的 OTel Counter)
    this.reportMetrics(params, costMicros)
  }

  // 查询租户本月剩余额度(实时计算)
  getTenantRemainingBudget(tenantId: string, monthlyBudgetUSD: number): {
    usedUSD: number; remainingUSD: number; usagePercent: number
  } {
    const monthKey = `${tenantId}:${new Date().toISOString().slice(0, 7)}`
    const record = this.tenantMonthly.get(monthKey)
    const usedUSD = (record?.totalCostMicros ?? 0) / 1_000_000
    return {
      usedUSD,
      remainingUSD: Math.max(0, monthlyBudgetUSD - usedUSD),
      usagePercent: Math.round(usedUSD / monthlyBudgetUSD * 100),
    }
  }
}

实践 2:三级预算阈值(借鉴 CC 的 warn → auto-compact → block)

CC 用四级阈值控制上下文使用率。客服场景也有类似的成本控制需求,但阈值语义不同:50% 警告运营人员、80% 自动切换到便宜模型、100% 暂停服务。

// 借鉴 autoCompact.ts:93-145 的多级阈值设计
type CostLevel = 'normal' | 'warning' | 'downgrade' | 'suspended'

class TenantBudgetManager {
  private thresholds = {
    warning: 0.50,     // 50% → 通知运营人员
    downgrade: 0.80,   // 80% → 切换到便宜模型
    suspended: 1.00,   // 100% → 暂停服务
  }

  // 便宜模型映射(降级时使用)
  private modelFallback = {
    'gpt-4o': 'gpt-4o-mini',           // $5/M → $0.15/M token,成本降 97%
    'claude-sonnet': 'claude-haiku',    // $3/M → $0.25/M token
    'deepseek-chat': 'deepseek-lite',   // 已是便宜模型,不再降级
  }

  evaluateBudget(tenantId: string, monthlyBudgetUSD: number): {
    level: CostLevel; effectiveModel: string; usagePercent: number
  } {
    const budget = costTracker.getTenantRemainingBudget(tenantId, monthlyBudgetUSD)
    const ratio = budget.usagePercent / 100

    if (ratio >= this.thresholds.suspended) {
      return { level: 'suspended', effectiveModel: '', usagePercent: budget.usagePercent }
    }
    if (ratio >= this.thresholds.downgrade) {
      const config = getTenantConfig(tenantId)
      const downgraded = this.modelFallback[config.model] ?? config.model
      return { level: 'downgrade', effectiveModel: downgraded, usagePercent: budget.usagePercent }
    }
    if (ratio >= this.thresholds.warning) {
      return { level: 'warning', effectiveModel: getTenantConfig(tenantId).model, usagePercent: budget.usagePercent }
    }
    return { level: 'normal', effectiveModel: getTenantConfig(tenantId).model, usagePercent: budget.usagePercent }
  }

  // 在每次 LLM 调用前执行预算检查(借鉴 CC 的 enforceBudget)
  async beforeLLMCall(tenantId: string, sessionId: string): Promise<{
    allowed: boolean; model: string; reason?: string
  }> {
    const budget = getTenantBudget(tenantId)
    const state = this.evaluateBudget(tenantId, budget.monthlyLimitUSD)

    switch (state.level) {
      case 'normal':
        return { allowed: true, model: state.effectiveModel }
      case 'warning':
        // 通知运营人员(邮件/webhook)
        await notifyOps(tenantId, `租户 ${tenantId} 本月已用 ${state.usagePercent}% 预算`)
        return { allowed: true, model: state.effectiveModel }
      case 'downgrade':
        return { allowed: true, model: state.effectiveModel, reason: '已降级到经济模型' }
      case 'suspended':
        // 暂停服务,转人工
        await transferToHuman(sessionId, '租户预算已耗尽')
        return { allowed: false, model: '', reason: '本月额度已用完' }
    }
  }
}

实践 3:会话级成本控制(借鉴 CC 的单会话 token 预算)

CC 用上下文窗口阈值控制单会话 token 消费。客服场景同样需要会话级限制——一个客户聊了 50 轮,单会话成本可能超过 5 元,需要自动压缩上下文或降级。

// 借鉴 autoCompact.ts 的阈值控制 + 自动压缩
const SESSION_COST_LIMIT_USD = 5.0     // 单会话成本上限
const SESSION_COST_COMPACT_AT = 3.0    // 触发上下文压缩的阈值

async function checkSessionBudget(sessionId: string): Promise<{
  action: 'continue' | 'compact' | 'downgrade' | 'terminate'
}> {
  const sessionCost = costTracker.getSessionCost(sessionId)
  const costUSD = sessionCost.totalCostMicros / 1_000_000

  if (costUSD >= SESSION_COST_LIMIT_USD) {
    // 单会话超过 5 元:降级到最便宜模型或转人工
    return { action: 'downgrade' }
  }
  if (costUSD >= SESSION_COST_COMPACT_AT) {
    // 单会话超过 3 元:压缩上下文(类似 CC 的 autoCompact)
    // 将前 20 轮对话摘要化,释放 token 空间
    return { action: 'compact' }
  }
  return { action: 'continue' }
}

实践 4:成本报告(按租户、按渠道、按日期统计)

// 借鉴 cost-tracker.ts:228-244 的 formatTotalCost
class CostReporter {
  // 按租户统计月度成本
  getTenantMonthlyReport(tenantId: string): TenantMonthlyReport {
    const monthKey = `${tenantId}:${new Date().toISOString().slice(0, 7)}`
    const record = costTracker.getTenantRecord(monthKey)
    return {
      tenantId,
      period: new Date().toISOString().slice(0, 7),
      totalCostUSD: (record?.totalCostMicros ?? 0) / 1_000_000,
      totalInputTokens: record?.totalInputTokens ?? 0,
      totalOutputTokens: record?.totalOutputTokens ?? 0,
      sessionCount: record?.sessionCount ?? 0,
      avgCostPerSession: record
        ? ((record.totalCostMicros / 1_000_000) / record.sessionCount).toFixed(4)
        : '0',
    }
  }

  // 按渠道统计(电话比在线聊天贵,因为有 ASR/TTS 成本)
  getCostByChannel(tenantId: string): Record<string, number> {
    const sessions = costTracker.getTenantSessions(tenantId)
    const byChannel: Record<string, number> = {}
    for (const session of sessions) {
      const channel = session.channelType
      byChannel[channel] = (byChannel[channel] ?? 0) + session.totalCostMicros / 1_000_000
    }
    return byChannel
  }
}

三、落地清单

必须抄的
  • 实时成本追踪——每次 LLM 调用后立即累计,按租户按会话双维度记录,用整数微美分避免浮点精度问题
  • 多级预算阈值——50% 警告 → 80% 切便宜模型 → 100% 暂停服务,借鉴 CC 的 warn/autoCompact/block 分级
  • 会话级成本控制——单会话超阈值自动压缩上下文或降级模型,防止单个长对话成本爆炸
不需要抄的
  • Advisor 递归计费——客服场景的子 agent(转交专家)少,不需要递归拆分成本
  • 服务端策略下发——CC 从 Anthropic 服务端获取 policy limits,客服的成本策略在本地配置文件或管理后台,不需要实时远程拉取
  • ETag/Checksum 缓存策略——客服预算配置变更频率低,不需要 CC 那样的 SHA-256 checksum 缓存验证

四、常见坑

  1. 用浮点数累加导致计费不准——$0.1 + $0.2 != $0.3。CC 用微美分整数(USD * 1,000,000)累加,只在显示时转换。客服场景涉及真金白银的计费,精度问题更严重。
  2. 只算 LLM 成本忽略 ASR/TTS 成本——电话渠道的语音识别(ASR)和语音合成(TTS)也是按调用计费的,一个 50 轮的电话会话 ASR/TTS 成本可能和 LLM 成本相当。成本追踪必须覆盖所有付费 API。
  3. 降级模型后客户感知到质量下降——从 GPT-4o 切到 GPT-4o-mini,回复质量可能明显下降(特别是复杂售后问题)。降级时应该同时通知运营人员,必要时转人工而非继续用差模型硬撑。
  4. 预算归零后直接断服务导致客诉——100% 阈值不是直接挂断客户,而是优雅转人工并提示运营续费。借鉴 CC 的 "fails open" 哲学,宁可多花几块钱也不要在客户面前直接断线。

代码索引

文件行数说明
cost-tracker.ts~324成本追踪核心:累加、格式化、持久化、模型分类统计
costHook.ts~23React Hook:进程退出时输出成本摘要并保存状态
services/policyLimits/index.ts~664策略限制服务:获取、缓存、轮询、查询
services/policyLimits/types.ts~27策略限制类型定义和 Schema
services/compact/autoCompact.ts~352自动压缩阈值计算:有效窗口、警告状态、阻塞限制
services/compact/microCompact.ts~531微压缩:工具结果清除策略,间接降低 token 消费
utils/modelCost.ts~mediumcalculateUSDCost():按模型和 token 类型计算美元成本
utils/context.ts~mediumgetContextWindowForModel():模型上下文窗口查询
utils/tokens.ts~mediumtokenCountWithEstimation():精确 + 估算混合 token 计数
bootstrap/state.js~large全局成本状态:getTotalCostUSD、getModelUsage、getCostCounter 等