智能体级联:终端自己挑出还活着的那个
Claude 部分宕机、一些模型停止响应时,我想切换到 Gemini——却发现自己的后备方案早就坏了好几个月。这篇讲的是怎么做一个真正扛得住的。
TL;DR
一个名为 agent 的 bash 启动器,在启动前挑选最佳可用的 AI 编程 CLI。它沿两个轴降级:先降模型(Opus → Sonnet,仍留在 Anthropic 内),再降传输层,最后才换供应商。每次探测都发起真实 API 调用,因为模型列表会说谎。看门狗每两小时检查底层级别,出问题就发 Telegram 告警。
$ agent --status
OK claude / claude-opus-5 api.anthropic.com reachable, claude-opus-5 answered
OK claude / claude-sonnet-5 api.anthropic.com reachable, claude-sonnet-5 answered
OK claude-proxy / claude-opus-5 claude-opus-5 answered
OK claude-proxy / claude-sonnet-5 claude-sonnet-5 answered
OK agy antigravity answered on Gemini 3.6 Flash (Low)
OK gemini localhost:8317 answered for gemini-3.1-pro-low问题
当 Claude 不可用时,我希望终端继续能用。天真的做法是「Claude 挂了就跑 Gemini」——我一直以为自己就是这么配置的。但有意思的故障并不是宕机。最常见的是:Opus 额度用尽,而 Sonnet 回答得好好的。这时候跳到另一家供应商很荒谬——隔壁的模型是健康的。
所以真正的后备必须先按模型降级,再按供应商降级。而且它必须知道哪一级真的活着——这才是难点,我在这里错了两次。
五个月的谎言
动手写之前我先检查了已有的东西。代理的认证目录用两个时间戳讲完了整个故事:
claude-newiqa@gmail.com.json —— 今天 06:41 刷新 · gemini-newiqa@gmail.com-….json —— 最后一次是 3 月 27 日
Claude 令牌每天刷新。Gemini 令牌五个月没动过。我的健康检查脚本只看 claude-*.json,所以没人发现。这个后备从春天起就只是装饰——空房间上方的一盏绿灯。这一个观察塑造了下面的一切:没人真正调用的后备不是后备。
为什么网关解决不了这个问题
显而易见的做法是本地 LLM 网关——Bifrost、LiteLLM——在配置里写供应商故障转移。我已经跑着一个(CLIProxyAPI),再加一个只会多出一个需要照看的守护进程。
更重要的是,网关做不了真正的活。API 层的故障转移意味着在同一个客户端底下换模型。把 Gemini 的响应经翻译层喂给 Claude Code,它会在 tool-use 和流式输出上崩掉。后备必须切换 CLI,而不是背后的模型。
六级,两个轴
供应商和模型是独立失效的,所以这个阶梯把两者交错排列。每一级都换来某种具体的东西,往下走的代价也写明了:
| Rung | What it survives | Cost of getting there |
|---|---|---|
| claude + opus | nothing — this is the happy path | — |
| claude + sonnet | Opus capped or overloaded | weaker model, same session shape |
| claude-proxy + opus | stale local login, broken CLI auth | separate OAuth token |
| claude-proxy + sonnet | both of the above at once | weaker model via proxy |
| agy | Anthropic outage / exhausted plan | different vendor, different CLI |
| gemini | agy itself broken | different CLI again, shares agy's quota |
在代码里,一级是一个三元组:后端、用于探测的模型、用于运行的模型。顶级那个空的运行模型比看上去更重要:不传 --model 才能保住配置里的 opus[1m] 及其 1M 上下文。显式写出模型名会在每次启动时悄悄把它砍成普通 Opus。
# Each rung: backend | model to probe | model to run with
# An empty run-model means "let the CLI use its configured default", which keeps
# the profile's opus[1m] (and its 1M context) instead of downgrading it to plain
# opus just to name it explicitly.
RUNGS=(
"claude|$AGENT_OPUS_MODEL|"
"claude|$AGENT_SONNET_MODEL|sonnet"
"claude-proxy|$AGENT_OPUS_MODEL|$AGENT_OPUS_MODEL"
"claude-proxy|$AGENT_SONNET_MODEL|$AGENT_SONNET_MODEL"
"agy||"
"gemini||"
)当顶级不可用时,降级会被明确宣告而不是悄悄发生——降级的会话绝不能被误认为正常会话:
$ agent -p "explain this bug"
[agent] Claude Code (claude-opus-5) unavailable — claude-opus-5 unavailable: {"error":...}
[agent] using Claude Code (claude-sonnet-5)
# ...the session continues on Sonnet, inside Anthropic, with no vendor switch.模型列表会说谎
第一直觉是廉价探测:向网关请求 /v1/models,确认模型在列表里。这毫无价值。在每次调用都返回 auth_unavailable 的那段时间里,我的代理一直乐呵呵地列着 gemini-*。Antigravity CLI 也一样——agy models 是从目录里回答的,不是从可用会话。
模型健康状况同理。没有任何列表接口会告诉你 Opus 此刻被限流了。只有真实请求才会显示 429 或 529。所以每次探测都发起一次:
# Real /v1/messages call for ONE model. This is what distinguishes "Opus is
# capped" from "Anthropic is down" — no model list can tell you that.
probe_model() {
local m="$1" body
body=$(curl -sS --max-time "$PROBE_TIMEOUT" \
"$AGENT_PROXY_URL/v1/messages" \
-H "Authorization: Bearer $AGENT_PROXY_KEY" \
-H 'content-type: application/json' \
-d "{\"model\":\"$m\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}")
case "$body" in
*'"type":"message"'*) REASON="$m answered"; return 0 ;;
esac
REASON="$m unavailable: $(printf '%s' "$body" | tr -d '\n' | cut -c1-140)"
return 1
}官方级别的单模型可用性是向代理借来的,因为它前置的是同一个 Anthropic 账号。整个预检实测为 7 个 token、约 1.3 秒:一次免费的未认证 401 存活检查加一次 1-token 调用,Opus 的结果会缓存。
两张网,而不是一张
预检探测抓不住所有情况。如果额度恰好在探测和启动之间耗尽,探测是对的但仍然没用。所以有第二张网:退出码加运行时长。
start=$SECONDS
run_rung "$backend" "$rmodel" "$@"
rc=$?
elapsed=$(( SECONDS - start ))
# Clean exit, or the user interrupted it — either way, done.
if [ $rc -eq 0 ] || [ $rc -eq 130 ]; then
exit $rc
fi
# Survived long enough to have been genuinely used: a real error, not a rung
# that never started. Do not silently rerun the work somewhere else.
if [ $elapsed -ge $FASTFAIL_SECONDS ]; then
exit $rc
fi
warn "$(label "$backend" "$pmodel") exited $rc after ${elapsed}s — treating as unavailable"25 秒这个阈值编码了一个判断。几乎立刻死掉的级别其实根本没启动起来——认证坏了、模型限额——所以继续往下走是安全的。跑了几分钟才失败的级别是真的在工作,把这份工作悄悄拿到另一家供应商重跑,比直接暴露错误更糟。
看门狗,以及它内部的陷阱
启动器只有我运行时才跑,所以某一级可能在两次会话之间烂掉。我在已有的健康守护程序(launchd,每两小时,Telegram 告警)里接了一个检查,调用同一套探测代码。我的第一版就有这篇文章讲的那个 bug:遇到第一个绿色级别就返回成功。
# Healthy — every non-Anthropic rung answers
$ agent --check-fallback
all provider-independent fallbacks OK: agy — antigravity answered...; gemini — ...
rc=0
# One rung rotted while the other still works. This is the case that used to
# pass silently, and the whole reason the check exists.
$ GEMINI_API_KEY=broken agent --check-fallback
DEGRADED: still covered by agy — antigravity answered on Gemini 3.6 Flash (Low)
but a rung died: gemini — gemini call failed: {"error":"Invalid API key"}
rc=1这是错的,因为两个非 Anthropic 级别持有各自独立的令牌——Antigravity CLI 有自己的,而 Gemini 级别用的是代理的。一个可能烂掉而另一个看起来完美无缺,在第一个成功处停下就会像当初那样把它藏起来。所以检查会探测每一级,并且把 DEGRADED 也当作失败:在还有备份时就提前预警。
三个值得记下来的错误
每一个都在坏掉的机器上方亮起了绿灯——同一种故障模式,一个下午出现了三次:
- 探测测的是我从不使用的模型。我探测的是
gemini-3.1-pro-preview,而 CLI 配置的是gemini-3.1-pro-low。两者解析到不同的供应商,于是我诊断出了一个根本不存在的「令牌过期」——真正的原因是某个供应商压根没登录。探测必须走真实客户端完全相同的路径。 - 一个悄悄变空的变量。用
awk加\x27表示引号来解析代理的 YAML 密钥,在 BSD awk 下得到空字符串——没有报错,没有警告。检查依然通过,因为它恰好测试的那一级不需要密钥。改用awk -v q="'"传引号即可。 - 探测通过了,真东西却失败了。对 Gemini 端点的
curl探测返回 200,但真正的gemini -p拒绝启动:无头模式在非受信目录下会被拦住。这一级恰恰在它存在的意义所在的场景中是坏的。
怎么用
输入 agent 就能拿到最佳可用级别,体验和平常的 Claude Code 一样。显式选择会完全跳过探测——你要 Gemini 就给你 Gemini:
agent # auto-pick the best live rung
agent --use gemini # force a backend
agent --use claude:sonnet # force a backend AND a model
agent --use claude-proxy:opus # opus/sonnet/haiku map to full ids per backend
agent --list # backend names
agent --status # probe every rung and report
agent --check-fallback # exit 1 if a non-Anthropic rung is missing
agent -p "..." # flags pass straight through to the chosen CLI结论
这里的工程本身平平无奇:一个 bash 脚本、几个 curl 调用、一个有序数组。真正花力气的是不信任自己写的检查。我三次做出了在底层已经坏掉时仍报告成功的东西,每次罪魁祸首都是那个又便宜又快的探测。
韧性不是你配置的后端清单,而是有没有东西在验证这份清单依然属实。我的清单纸面上有六级,而五个月里只有一级是真的。
参考
- Claude Code Documentation — headless mode,
ANTHROPIC_BASE_URL, model flags - CLIProxyAPI — the local multi-provider gateway behind the proxy rungs
- Gemini CLI — the last rung, and its trusted-folders behaviour
- Apple Developer — launchd jobs, used by the watchdog
发现错误了吗?
本文里有事实错误、别扭的翻译,或者哪里读起来不对?告诉我——用你自己的语言。