Claude Code 用量分析揭示上下文成本
作者解析 30 天本地会话日志,发现 445.7 亿 token 中约 97% 是缓存读取,子代理占按 API 标价折算成本的 58%。该样本中,限制工具输出的预估收益低于削减启动上下文;折算金额并非其订阅实际账单。
AI 深度解读
Claude Code 写代码时,开销最大的一项可能是反复读取已经看过的上下文。作者分析了自己约 30 天的本地日志:44.57B tokens 中,97.03% 用在缓存重读,真正的输出只占 0.27%。
这份分析的价值在于纠正优化顺序:先检查每个 agent 启动时背着多少材料、会话拖了多久,再考虑截断工具输出。不过,这是一名重度使用子代理的开发者的样本,节省比例也只是模拟结果,不能直接当成团队预算承诺。
缓存已经很便宜,为什么还是最大开销
作者统计了 192,178 次 API 请求、889 个主会话和 5,696 个子代理。期间 Claude 输出约 1.22 亿 tokens,却读取了约 430 亿 tokens。
原因在于会话的工作方式:每一步请求都会再次发送上下文。工具返回的文件内容、此前生成的代码、常驻指令,进入会话后都会被后续请求反复读取。
一次工具调用的成本,还取决于它的结果会在上下文里留多久。
按 API 标价折算,作者这个月的用量约值 $15,290,其中缓存读取占 56.6%,缓存写入占 31.7%,输出占 11.6%。作者使用订阅,这不是实际账单。
缓存本身仍然划算。同样的 43.24B tokens,如果按普通输入计价,作者估算约需 $133,000;缓存把这一项压低了约 15 倍。问题出在累计读取量。
长会话尤其明显:超过 200K tokens 上下文的请求占 41.7%,却贡献了 59.6% 的成本。最长的一个会话跑了 2,436 次请求,单独占整月成本的 2.3%。
先把日志算对,再找哪些工具留下了长尾
日志不能逐行直接相加。一个 API 响应可能按内容块写成多行 JSONL,每行都重复同一个 usage 对象,必须按 message.id 去重。
作者的日志里有 400,880 行 assistant 记录,实际只有 192,178 次请求。直接求和会得到 93.0B tokens,是真实约 44.6B 的 2.09 倍;恢复会话还让 287 次请求出现在两个文件中。
下面这段代码读取本地日志,并按响应 ID 汇总用量:
import glob, json, os, time
from collections import Counter
cutoff = time.time() - 30 * 86400
requests = {} # message.id -> usage
root = os.path.expanduser("~/.claude/projects/**/*.jsonl")
for path in glob.glob(root, recursive=True):
if os.path.getmtime(path) < cutoff:
continue
for line in open(path, errors="ignore"):
if '"usage"' not in line:
continue
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
msg = row.get("message") or {}
if row.get("type") == "assistant" and msg.get("usage") and msg.get("id"):
requests[msg["id"]] = msg["usage"] # last line wins
totals = Counter()
for usage in requests.values():
for key in ("input_tokens", "cache_creation_input_tokens",
"cache_read_input_tokens", "output_tokens"):
totals[key] += usage.get(key) or 0
print(len(requests), totals)
重点看 requests[msg["id"]] = msg["usage"]:它负责跨内容块、跨文件去重。另一个边界是 os.path.getmtime(path),它筛选整个文件,没有逐条按时间截取,所以结果略高于严格的 30 天窗口。作者处理 21GB 日志约用了 35 秒。
费用还要按每次请求的模型分别计算。原文采用的 Opus 5.5、Sonnet 5.5 缓存读价均为每百万 tokens $0.20,5 分钟和 1 小时缓存写入分别按普通输入价的 1.25 倍、2 倍计算。
算清总量后,作者又估算了工具结果的后续重读开销。结果中位数只有 865 tokens,却会被重读 23 次,平均存续 42.8 次请求。最极端的一次 MCP 资源读取返回 15,183 tokens,此后又留了 334 次请求,累计产生 507 万缓存读取 tokens,约值 $1。
Bash 占可归因工具重读量的 67%。其中 cat、sed、head、tail、awk、nl、wc 这些读文件命令共调用 42,453 次,贡献 4.22B 重读 tokens。相比之下,Read 调用 29,968 次;Shell 读文件的次数是它的 1.4 倍,重读开销是 1.6 倍。
这不能证明 Bash 单次读取更贵,只能说明作者的使用方式里,它调用得多。Write 同样会留下上下文:文件虽然落盘了,作为工具输入的文件正文仍可能被后续读取。
真正该先减的,是每个代理开工前的行李
子代理占作者总成本的 58%。它们第一次调用工具之前,上下文中位数已经达到 68,815 tokens,而整个生命周期的请求数中位数只有 15 次。
作者把启动上下文的开销拆开计算:首次缓存写入 $923,后续重读 $1,934,首次缓存读取 $17,合计 $2,874,占整月 18.8%。首次请求中,只有约 22% 的 tokens 命中缓存。
作者的全局 CLAUDE.md 有 25KB,每个 agent 还要携带工具定义和技能列表。指令只写过一次,但会被许多代理反复处理。
全部缓存读取中,启动上下文占 32.2%,工具结果占 34.4%,剩余 33.4% 包含 assistant 文本、注入提醒和未能归因的内容。
缓存过期反而没那么突出。作者识别出 649 次缓存重建,重写 183.7M tokens,占全部缓存写入的 15.3%,估值约 $740,占总成本 4.8%。其中 257 次发生在前一次请求后不到 5 分钟,另有 61 次与模型切换重合,不能全部归咎于空闲过久。
作者的模拟里,精简启动上下文,比统一截断工具结果更划算。
工具输出限制为 5K tokens,估算节省约 2.4%,却要截断超过 10,000 条结果;启动上下文减少 10K tokens,估算能节省约 3.8%。2K 上限看起来更省,但接近五分之一的调用会丢失部分数据。代理可能因此再跑命令,模拟没有计算这笔补偿开销。
我的判断
这份分析最有用的地方,是把「输出了多少代码」和「实际处理了多少上下文」分开了。对大量使用 agent 的团队,常驻规则、工具返回和会话长度,都应进入成本审查。
但工具归因属于估算。作者根据相邻请求的上下文增量,按工具内容字符数分摊,以字符数除以 2.5 限制估计值,再累计到上下文下降超过 30% 为止。它无法精确还原每段内容的缓存生命周期,而且约三分之一读取没有归因。
因此,前 1% 工具调用贡献 17.4% 重读开销、前 10% 贡献 58.4%,适合用来定位重点排查对象,不宜当作精确账目。
精简指令也有代价。如果删掉的是必要约束,返工可能抵消节省。相比统一截断,按任务加载相关规则、让工具返回更聚焦的内容,更容易保留信息完整性,但原文没有提供这些替代做法的对照数据。
作者也没有做 A/B 实验。3.8% 和 2.4% 应当作为优化顺序的参考,效果仍需在自己的任务上验证。
能拿来做什么
- 先用日志建立用量基线,按
message.id去重,分别统计普通输入、缓存写入、缓存读取和输出;费用计算保留模型维度,并明确时间窗口的误差。 - 审查全局
CLAUDE.md,优先找出每个子代理都会携带、实际却很少用到的规则,把任务专属材料移到需要时再加载的位置。 - 前端或 Node 排障时,检查 AI 是否反复整段读取同一文件或输出完整日志,优先让查询范围贴近当前问题,并观察是否引发额外调用。
- 对跨越多个任务、上下文超过 200K tokens 的会话,考虑整理必要结论后开启新会话,同时记录信息遗漏和返工,避免只看 token 降幅。