ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenClaw 源码解读——入门与破局:6从“错误分类“到“模型路由“:多 Agent 集群的容灾进化

OpenClaw 源码解读——入门与破局:6从“错误分类“到“模型路由“:多 Agent 集群的容灾进化 上一篇我们记录了一场 4.5 小时空转的故障token 配额耗尽被当普通超时无限重试。 这一篇是它的续集从昨天的错误分类到今天落地的模型故障自动切换—— 目标不再是挡住故障而是故障发生时集群自己活下去。一、开场同一个故障两种处理哲学2026-08-19 的教训很痛insufficient_quota这种重试一万次也不会好的错误被当成普通超时4 个 Worker Manager 空转了 4.5 小时。当时我们做的 Quota Guard 是防守确定性错误欠费/权限/模型不存在 → 立即熔断 → 阻断 LLM 调用fail-fast → 任务 BLOCKED → CRITICAL 通知人工 → 人工确认后恢复但复盘时我们意识到一个更深的问题熔断只是停下来不是继续干活。配额耗尽时如果集群配置了多个模型A 欠费了还有 B、C为什么不让它自己切过去于是 2026-08-20 的需求来了admin 原话配置多个模型按优先级 A → B → C模型 A 欠费 → 自动切换到 B → B 欠费 → 切换到 C直到有一个可用的模型继续工作如果所有模型都欠费 → 汇总报告给 admin哪些模型欠费、什么原因、预计恢复时间。这就是本篇的主角ModelFailoverRouter模型故障自动切换。它和 Quota Guard 的分工一句话讲清QuotaGuard 单服务熔断确定性错误 → fail-fast 阻断空转 → 人工介入 FailoverRouter 多模型容灾先自动切换可用模型全挂才上报人工一个负责停一个负责绕。二、昨天的地基ErrorClassifier错误分类模型路由的心脏是错误分类——不知道错误能不能重试就无法决定切不切换。昨天2026-08-19我们从故障里提炼出 ErrorClassifier规则很简单错误分三类处理路径彻底分离。TRANSIENT瞬时: 429 / 5xx / timeout / 网络抖动 → 走重试链现状不变 DETERMINISTIC确定: insufficient_quota / AccessDenied / ModelNotFound / 401 → 重试无意义 → 立即熔断/切换 → 短路人工 UNKNOWN未知: → 保守处理走原有重试2.1 识别规则正则匹配错误消息export const DETERMINISTIC_ERROR_PATTERNS [ { failureType: QUOTA_EXHAUSTED, patterns: [ /quota\shas\sbeen\sexhausted/i, /insufficient_quota/i, /token[-_ ]?plan/i, /out\sof\squota/i, ]}, { failureType: ACCESS_DENIED, patterns: [/* access denied / unpurchased / forbidden */]}, { failureType: AUTH_INVALID, patterns: [/* invalid api key / unauthorized / 401 */]}, { failureType: MODEL_NOT_FOUND, patterns: [/* model not exist / unknown model */]}, { failureType: ACCOUNT_SUSPENDED, patterns: [/* account suspended / billing issue */]}, ];注意一个顺序细节先匹配确定性签名再匹配瞬时签名。因为429这类正则太宽泛而错误消息可能同时命中多条规则比如 rate limit ... quota ... reset——确定性优先避免把配额耗尽误判成限流可重试。2.2 恢复时间提取一个 V8 的坑阿里云的配额错误消息长这样Your token-plan 1-week quota has been exhausted. The quota will reset at 08-25 01:37:00 UTC.把08-25 01:37:00 UTC提取出来告诉人工什么时候恢复比干巴巴一句欠费了有用得多。但这里有个 V8 的经典坑new Date(08-25 01:37:00 UTC) // → 2001-08-25 01:37:00 UTC 无年份日期被解析成 2001 年解法先匹配 year-less 格式MM-DD HH:mm:ss用Date.UTC(当前年, ...)手工构造如果构造出的时间已经过去自动 1 年配额按周循环下一年才会再匹配到同一日期const yearless raw.match(/^(\d{2})-(\d{2})\s(\d{2}:\d{2}:\d{2})\s*([A-Za-z])?$/); if (yearless) { const candidate new Date(Date.UTC(now.getUTCFullYear(), month - 1, day, h, m, s)); if (candidate.getTime() now.getTime()) { candidate.setUTCFullYear(now.getUTCFullYear() 1); // 已过期 → 下一年 } return candidate; }2.3 一句话 APIclass ErrorClassifier { classify(error): { errorClass, failureType, estimatedRecoveryAt? } isRetryable(error): boolean // DETERMINISTIC 之外的都可重试 }分类器 26 个测试全绿。它是后面所有容灾逻辑的裁判。三、今天的核心ModelFailoverRouter模型故障自动切换3.1 需求与一个狡猾的真实场景需求本身很直接模型按优先级排列A 挂了切 BB 挂了切 C全挂出汇总报告。但 2026-08-20 17:59 用户补了一个关键信息直接改变设计阿里云 token-plan 欠费后控制台模型状态仍显示生效中按周结算到点才转欠费但请求已全部超时request timed out。也就是说欠费可能表现为持续超时而不是 insufficient_quota如果只按错误类型切换这类隐身欠费根本触发不了切换——错误是request timed out被分类成 TRANSIENT走重试链又回到 4.5 小时空转的老路。于是有了 v2 设计连续瞬时错误熔断CONSECUTIVE_TIMEOUT同一模型连续 N 次瞬时错误默认 3 次可配 timeoutSwitchThreshold → 判定疑似不可用 → 标记 CONSECUTIVE_TIMEOUT → 自动切换 → 避免把已欠费的模型当网络抖动反复重试3.2 核心逻辑executeWithFailoverasync executeWithFailoverT( call: (modelId: string) PromiseT, onSwitch?: (from, to, reason) void, ): PromiseFailoverResultT { const candidates this.getAvailableModels(); // 按优先级跳过已标记不可用的 ​ for (const model of candidates) { try { const result await this.withAttemptTimeout(model.id, call); // 单次超时包裹 this.transientFailures.delete(model.id); // 成功 → 清零连续失败计数 return { ok: true, result, modelId: model.id, switched: attempts.length 1, attempts }; } catch (err) { const classification this.classifier.classify(err); ​ // A. 确定性模型/账号级故障 → 立即切换欠费/权限/模型不存在 if (this.shouldSwitch(classification)) { this.markUnavailable(model, classification, message); continue; // 切下一个 } ​ // B. 瞬时错误 → 连续计数达到阈值判定疑似不可用并切换 if (classification.errorClass ErrorClass.TRANSIENT) { const count (this.transientFailures.get(model.id) ?? 0) 1; if (count this.timeoutSwitchThreshold) { this.markUnavailable(model, {...classification, failureType: CONSECUTIVE_TIMEOUT×${count}}, ...); continue; } } ​ // 其他情况瞬时未达阈值 / 未知→ 不切换原样抛出上层重试 throw err; } } ​ // 全部不可用 → 汇总报告 return { ok: false, attempts, allUnavailableReport: this.buildAllUnavailableReport() }; }两个关键设计点① 只有换模型就能解决的确定性错误才切换const SWITCHABLE_FAILURE_TYPES new Set([ QUOTA_EXHAUSTED, ACCESS_DENIED, AUTH_INVALID, MODEL_NOT_FOUND, ACCOUNT_SUSPENDED, ]);注意不是所有 DETERMINISTIC 错误都切换。比如某个错误是任务参数非法也是确定性换了模型照样失败——切了白切。shouldSwitch()同时检查errorClass DETERMINISTIC和failureType ∈ SWITCHABLE_FAILURE_TYPES。② 单次尝试超时attemptTimeoutMs默认 5 分钟OpenClaw run 级兜底超时是 30 分钟timeoutSeconds1800。如果放任不管一个坏模型能让用户等半小时。所以每次调用包一层Promise.race超时private async withAttemptTimeout(modelId, call) { return await Promise.race([ call(modelId), new Promise((_, reject) setTimeout(() reject( new Error([ModelFailoverRouter] ${modelId} attempt timed out after ${this.attemptTimeoutMs}ms) ), this.attemptTimeoutMs)), ]); }超时按 1 次瞬时失败计数 → 3 次后切换 →最多 15 分钟出结果而不是 30 分钟 × 无限重试。超时用的 timer 记得unref()防止 timer 阻止进程退出。3.3 全挂时的汇总报告给 admin 看的东西[ClawForge ModelFailoverRouter] 所有模型均不可用已暂停 LLM 调用请人工处理 ❌ MiniMax-M3 (minimax) — QUOTA_EXHAUSTED — Your token-plan 1-week quota has been exhausted... — 预计恢复: 2026-08-25T01:37:00.000Z ❌ qwen3.8-max (alibaba) — CONSECUTIVE_TIMEOUT×3 — request timed out 连续 3 次瞬时错误疑似欠费/不可用— 预计恢复: 未知需人工确认 处理建议充值 / 更换 API Key / 在配置中加入新的可用模型然后调用 markAvailable() 或 probeRecovery() 恢复。每个模型一行哪个模型、什么故障类型、原始错误、预计恢复时间。恢复时间来自 2.2 的提取逻辑——欠费类错误直接告诉 admin8/25 01:37 才恢复比请处理有信息量得多。3.4 恢复机制手动 自动探测// 手动恢复充值完成后 router.markAvailable(MiniMax-M3); ​ // 自动探测恢复未到预计恢复时间的跳过探测成功的自动恢复 const recovered await router.probeRecovery(async (modelId) { await callLLM(modelId, { maxTokens: 1 }); // 轻量探测1 个 token });probeRecovery有个细节如果错误消息里带预计恢复时间且还没到直接跳过探测——别拿探测请求去骚扰一个注定失败的模型。3.5 测试15 个用例覆盖全部分支✅ 主模型成功 → 不切换 ✅ 主模型确定性失败 → 切备用模型 ✅ 主模型连续 3 次超时 → CONSECUTIVE_TIMEOUT 切换 ✅ 瞬时错误未达阈值 → 原样抛出不切换 ✅ 全部不可用 → allUnavailableReport 含每个模型的故障明细 ✅ markAvailable / probeRecovery 恢复路径 ✅ attemptTimeoutMs 超时按瞬时失败计数 ✅ 空模型列表 → 构造抛错 ...本次改动 9 个新测试全量211 个测试全绿git 提交 d2036dc / 52bf725 / fede7ee / 90ad2c9。四、落地把路由接进真实的 AgentTeams代码全绿只是第一步。把模型路由接到 AgentTeams 集群又踩了三个配置真相源的坑——每一个都是改完重启就被打回原形。坑 1Manager 反复回退真凶是 Controller 的 CR改 Manager 模型 → 重启 → 回退成旧模型。改文件、改 MinIO、重建容器全挡不住。排查到最后发现Controller 的 Manager CRSQLite里存着旧模型容器/gateway 重启时 Controller 重新生成旧配置推 MinIO。文件/MinIO 都是下游产物CR 才是真相源。# 正解直接改 CR agt update manager --name default --model MiniMax-M3改完 Controller 自动重新生成配置 → 推送 MinIO → Manager 重启生效。坑 2worker 的 file-sync 是 local-firstagt update worker不重建容器而 worker 的 file-sync 合并策略是local-first本地 primary 配置不会被 MinIO 覆盖。所以改完 MinIO 上 worker 的配置本地还是旧的。解法agt update worker之外还要手动改 worker 本地 openclaw.json 的 primary 模型。坑 3Manager 有 5 分钟 fallback pullManager 有个start-mc-mirror.sh兜底任务每 5 分钟把 MinIO 的旧配置拉回本地覆盖所以必须三步齐改缺一步都会被 5 分钟后的 fallback 覆盖回去# 1. 本地配置 # 2. MinIOmc cp # 3. agentteams-manager.env 的 AGENTTEAMS_DEFAULT_MODEL这套操作最终沉淀成了knowledge/model-switch-guide-2026-08-20.md操作手册。五、环境治理版本不一致让分析-修复闭环失效模型路由落地后我们回头处理了一个一直存在的隐患AgentTeams 环境里 OpenClaw 版本不一致。翻 worker 的会话记录实锤了问题机制任务 spec 引用 /host-share/openclaw-main/...2026.7.2 源码分支 test-pr-workflow ↓ 但 worker 容器没挂 /host-share只有 manager 挂了 ↓ worker 退回分析 /opt/openclaw2026.4.14 ↓ issue 描述的符号currentTurnFence / commitTurn / transcriptRecorder 在 2026.4.14 里 0 匹配 ↓ worker 只能写推测 issue 在描述一个尚未实现的协议——分析跑偏当时的版本矩阵位置版本说明AgentTeams 容器/opt/openclaw2026.4.14worker 实际分析/运行的环境宿主 npm 全局包2026.4.29我自己的运行环境宿主源码 openclaw-main2026.7.2研究用源码最小改动方案用户拍板把 Windows 侧/host-share/openclaw-main/...的源码2026.7.2且不完整——只有 apps/ 结构、36M替换成与容器一致的2026.4.14。这样 spec 引用的源码 worker 实际分析的源码闭环恢复。从容器直接导出docker exec tar排除 node_modules/dist198M/18996 文件关键文件 md5 与容器逐一比对一致。中间还踩了 Windows 侧 root 属主权限的坑PowerShellicacls /grant qinyi:(OI)(CI)F /T修复和 NTFS 大小写不敏感的文件冲突坑。六、收尾这一轮的进化清单层2026-08-19上一篇2026-08-20本篇错误认知确定性错误要短路人工确定性错误要自动切换全挂才人工故障形态只看insufficient_quota连续超时也可能是欠费隐身形态恢复路径人工 reset → 探针 → 恢复markAvailable / probeRecovery 自动恢复配置管理改文件/MinIO认 CR 为真相源三步齐改源码一致性—环境版本对齐分析-修复闭环最深的感悟容灾设计里最难的往往不是识别故障而是故障的形态比你想的多。昨天的教训是确定性错误被当瞬时错误重试今天的新教训是瞬时错误的表象下可能藏着确定性故障欠费 → 超时。所以模型路由不能只信错误分类还要有连续失败阈值这种行为级兜底——不看错误说什么看它连续失败多少次。下一篇预告模型路由与 Quota Guard 如何作为 OpenClaw Plugin 同时挂进 Worker 执行链路以及 Manager 侧任务编排BLOCKED 状态转换 人工通知投递的联动设计。本文为《OpenClaw 源码解读》系列第 24 篇 · 实战篇配套代码C:\Users\ThinkPad\clawforge\src\harness\error-classifier.ts、model-failover-router.ts上一篇从 4.5 小时空转故障到 Quota Guard 插件
返回列表