Skip to main content
返回博客
Claude CodeAICLIBashAutomationResilience

智能体级联:终端自己挑出还活着的那个

Claude 部分宕机、一些模型停止响应时,我想切换到 Gemini——却发现自己的后备方案早就坏了好几个月。这篇讲的是怎么做一个真正扛得住的。

发布于 2026年8月29日9 分钟阅读

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,而不是背后的模型。

六级,两个轴

供应商和模型是独立失效的,所以这个阶梯把两者交错排列。每一级都换来某种具体的东西,往下走的代价也写明了:

RungWhat it survivesCost of getting there
claude + opusnothing — this is the happy path
claude + sonnetOpus capped or overloadedweaker model, same session shape
claude-proxy + opusstale local login, broken CLI authseparate OAuth token
claude-proxy + sonnetboth of the above at onceweaker model via proxy
agyAnthropic outage / exhausted plandifferent vendor, different CLI
geminiagy itself brokendifferent CLI again, shares agy's quota

在代码里,一级是一个三元组:后端、用于探测的模型、用于运行的模型。顶级那个空的运行模型比看上去更重要:不传 --model 才能保住配置里的 opus[1m] 及其 1M 上下文。显式写出模型名会在每次启动时悄悄把它砍成普通 Opus。

agent
# 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。所以每次探测都发起一次:

agent
# 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 的结果会缓存。

两张网,而不是一张

预检探测抓不住所有情况。如果额度恰好在探测和启动之间耗尽,探测是对的但仍然没用。所以有第二张网:退出码加运行时长。

agent
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 调用、一个有序数组。真正花力气的是不信任自己写的检查。我三次做出了在底层已经坏掉时仍报告成功的东西,每次罪魁祸首都是那个又便宜又快的探测。

韧性不是你配置的后端清单,而是有没有东西在验证这份清单依然属实。我的清单纸面上有六级,而五个月里只有一级是真的。

参考

发现错误了吗?

本文里有事实错误、别扭的翻译,或者哪里读起来不对?告诉我——用你自己的语言。