不是额度,是指纹:CPA 反代谷歌模型,子代理为什么总在 429
两周前第一次遇到这个问题时,我把根因错定成了 TPM 限流(每分钟 token 流速超了)。后来发现那个解释是错的:429 不一定是限流,它可能是上游拿来做伪装的。这篇文章把整条链路为什么出问题、我怎么一步步查到根因、每一层具体怎么修,全部写清楚,命令都可以直接抄。
一、这条链路是怎么搭起来的
先把结构说清楚,后面每个问题都能在结构里找到位置。
我用的谷歌模型通道是四层:
Claude Code(主会话 / 子代理) → 自建网关(Anthropic 协议转发) → CPA(CLIProxyAPI,协议转换 + OAuth 凭据管理) → mihomo(代理,选一个海外节点出去) → Google(Antigravity 免费池)为什么要 CPA:Antigravity 是 OAuth 订阅制,而 Claude Code 这类 Agent 工具说的是 Anthropic/OpenAI 协议,中间需要一个翻译层,顺便管理凭据和限流。这是 GitHub 上的开源项目,很多人在用。
为什么要 mihomo:Google 按出口 IP 拒绝服务,服务器直连不行,必须挂海外节点。订阅机场有九十多个节点可选——这个”可选质量”是后面第二个问题的根源。
这条链路上其实埋着三个雷,位置各不相同:
- 子代理的 system prompt 里带着框架身份声明——在请求体里
- 机场节点的 IP 质量参差不齐——在出口上
- 大请求会触发上游的深度检查——在请求体积上
第一晚引爆的是第一个。
二、子代理连环 429:额度是假的
深夜开子代理跑任务,几分钟一条死亡通知:
429 · All credentials for model xxx are cooling down(last error: 429 RESOURCE_EXHAUSTED)我的第一反应是信了。因为网关面板上白纸黑字写着”额度限制,退避中”,快照里有截止时间、退避等级,看起来非常权威。
但有两个事实拼不上:当天早些时候我做过三个子代理并发的判别实验,零 429;中间只有几个小时的轻度使用。额度不可能一夜蒸发。
怎么验证”额度到底耗没耗尽”?额度耗尽是账号级状态,所有请求都该死。所以我发了一条最轻的请求——不带 system prompt、只有一句 hi——同一个模型、同一个凭据池。结果 200,一秒多返回。
额度没耗尽,429 是选择性拒绝。那它选的是什么?主会话和子代理的请求,差异只在 system prompt:子代理会在 system 里注入一段身份声明,类似”你是基于某某 SDK 构建的 agent”这样的话。带 system 的死,不带 system 的活,嫌疑非常集中。
为了确认不是我们的个例,我去 CPA 的 GitHub 仓库翻 issue,用 antigravity + 429 搜,找到 #5695,跟我的现象一模一样:主 agent 一切正常、子代理必 429、账号额度明明可用。维护者的回复给出了机制:子代理 system prompt 里的身份短语会触发 Google 的内容过滤,而 Google 的网关在过滤命中时,返回的不是”内容被拒”,是 429 RESOURCE_EXHAUSTED——一个伪装响应。90% 以上的这类案例都是这个原因。
这也解释了为什么白天的并发实验没事:实验的子代理提示词里没有踩中过滤词,正式任务的提示词踩中了。跟并发、跟额度都无关。
三、药方一:敏感词混淆
CPA 自带一个配置项,会在请求发往上游之前,把 system prompt 里的指定短语用零宽字符打乱,让过滤器匹配不上:
antigravity: sensitive-words: - "Claude Agent SDK" - "Claude Code"词从哪来?官方 issue 里点名的是 “Claude Agent SDK”,我自己从失败日志里把 system 原文捞出来对了一遍,把框架相关的身份短语都加上。失败请求的原文在 CPA 的 error 日志里(logs/error-*.log),每份日志带完整请求体,system 字段就在里面。
改完配置要重启 CPA 生效,然后验证方式很简单:再开一个子代理,看 429 是否消失。我的实测是消失了——子代理跑了几轮正常干活,429 一个没有。
但重启这个动作,把第二个雷引爆了。
四、400:本质就是 IP 太差
重启之后,错误变成了 400:
User location is not supported for the API use这个问题的本质一句话能说完:Google 按出口 IP 拒绝服务,会 400 纯粹是因为用的 IP 太差。IP 质量够好,这个错误根本不会出现。机场的流媒体优化 IP、广播段 IP,早就被各类风控标记烂了,打着”原生”标签的也不可信。所以这个问题不是”修”,是”换”——找一个还没被标记的出口。
但找的过程有个坑。重启前一切正常,是因为 CPA 到 Google 的 keepalive 长连接一直钉在一个干净的节点上;重启把连接池清空,新连接按代理组的当前指向走,落到了香港——Google 不支持的地区。也就是说”重启前好的、重启后坏了”,两个都是真的,因为走的不是同一条路。
排查用的办法是抓现行:一边发探测请求,一边轮询 mihomo 的 connections 接口,看 Google 域名的连接实际走了哪个组、哪个节点。这一抓还发现分流比想象的复杂——这个服务对 Google 的请求分散在两个域名上,命中两个不同的代理组,我一个月前只锁过其中一个组,另一个组一直开着自动选择,悄悄落到香港没人知道。只锁一个组等于只修了半条路。
接下来要做的就是把另一个组也指到干净节点上。问题变成:哪个节点是干净的?
五、药方二:金标准节点测法
先说为什么不能信面板:机场面板和 mihomo 面板上的”节点测试”,测的是延迟和连通性,测的是”这个节点活着吗”,而 Google 的 400 测的是”这个 IP 的信誉够不够”。两个维度完全不相干,面板上全绿的节点,API 层面可能拒绝率 100%。我自己就用面板测过一个节点显示正常,走真实调用直接 400。
唯一可信的测法是让每个候选节点走一遍完整链路的真实调用,一轮十几秒:
# 1. 切组:把流量组指向待测节点(🔴 必须校验返回 204)g=$(python3 -c "import urllib.parse;print(urllib.parse.quote('✨ Gemini'))")curl -s -X PUT "127.0.0.1:9090/proxies/$g" \ -d '{"name":"🇬🇧英国伦敦01|CTCU|0.1x"}' -w "switch:%{http_code}\n"
# 2. 重启 CPA:清掉 keepalive,强制新连接走新节点systemctl restart cliproxyapi && sleep 5
# 3. 真实调用:看状态码,200=节点干净,400=IP 被拒curl -s --max-time 20 -o /dev/null -w "%{http_code}\n" \ "$GATEWAY/v1/messages" \ -H "Authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{"model":"gemini-3.8-flash-high","max_tokens":16, "messages":[{"role":"user","content":"hi"}]}'两个容易翻车的细节:
第一,切组必须校验返回码。有些节点的全名带地区前缀(比如”【北美洲】美国01”),用短名去切,接口会报 proxy not exist——如果脚本把报错吞了,组根本没切,探针打在旧节点上照样 200。我第一轮扫描就有两个节点的”可用”数据是这么来的假阳性,用 API 取全名重测后才推翻。节点全名从 GET /proxies 的组列表里取,不要手打。
第二,每轮之间必须重启 CPA。mihomo 切组只影响新连接,CPA 的旧连接还钉在老节点上,不重启的话测的是上一个节点。
我按这个方法扫了十七个节点:英国、美国、新加坡、日本,流媒体的、CTCU 的、“原生”的——只有一个 200。结论很干脆:不是地区问题,是 IP 信誉问题,绝大多数机场节点在 Google 那里都是脏的,能活下来的节点纯属运气。测出来那一个,把两个组都锁上去,400 消失。
六、大请求的间歇 400:同一个 IP 的另一面
两层修完,轻量请求连发十个,十个 200。开子代理跑正式任务,三轮之后又死了,还是 400,但这次是间歇的:几百字节的小请求稳定通过,一百多 KB、带完整工具定义的流式大请求,大约一半概率被拒。
这还是 IP 信誉问题,只是换了个体面:上游 WAF 对大请求做深度检查,检查不过就按地区封锁的口径拒掉。同一个 IP,小请求放行、大请求拦截,说明这个 IP 的信誉也是”及格线上下”。这一层没有任何参数可以调,路只有两条:换更好的 IP(住宅原生,贵),或者等(上游的判定会漂)。
次日早上我在同一个节点上重跑同样的任务验证这个判断:52 次工具调用,零失败。间歇拒绝没有复发。所以这一层我最后什么都没修,它自己好了。把它写进清单的原因是:如果哪天又出现”小请求过、大请求拒”的模式,先怀疑 IP 信誉而不是配置,别把配置翻个底朝天。
七、药方三:排障期间怎么保交付
三层问题的排查横跨了一整晚,但当晚的任务不能等。我的做法是给生产任务留一条稳定降级通道:不走出问题的模型,换网关里另一个稳定的模型档位,用命令行模式起一个独立进程跑:
nohup claude -p "$(cat PROMPT.md)" \ --model haiku \ --allowedTools "Bash" "Read" "Write" "Edit" "Glob" "Grep" "Skill" "ToolSearch" \ > run.log 2>&1 &--model haiku 走的是配置里 haiku 别名映射的模型,--allowedTools 白名单解决了 root 环境下不能跳过权限确认的问题。任务照常完成,排障并行进行,互不耽误。
八、复盘清单
三层问题,三个位置,三种性质:
| 层 | 现象 | 本质 | 修复 |
|---|---|---|---|
| 一 | 子代理 429”额度耗尽” | 身份指纹触发内容过滤,429 是伪装 | sensitive-words 混淆 |
| 二 | 400 地区不支持 | 出口 IP 信誉差,Google 按 IP 拒绝 | 金标准测出干净节点并锁定 |
| 三 | 大请求间歇 400 | 同一 IP 在 WAF 深检下的另一面 | 无参数可调,换 IP 或等 |
几条我认为可迁移的:
错误码不可当证据用。429 可以是内容过滤的伪装,400 可以是 IP 信誉也可以是深度检查,错误文本是线索,不是结论。推翻一个错误码说法最快的办法,是找一个”如果它说的是真的,就不该活着的请求”——我用一条不带 system 的轻请求推翻了”额度耗尽”。
面板测试和真实调用是两个维度。以后凡是涉及 IP 信誉、地区准入的判断,一律只认全链路真实调用的返回码。
修好一层不要收工。每一层的修复都会改变链路行为,把下一层顶出来。这次三层的顺序是 429 → 400 → 间歇 400,第一层修好之前,后两层的症状完全被掩盖。
最后留一句给未来的自己:再见到 RESOURCE_EXHAUSTED,先问这条错误是谁发给你的、它有没有理由说谎,再问额度还剩多少。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!







