跳到主要内容
你面板上那行三五个字的状态,账单比你正经对话还贵

你面板上那行三五个字的状态,账单比你正经对话还贵

锈剑
锈剑

· 阅读约 5 分钟

上个月月底,我把 Claude Code 的 OTel 按 query_source 拉了个分组,盯着看了一会儿,没说话。

query_source=agent_summary   16.4%
query_source=main            15.7%

装钩子之前那一周的账。给后台代理那张进度条写三五个词的路径,比我自己跟模型的正经对话还贵。

我第一反应是算错了。第二反应是——这数要真长这样,别的地方是不是也这么收过我钱。看了这么多年成本面板,头一回是被一行状态栏干趴下。

往下翻,比数本身难看。

机制是这样的:大约每 30 秒,对每个后台代理,把它完整的对话记录整个打包发给模型,让它吐一句三到五个词的动作描述。这句话唯一的去处,是代理面板里那行状态栏。不进主对话,不进上下文。输入输出比 9000:1——九千个 token 进去,一个词出来。这里面约 96% 的钱花在 Opus 上。

为了三个词。不是三段话,三个词。

这功能从 2026 年 8 月的 2.1.232 起,对交互式后台代理默认开启,没有能直接关掉的官方设置。间隔由服务端按模型家族下发,卡在 30 到 240 秒之间——默认值已经是最小那一档。也就是说你本来就站在最省的格子里,而且没有任何一个格子能把定时器关掉。

然后是最精彩的部分:开关。

DISABLE_NON_ESSENTIAL_MODEL_CALLS,名字听着挺像那么回事吧?Claude Code 的二进制里压根不存在这个东西。SDK 那边 agentProgressSummaries 默认写的确实是 false,可打包后的代码里它只是某个 OR 表达式的一项,所有写入路径都会把它设成 true。那个没进文档的 CLAUDE_CODE_AGENT_PROGRESS_SUMMARIES,也只能开,不能关。真想停,剩下的路是 CLAUDE_CODE_FORK_SUBAGENT=0,或者 CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1——为了关掉一行状态栏,把 fork 子代理或者后台代理整个砍了。

三个开关,一个不存在,一个只会往 true 写,一个只能开。🙃

我一开始以为是自己 context 拖太长,费用才涨的。查完发现不是。是有人在后台替我花钱,花得比我自己还凶。

而且这笔钱在你翻得到的地方基本不存在。摘要请求不落在磁盘上的会话日志里,只有 OTel 导出看得见;官方文档讲后台 token 消耗那一节,也没列它。一条开销跟主对话平级的路径,默认开着,关不掉,文档里没写。这三件事凑一块儿,不叫疏漏,叫结构。

对订阅用户来说,这笔钱不是账单,是速率限制余量。难受的正是这个——凌晨三点线上挂了,你正靠它排障,它告诉你额度到了。你第一反应是模型挂了,是网关抽风,是网络抖了。都不是,是进度条把你吃完了。

这个剧本我看过。以前啃额度的是 CI 重跑和野查询,现在换了层皮,叫状态栏。背锅的还是值班那个冤种,也就是我。

补一刀:这开销不是某天突然翻上去的。作者自己那份遥测,7 月 3.5% 到 7%,8 月 7.3%,后来 11.8%,10 月中旬 16% 上下。慢慢长起来的东西不触发告警。它跟你主对话平起平坐那天,不会有人通知你,等你想起来看,它已经坐在你旁边了。

那怎么办。

说句实话,Flamerix AI 那份 gist 写得比官方文档诚实——它先承认这数只是 cost_usd 估算、不是真账单,再告诉你钩子在什么情况下会静默失效,最后才给方案。这个顺序对。先划清自己知道什么、不知道什么,再开口,现在挺稀缺的。

方案本身不碰二进制:用 BUN_OPTIONS=--preload 在进程启动时注入一个约 80 行的钩子,包 setTimeout 和 setInterval,靠回调源码文本里有没有摘要器锚点来认目标定时器。命中就返回一个惰性句柄,延迟设成 2³¹−1 毫秒。为什么是这个数?再大一点,运行时把它当成 1 毫秒,立刻执行。真的,就差这一步。

为什么绕这么远——Claude Code 以预编译的 Bun 字节码分发,改里面 JS 文本没用;API 客户端也不走可以包装的 globalThis.fetch。没别的把手,只能从定时器上撬。

这其实是个更大的话题。现在这波用 Bun、Node 单文件打包的工具全是黑盒,出了事你只能从运行时边界上想办法,正经的扩展点一个都不给你。扯远了,回到这条定时器。

代价是每遇到一个不同的回调就要跑一次 Function.prototype.toString,结果用 WeakMap 缓存着。实测 claude --version 从 10 毫秒变成 17 毫秒。7 毫秒,换掉那 16%。这买卖不用想。

但它是 fail-open 的。下次更新把日志行改个名、把锚点挪进共享常量,或者把定时器挪到 timers/promises 这种没被包装的接口上,钩子就失效——不报错,安安静静地失效。你下次发现它,多半是下个月看账单的时候。

所以这不是装一次管一辈子的东西,是每次升级之后都得重新验一遍的临时夹板。三样都得看:

preload active
summary timer suppressed
query_source=agent_summary == 0

我原本想把这篇文章写成「厂商偷钱」。想了两天,觉得没这么坏,也没这么简单。多半是先做了个面板上的东西,为了把状态栏填满,顺手加了条后台请求,成本谁都没算——因为埋单的人不在这张表上,做这个决定的人也不接 pager。

这不是恶意,是没人负责。没人负责的东西最后都长成屎山,区别只是这次那座山长在你的额度上,不在仓库里。

本期教训一句话:任何 agent 工具的支出,先按 query_source 分一次组再谈别的;分不出来,或者分出来发现有一条自己没听说过的,当故障处理。

锈剑
锈剑

接太多半夜电话的系统老兵,第一人称短段干冷吐槽,戳管理鸡汤与行业废话。

查看主页 →