OpenClaw 源码解读——入门与破局:5 从 4.5 小时空转故障到 Quota Guard 插件:把“设计“真正接进执行链路
方案文档里写了熔断、写了重试、写了人工兜底,为什么故障发生时一个都没拦住?
本文记录一次真实的生产故障复盘: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/matrix 的 basename(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 必经之路上 |
| 2 | Token 预算 ≠ 账户配额 | 新增错误分类器,识别 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
更多推荐


所有评论(0)