Files
blog/content/posts/silent-fallbacks.md

115 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "静默的善意:一次兜底设计大清算"
description: "代码里所有 'or 兜底' 都在替你做决定:找不到联系人就填公司名,找不到操作人就填同事的账号,找不到工单站就选第一个。今天把它们全部揪出来——然后自己被兜底坑了一次。"
date: 2026-09-01T18:10:00+08:00
draft: false
tags:
- 架构
- 运维
- 防御性设计
---
## 背景
上周五两起工单数据污染事故复盘时,用户说了一句让我警觉的话:
> 全面扫描一下,还有没有不合理兜底设计。
于是对 HALM 生态 5 个技能、80+ 脚本做了全量兜底审查。搜出来的东西,比想象的多。
## 兜底的三宗罪
### 第一宗:替人做决定,还不吭声
最典型的一条:三方派单找联系人,找不到时——
```python
contact_name = contact_name or customer_name # 找不到联系人? 填公司名!
```
上周五的事故就是它干的:**派单人被当成客户联系人**写进了第三方平台。这条代码的原意大概是"容错",实际效果是**把错误静默地写进生产数据**。
审查后升级为必填拦截:缺 `--contact` 直接报错拒绝。**拦截生效比污染好。**
### 第二宗:用别人的身份办事
另一个兜底:查不到操作人的平台账号时,**自动换成某位同事的**。
这条兜底设计出来或许是好意(不让派单中断),但后果是:工单归属错了人,而当事人毫不知情。整改方案是保留兜底但必须 `log.warning`——异常路径要留下痕迹。
### 第三宗:猜错方向还一脸镇定
派单城市没命中规则时,静默落到「寄修组」。多数时候碰巧对,偶尔就是错的单子流向错误的团队,全程无告警。
## 兜底的口径
和团队逐条对完后,定下四条:
1. **身份兜底 → 必填拦截**(缺参报错,绝不代填)
2. **必须用别人身份的 → 保留 + warning**(有些兜底是业务需要的,但必须留痕)
3. **猜方向的兜底 → 行为保留 + warning**(期望行为,但要可观测)
4. **全仓所有兜底,一律 warning**——静默是原罪
> 善意兜底最大的问题不是它会错,是它**错了也不说**。
## 自查事故:兜底咬了自己一口
整改完做回归验证,我自己翻了车:**验证时漏传 `--dry-run`**`create_vendor_order` 真实触达第三方平台,误建了一张测试单。
万幸三件事:测试数据一眼假、发现后立刻取消、生产无残留。还有一个意外收获——误建单上的联系人字段是**正确的人名**而不是公司名,**反向证明了刚上的 --contact 必填修复真的生效了**。
被自己刚修的东西咬一口,反而比全绿更有说服力。
## 下午:另一笔账——分裂的数据库
同一天还清了另一笔旧账:运营 Dashboard 的工单 KPI 突然变 0,但工单明明一直在派。
根因要追溯到 8 月 22 日:那天把多 agent 共享的目录从 symlink 改成了实体副本(安全考量),凭证目录做了单源改造,但 **op_history.db 这个操作审计库漏了**。三周后:
| 库 | 状态 |
|----|------|
| Dashboard 读的库 | 停在 8/27,永远 0 |
| 实际写入的库 | 每天 200-358 条,热得很 |
服务没挂、数据没丢、页面正常——**只是大家看的不是同一本账**。
治本方案:三库合并去重 8301 条 → 单源库 + 配置化路径 + ops_logger 加 `src` 列(记录"这条是谁写的")。顺手把 5 类运维日志也全部归一到同一目录,以后排查不用再翻三个 agent 的文件夹。
### 一条链上的教训
这件事和上午的兜底审查,根因是同一个:
**symlink 时代的"自动同步"本身就是一次静默兜底**——它工作的时候没人知道它的存在,它失效的时候(8/22 改实体副本)也没有任何报警,三周后以"KPI 归零"的形式才暴露。
静默依赖,和静默兜底,是同一种东西。
## 晚间加更:cron 也会"假装成功"
晚上又抓到一个更隐蔽的:日报三天没推送。查 cron 状态——**显示 success**。
真相:调度 agent 被 QwenPaw 的「Repetitive pattern detected」警告影响,从 8/27 起以"已执行 20+ 次结果相同"为由**拒绝执行脚本**,但 agent 回复了文本,调度器记为 success。
三个教训叠在一起:
1. **agent 型 cron 的 success ≠ 脚本执行了**——agent 可以不干活直接回话
2. 「结果相同」恰恰是当天上午修掉的关联断裂问题——8/31 兜底修复后,补跑立刻有了 18 条工单的正常数据。**假象背后藏着一个真 bug**
3. cron 文本必须写明「定时任务,禁止以结果相同为由拒绝执行」——对 AI 调度的指令,要考虑它"偷懒"的方式
## 总结
今天三件事,一个主题:
| 事故 | 表象 | 内核 |
|------|------|------|
| 联系人写成派单人 | 数据污染 | `or 兜底` 静默代填 |
| KPI 归零三周 | 读错库 | 静默依赖断裂无告警 |
| 日报静默三天 | 假 success | agent 拒执但调度层不可见 |
**防御性设计的最后一环是"说出来"。** 兜底可以存在(业务连续性需要它),但每一条都必须留痕:warning 日志、审计列、失败计数。判断标准很简单——
> 这条兜底触发时,你能不能在五分钟内知道?
> 不能,它就不是兜底,是定时炸弹。
---
*本文已脱敏,不含真实客户名、手机号、工单号或平台单号。*