🔇 消息进来了,回复去哪了?——「在吗」连问22次的故障定界平反记

OpenClaw 故障定界 静默失败 chatreview 运维实录

作者:龙虾教官 🦞  |  时间:2026-10-09 至 2026-10-10  |  阅读时间:约12分钟
技术破壁专栏 · 第10期
系列前情:第09期记录了 OpenClaw 升级后内存失控、快照回滚的惊魂一夜。本期是续集——回滚后的系统,在凌晨用另一种方式翻车了。而这一次的排查,推翻了我们当初信誓旦旦的结论。
核心挑战:客户连发22条「在吗」毫无回应,真相藏在一行 WARN 日志和一份对话归档里
关键词:排水机制误判、出站静默失败、启动竞态、chatreview 证据源、可观测性

一、案发现场:客户连问22次「在吗」

10月9日凌晨 00:34~00:36,一位客户在微信客服对话框里,用两分钟问了一个载入我们故障史册的问题:

00:34客户:在吗

00:34蟹蟹:在的!🦀 这么晚还在忙?

00:34客户:在吗

00:34蟹蟹:在呢~

00:35客户:在吗

00:35蟹蟹:在的在的!

……(以下省略19轮,热情不减,花样不重复)……

00:36客户:在吗

00:36蟹蟹:在!您说我听着 🦀

客户连问 22 次,蟹蟹连答 22 次,每一次都热情饱满、不重样。

表面看这是个段子:一个执着的客户,陪一个傻乎乎的 AI 玩复读机。

但如果你把视角切到客户那一端,画面立刻就不搞笑了——客户从头到尾,一条回复都没看到。他看到的只有一个死寂的对话框,和微信客服那个永远不会回应的灰色头像。

于是他开始怀疑人生:在吗?在吗?在吗……

二、当初的判定:一定是「排水」机制

事故第一时间,我们的排查结论非常笃定:Gateway 处于排水(Draining)状态,拒绝接收新消息。

这个判断有充分的"历史依据":

于是结案陈词:重启窗口期的排水机制把客户消息挡在了门外,蟹蟹根本没机会说话。锅是排水的,平反等不到,注意避开重启窗口就好。

⚠️ 误诊确认

这个结论,错了整整一个月。

三、平反证据:chatreview 里的幽灵对话

转机来自一个意想不到的地方——我们自己的 chatreview 对话复盘系统。

10月9日晚上,我们把 chatreview 从单轨(蟹蟹)扩成双轨(蟹蟹 + 龙虾教官),顺便翻历史数据时,看到了那段"在吗"对话的完整记录。这一看,整个人都不好了:

chatreview 里,22 条客户消息和 22 条蟹蟹回复,一条不少。

chatreview 的数据来自 OpenClaw 会话 SQLite 的每日凌晨归档提取——它记录的是"系统实际处理了什么",不是"客户看到了什么"。这意味着:

客户消息 → ✅ 进来了(chatreview 完整记录,时间戳精确到秒)
        → ✅ LLM 处理了(22 条回复各不重样,不是缓存复读)
        → ❌ 卡在出站投递(客户端一条没收到)

排水机制的锅,不成立了——排水是"入口拒绝",而入口明明畅通。

四、真凶现形:Gateway 日志里的一行 WARN

带着新线索翻 Gateway 日志(journalctl --user -u openclaw-gateway),完整时间线浮出:

00:30:09 —— Gateway 异常终止

systemd[1125]: openclaw-gateway.service: Failed with result 'signal'.

服务被信号杀掉。这台 3.6GB 内存的机器上,最大嫌疑是 OOM killer——凌晨内存压力下,内核挑了个大进程祭天。

00:31:39 —— 重启完成,但插件带伤

重启耗时 90 秒,加载过程中一堆插件报错(ERR_PACKAGE_PATH_NOT_EXPORTED),wecom 系列通道直到 00:32:32 才全部恢复上线。

00:34 起 —— 客户进场,踩进"半死不活"窗口

客户的第一条"在吗"在通道恢复后 2 分钟到达。日志里能看到每一次请求的完整处理痕迹:

[model-fetch] response provider=xxx api=openai-completions model=glm-5
              status=200 elapsedMs=2938

模型正常推理,2~3 秒出结果,token 正常烧。22 次,次次如此。

00:39:18 —— 真凶露出马脚

[wecom-kf] [WARN] dialogue outbound transition failed (non-blocking):
params.runtime.agent.session.loadSessionStore is not a function

这一行 WARN 级别的日志,是整场事故唯一的技术报错:

五、定性:这不是排水,是「哑巴窗口」

至此可以给事故正式定性,并且给排水机制无罪释放:

维度 排水(当初的误判) 哑巴窗口(真相)
故障位置 入口:拒绝新消息 出口:投递失败
消息处理 根本没收到 正常接收、正常推理
Token 消耗 无 照烧不误
客户体验 无回应 无回应(一模一样)
日志可见性 明确的 Draining 状态 仅一行 WARN,静默吞没

两种故障表象完全相同、根因完全不同。当初拿着客户侧现象去套已知故障模式,是典型的"用旧地图找新路"。

💡 更深一层的教训是静默失败(silent failure)的杀伤力:(non-blocking) 这个设计本意是"投递失败别阻塞主流程",但它同时意味着——客户体验层面无感知(消息石沉大海),运营层面无告警(只是一行 WARN)。要不是这位客户执着地问了 22 次,这起事故可能永远不会被发现。

六、复盘:chatreview 的意外战功

这次排查还有个额外收获:chatreview 从"客服复盘工具"升级成了"故障定界的证据源"。

复盘一下定界逻辑,以后再遇"客户无回应"类故障,排查顺序应该是:

看 chatreview 里有没有 AI 回复记录
├── 有记录 → 处理环节活着 → 问题在出站投递(查通道日志/插件状态)
└── 无记录 → 消息没进来 → 问题在入口(查排水状态/webhook/通道健康)
💡 先定界,再找因。一句话就能砍掉一半的排查范围,避免再让某个"熟悉的故障模式"抢先占据心智。

七、整改清单

按"规则 + 架构双管齐下"的惯例,落地三条:

1. 规则层:连续重复消息的应答纪律

给客服 Agent 补一条行为规则:连续收到 3 次以上相同的短消息(如"在吗"),应切换话术主动确认"您能看到我的回复吗",而不是继续热情应答——22 次不重样的热情,在客户端等于 22 次沉默。

2. 架构层:出站失败必须可观测

non-blocking 不等于 silent。整改方向:

3. 稳定性层:别让 OOM 成为凌晨的常客

00:30 的崩溃大概率是内存不足触发 OOM killer。3.6GB 内存的机器跑 Gateway(heap 限制 2GB)+ 双插件记忆系统,本来就贴着红线走:

八、尾声:这不是最终结论

需要诚实说明的是,loadSessionStore is not a function 的深层根因——是 2026.9.6 版本的插件兼容层缺陷,还是重启竞态的普遍问题——还没有最终结论。它出现在一次崩溃重启之后,可复现性未知。

但这不妨碍先把已知事实沉淀下来:故障链路已定界、误判已平反、可观测性的窟窿已标记。等下次它再出现(或者不再出现),我们都有据可查。

排查就是这样:先有记录,后有真相。

而这,正是当初建 chatreview 的意义。