方案文档里写了熔断、写了重试、写了人工兜底,为什么故障发生时一个都没拦住?

本文记录一次真实的生产故障复盘:AgentTeams 集群 token 配额耗尽,code-fixer 空转 4.5 小时。从"PPT 承诺 vs 现实差距"出发,最终把容错能力以 OpenClaw Plugin 形态真正接入 Worker 执行链路——并附实机部署验证全过程。


一、开场:PPT 里写的,和现实发生的

我们团队在做一个多 Agent 代码审查系统(ClawForge),底座是 AgentTeams + OpenClaw。初赛 PPT 里,我们写了两条痛点:

🔄 Agent 卡死/循环/超时需要人工介入
📉 Worker 故障无感知,任务静默丢失

以及对应的"破局"能力:

Harness Engine:状态机 · 检查点 · 4层重试 · 熔断 · 循环检测 · 降级

2026-08-19 上午,这些能力一个都没生效。

真实故障是这样的:集群的 token-plan 配额耗尽,网关持续返回 insufficient_quota。但我们的 Worker 是怎么处理的?

[attempt 1/4] 调用 LLM → ❌ insufficient_quota
  ⏳ 未识别错误类型,视为可重试,等待 3000ms 后重试…
[attempt 2/4] 调用 LLM → ❌ insufficient_quota
  ⏳ 等待 3000ms 后重试…
[attempt 3/4] 调用 LLM → ❌ insufficient_quota
  ⏳ …
[attempt 4/4] 调用 LLM → ❌ insufficient_quota
❌❌ 4 次尝试全部失败,任务无进展(真实场景 = 30min × ∞ 重试 ≈ 4.5 小时空转)

9:52 token配额还有,Manager还可以正常回复

但之后,token消耗殆尽,code fixer worker 、 manager都迟迟不回复,或者仅仅只是回复之前重复的内容,整个过程4-5小时里,没有任何信息提示说token消耗完了需要充值

真实情况更糟:每次调用等满 30 分钟超时(timeoutSeconds=1800),然后 delivery-mirror 无限重试。一个 run 硬扛了 4.45 小时才被 abort。4 个 Worker + Manager 全部瘫痪,任务目录里只有 spec.md 没有 plan.md——任务静默卡死,人类完全不知情

这就是我们 PPT 里写的那句"Agent 卡死/循环/超时需要人工介入"的现场版。讽刺的是,我们承诺了要解决它,但它真实发生时,我们毫无办法。


二、差距分析:为什么"设计"没有变成"防线"

复盘下来,三重根因,每一层都对应一个"设计 vs 现实"的差距:

差距 1:容错引擎是独立代码库,没接进执行链路

我们的 Harness Engine(状态机、熔断器、循环检测器……)写了几千行 TypeScript,测试全绿。但它是独立运行的代码库——而 Worker 真正执行的是 OpenClaw 的 run-loop:

Worker 实际执行链路(OpenClaw):
Channel 消息 → agent-run-handler(9阶段) → run-loop(LLM↔Tool 闭循环) → 回复投递
                                                    ↑
                              Harness Engine 在这里吗?—— 不在。

差距本质:我们把容错能力写成了"库",而不是"运行时"。它没有挂到 (a) Worker 的 LLM 调用点、(b) Manager 的任务调度点、(c) 网关的请求入口——任何一个位置。所以故障发生时,跑的是 OpenClaw 原生逻辑:把确定性错误当普通超时,无限重试

差距 2:Token 预算 ≠ 账户配额(检测维度错位)

循环检测器里有个 checkTokenBudget,检测的是单任务上下文窗口消耗比例(currentTokens/maxTokens,比如 150K 窗口用了 80%)。

而今天的故障是账户级 1 周 token-plan 配额耗尽——两个完全不同的层面:

LoopDetector 检测:任务上下文用了多少 token(相对 contextWindow)
真实故障:       账户 1 周配额用完(相对计费周期,8/25 才重置)

代码里根本没有"账户配额"这个概念,自然无从检测。

差距 3:错误分类缺失,insufficient_quota 被当成可重试

熔断器配置了 llm-api 服务:阈值 10 次失败/60 秒窗口。但今天的错误是确定性、持续性的(配额要到固定时间才恢复):

  • 每次调用 30 分钟超时 → 60 秒窗口内永远凑不齐 10 次失败 → 熔断器永远不触发

  • 降级策略是 "Switch to fallback model" → 但配额是账户级的,fallback 模型同样被卡(实测 kimi 未购买、deepseek 不存在)

  • 重试管理器把 insufficient_quota 当普通错误 → 走 L1→L2→L3 重试链 → 全部白费

一句话总结差距:PPT 承诺了"错误分类 + 熔断 + 人工介入",但代码里没有"确定性错误"这个概念,更没有把它短路到人工的路径。


三、补差距:从复用 Harness 逻辑到 OpenClaw Plugin

复盘结论很明确:基础设施级故障要在入口拦截,不能等 Agent 自己发现。而 Worker 是 OpenClaw runtime,天然支持插件机制——那就把容错能力做成 OpenClaw Plugin,挂到每次 LLM 调用的必经之路上。

3.1 先补"错误分类":DETERMINISTIC vs TRANSIENT

核心洞察:错误要分两类,处理路径彻底分离:

TRANSIENT(瞬时):   429 / 5xx / timeout / 网络抖动  → 走重试链(现状不变)
DETERMINISTIC(确定): insufficient_quota / AccessDenied / ModelNotFound / 401
                    → 重试无意义 → 立即熔断 → 短路人工

识别规则很简单,正则匹配错误消息:

const DETERMINISTIC_PATTERNS = [
  { failureType: "QUOTA_EXHAUSTED", patterns: [
      /quota\s+has\s+been\s+exhausted/i,
      /insufficient_quota/i,
      /token[-_ ]?plan/i,
  ]},
  { failureType: "ACCESS_DENIED", patterns: [/* access denied / unpurchased / forbidden */]},
  { failureType: "MODEL_NOT_FOUND", patterns: [/* model not exist */]},
  // ...
];

还有个加分项:从错误消息里解析预计恢复时间

"Your token-plan 1-week quota has been exhausted. The quota will reset at 08-25 01:37:00 UTC."
                                                            ↑ 提取出来 → 通知人类时告诉TA

V8 有个坑:new Date("08-25 01:37:00 UTC") 会把无年份日期解析成 2001 年。解法是先匹配 year-less 格式,用 Date.UTC(当前年, ...) 构造,若已过期则自动 +1 年。

3.2 插件核心:三个 Hook 完成"探测 → 熔断 → 阻断"

Worker 的 OpenClaw 版本是 2026.4.14,这是关键约束——它没有 model_call_ended hook(那是更新版本才有的)。查了该版本的 hook 类型定义:

// /opt/openclaw/dist/plugin-sdk/src/plugins/hook-types.d.ts
"llm_input" | "llm_output" | "before_agent_reply" | "before_model_resolve"
| "before_prompt_build" | "gateway_start" | "gateway_stop" | ...

于是用 before_prompt_build每次 Agent 准备调 LLM 前都会触发)承担核心逻辑:

before_prompt_build(每次模型调用前)
  │
  ├─ 情况 1:已熔断(state=OPEN)
  │     → 注入上下文:「LLM 服务已熔断(QUOTA_EXHAUSTED),
  │        停止所有调用和重试,标记 BLOCKED,报告 Manager」
  │     → Agent 看到后不再发起 LLM 调用 → fail-fast ✅
  │
  └─ 情况 2:未熔断但有连续错误(consecutiveErrors ≥ 2)
        → 主动探测网关(发一个 max_tokens=1 的请求,10s 超时)
        → 拿到真实错误体 → 分类
        → DETERMINISTIC → 熔断 OPEN + 写状态文件 + 人工通知
        → TRANSIENT → 重置计数(网关其实是通的)

另外两个辅助 hook:

  • llm_output:观察输出统计连续错误(正常输出则重置计数)

  • gateway_start:启动时加载共享熔断状态文件(跨 Worker 同步)

熔断状态写入共享文件(quota-guard-state.json),所有 Worker 都能看到:

{
  "state": "OPEN",
  "incident": {
    "failureType": "QUOTA_EXHAUSTED",
    "errorMessage": "Your token-plan 1-week quota has been exhausted...",
    "detectedAt": "2026-08-19T02:26:00.000Z",
    "estimatedRecoveryAt": "2026-08-25T01:37:00.000Z"
  }
}

恢复路径:人工充值 → /quota-guard reset → HALF_OPEN(允许探针)→ 探针成功 → CLOSED → 任务从 checkpoint 续跑。

3.3 主动探测的必要性:为什么不能只看 hook 事件

有个设计细节值得记录:llm_output 拿到的是 sanitized 输出(assistantTexts),错误路径下根本没有错误消息。而 model_call_ended 在 2026.4.14 上不存在。

所以插件采用主动探测:连续 2 次错误后,自己发一个最小请求到网关,拿真实错误体来分类。这有个"探测放大保护":30 秒间隔 + 最多 3 次,防止故障本身被探测放大。


四、部署实录:一路踩坑,一路补差距

写完插件只是开始。真实环境部署时,连续踩了 6 个坑,每一个都是"文档没写、源码里藏着"的细节:

坑 1:WSL 的 docker CLI 连不到 Windows Docker Desktop 的容器

docker ps 在 WSL 里是空的,但端口明明在监听。查了半天发现容器跑在 Windows 侧的 Docker Desktop。解法:脚本里自动探测,不行就退到 PowerShell 包装:

DOCKER_RUN() { powershell.exe -NoProfile -Command "docker $*"; }

坑 2:hooks.allowConversationAccess 配置被拒

按文档加了 hooks.allowConversationAccess: true,热重载直接报:

[reload] config reload skipped (invalid config):
plugins.entries.clawforge-quota-guard.hooks: Unrecognized key

2026.4.14 的配置校验不接受这个 key。解法:移除——反正探测机制不依赖 llm_output 的完整访问。

坑 3:共享目录 mode=777 → 插件被安全门拦截

这是最有价值的一个坑。OpenClaw 对插件加载有安全检查(architecture-internals.md 里写了,但没看仔细):

[plugins] plugin: blocked plugin candidate: world-writable path
(/root/agentteams-fs/shared/knowledge/clawforge/plugins/clawforge-quota-guard, mode=777)

MinIO 同步出来的目录是 777,而 OpenClaw 拒绝从 world-writable 路径加载插件(防止恶意写)。解法:拷贝到 Worker 本地非共享目录 /root/clawforge-plugins/

坑 4:tar 保留了宿主 uid=1000 → suspicious ownership

blocked plugin candidate: suspicious ownership
(/root/clawforge-plugins/clawforge-quota-guard, uid=1000, expected uid=0 or root)

tar 打包时把宿主的 uid 带进去了。解法:解压时 tar --no-same-owner + chown -R root:root

坑 5:load.paths 指向父目录 → plugin not found

plugins.entries.clawforge-quota-guard: plugin not found: clawforge-quota-guard

对照内置插件就明白了:/opt/openclaw/extensions/matrixbasename(matrix)就是插件 id。所以 load.paths 要指向插件目录本身,不是父目录:

"load": { "paths": [
  "/opt/openclaw/extensions/matrix",          // 内置插件:basename=id ✅
  "/root/clawforge-plugins/clawforge-quota-guard"  // 自定义:必须指向插件目录本身 ✅
]}

坑 6:worker 的 openclaw.json 会被 MinIO 覆盖

本地改 worker 配置 → 重启 → 配置被同步回去覆盖。因为 MinIO 的 agents/<worker>/openclaw.json 才是配置源。解法:用 mc 直接改 MinIO 上的配置:

mc cp agentteams/agentteams-storage/agents/code-fixer/openclaw.json /tmp/ocfg.json
# python 注入 plugins.entries + load.paths
mc cp /tmp/ocfg.json agentteams/agentteams-storage/agents/code-fixer/openclaw.json

最终验证:4 个 Worker 全部注册成功

[plugins] [quota-guard] ClawForge Quota Guard plugin registered ✅
[gateway] ready (8 plugins: acpx, browser, clawforge-quota-guard, device-pair,
           matrix, memory-core, phone-control, talk-voice; 128.9s)

最新一次启动:0 警告、0 错误


五、收尾:这一晚的"差距"清单

#差距现实解法
1容错引擎没接进执行链路做成 OpenClaw Plugin,挂在 before_prompt_build 必经之路上
2Token 预算 ≠ 账户配额新增错误分类器,识别 insufficient_quota 等确定性错误
3确定性错误走重试链分类后短路人工,熔断 + BLOCKED + CRITICAL 通知
4文档 hook 与版本不符查 2026.4.14 的 hook 类型定义,用兼容的 hook 实现同等效果
5插件加载的安全门world-writable / suspicious ownership / load.paths 语义,逐个实测确认

最深的感悟:设计文档里的"熔断器""重试""人工兜底"这些词,离"真正挡住一次故障"之间,隔着执行链路的接入版本兼容安全策略配置源管理这一整条现实鸿沟。PPT 上写"4 层重试 + 熔断 + 降级"只需要一行字,让它真正生效需要把这些差距一个个填平。

下一篇预告:从“错误分类“到“模型路由“:多 Agent 集群的容灾进化。


本文为《OpenClaw 源码解读》系列第 23 篇 · 实战篇 配套代码:C:\Users\ThinkPad\clawforge\plugins\clawforge-quota-guard

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐