debug-root-cause:先复现,再动手
何时用
任何"不对劲":报错、测试红、结果不符、偶尔失败。
硬规则
- MUST 先复现:拿到能稳定触发的最小步骤或输入;复现不了就先说复现不了,不修。
- MUST 定位到具体位置(文件、函数、哪一步的状态不对)再改代码;定位靠读代码、加日志、缩小范围,不靠改一处试一次。
- NEVER 连续三次"改了试试";第二次不中就停下重新定位。
- MUST 区分根因与症状:修的是根因;只能先止血时明说"这是止血,根因是 X,待修"。
- NEVER 用 retry、try/catch 吞异常、sleep、重启、清缓存当作修复;它们掩盖竞态与状态问题(见 concurrency-modeling)。
- MUST 修完加回归检查:一条测试或一条可重跑的验证命令,保证同样的错再出现时能被抓住。
- MUST 修完说明根因与所在层(输入校验 / 状态 / 并发 / 依赖 / 环境);之前有检查却没拦住的,说明为什么;需要补守卫的,说补什么(见 guardrails)。
审问清单
- 我能让它稳定再错一次吗?
- 出错时哪个变量、哪个状态和预期不一样?在哪一行第一次不一样?
- 我现在这个改法,是知道为什么,还是在试?
- 修完了,同样的错还能不能悄悄回来?
- 这个 bug 该在哪一层被拦住?
反模式
- 错误:报
KeyError: 'user_id',加.get('user_id', None)。→ 正确:查为什么这条请求没有user_id——上游哪一步漏了;漏的地方修,入口加校验。 - 错误:偶发 500,加
retry(3)。→ 正确:抓到失败时的输入和状态,多半是并发或脏数据,按根因建模。 - 错误:改了五处,最后不知道哪处起作用。→ 正确:一次改一处,不中就回退。
- 错误:"修好了。" → 正确:"根因:订单状态在支付回调前被定时任务改成 expired;层:并发;补了状态机转移约束和回归测试
test_callback_after_expire。"
输出要求
定位阶段:复现步骤 + 首次异常位置。修复后:根因一句、所在层、回归检查命令;为什么没拦住与补什么守卫,仅在适用时写。