P2 ops-practical 问题日志AI工程Synapse

当自动化系统静默失败23次:为什么零日志比报错更危险

pmo-wbs-trigger 连续23次触发零消息,暴露了自动化系统中'假成功'比'真报错'更难排查的反直觉问题

当自动化系统静默失败 23 次:为什么零日志比报错更危险

上周三凌晨,我在清理 n8n 工作流日志时,发现了一个让我后背发凉的数字:pmo-wbs-trigger 这个每 30 分钟触发一次的定时任务,连续 23 次触发记录里,零错误、零告警、零业务日志。Grafana 仪表盘上它是一条平稳的绿线——因为它从来没”失败”过,它只是什么都没做。

那一刻我意识到一件事:我花了三个月搭建的监控体系,只能抓住”报错”,却抓不住”假成功”。而在自动化系统里,后者才是真正的杀手。

问题背景:一个本该工作的触发器

pmo-wbs-trigger 的职责很简单——每 30 分钟扫描 active_tasks.yaml,把阻塞已解除的任务重新派发给执行 Agent。这是我们执行链里最”不起眼”的一环,但它支撑着跨会话任务续接这个核心承诺。

它的健康检查是这样写的:HTTP 200 = 健康。n8n webhook 返回了 200,所以监控系统从不告警。可问题在于,那个 webhook 背后的处理逻辑里有一个 try/except Exception: pass——原本是前任工程师为了”防止任务中断”加的容错。结果是:YAML 解析失败、Agent 派发失败、Slack 通知失败,统统被吞掉,返回 200。

为什么静默失败比报错更难排查

我带团队复盘时画了一张矩阵:

一类故障是”真报错”:堆栈、500、超时。这类故障的信号强度高,告警链完整,平均修复时间(MTTR)通常在分钟级。另一类是”假成功”:HTTP 200、退出码 0、“任务已触发”的日志——但下游什么都没发生。这类故障没有信号,只能靠”业务结果异常”反推,MTTR 按天算。

我们这次的 23 次静默失败,是因为一个下游依赖的 Agent 签到文件被误删,导致任务派发路由找不到接收方——整个调用链在第二跳就空转,但第一跳(触发器本身)认为自己工作良好。

更反直觉的是:系统越可靠,静默失败越危险。因为团队会把”没有告警”等同于”一切正常”,然后把注意力转移到别的地方。等我们发现时,已经有 11 个 PMO 任务在 active_tasks.yaml 里腐烂了整整两天。

修复:从”监控错误”到”监控结果”

我让 harness_engineer 做了三件事。

第一,禁止裸 except。全仓库 grep except Exception: pass,37 处全部改成带结构化日志的显式处理,异常类型、上下文、trace_id 必须进日志。那个 “防止中断” 的容错逻辑,实际上是”防止你知道系统坏了”。

第二,增加语义健康检查。触发器的健康定义不再是 “HTTP 200”,而是”过去 2 小时内至少完成了 1 次有效的任务派发”。没有派发就是病,哪怕 webhook 返回 200。这个检查我们加到了 Prometheus 的 absent_over_time 规则里。

第三,心跳日志常态化。每个自动化任务每次执行,无论是否有业务动作,都必须输出一条结构化心跳:{task_id, trigger_time, scanned_items, dispatched_items, skipped_reason}dispatched_items=0 连续出现 3 次立即告警——因为”什么都没做”本身就是值得告警的状态。

三条可复用的原则

1. 健康检查必须是业务语义的,不是技术信号的。 HTTP 200、进程存活、CPU 正常——这些只能证明”服务器还在呼吸”,证明不了”服务在创造价值”。定义健康时,问自己:如果这个服务彻底摆烂但仍保持心跳,监控会抓到吗?

2. 零输出是异常,不是正常。 一个本应产生数据的任务连续多次产生 0 条数据,应该和”产生错误数据”同等级别告警。大多数团队的告警规则里只有”数值过高”,没有”数值为零”。

3. 容错不等于吞异常。 真正的容错是”失败后降级 + 显式记录 + 通知人类”,而不是假装一切都好。写 except: pass 的那一刻,你其实是在给未来的自己埋一颗延时炸弹。

回头看,这 23 次静默失败没造成灾难性损失,算是运气。但它让我重新校准了一个判断:在自动化系统里,“一切正常”是一个需要被证明的结论,不是一个默认的状态。

如果你在构建 AI 工程团队,欢迎参考我们开源的 Synapse 框架,里面包含了完整的执行链审计、静默失败检测和跨会话任务续接机制,希望能帮你少踩几个我们踩过的坑。