OpenClaw 源码解读——入门与破局8 从“报错不切换“到“秒级自动切换“:模型路由插件在 2026.4.14 上的五次迭代实录

上一篇我们部署了模型路由插件,但验证时发现它"报错却不切换"。 从"不切换"到"真正自动切换",我们迭代了四轮,踩了三个 hook 语义陷阱, 最后在真实故障场景里验证通过:主模型欠费报错 → 秒级自动切到备用模型 → 正常回复。

这篇记录完整的调试过程------每一个"看起来合理"的设计,都被 2026.4.14 的运行时语义打脸一次。


一、开场:插件的"切换"为什么没发生

第 20 篇我们把 model-router 插件部署到了 4 个 worker(v2:before_prompt_build 失败推断 + llm_output 成功确认), 注册成功、gateway ready、一切正常。

然后真实验证来了:主模型 qwen3.6-plus 欠费,请求返回:

复制代码
HTTP 400: Access denied, please make sure your account is in good standing.
For details, see: https://help.aliyun.com/zh/model-studio/error-code#overdue-payment

期望 :自动切到备用模型。 实际:报错,不切。

日志里连一条检测记录都没有。状态文件 switchCount=0------插件像睡着了一样。 接下来四轮迭代,每一轮都暴露一个"设计时没想到"的运行时语义。


二、第一轮(v2→v3):确定性错误被当成"连续失败"

v2 的切换逻辑是:

复制代码
before_prompt_build(每次调用前):
  上次调用失败 → 计数 +1 → 达到 3 次 → 探测确认 → 切换

问题:欠费是确定性错误,不是连续失败 。400 Access denied 重试一万次也不会好, v2 却要求"连续 3 次失败"才切------用户发 1 次消息 = 1 次失败 = 计数 1,不切。

修复 v3:失败后立即探测 网关拿真实错误体,确定性错误(quota/权限/模型不存在)当场切换; 只有探测无果(超时/不可达)才走连续失败计数(应对"隐身欠费")。

代码看起来没问题了。但验证......还是不切。


三、第二轮(v3→v4):检测是"被动"的,一次消息只有一次失败

v3 的检测点还是 before_prompt_build------它在下一次 LLM 调用前检查"上次是否失败"。 而真实对话里:

复制代码
用户发 1 条消息 → run 开始 → 1 次 LLM 调用 → 失败 → run 结束
                                                    ↑ 没有"下一次调用"了!

一次消息只产生一次失败,run 就结束了。检测永远等不到"下次调用"。

修复 v4:新增 agent_end hook(run 结束触发,带 success/error 字段)------ run 失败立即处理,不等下次消息。

结果:还是不切。而且这次能看日志了------agent_end 确实运行了(running agent_end (1 handlers)), 但我们的 handler 没有任何输出,也没报错。它像被静默吞掉了。


四、第三轮(v4→v5):两个 hook 语义陷阱,逐个实锤

陷阱 1:agent_end 的 success 字段是"假"的

给 agent_end handler 加了诊断日志,真相大白:

复制代码
[model-router] agent_end 事件: success=true error=(empty)

LLM 调用失败(HTTP 400)时,agent_end 事件里 success=true、error=empty!

翻 2026.4.14 源码(attempt.ts)找到原因:

复制代码
success: !aborted && !promptError,
error: promptError ? formatErrorMessage(promptError) : undefined,

promptError 只覆盖 prompt 构建阶段 的错误;LLM 调用阶段的错误(HTTP 400)根本不设置它 → success 恒为 true、error 恒为 undefined → 我们的 handler 第一行就 return 了。

教训 :hook 事件字段的语义要看运行时源码,不能看类型定义。 类型写着 error?: string,实际只有特定错误路径才会填。

陷阱 2:llm_output 在失败时也会触发,且误清计数

日志里另一行引起了注意:running llm_output (2 handlers)------失败 run 也触发了 llm_output!

而我们的 llm_output handler 无条件执行:

复制代码
router.clearFailures(ref.id);   // 清失败计数
lastSuccessAt = Date.now();     // 标记"成功"

这意味着:失败 run 也会把失败计数清掉、把"上次成功时间"更新------ before_prompt_build 的失败检测被自己人废掉了。

修复 v5:

复制代码
// llm_output 在失败时也会触发(assistantTexts 为空)——只有真实成功才算
if (!event.assistantTexts || event.assistantTexts.length === 0) return;

五、验证实录:从 400 到正常回复,两跳完成

v5 部署后,真实故障场景验证:

复制代码
消息 1:qwen3.6-plus → HTTP 400 欠费报错(失败)
消息 2:before_prompt_build 检测到上次失败
        → 立即探测网关 → 400: Access denied → classifyError = QUOTA_EXHAUSTED
        → 🔴 切换至 MiniMax-M3
        → 本条消息用 MiniMax-M3 调用成功,正常回复 🎉

日志实锤:

复制代码
[model-router] probe agentteams-gateway/qwen3.6-plus → 400: Access denied...
[model-router] 🔴 agentteams-gateway/qwen3.6-plus 不可用(QUOTA_EXHAUSTED)→ 切换至 agentteams-gateway/MiniMax-M3
[agent/embedded] embedded run agent end: ... isError=false

状态文件:switchCount=1,unavailable 里记录了故障类型和检测时间。

链式切换也验证过(三个模型:欠费 → 不存在的模型 404 → 正常): 两次 🔴 ... 不可用 → 切换至 ...,最终兜底模型正常回复。


六、插曲:qwen3.8-max 的"挂起"坑

验证链中间本来放 qwen3.8-max(也是欠费),结果它请求挂起不返回 ------ 阿里云欠费有两种形态:qwen3.6 是快速 400,qwen3.8 是无限超时("隐身欠费")。 挂起意味着 run 要等 OpenClaw 的 30 分钟兜底超时(timeoutSeconds=1800)------ 8/19 那次 4.5 小时空转的翻版。

这就是为什么模型路由不能只认错误码:错误形态比错误类型更多。 最终测试链把 qwen3.8-max 换成不存在的模型(404 秒级失败),既验证了链式切换,又绕开了挂起坑。


七、收尾:这一轮的"教训"清单

# 教训 具体表现
1 确定性错误 ≠ 连续失败 欠费要当场切,不能等 3 次(v3)
2 "下次调用前检测"是伪主动 一次消息只有一次失败,run 结束就没有"下次"(v4)
3 hook 事件字段语义要看源码 类型写 error?,实际 LLM 失败时 success=true/error=empty(v5)
4 失败路径也可能触发"成功" hook llm_output 失败时也触发,误清计数(v5)
5 错误形态 > 错误类型 同是欠费:快速 400 / 无限挂起,处理路径完全不同
6 日志是最好的调试工具 每轮都在 handler 里加诊断日志,实锤每一个假设

最深的感悟 :插件机制给了你"挂载点",但每个挂载点在你跑的版本上是什么语义、什么时候触发、 字段怎么填,只有运行时源码说了算。设计时想的"run 结束触发、带错误信息", 实测可能是"success=true, error=empty"。把假设变成日志,把日志变成证据,才能迭代到"真正可用"。

下一篇预告:Quota Guard 与 Model Router 双插件在 worker 上的协同(熔断 + 切换的分工与联动), 以及 Manager 侧如何消费插件状态做任务编排。


本文为《OpenClaw 源码解读》系列第 21 篇 · 实战篇 配套代码:C:\Users\ThinkPad\clawforge\plugins\clawforge-model-router(v5 已验证) 部署脚本:C:\Users\ThinkPad\clawforge\scripts\deploy\deploy-model-router.sh

相关推荐
Markus实验室12 分钟前
一个任务烧了54亿token,我盯着账单看了十分钟
agent·ai编程
烬羽18 分钟前
给 RAG Agent 装上"仪表盘":LangSmith 全链路观测与量化评估实战
深度学习·openai·ai编程
用户119770794807319 分钟前
服装电商的虚拟试穿怎么落地?拆解开源 OOTDiffusion:原理流程、本地推理与商用边界
aigc
徐开泉26 分钟前
推荐判定的规则管线:2026年否定句、并列句与位置权重的处理
aigc
Behavior30 分钟前
OpenAI发布GPT-6.1 Terra:能自主操作电脑!成本砍半、速度快3倍
chatgpt·aigc·openai
LEE35 分钟前
别把 Agent 架构画成组织架构图——子代理该不该拆,用任务级 Eval 说话
前端·后端·ai编程
梦醒江南35 分钟前
RAG 证据链验收:别只测答案像不像,要证明它从哪里来
前端·agent·ai编程
郝琳43 分钟前
Claude Code 额度优化实践,缓存写入单价是读取的 40 倍,四条可落地的会话习惯
ai编程
红尘伴蝶舞1 小时前
加密拦截器让抓包失效,我只能从 4KB 的 logcat 碎片里拼回 JSON
android·kotlin·ai编程
阿零的AI实验室1 小时前
给访谈型 Skill 做一次文件级验收:三轮追加通过,编辑同步待确认
aigc