不是额度,也不是指纹:这次真凶藏在 system 首块

2197 字
11 分钟
不是额度,也不是指纹:这次真凶藏在 system 首块

上一篇《不是额度,是指纹》的结尾我说:再见到 RESOURCE_EXHAUSTED,先问这条错误是谁发给你的、它有没有理由说谎。两周后它真的又来了,而且这次连我自己上篇的结论也一起被推翻了——词指纹不是唯一凶手,甚至不是这次的凶手。这次排查里最有价值的一课:旧结论和错误码一样,都是待验证的假设,不是免死金牌。

一、链路换了,老朋友换了个地方赴约#

先把结构变化说清楚。上篇的链路是 Claude Code → 自建网关 → CPA → mihomo → Google,排查结束后我把中间两层合并了:CPA 退役,换成 magpie——一个本地多模型网关,顺手接管了模型路由和插件管理。链路从五层变四层:

Claude Code(主会话 / 子代理)
→ magpie(协议转换 + 路由分流 + 中间件)
→ mihomo(代理,海外节点出去)
→ Google(Antigravity 订阅池)

换完当晚主会话一切正常。第二天开 haiku 档的子代理——分流规则把这档送去 gemini-flash——429 回来了,文案还是那熟悉的配额耗尽。

我的第一反应非常自然:这病上个月刚治过,药方还在。上篇的结论是身份词指纹,修法是敏感词零宽混淆。magpie 有网关中间件机制,能在请求出网关前改写请求体,我把等价的混淆逻辑写成中间件挂上,测:还是被拒。

药方无效只有两种解释:要么药方本身不对,要么这是另一种病。上篇那套验证过三层问题,词指纹是实打实测出来的——所以更像后者。但”更像”不是结论,是下一个待验证的假设。

二、先审问网关:这条错误到底是谁说的#

上篇最硬的教训是错误码不可当证据用。这次我把顺序提前:不看错误文案,先看网关的路由日志。magpie 对每个请求都会记一条路由决策,谁进的、送到哪个上游、上游回了什么,这是链路里最原始的证词。

一看就发现症状是转述过的:Antigravity 实际回的是 400 配额文案,根本不是 429;而且失败之后路由组立刻兜底重试,最后落到我眼前的错误已经是第三手转述——原始 400、网关兜底失败、错误聚合器改写文案。我之前对着最终文案推理了半天”配额为什么耗尽”,等于在审一份传闻证据。

所以第一件事不是改代码,是抓原话。

三、抓原话:tee 代理截获 87KB#

要比对”成功的请求”和”失败的请求”,得先有两个真实样本。成功样本好办,curl 手搓一个就过;失败样本必须是真的 Claude Code 发出去的原始字节。办法是老一套:本机起一个 tee 代理(监听 3426,转发到网关 3425,把原始字节落盘),再把 Claude Code 指过去。

这里踩了两个坑,都值得记:

一是环境变量压不过配置文件。网关地址之前写进了 Claude Code 的 settings.json,单纯 export 环境变量根本不生效,客户端压根没走我的 tee。要用 claude --settings 显式覆盖。

二是删文件不等于断流。tee 进程还握着旧文件的 inode,我删掉抓包文件重建,它还在往那个已经 unlink 的旧 inode 里写,新文件永远是空的。重启 tee 才算换手。

拿到 87KB 真实请求后,和 curl 成功样本做结构对比,列出所有 curl 没有的特征:一套客户端请求头、请求体里的用户标识、工具定义数组、system 开头多出来的一个块、流式开关。六个候选,凶手在其中一个里。

四、消融实验:一次只放回一块#

六个候选逐个”推理”是没有尽头的,每个都像、每个都不像。这种时候不该继续想,该做实验:拿 curl 干净样本当基线(它稳定 200),每次只加回一块真实特征,谁引爆谁负责。

加回的特征结果
Claude Code 全套请求头200
metadata.user_id(设备指纹)200
完整 tools 数组200
流式请求200
模型名上的 [1m] 窗口标记200
system 首块:x-anthropic-billing-header被拒,每次都是

真凶是 Claude Code 放在 system 数组第一位的计费元数据块,内容是一行 cc_version=…; cc_entrypoint=sdk-cli; …。这个块是写给 Anthropic 自己计费系统看的记账便签,对谷歌毫无用处,却被上游拿来当了识别特征。把这一块去掉,87KB 的完整真实请求体原样重放——200。

回头看词混淆为什么”没用了又没完全没用”:身份词还散在 system 其余部分和 messages 里,混淆它们确实消掉了词指纹这条触发线,但计费块是另一条独立的触发线,一条词一条块,切掉一条另一条还在。上篇的病里两条线可能都存在,只是当时消融没细分到 system 块级别,药方恰好把两条都盖住了大半。

回头看本质就一句:错误码会说谎,上游的拦截特征也不止一条,每次复发都得重新归因。

五、药方:两层中间件,热重载免重启#

修法落在 magpie 的网关中间件上。这是个单文件 JS 插件,onRequest(body, ctx) 里改写请求体再放行,热重载,一秒生效,网关不用重启,生产链路中途不用动。

两层逻辑,都很短:

// 层一:丢掉含计费标记的 system 块
const billing = o.billing ?? ["x-anthropic-billing-header"]
if (Array.isArray(body.system) && billing.length) {
const kept = body.system.filter(b =>
!(b && typeof b.text === "string" &&
billing.some(k => b.text.includes(k))))
if (kept.length !== body.system.length) { body.system = kept; changed = true }
}
// 层二:零宽混淆身份词(上篇药方,降级为保险层)
const re = new RegExp("(" + words.map(escapeRe).join("|") + ")", "gi")
const hide = (t) => t.replace(re, (w) => w.split("").join(ZW))

词混淆从主角降成配角,但我没删它:哪天上游改规则,它是第一道闸。所有阈值(词表、计费标记)都做成 options 可换,[] 可单独关掉一层。

中间件机制有个容易忽略的行为:钩子抛异常等于静默放行,请求原样过去,症状是”药没生效”而不是报错。我第一版就踩了——注入的 changed = true 写在了 let changed 声明之前,TDZ 崩溃,中间件当场变哑巴,我还以为逻辑错了查了半天。所以这个插件带了个探针:options 里开 probe 后,发一条内容恰为 PROBE_CLAUDE_SCRUB 的用户消息,网关回 418,一键判活。

六、验证与开源#

三重验证,全部真实链路:

  • 87KB 真实请求体去掉计费块后原样重放——200;
  • 真 Claude Code 开 haiku 档子代理,让它只回一个单词——返回 ok,退出码 0;
  • 探针消息——418,中间件在岗。

整套东西整理成了 magpie 插件开源:仓库在 heaven-1314/magpie-plugin-claude-scrub,一条命令安装:

Terminal window
magpie plugin add github:heaven-1314/magpie-plugin-claude-scrub

十一个测试用例按官方契约写,也提了 PR 进社区插件仓库。适用范围写在 README 里:自己的订阅账号、自己的网关,一人一户。

七、复盘清单#

层症状真凶修法
一429 复发,旧药方无效症状是网关层层转述过的 400查路由日志拿原始证词
二六个候选特征不知谁负责system 首块计费元数据消融:基线 200,逐块放回
三中间件挂了没反应钩子抛异常被静默吞掉探针判活,改完先 418 再收工

几条我认为可迁移的:

旧结论也是待验证的假设。复发性问题最诱人的捷径是直接搬上次的药方,但上游的拦截规则是多条的、会变的。药方无效时先别怀疑药方,重新归因。

多候选归因不做推理做消融。候选超过三个就不该继续在脑子里排列组合,找一个干净基线,一次只放回一个变量,让每次实验只回答一个是非题。

长链路里的错误文案都是转述。离你最近的那个错误,往往经过聚合、兜底、改写三道手。归因前先找到最原始的那份日志,网关的路由决策日志是首选。

中间件类组件必须自带心跳。凡是”抛异常即放行”的 fail-open 设计,都要配一个探针端点,否则它坏了的样子和没装一模一样。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
不是额度,也不是指纹:这次真凶藏在 system 首块
https://heaven-1314.github.io/posts/antigravity-429-billing-block/
作者
赵培州
发布于
2026-10-12
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
赵培州
AI 应用研发工程师 · 用 Agent 做默认交付
公告
记录 AI 应用落地实践与踩坑经验,欢迎交流。
分类
标签
最新动态
站点统计
文章
36
分类
9
标签
87
总字数
51,175
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.15.6
文章许可
CC BY-NC-SA 4.0