OpenClaw 跑了 6 周,日均处理 200 条消息。3 次严重故障导致服务中断超过 1 小时。复盘发现:没有一次是模型回答错了导致的。问题全部出在模型之外的地方。
大家都在讨论错误的东西
社区里天天在讨论 SWE-bench 涨了几个点、哪个模型 MMLU 更高、Agent 推理准确率到了多少。好像只要模型够强,Agent 就能稳定运行。
我跑了 6 周 OpenClaw 之后的体感完全不同:模型本身从来不是故障的根源。 它回答的质量可能有高有低,但从没有因为"模型太蠢"导致服务挂掉。
3 次真正的故障,全部出在基础设施层。
第一次故障:飞书 WebSocket 断连丢消息
发生了什么
第 9 天早上 8:47,飞书群里有人发了条消息,OpenClaw 没有回复。看了一下 Dashboard,显示在线。又发了一条,还是没回。
手动重启之后恢复了。但我翻日志发现,从 8:12 开始就断连了——35 分钟内的所有消息全部丢失,没有任何告警。
根因
飞书的 WebSocket 长连接每隔 3-5 天会因为网络抖动断一次。OpenClaw 有自动重连逻辑,但在这次场景里出了竞态条件:
8:12:03 WebSocket 断开
8:12:04 自动重连(第 1 次)—— 失败(网络还在抖)
8:12:07 自动重连(第 2 次)—— 成功
8:12:07 旧连接的 close 事件到达,触发了关闭逻辑
8:12:08 刚建立的新连接被旧事件关掉了
8:12:08 重连计数器归零,认为"已连接",不再重试
新连接被旧连接的关闭回调干掉了。经典的竞态问题。而且日志里显示状态是"已连接",所以健康检查也没报警。
修复
// 给每个连接分配唯一 ID,close 事件只能关闭对应 ID 的连接
let activeConnectionId = 0;
function reconnect() {
const myId = ++activeConnectionId;
const ws = new WebSocket(url);
ws.on("open", () => {
if (myId !== activeConnectionId) {
// 我已经不是最新的连接了,自行关闭
ws.close();
return;
}
currentWs = ws;
});
ws.on("close", () => {
if (myId !== activeConnectionId) {
// 旧连接的关闭事件,忽略
return;
}
// 只有最新连接断了才触发重连
setTimeout(reconnect, 3000);
});
}
同时加了一个心跳探针——每 60 秒主动发一个测试消息给自己,如果 10 秒内没收到回复就强制重连:
setInterval(async () => {
const probe = `__heartbeat_${Date.now()}`;
const received = await sendAndWaitForEcho(probe, 10_000);
if (!received) {
console.error("心跳超时,强制重连");
activeConnectionId++;
reconnect();
}
}, 60_000);
修完之后又跑了 4 周,再没出过这个问题。
第二次故障:上下文积累导致进程 OOM
发生了什么
第 22 天下午,OpenClaw 突然停止响应。SSH 上去一看,Node.js 进程被 OOM Killer 干掉了。机器内存 4GB,进程峰值吃到了 3.7GB。
根因
OpenClaw 会在内存里维护每个会话的上下文历史。我有 3 个活跃的飞书群 + 2 个 Discord 频道,每个频道都有独立的会话上下文。
问题是上下文只增不减。每次对话、每次工具调用的结果,全部追加到内存里的消息数组。跑了 22 天,某个高频群的上下文积累到了 14 万 token——光这一个会话的消息对象就占了 800MB 内存。
我之前做过上下文管理的优化(滚动窗口 + 摘要),但那个优化只作用于发送给模型的上下文。内存里的完整历史没有清理。
发送给模型的上下文: 最近 3 轮 + 摘要 ≈ 4000 token ✓
内存中的完整历史: 全量保留 ≈ 140,000 token ✗ ← 这个爆了
修复
两层清理策略:
const MAX_MEMORY_MESSAGES = 200; // 内存最多保留 200 条消息
const PERSIST_INTERVAL = 300_000; // 5 分钟持久化一次
function trimConversationMemory(conversation) {
if (conversation.messages.length > MAX_MEMORY_MESSAGES) {
// 超出的部分写入 SQLite,从内存移除
const overflow = conversation.messages.splice(
0,
conversation.messages.length - MAX_MEMORY_MESSAGES
);
db.prepare(`INSERT INTO message_archive (conv_id, messages, archived_at)
VALUES (?, ?, datetime('now'))`)
.run(conversation.id, JSON.stringify(overflow));
}
}
// 定期执行
setInterval(() => {
for (const conv of conversations.values()) {
trimConversationMemory(conv);
}
}, PERSIST_INTERVAL);
同时加了进程内存监控:
setInterval(() => {
const usage = process.memoryUsage();
const heapMB = usage.heapUsed / 1024 / 1024;
if (heapMB > 2048) {
console.error(`内存告警: ${heapMB.toFixed(0)}MB, 触发 GC`);
global.gc?.(); // 需要 --expose-gc 启动参数
}
if (heapMB > 3072) {
console.error(`内存危险: ${heapMB.toFixed(0)}MB, 主动重启`);
process.exit(1); // PM2 会自动拉起
}
}, 30_000);
反思
这个 bug 暴露了一个认知盲区:给模型的上下文和进程内存里的上下文是两个东西。 我优化了前者但忘了后者。在 Agent 长期运行的场景下,任何"只增不减"的数据结构最终都会爆。
第三次故障:模型 API 限流 + 降级链失效
发生了什么
第 31 天上午 10:15,大量消息堆积无响应。日志里全是 429 错误(Rate Limited)。
根因
那天早上公司飞书群里有个热点讨论,10 分钟内涌入了 47 条消息。每条消息触发一次模型调用,瞬间打满了 DeepSeek V4 的 API 速率限制(60 RPM)。
我配了降级链:DeepSeek V4 → Qwen3.5-Plus → GPT-4o。理论上 DeepSeek 限流了应该自动切 Qwen。
但实际情况:
10:15:01 消息 1 → DeepSeek V4 → 200 OK
10:15:02 消息 2 → DeepSeek V4 → 200 OK
...
10:15:18 消息 15 → DeepSeek V4 → 429 Rate Limited
10:15:18 降级到 Qwen3.5-Plus → 200 OK
10:15:19 消息 16 → DeepSeek V4 → 429 ← 又先试 DeepSeek?
10:15:19 降级到 Qwen3.5-Plus → 200 OK
10:15:20 消息 17 → DeepSeek V4 → 429 ← 还是先试 DeepSeek
...
每条消息都先尝试 DeepSeek,被拒后再降级。 47 条消息产生了 47 次无效请求 + 47 次降级请求 = 94 次 API 调用。而且这 47 次 429 响应进一步恶化了我在 DeepSeek 端的限流状态。
降级逻辑没有"记忆"——它不知道 DeepSeek 正在限流,每次都重新从头试。
修复:熔断器(Circuit Breaker)
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: "closed" | "open" | "half-open" = "closed";
constructor(
private threshold: number = 3, // 连续失败 3 次触发熔断
private cooldownMs: number = 60_000 // 熔断后 60 秒冷却
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.cooldownMs) {
this.state = "half-open"; // 冷却期过了,试一次
} else {
throw new Error("Circuit breaker is open");
}
}
try {
const result = await fn();
this.failures = 0;
this.state = "closed";
return result;
} catch (e) {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = "open";
}
throw e;
}
}
}
// 每个模型一个熔断器
const breakers = {
"deepseek-v4": new CircuitBreaker(3, 60_000),
"qwen3.5-plus": new CircuitBreaker(3, 60_000),
"gpt-4o": new CircuitBreaker(3, 60_000),
};
async function callWithSmartFallback(messages) {
const chain = ["deepseek-v4", "qwen3.5-plus", "gpt-4o"];
for (const model of chain) {
try {
return await breakers[model].call(() =>
client.chat.completions.create({ model, messages })
);
} catch (e) {
console.warn(`${model}: ${e.message}`);
// 熔断器 open 时直接跳到下一个,不发请求
}
}
throw new Error("所有模型都不可用");
}
熔断器的效果:DeepSeek 连续失败 3 次后直接跳过(不发请求),60 秒后试探性地发一次——如果成功就恢复,失败就继续熔断。
加了熔断器之后,同样 47 条消息的场景:
- 之前:94 次 API 调用(47 次无效 + 47 次降级)
- 之后:50 次 API 调用(3 次无效 + 47 次降级)
无效请求从 47 次降到 3 次。
三次故障的共同规律
| 故障 | 表面原因 | 深层原因 | 和模型有关吗 |
|---|---|---|---|
| WebSocket 断连 | 网络抖动 | 重连的竞态条件 + 健康检查失效 | 无关 |
| 进程 OOM | 内存不足 | 上下文历史无限积累 | 无关 |
| API 限流 | 请求过多 | 降级链没有熔断记忆 | 无关 |
三次都跟模型的"智力"没有任何关系。DeepSeek V4 的回答质量没有任何问题,Opus 4.7 的 SWE-bench 再高 10 个点也救不了这些 bug。
这就是生产环境和 demo 的区别。 demo 里你只关心"模型能不能回答对"。生产里,连接管理、内存管理、限流处理、健康检查这些"脏活"决定了你的 Agent 能不能跑得住。
我现在监控什么
经历了这三次教训之后,我加了 6 个核心监控指标:
| 指标 | 告警阈值 | 为什么重要 |
|---|---|---|
| 心跳延迟 | > 10s | 检测连接是否真的活着 |
| 进程 heap 内存 | > 2GB | OOM 的前兆 |
| 会话上下文长度 | > 500 条消息 | 内存泄漏的前兆 |
| API 429 错误率 | > 5%/分钟 | 限流即将触发 |
| 模型调用延迟 P99 | > 15s | 上游服务可能在降级 |
| 消息处理积压量 | > 20 条 | 处理能力跟不上输入速率 |
这 6 个指标跟模型好不好没关系,但它们是我 Agent 稳定性的生命线。
一个不太舒服的结论
跑了 6 周 AI Agent 之后,我对"模型能力"的执念减轻了很多。
社区里的讨论几乎全是模型层面的:哪个模型代码写得更好、哪个推理更准、benchmark 谁第一。但在真实的 7×24 运行环境里,模型是整条链路里最稳定的部分。反而是连接管理、内存管理、限流处理、错误恢复这些"看不见"的基础设施决定了 Agent 能不能活过第一周。
如果你正在跑或者打算跑生产级 AI Agent,建议:
-
先搞定基础设施,再优化模型。 WebSocket 心跳、内存上限、熔断器——这三样比选 Opus 还是 Sonnet 重要得多。
-
用 API 网关别直连。 网关层的限流处理、自动降级、健康检查比你自己在代码里写的可靠。上面第三次故障如果我一开始就用网关的熔断能力,根本不会发生。
-
监控"无聊"的指标。 内存、延迟、积压量——这些不性感,但它们会在凌晨 3 点救你的命。
模型会越来越强。但基础设施的坑,每个新模型都救不了你。
TheRouter — 多模型 API 网关,内置熔断器、自动降级和健康检查。一个 Key 调 30+ 模型,限流和故障恢复在网关层处理,不用自己写。