10 模型回退与鲁棒性原理(失败时为什么还能继续)
这篇聚焦“失败处理系统”,不是讲模型能力。
核心源码入口
src/agents/model-fallback.tssrc/agents/failover-error.tssrc/agents/pi-embedded-helpers/errors.tssrc/agents/pi-embedded-runner/run.ts
先看核心目标
真实目标不是“永不失败”,而是:
- 快速识别可回退失败
- 对不可回退失败立即停止
- 把失败轨迹结构化记录下来
模块一 错误分类器(failover-error.ts)
FailoverError 是统一错误容器,核心字段:
reason:billing/rate_limit/auth/timeout/format/unknownprovider/model/profileIdstatus/code
分类逻辑 resolveFailoverReasonFromError(...):
- 先看 HTTP status(402/429/401/403/408/400)
- 再看错误码(
ETIMEDOUT等) - 最后看 message 文本(通过
classifyFailoverReason(...))
辅助函数:
coerceToFailoverError(...):把普通错误转成FailoverErrordescribeFailoverError(...):提取 message/reason/status/code
模块二 候选模型序列生成(model-fallback.ts)
resolveFallbackCandidates(...) 不是简单拼数组,做了这些事:
- 主模型入队(当前 provider/model)
- 解析
agents.defaults.model.fallbacks - 别名解析(
resolveModelRefFromString(...)) - allowlist 约束(
buildConfiguredAllowlistKeys(...)) - 去重(
seen+modelKey(...)) - 无 override 时补入 primary(防配置漂移)
还支持 fallbacksOverride:
- 传入数组(包括空数组)会替代默认 fallbacks
- 常用于特定 run 的临时策略
模块三 回退执行主循环
runWithModelFallback(...) 关键流程:
- 遍历 candidates
- 调
params.run(provider, model) - 成功立即返回
{result, provider, model, attempts} - 失败后:
shouldRethrowAbort(...)命中则直接抛(用户中断优先)coerceToFailoverError(...)失败则视为非回退错误,直接抛- 记录
attempts.push({provider, model, reason, status, code})
- 全失败抛汇总错误:
All models failed (...)
模块四 与认证轮换联动
run.ts 里在 attempt 失败后不仅能 fallback 模型,还会:
- 标记 auth profile 失败
advanceAuthProfile(...)切账号重试- 上下文溢出时优先 compaction,再考虑 fallback
重点:OpenClaw 是“账号轮换 + 会话恢复 + 模型回退”三层联动,不是单一重试。
模块五 不应该回退的场景
以下场景应立即停止:
- 用户主动 Abort(
AbortError且非 timeout) - 业务逻辑错误(无法归一化为 failover reason)
- 参数/数据结构严重错误(需要修代码,不是换模型)
最小复刻骨架
ts
async function runWithFallback(candidates: ModelRef[], run: RunFn) {
const attempts: Attempt[] = [];
let lastErr: unknown;
for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
try {
return await run(c.provider, c.model);
} catch (err) {
if (isUserAbort(err) && !isTimeout(err)) throw err;
const fe = coerceToFailoverError(err, c);
if (!fe) throw err;
attempts.push({
provider: c.provider,
model: c.model,
reason: fe.reason,
status: fe.status,
code: fe.code,
});
lastErr = fe;
}
}
throw new Error(`All models failed: ${JSON.stringify(attempts)}`, {
cause: lastErr instanceof Error ? lastErr : undefined,
});
}自检清单
- abort 是否不会误进 fallback。
- 非 failover 错误是否会直接抛出。
- attempts 是否记录了 reason/status/code。
- fallbacksOverride 传空数组时是否按预期仅尝试主模型。
- 全失败时是否返回可读的汇总错误。