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=trueerror=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=1unavailable 里记录了故障类型和检测时间。

链式切换也验证过(三个模型:欠费 → 不存在的模型 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

相关推荐
Carson带你学Android2 小时前
腾讯正式上线AI应用生成平台:一句话生成 App,还能直接上架商店
android·ai编程
李姆斯10 小时前
为啥Agent在coding表现这么好,但是在别的领域就是差的不少?
前端·agent·ai编程
AI_小站11 小时前
刚面完百度的 Agent 开发岗,我才发现:世界就是个巨大的草台班子
java·开发语言·人工智能·spring·百度·langchain
芯小途202612 小时前
微短剧新规 9 月 1 日施行:AIGC 微短剧的「AI 标识 + 占比追溯」怎么在制作流水线里落地(附自查脚本)
人工智能·aigc
kaliarch12 小时前
WorkBuddy Skill:把重复 SOP 打成可调用的手册包
aigc
Dawson Zhu15 小时前
图结构(Graph)如何重构智能体认知?从DAG规划到GraphRAG的技术拆解
人工智能·语言模型·重构·架构·aigc
lancyu15 小时前
关于多轮对话机器人的上下文Token优化和解决方案
人工智能·python·深度学习·机器学习·chatgpt·机器人·prompt
奈斯先生Vector15 小时前
从 127.0.0.1:3080 到插件运行时:DeepSeek Harness 远程开发与版本治理实战
linux·运维·人工智能·ubuntu·aigc
COOLMO研究AI16 小时前
Python 如何实现 AI API 的提示词(Prompt)版本管理与热更新
人工智能·python·prompt