小岛AI
| ONLINE |

posts/claude-code-cost-correctness.md

Claude Code 修了一个会把账单翻倍的 bug

小岛AI 2026 / 08 / 24

一个 AI 编程 Agent 可以任务完成、文件改对、测试跑绿,同时把同一轮模型请求算两次钱。

终端上看不出异常。

甚至连它自己显示的成本,也可能比真实计费少一截。

这不是我编的恐怖故事。Anthropic 在 Claude Code 2.1.239 的官方发布说明里写得很直接,Bedrock 的流式响应经过某些会剥离 Content-Type 的代理后,Claude Code 会把每一轮重新按非流式方式执行,已经产生的计费 API 调用因此静默翻倍。

同一版还修了另一处成本偏差。/cost、状态栏与 --max-budget-usd 过去没有计入美国限定推理工作区的 1.1 倍数据驻留溢价。

好家伙,一个让调用多算一遍,一个让屏幕少显示 10%。

两件事凑在一起,特别适合给今天的 AI Agent 工程提个醒。模型回答对不对,只是正确性的一半。请求有没有被重复执行,成本有没有按真实规则计算,预算到顶能不能真停,这些也属于正确性。

而且它们最麻烦的地方都一样。

业务不一定会报错。

答案回来了,为什么钱会付两遍

先看那个静默翻倍的问题。

Amazon Bedrock 的流式接口不是普通 JSON 响应。按照 AWS 的 InvokeModelWithResponseStream 文档,流式响应的 Content-Type 固定为 application/vnd.amazon.eventstream,真正的模型内容类型会放在另一个响应头里。

这个响应头不是装饰。客户端靠它判断,眼前收到的是可以逐块解析的 event stream,还是一个普通响应。

企业网络里偏偏最容易动响应头。反向代理、WAF、安全网关、自己搭的转发层,常见操作是规范化 header、删掉不认识的字段、改写内容类型。代理觉得自己在打扫卫生,客户端那边却像被人抽走了协议路标。

Claude Code 2.1.239 的发布说明给出的现象很明确。响应头被剥离后,旧实现会重新以非流式方式执行这一轮。第一次请求其实已经到达模型服务并产生计费,第二次又完整跑了一遍。

用户最后仍然拿到一个答案。

从交互层看,棒棒的,成功了。

从账单层看,同一个意图付了两遍钱。

响应头丢失后,一次流式请求分叉成两次计费调用

代理层拿掉协议路标,恢复动作就可能变成重复执行。

这里要分清两种重试。一种是连接在真正送达服务器前失败,安全重试有机会只执行一次。另一种是服务器已经处理请求,客户端因为没认出响应格式,又发起第二次调用。后者没有幂等保护时,重试不是恢复,是复制。

模型 API 比普通查询更怕这个问题。一次请求可能带着几十万 token 的上下文、缓存写入、extended thinking,还可能触发后续工具调用。复制的不是一条廉价 GET,而是一整段昂贵计算。

假设一个团队正常每天在这条链路上花 1000 元,若每一轮都稳定触发一次重复调用,显示层还能继续说任务成功,真实成本却可能接近 2000 元。这个数字只是为了说明量级,不是对某个真实团队账单的陈述。具体影响要回云厂商账单和请求记录里核。

更有意思的是,2.1.239 还修了一个看似不相关的竞态。用户按 Esc 时,如果队列里已经有下一条 prompt,旧行为可能让下一轮提前结束,会话表面进入空闲,但 Claude 其实还在工作。稍后再次提交,动作就可能重复。

一边是网络协议让模型请求重复,一边是交互状态让 Agent 动作重复。表面原因不同,底下都在问同一件事。

这轮工作到底有没有执行过。

传统后端遇到支付、发券、建订单,大家早就知道要做幂等键。轮到 AI Agent,很多系统又退回了「没看见结果就再跑一次」的朴素年代。模型调用、工具执行、消息投递全都靠重试撑可靠性,却没有稳定的任务 ID、尝试次数与副作用记录。

厉害了,Agent 学会写代码之后,工程团队又重新发明了一遍分布式系统。

少算的 10%,会把预算闸顶歪

再看 /cost 那处修复。

Anthropic 的数据驻留文档写得很清楚,Claude 4.6 及后续模型使用美国限定推理时,输入 token、输出 token、缓存写入与缓存读取都按标准价格的 1.1 倍计费。全局路由走标准价格。

这个 1.1 倍不是只加在输出上,也不是月底才冒出来的一项固定费用。每一种 token 价格都要乘。

旧版 Claude Code 的本地成本估算漏了这层。你在 /cost 里看见 90 元,按同样 token 量与简单条件估算,账单可能是 99 元。单个会话差 9 元,看着不吓人。团队同时跑几十个会话,月底拿状态栏数字做预算预测,偏差就会稳定累积。

坦率讲,10% 最容易骗过人。

翻倍会让人立刻查事故,10% 常被当成云账单波动、缓存命中率变化或团队多跑了几次任务。它不会撞响警报,只会让财务数字长期比工程仪表盘难看一点。

Claude Code 的成本文档其实一直提醒,本地显示的美元金额是估算值,权威数据应以 Claude Console 或云厂商账单为准。Agent SDK 的成本跟踪文档说得更直白,客户端价格表会因价格变化、未知模型和无法建模的计费规则产生漂移,不该拿这些估算直接给最终用户计费,也不该单独驱动财务决策。

问题来了。如果本地数字不是账单,为什么还要认真修那 10%。

因为预算控制靠的往往正是本地数字。

--max-budget-usd、状态栏提示、会话内成本提醒,都是离执行现场最近的反馈。它们不需要等隔天账单,也不用等财务系统做归集。数字越接近真实,Agent 越有机会在今天就收手。数字偏低,预算闸就会比设定值晚关。

这块需要注意一下,本地上限不能替代云端硬限制。它适合做第一道快速闸,真正的组织级止损还得在 Claude workspace、AWS、Google Cloud 或 Azure 的计费和额度系统里落地。

一个是方向盘上的油耗预估,一个是油箱里真实剩余的油。

两个都得有,而且要定期对表。

客户端估算、遥测链路与云账单需要用同一组任务关系对齐

三本账不必逐行相同,但每一笔偏差都应该能解释。

对表的时候也别只盯总金额。AI 调用的价格不是输入 token 乘一个单价那么干净。输入、输出、缓存写入、缓存读取可以有不同费率,长上下文、fast mode、数据驻留还可能继续叠乘数。同一个模型名,在客户端旧价格表里与云端当前计价规则里,未必是同一笔算式。

更稳的做法,是把偏差按来源拆开。

价格规则偏差,像这次漏掉 1.1 倍溢价。执行次数偏差,像代理触发第二次模型调用。归属偏差,像 trace 断开后同一件任务被算进两个会话。三种偏差在月底都表现为「账单比预估高」,修法却完全不同。

价格规则错了要升级计价表,执行次数多了要查重试与幂等,归属断了要补任务 ID 和 trace 关系。只做一个总额告警,最后通常会拉一群人进会议室,对着一条向上的曲线互相猜。

很多朋友可能不知道,缓存也会把这种猜测搅得更乱。一次重复调用若命中 prompt cache,第二次未必与第一次同价。账单可能不是整齐的 2 倍,而是 1.2 倍、1.4 倍或别的比例。没有 request ID、attempt 与 cache usage,团队甚至会误以为缓存策略突然变差。

所以「没有刚好翻倍」不能证明没有重复请求。

它只能证明计费规则比乘法题复杂。

Agent 的成本问题,通常藏在控制流里

很多人聊 AI 成本,第一反应是换便宜模型、缩 prompt、开缓存。这些当然有用,但 2.1.239 暴露的是另一类更难缠的成本。

控制流成本。

同一个请求重跑几次,预算耗尽后 watchdog 还在不在无限等待,组织策略已经拒绝的请求会不会在提示出现前又发一次,trace 被 hook 切断后还能不能看见完整的重试链。这些不由单价决定,而由运行时怎么处理失败、状态与观测决定。

这版 release 里恰好还有三条能拼上这张图。

开启 CLAUDE_CODE_RETRY_WATCHDOG 后,遇到组织消费上限或余额不足,现在会立即失败,不再一直等额度重置。被组织策略拒绝的请求,不会在拒绝信息展示前再次发送。被 PreToolUse hook 延迟的工具执行,也会回到原 turn 的 OpenTelemetry trace,不再另起一段看不出父子的链路。

你想想看,如果一条链路被拆成两段,第二段的重试与工具调用无法回到原任务,成本归因会变成什么样。

仪表盘上也许出现两个都不算离谱的会话,实际却是一件工作。你按会话看不出尖峰,按用户看只觉得今天稍微多花了一点,按项目又找不到哪次提交对应这笔钱。

这也是为什么我越来越不喜欢只看「每百万 token 多少钱」。单价是价签,控制流才是购物车。价签便宜,购物车把同一件货放进去两次,结账照样肉疼。

Claude Code 的 OpenTelemetry 文档已经给出不少能用的信号,包括 claude_code.cost.usage、token usage、request_id、客户端请求 ID、总尝试次数、状态码和完整 trace。成本指标仍然是估算,但拿来发现异常趋势很合适。

最小配置不复杂。

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.internal:4317

真正难的不是把指标发出去,而是团队要提前约定怎么读。

成本尖峰告警当然要有,但它发现问题通常已经偏晚。更贴近根因的信号,是每个稳定任务 ID 下面挂了多少次模型 request、多少次 attempt、多少次产生副作用的工具调用。一个修改 README 的任务突然发出六次模型请求,哪怕总成本还没越线,也该进入异常队列。

我还会多看一个比例,完成任务数与模型调用数。它不需要跨团队比较,也不适合拿来做绩效。只看同一个工作流自己的历史基线就够。昨天每完成一个依赖升级平均需要四次调用,今天同类任务突然需要八次,先查代理、重试与上下文恢复,再讨论模型是不是变笨了。

这里有个很容易踩的坑。不要把 tool call 数直接当模型请求数。一次模型响应可以连续提出多个工具调用,一轮 Agent 任务也可以经过多次模型响应。要是监控系统只数终端上出现了多少个工具卡片,重复的模型计费仍然可能藏在下面。

request ID 管模型请求,task ID 管用户意图,tool execution ID 管副作用。

三层 ID 不用长得多漂亮,但不能拿一个 session ID 全部代替。会话可以跨任务,任务可以跨多轮请求,一轮请求又可以发出多个工具动作。把它们压成一个 ID,查成本时迟早会糊成一团。

一次用户意图对应哪个稳定任务 ID,一次模型调用对应哪个 request ID,重试的 attempt 是否递增,工具副作用是否记录,客户端估算与云账单允许多大偏差,超过阈值以后谁负责停。没有这些约定,采集再多指标也只是把雾搬进数据库。

给成本做一次故障演练

如果团队正在把 Claude Code、Agent SDK 或其他编码 Agent 接进生产网络,我建议别只做功能验收。专门给成本链路做一轮故障演练。

先让测试流量走一遍与生产相同的代理、WAF 与身份链。不是在开发机直连成功就算过关,要确认 Bedrock 的 event stream 响应头穿过每一层之后仍然完整。很多代理问题只在真实拓扑里出现,本地 demo 天生看不见。

然后给同一条任务保留三本账。客户端估算一份,OpenTelemetry 的 token、cost 与 attempt 一份,云厂商的权威 usage 一份。按天对账,不用等月底。发现稳定 10% 偏差,比发现某一天突然翻倍更能说明计价规则漏了一层。

再做一次明确的失败注入。让代理返回不完整的响应头,让上游制造一次可重试 5xx,让组织预算进入耗尽状态。观察系统究竟是原地等待、自动降级、重新发请求,还是立即失败。每条路径都应该能回答四个问题。

请求发了几次。

模型实际处理了几次。

工具副作用发生了几次。

最终按哪几次计费。

这四个数对不上,别急着扩大 Agent 并发。

收尾才轮到预算闸。会话级上限负责及时刹车,组织级 spend limit 负责硬止损,告警负责把异常送到人面前。三层不要互相假装自己是对方。尤其别用一个客户端估算值承担最终账单真相,它干不了这活。

这次具体问题的短期动作很朴素,把 Claude Code 升到 2.1.239 或更新版本。但团队环境里别只在一个人的电脑上敲完升级命令就宣布收工。先盘清楚哪些人直连 Anthropic,哪些走 Bedrock、Vertex 或 Foundry,哪些机器还经过公司代理,CI、远程开发机与自托管 runner 又分别锁在哪个版本。

同一家公司里,这几条链经常同时存在。

版本修复只覆盖装上它的那一段。代理仍在改写响应头,旧 runner 仍在后台跑,或者内部封装把 Claude Code 版本钉死,风险就没有真的离场。更稳的收口方式,是给最低版本设组织策略,拿一条小任务穿过每种真实网络拓扑,再从 request、trace 与云账单三边核一次。

别拿大任务做第一次验证。选一个上下文短、输出固定、不触碰生产数据的 canary,重复跑几次。这样成本差异更容易看,失败也不会顺手改坏半个仓库。等链路数字能互相解释,再把并发放开。

可能有小伙伴觉得,这套验收是不是太像给支付系统做审计了。

我自己的判断是,还真得往那个方向靠。

Agent 已经不只是聊天框。它会长时间运行,会跨会话发消息,会调用外部工具,会在失败后重试,还会替人修改代码和触发流水线。只要这些动作开始消耗真实预算,成本记录就不再是报表边角料,而是执行日志的一部分。

Claude Code 2.1.239 修掉的那个 bug 很具体,某类代理剥掉一个 header,计费调用就可能翻倍。但它留下的问题比这个版本号大。

当答案已经回来,我们凭什么确认只付了一次钱。

以前工程师守的是日志、指标、trace 三件套。到了 Agent 时代,账本也得摆上桌。

不然灯全是绿的,船也确实到了岸,只有油箱莫名其妙空了两遍。