Day 07 / 共 20 天 · 第 2 周 Agent 大脑
模型故障转移(Failover)
昨天知道了大脑靠内嵌 Pi 来调模型。可"调模型"随时会失败——限流、key 用尽、token 过期。今天讲 OpenClaw 怎么"永不掉线":README 那句"Auth profile rotation (OAuth vs API keys) + fallbacks",在代码里是两层嵌套循环:外层逐个换模型、内层逐个换鉴权 profile。为明天讲执行循环里"上下文超长自动压缩"这类恢复策略打底。
📍 你在整门课的位置(第 2 周 · Agent 大脑)
W1 全景·
Day6 Pi 内核→
Day7 故障转移→
Day8 执行循环→
Day9 提示词→
Day10 记忆·
W3 技能/渠道
L01
为什么要故障转移
🤔 痛点:凌晨 3 点模型限流了,助理就该罢工吗?
你的助理号称 7×24 在线。可某个模型突然 429 限流、某把 API key 额度当月用尽、OAuth token 半夜过期……只要"当前这条路"一断,助理就失联——对一个"always-on"的产品是致命的。
💡 本质:像开车的导航,堵了自动改道
导航发现前方堵车,会自动切备用路线,而不是让你干等。故障转移就是给助理配两层备用路线:先"换条路"(同一模型换一把钥匙 profile),还不行就"换目的地"(换一个模型)。用户几乎无感。今天全天沿用这个"导航改道"的类比。
一个 24 小时在线的个人助理,随时可能遇到:模型限流(429)、某把 API key 额度用尽、OAuth token 过期、某 provider 宕机、请求过载。如果一遇错就放弃,助理三天两头"失联"。故障转移让它自动换下一个可用的模型/凭据继续。
像"多条备用路线的导航"
你开车时导航发现前方堵车,会自动切到备用路线,而不是让你停在原地。OpenClaw 也一样:当前模型/凭据不通,就自动切到下一个备选,用户几乎无感。而且它有两层备选——"换一条路"(换鉴权 profile)不行,就"换个目的地"(换模型)。这一整套让助理具备生产级的韧性。
L02
两层结构总览
外层循环:模型级 failover — runWithModelFallback(model-fallback.ts:505)
遍历候选模型链(主模型 + 配置的 fallbacks,可跨 provider)。每个候选模型 →
内层循环:鉴权 profile 轮换 — runEmbeddedPiAgent(run.ts:256, while 在 :809)
对当前模型的 provider,遍历它的所有鉴权 profile(OAuth / token / API key),逐个尝试;失败就换下一个 profile 重试。
读法:外层"这个模型不行换下个模型",内层"这个模型的这把钥匙不行换下把钥匙"。顶层把两者拼起来的地方在
agent-runner-execution.ts:202-205:把 runEmbeddedPiAgent(内层)作为回调传给 runWithModelFallback(外层)。两层嵌套:内层先把当前模型的几把钥匙轮完(换条路),全失败才由外层换到下一个模型(换目的地)。
L03
外层:换模型
// resolveFallbackCandidates(model-fallback.ts:252):主模型 + agents.defaults.model.fallbacks
for (let i = 0; i < candidates.length; i++) { // model-fallback.ts:531
const candidate = candidates[i];
// 先检查该 provider 的所有鉴权 profile 是否都在冷却(:539-560)
// 全冷却 → 按策略 skip 或 probe
// 否则调用内层 run(= runEmbeddedPiAgent)
}
📝 举个例子:配一条跨家的备用链
在
→ 平时用 Claude(订阅 OAuth,省钱);Claude 全家限流 → 自动退到 GPT-4o;连 GPT 也挂了(比如断网付费服务全不可用)→ 退到本机 Ollama,哪怕降智也能应急。"目的地"从云端一路退到本地。
openclaw.json 写 agents.defaults.model.fallbacks = ["gpt-4o", "ollama/llama3"],主模型是 Claude:→ 平时用 Claude(订阅 OAuth,省钱);Claude 全家限流 → 自动退到 GPT-4o;连 GPT 也挂了(比如断网付费服务全不可用)→ 退到本机 Ollama,哪怕降智也能应急。"目的地"从云端一路退到本地。
读法:候选模型链来自配置
agents.defaults.model.fallbacks,可以跨 provider(如"先 Claude,挂了退 GPT,再退本地 Ollama")。还有一条 CLI provider 分支(agent-runner-execution.ts:214 的 isCliProvider):有些 provider 由外部 CLI 承载(如 Claude CLI),走 runCliAgent 而非内嵌 Pi。L04
内层:换 profile
// resolveAuthProfileOrder(run.ts:423 → order.ts:67)算出 profile 尝试顺序
const profileCandidates = ...; // run.ts:432-436
while (true) { // run.ts:809 主重试循环
// 上限 MAX_RUN_LOOP_ITERATIONS(run.ts:741,随 profile 数增长)
await runEmbeddedAttempt(...); // run.ts:851 一次完整对话尝试
// 失败 → advanceAuthProfile(跳到下个可用 profile,跳过冷却中的)
// markAuthProfileFailure(记失败 + 设冷却)
// 成功 → markAuthProfileGood / markAuthProfileUsed
}
读法:内层对"同一个模型"尝试它的多个鉴权 profile。一个 profile 失败 → 记冷却、跳下一个。profile 存储/类型在
src/agents/auth-profiles/(types.ts:ApiKeyCredential / TokenCredential / OAuthCredential)。像单步调试一样,看一次真实故障转移的"改道过程"(假设 Claude 有 2 个 profile 都限流、退到 GPT-4):
| 尝试 | 此刻发生什么 | 状态变化 |
|---|---|---|
| 1 | Claude + OAuth 发请求 | 返回 429 → markAuthProfileFailure 设冷却 |
| 2 | 内层 advanceAuthProfile → Claude + api_key | 又 429 → 该 key 也进冷却 |
| 3 | Claude 的 profile 全冷却 → 外层升级 | 换到候选链下一个:GPT-4 |
| 4 | GPT-4 + OAuth 发请求 | 成功 → markAuthProfileGood |
| ✅ | 用户拿到回复 | 全程无感,只是慢了一两秒 |
L05
OAuth > token > key 排序
profile 顺序由 orderProfilesByMode(order.ts:162-196)决定:
const typeScore = type === "oauth" ? 0 : type === "token" ? 1 : type === "api_key" ? 2 : 3;
const lastUsed = store.usageStats?.[profileId]?.lastUsed ?? 0;
// 先按类型分:oauth 优先于 token 优先于 api_key
// 同类型内:lastUsed 最旧的优先(round-robin 轮换,均摊负载)
// 冷却中的:排到最后(按冷却到期时间升序)
为什么 OAuth 排在 API key 前面?
OAuth(如 ChatGPT/Codex 订阅登录)通常是"包月订阅"——多用不额外花钱;API key 往往按量计费。所以优先用 OAuth(省钱),实在不行才用按量付费的 key。同类型内部又按"最久没用的先用"轮换——把负载均摊到多把钥匙上,避免单把 key 被打限流。这套排序把"成本优化 + 负载均衡 + 冷却规避"揉进了一个打分函数里,非常见工程巧思。
L06
冷却与退避
失败的 profile 会被设置冷却期(markAuthProfileFailure,run.ts:762),一段时间内不再尝试。过载错误(provider 说"我忙")还有指数退避:
// OVERLOAD_FAILOVER_BACKOFF_POLICY(run.ts:89-94)
// maybeBackoffBeforeOverloadFailover(run.ts:781):过载时先等一会儿再切,避免雪崩
读法:冷却 = "这把钥匙刚失败,先晾一会儿别再试";指数退避 = "越试越等更久",防止把已经过载的服务打得更狠。这是分布式系统标准的容错手段,OpenClaw 用在鉴权凭据上。GitHub Copilot 还有专门的 token 刷新状态机(
refreshCopilotToken)。L07
错误分类
不是所有错误都该"换 profile"。错误分类在 pi-embedded-helpers.ts(run.ts:40-57 导入):
isAuthAssistantError:鉴权错 → 换 profile。isRateLimitAssistantError:限流 → 冷却 + 换 profile。isBillingAssistantError:额度/账单 → 换。isLikelyContextOverflowError:上下文超长 → 触发压缩重试(Day 08)。
读法:根据错误类型走不同恢复策略,统一抛
FailoverError(failover-error.ts)。比如"上下文超长"不该换 key(换了还是超长),而该压缩历史。精准的错误分类是有效故障转移的前提——否则会瞎切一通。⚠️ 常见误解:以为"报错就换一把 key/换个模型准没错"。其实换错方向反而更糟:上下文超长(
isLikelyContextOverflowError)时换 key,新 key 一样超长、白白烧掉一把好钥匙——正确做法是压缩历史(Day 08)。这就是为什么必须先分类错误、再决定改道方式,而不是无脑改道。L08
今日小结 + 动手
🧠 今天你应该能回答
- 故障转移解决什么问题?为什么个人助理特别需要它?
- 两层循环各自换什么?在哪拼起来?
- profile 排序为什么 OAuth > token > api_key?同类型内怎么排?
- 冷却和指数退避各防什么?
- 为什么要先做错误分类再决定恢复策略?
🎵 记忆口诀(故障转移一句话)
"先换钥匙(内层 profile),再换模型(外层 fallback);OAuth 先上、按量垫后;失败晾冷却、过载先退避;错误先分类、别瞎改道"——一条导航改道的完整规矩。
✋ 动手
cd /Users/bitmart/work/codes/github/openclaw
sed -n '500,560p' src/agents/model-fallback.ts # 外层
sed -n '805,860p' src/agents/pi-embedded-runner/run.ts # 内层 while
sed -n '160,196p' src/agents/auth-profiles/order.ts # 排序打分
明天预告 · Day 08:Agent 执行循环——一条消息怎么变成 LLM 调用、工具调用、回复?会话怎么串行排队防并发写坏?
session.prompt() 触发点 + 订阅式输出观察 + 上下文溢出自动压缩。