跳转到内容

系统化调试 (systematic-debugging)

systematic-debugging 是 Superpowers 里最“反直觉”的 Skill 之一。因为它明确抵制了一种根深蒂固的程序员习惯:

看到问题 -> 马上动手修 -> 看看能不能修好

这种模式在短期内看似效率高,但实际上:

  • 经常修错地方
  • 引入新 bug
  • 掩盖真正的问题
  • 同一个问题反复出现

systematic-debugging 的核心原则非常清晰:

在找到根本原因之前,绝对不能动手修复。修复症状就是在制造失败。

这个 Skill 把调试拆成 4 个严格阶段,每个阶段都有明确的完成标准,只有全部通过才能进入下一阶段。

graph TD
  A[Phase 1: 根本原因调查] --> B{完成调查?}
  B -->|否| A
  B -->|是| C[Phase 2: 模式分析]
  C --> D{找到模式?}
  D -->|否| A
  D -->|是| E[Phase 3: 假设与验证]
  E --> F{假设确认?}
  F -->|否| E
  F -->|是| G[Phase 4: 实现修复]
  G --> H[修复通过?]
  H -->|否| I{修复尝试>=3?}
  H -->|是| J[完成]
  I -->|否| A
  I -->|是| K[质疑架构]
  K --> L[与人类讨论]

在尝试任何修复之前,必须先完成以下步骤:

  • 不要跳过错误或警告
  • 它们通常包含确切解决方案
  • 完整阅读堆栈跟踪
  • 记录行号、文件路径、错误码
  • 能可靠地触发吗?
  • 确切步骤是什么?
  • 每次都发生吗?
  • 如果无法复现 → 收集更多数据,不要猜测
  • 什么变更可能导致这个问题?
  • Git diff、最近提交
  • 新依赖、配置变更
  • 环境差异

当系统有多个组件时(CI → build → signing,API → service → database):

  • 在提出修复之前,为每个组件边界添加诊断 instrumentation
  • 记录进入组件的数据
  • 记录离开组件的数据
  • 验证环境/配置传播
  • 检查每一层的状态

当错误出现在调用栈深处时,使用 root-cause-tracing.md 中的完整回溯技术:

  • 坏值从哪里来?
  • 什么调用了它?
  • 一直回溯直到找到源头
  • 在源头修复,而不是症状处

在修复之前先找到模式:

  • 找到代码库中类似的正常工作代码
  • 什么在正常运行而什么坏了?
  • 如果正在实现某种模式,完整阅读参考实现
  • 不要略读 —— 每行都要读
  • 在应用之前完全理解这个模式
  • 工作和坏掉的部分有什么区别?
  • 列出每一个差异,不管多小
  • 不要假设”那个不重要”
  • 这个需要什么其他组件?
  • 需要什么设置、配置、环境?
  • 它做什么假设?

科学方法:

  • 清楚地说明:“我认为 X 是根本原因,因为 Y”
  • 写下来
  • 要具体,不要模糊
  • 做最小的可能改变来测试假设
  • 一次只改一个变量
  • 不要一次修复多个东西
  • 成功了吗?→ Phase 4
  • 没成功?→ 形成新假设
  • 不要在上面再加修复
  • 说”我不理解 X”
  • 不要假装知道
  • 寻求帮助
  • 继续研究

修复根本原因,而不是症状:

  • 最简单的复现方式
  • 如果可能,自动化测试
  • 如果没有框架,一次性测试脚本
  • 修复前必须先有测试
  • 使用 test-driven-development skill 来写正确的失败测试
  • 解决已识别的根本原因
  • 一次只改一个东西
  • 没有”既然来了”的改进
  • 没有打包的重构
  • 测试现在通过了吗?
  • 其他测试坏了吗?
  • 问题真的解决了吗?
  • 停下来
  • 数一下:你试了多少次修复?
  • 如果 < 3:回到 Phase 1,用新信息重新分析
  • 如果 ≥ 3:停下来质疑架构(见下)
  • 不要在架构讨论前尝试第 4 次修复

5. 如果 3+ 次修复失败:质疑架构

Section titled “5. 如果 3+ 次修复失败:质疑架构”

表明架构问题的模式:

  • 每次修复都揭示新的共享状态/耦合/不同地方的问题
  • 修复需要”大规模重构”才能实现
  • 每次修复都在其他地方产生新症状

停下来质疑基础:

  • 这个模式从根本上是否合理?
  • 我们是不是”纯粹因为惯性而坚持”?
  • 应该在架构层面重构,而不是继续修症状?

在尝试更多修复之前与人类伙伴讨论

这不是假设失败——这是错误的架构。

何时使用

  • 测试失败
  • 生产 Bug
  • 意外行为
  • 性能问题
  • 构建失败
  • 集成问题

尤其要使用

  • 时间紧迫时(紧急情况让猜测更诱人)
  • “就一个 quick fix”看起来很明显时
  • 你已经试过多个修复时
  • 之前的修复没奏效时
  • 你不完全理解问题时

关键要点

  • 没有根本原因调查,就不能修复
  • 一次只形成一个假设
  • 最小化改动测试
  • 3+ 次失败 = 架构问题,停下来讨论

常见错误

  • “快速修复,以后再调查”
  • “先试试这个,再调查”
  • 一次改多个东西
  • 跳过测试,手工验证
  • 还没追踪数据流就提议修复
英文术语中文翻译解释
Root cause根本原因导致问题的真正源头,而不是表面症状。
Symptom fix症状修复只修复问题表现,不解决真正原因。
Hypothesis假设对问题原因的具体、可测试的理论。
Defense-in-depth纵深防御在多个层添加验证,让 bug 在结构上不可能。
Condition-based waiting条件等待用条件轮询替代任意超时时间。
Root cause tracing根本原因追踪回溯调用链找到原始触发点。
错误行为为什么是错的正确做法
”问题很简单,不需要流程”简单问题也有根本原因流程对简单 bug 同样快
”紧急情况,没时间走流程”系统化调试比猜猜改改更快匆忙保证返工
”先试试这个,不行再调查”第一次修复就定了模式从一开始就做对
”修复后再写测试”没测试的修复不持久先写测试再修复
一次改多个东西无法隔离哪个有效一次只改一个
3 次修复失败后再试第 4 次3+ 次失败 = 架构问题停下来质疑架构

这个 Skill 的设计哲学非常明确:

调试不是改代码,而是理解系统。

它把调试分成 4 个阶段,每个阶段都有明确的”完成标准”,这在其他 Skill 里很少见。这种结构化不是为了复杂化,而是为了对抗人性中的一个强烈倾向:

看到错误 -> 马上动手 -> 快速尝试 -> 不行再试

这种模式短期看似高效,但长期来看会让开发者陷入:

  • 修错地方
  • 引入新 bug
  • 同一个问题反复出现
  • 越来越怕改代码

systematic-debugging 用严格的流程来对抗这种倾向,强制你在”理解”之前不动手。

你一定见过这类情况:

  • “我就改一行,不会有问题” —— 结果引入回归
  • “先试试能不能跑通” —— 结果绕了远路
  • “应该就是这个原因” —— 结果猜错了
  • 连续修了 3 次都没搞定 —— 最后发现方向错了

systematic-debugging 的价值就是让这类”盲目调试”变成”有纪律的调查”。

错误做法:

  • 看一眼错误信息就开始改代码
  • 多改几个地方希望”碰对”

正确做法:

  • 先完整读错误信息和堆栈
  • 找到最近哪次提交可能影响了这个测试
  • 形成假设后再动手

错误做法:

  • 赶紧热修复一个版本上线

正确做法:

  • 先复现问题
  • 分析数据流找到真正触发点
  • 添加防御性验证到多个层

错误做法:

  • 每次出现就再修一次

正确做法:

  • 停下来质疑架构
  • 是否需要重构而不是继续打补丁
  • 和人类伙伴讨论是否该重构
测试运行失败,错误信息显示 "TypeError: undefined is not a function"
❌ 错误做法:直接搜 "undefined is not a function" 试图修复
✅ 正确做法:
1. 读取完整堆栈跟踪,找到报错文件行号
2. 检查调用链,确认是哪个变量为 undefined
3. 追溯这个变量的来源,为什么会是 undefined
4. 在源头修复,而不是在报错位置打补丁
线上用户反馈:提交表单后显示"服务器错误"
❌ 错误做法:随便加个 try-catch 掩盖错误
✅ 正确做法:
1. 查看服务器日志,找到具体错误
2. 复现问题,理解触发条件
3. 检查数据验证逻辑
4. 添加防御性验证(单点 + 多层)
同一个 Bug 修了 3 次还在出现
❌ 错误做法:再试一次不同的修复方法
✅ 正确做法:
1. 停下来,3 次失败 = 架构问题
2. 质疑是否走错了方向
3. 和人类伙伴讨论是否需要重构
4. 不要用打补丁的方式掩盖架构缺陷
Terminal window
# 1. 查看堆栈跟踪
npm test 2>&1 | head -50
# 2. 查看 Git 最近变更
git log --oneline -10
git diff HEAD~1
# 3. 添加调试日志(临时)
console.error('DEBUG:', { variable })
# 4. 追踪变量来源
# 在报错位置之前添加
console.trace('reached here with:', variable)
# 5. 验证修复后测试通过
npm test
# 确保输出:X tests passed, 0 failures
表现解决方案
”就一个 quick fix”以为很小,实际很大先走完整调查流程
修完就跑不验证是否真的修好运行完整测试套件
3 次修复失败还继续试第 4 次停下来质疑架构
只修表面不找根本原因用 root-cause-tracing 追溯
跳过验证手工测一下就认为行了用自动化测试验证
  • 前置using-superpowers,用于判断当前是否该进入调试流程。
  • 互补test-driven-development(创建失败测试用例)、verification-before-completion(验证修复有效)。

这份文档教你在调用栈深处定位问题。

核心思想:

  • 错误出现在深层代码,不代表根源在那里
  • 要从错误发生点往回追踪调用链
  • 找到原始触发点后,在源头修复

实用技巧:

  • 添加 console.error 日志来捕获堆栈跟踪
  • 使用 find-polluter.sh 脚本来定位哪个测试造成了污染

这份文档和 defense-in-depth 配合使用效果最好:先追踪到源头,再在每一层添加防御。


查看源文件: GitHub原始文件