Claude Code用量统计陷阱:一个API响应为何被多次计数

Claude Code transcript 中一个 API 响应被拆成多行且每行带完整 usage,直接求和会高估用量,应按 message.id 去重。
Claude Code 将会话记录以 JSONL 格式存储,当一个 API 响应包含多个内容块时,会为每个内容块写入一条 assistant 记录,每行都带有完整的 usage 对象。直接累加这些记录会显著高估 token 消耗,issue #87303 实测显示 cache_creation 高估 1.92 倍。Anthropic 确认这是文档化的设计行为,消费者应按 message.id 只计一次。去重后输入和缓存 token 较准确,但 output_tokens 仍是占位值,真实输出需从结果消息或结构化输出获取;金额也只是客户端估算,应以 Usage and Cost API 为准。此外,分支复制、压缩重写、恢复会话累计和高负载 ID 重复是另外四种重复计数来源。
如果你在脚本中直接累加Claude Code会话记录(transcript)里的 token 用量,得到的数字很可能远高于实际消耗。问题的根源在于:一个 API 响应可能在记录文件中被拆成多行,而每一行都带有完整的 usage 对象。
一个响应,多行记录
Claude Code 将会话以 JSONL 格式存储在 ~/.claude/projects/<project>/<session-id>.jsonl,每行对应一个消息、工具调用或元数据。当一个 API 响应包含多个内容块(thinking、text、tool_use)时,Claude Code 会为每个内容块写入一条 assistant 记录。这些记录各自拥有独立的 uuid,但共享同一个 message.id 和完全相同的 usage 对象。
GitHub issue #87303 中的真实案例很有说服力:该会话包含 521 条 usage 记录,却只对应 323 个不同的 message ID。直接求和得到的 cache_creation_input_tokens 为 1,075,420,按 ID 去重后为 560,751,高估了 1.92 倍;缓存读取字段则高估了 1.59 倍。Anthropic 当天关闭该 issue,将其标记为“文档化的设计行为”。
这个模式并不新鲜。2025 年 8 月的 issue #6805 就描述过同样的问题:在 stream-json 输出中,一个包含 thinking、text、tool_use 的消息被拆成三个事件,每个事件都携带完整的 usage 数据。当时有报告者称某个 message ID 的 cache_read_input_tokens 在一个会话文件中出现了 8 次。
Anthropic 的规则:按 message.id 只计一次
Anthropic 维护者在关闭 #87303 时明确表示,这种记录形态是有意为之且已文档化,消费者应当对每个 message.id 只计数一次。Agent SDK 的成本追踪指南也给出了同样的建议:当 Claude 并行使用工具时,多条消息会共享同一个 ID 和相同的 usage 数据,需要跟踪已计数的 ID 并跳过重复项。
不过去重之后仍有两个需要注意的地方。
输出 token 是占位值
按照文档,去重后的 per-step 数值对输入和缓存 token 是准确的,但 output_tokens 只是响应生成前 message_start 时的占位值。真实的输出量在 SDK 最终结果消息或 claude -p --output-format json 的结构化输出中。维护者在关闭 #87303 时也确认,transcript 文件和 stream-json 输出属于同一个消息流,因此磁盘上的记录同样适用这一规则。
message_start 事件是 Anthropic Messages API 流式响应的首个事件,它在模型实际生成任何 token 之前就已发出,携带的 usage 中 output_tokens 只是在请求发起时估算的初始值(通常很小)。随着流式生成推进,message_delta 事件会携带持续更新的 usage,最终值出现在 message_stop 之前。Claude Code 的 transcript 记录的是响应开始时的快照,因此 output_tokens 停留在占位值。真实输出量需要从 SDK 最终结果消息(result 类型)或 claude -p --output-format json 返回的 usage 字段读取,那里汇总了本次调用所有响应的实际输出 token。
金额只是客户端估算
Claude Code 计算的所有美元数字都是基于内置价格表的本地估算。要了解实际账单,应查询 Usage and Cost API 或 Console 用量页面。transcript 求和适合观察 token 流向,但引用金额前务必核对官方账单数据。

Anthropic 的 Usage and Cost API 提供按组织、工作区、API 密钥维度的用量与费用查询,数据直接来自计费系统,是官方账单依据。Console 用量页面则是可视化版本。两者与 Claude Code 本地估算的关键差异在于:官方数据包含所有客户端(不只是 Claude Code)的调用、反映实际计费规则(如批量折扣、缓存写入/读取的不同单价),并且会及时更新。本地估算基于 Claude Code 内置的价格表,该表可能滞后于官方调价,且不区分企业合同价。因此,transcript 求和适合观察 token 流向和相对比例,任何对外引用或预算决策都应核对官方账单。
用脚本验证你自己的记录
最直接的验证方式是写一个脚本,对同一份 transcript 分别做朴素求和与按 ID 去重求和,对比差距。核心逻辑是:遍历所有 type === "assistant" 且带 message.usage 的行,朴素累加每个字段;去重时以 message.id 为键(无 ID 时回退到 uuid),保留带 stop_reason 的那条,否则取最新一条。
如果你更习惯 shell,用 jq 也能快速完成单字段对比。第一条命令是朴素求和,第二条用 group_by(.message.id) 取每组首条再求和。需要注意的是,jq -s 遇到无法解析的行会直接停止,而 Node 脚本会跳过无效行;group_by 也会把所有无 ID 的记录归入同一组。
还有一个共同限制:会话文档明确指出记录格式是 Claude Code 内部实现,可能随版本变化,直接解析文件的脚本随时可能失效。用于自动化时,claude -p --output-format json 是更稳妥的选择。
另外四种重复计数的来源
拆分响应是最常见的情况,但跨文件或跨运行时还有更多陷阱。
分支会复制历史。 /branch 和 --fork-session 创建的新会话会复制截至分支点的完整对话。如果按项目文件夹逐文件求和,被复制的部分会被每个分支各计一次。正确做法是跨所有文件按 message.id 全局去重。
压缩可能重写历史。 issue #66947 报告称每次压缩或续接似乎会把先前的记录重写进同一 transcript,使用相同的 uuid 和时间戳,某条消息在一个文件中出现了五次。Anthropic 未确认此问题,但按 ID 去重的策略无论如何都能吸收这种影响。
恢复会话的总量已包含过去。 Claude Code 在正常退出时会把会话总量保存到 transcript,后续恢复时再读出,因此每次恢复调用的结果已经包含此前的花费。文档原话是“summing results double-counts the restored spend”——累加多次调用的结果会重复计算已恢复的部分,应该读取最新一次结果作为会话总量。
高负载下 ID 可能重复。 仍处于开放状态的 issue #64880 报告称,在服务器高负载下,六个独立的顺序响应返回了相同的 message.id。报告者随后在正常负载窗口复查,未发现同一个 ID 出现在多个 requestId 下。脚本中的 split 检查正是为此设计:如果输出大于零,说明该文件需要按 message.id 加 requestId 组合去重。
/branch 和 --fork-session 是 Claude Code 的会话分支机制:前者在当前会话基础上创建新分支,后者从已有会话派生新会话。两者都会复制截至分支点的完整对话历史到新文件,新会话拥有独立的 session ID 但继承相同的 message.id。如果按项目文件夹逐文件求和,分支点之前的历史会被每个分支各计一次,分支越多重复越大。正确做法是跨所有 transcript 文件按 message.id 全局去重,而不是按文件或按会话分别求和。
成本统计工具也踩过同样的坑
这类 bug 并不只出现在用户脚本里。Hermes IDE 的 2.2.9 版本修复了一个同族问题:会话打开时成本和 token 总量会持续增长,原因是每个会话的用量被重复计数;受早期版本影响而虚高的总量会在首次启动时被修正。
发布说明没有明确归因于 transcript 重复行,因此不应过度推断。但问题的形态值得记住:任何持续读取一个正在被追加写入的文件的工具,都必须记住已经统计过的内容,否则数字会在会话闲置时仍不断攀升。
实践建议
把 transcript 求和当作诊断工具而非账单依据。去重后的输入和缓存数字相对可信,输出 token 需要从结果消息获取,金额则应以 Usage and Cost API 为准。跨分支、跨恢复的场景务必全局按 ID 去重,并在高负载异常时增加 requestId 作为第二维度。
背景补充
JSONL 与 transcript 的结构。 Claude Code 的会话记录采用 JSONL(JSON Lines)格式,每行是一个独立的 JSON 对象,对应一条消息、一次工具调用或一条元数据。assistant 类型的行包含 message.id(对应 API 响应 ID)、uuid(记录唯一标识)和 message.usage(token 用量)。user 类型的行承载工具结果和用户输入。system 类型的行记录压缩、恢复等元事件。理解这种逐行结构是正确求和的前提:每行独立解析,但同一 message.id 的多行共享同一份用量数据。
相关推荐

Workbuddy零基础入门:桌面AI智能体如何落地工作流
Workbuddy桌面AI智能体零基础教程解读:从安装、积分领取到文件管理与自动化工作流搭建,对比CodeX的适用场景与成本差异,帮助职场人完成从AI提问者到AI管理者的思维转变。

OpenAI智能体新栈:计算机操作、决策API与超速工作流
OpenAI Dev Day发布智能体新栈:Computer Use操作速度超越普通人,Decisions API基于Luna实现极低延迟分类,Agents API开放同源计算机操作能力,GPT-6.1 Sol成本仅为Astra七分之一。本文梳理核心更新、实测数据与开发者落地要点。

梁文峰开源推理框架DeepInfo,硬刚美国禁令
DeepSeek在开源模型后再开源推理框架DeepInfo,包含GPU/CPU交叉流水线、小模型预判大模型复核、华为昇腾与英伟达芯片全面适配等技术,直面美国联邦禁令。本文解析其对AI产业落地的意义与开源策略。