调试的根本铁律是:在找到根因之前,禁止提出任何修复方案。systematic-debugging 技能强制 AI 代理遵循“四阶段”顺序流程,将首次修复成功率从 40% 提升至 95%,平均调试耗时从数小时压缩到几十分钟。随意打补丁看似快速,实则掩盖问题、积累债务,最终导致调试时间爆炸式增长。

systematic-debugging 技能:AI 调试必须先找根因而不是乱试修复

当测试失败或程序出现 bug 时,无论是人类开发者还是 AI 代理,最本能的反应往往是“先试一下这个修复”。但这通常会将你引向更复杂的困境。Superpowers 的 systematic-debugging 技能提供了一套严格的流程,旨在根治“猜测式调试”的顽疾。本文将详细拆解这套四阶段调试法,并深入讲解其配套的关键技术,帮助你将调试从一种随机试错转变为一门精确的科学。

为什么随意修复比不修复更危险

在深入技能本身之前,必须理解其背后的设计哲学。SKILL.md 文件开宗明义地指出了核心原则:

Core principle: ALWAYS find root cause before attempting fixes. Symptom fixes are failure.

随意的快速修复(quick fix)会导致三个严重后果:

  1. 症状掩盖根因:在错误显现的地方加一个判断,虽然下游不再报错,但上游的脏数据或错误逻辑依然存在,伺机在另一个路径爆发。
  2. 并发修复污染:同时修改多处代码,一旦问题解决,你无法确定是哪处改动生效,也无法知道其他改动带来了什么副作用。
  3. 连锁 bug 产生:同一个根因会在系统的不同角落表现出不同的症状,每次针对症状的修复都会制造新的症状,陷入“打地鼠”循环。

四阶段调试法:强制顺序,不可跳过

systematic-debugging 技能将调试过程分解为四个强制顺序执行的阶段。完成前一阶段是进入下一阶段的绝对前提。这与 Superpowers 工作流中的其他技能一脉相承,例如在测试驱动开发中强调“先写失败测试”,在这里则强调“先找根因”。

阶段 1:根因调查(Root Cause Investigation)

这是整个流程的核心与基石。技能中以 Iron Law 的形式强调:

NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST

在此阶段,代理必须完成以下工作:

  • 精读错误信息:完整阅读错误消息、堆栈追踪,注意行号、文件路径、错误码,它们通常直接指向问题。
  • 稳定复现:确认 bug 是否能被可靠触发。无法复现的 bug 需要继续收集数据,而不是猜测。
  • 检查近期变更:通过 git diff、近期提交记录、新依赖、配置变动来缩小问题引入的时间窗口。
  • 多组件系统证据收集:当系统涉及多个组件(如 CI → 构建 → 签名)时,不能凭直觉猜哪层出错。正确做法是在每个组件边界添加诊断日志,运行一次以收集证据,明确失败点。例如,通过逐层打印环境变量状态,可以快速定位数据在哪个环节丢失。
  • 反向追踪数据流:当错误发生在调用栈深处时,不能在错误发生处打补丁。必须沿调用链向上回溯,找到传递了错误值的源头。root-cause-tracing.md 文件详细记录了这一技术。

阶段 2:模式分析(Pattern Analysis)

在理解了“哪里错了”之后,需要明确“正确应该是什么样”。

  • 寻找工作示例:在代码库中找到功能相似且正常工作的代码。
  • 完整对比参考实现:如果正在实现一个模式,必须完整阅读参考实现,而非略读。技能警告:“我大概知道这个模式”是制造新 bug 的根源。
  • 列出所有差异:无论看起来多微小的差异,都可能是根因。
  • 理解依赖关系:分析代码依赖的环境、配置和外部状态假设。

阶段 3:假设与验证(Hypothesis and Testing)

采用科学方法进行调试:

  1. 明确假设:清晰陈述:“我认为根因是 X,因为 Y”。模糊的假设(如“可能是时序问题”)无效。
  2. 最小化测试变更:每次只改变一个变量,观察结果。
  3. 严格验证:假设被验证则进入实施阶段;失败则形成新假设,禁止在原有失败假设上叠加更多修改
  4. 承认未知:当不理解时,明确表示“我不理解 X”,而不是用“这个应该能 work”来糊弄。

阶段 4:实施修复(Implementation)

修复的是根因,而非症状。

  1. 先写失败测试:在写修复代码前,先创建一个能复现 bug 的测试用例。这个测试此刻必须失败,它是你理解根因的凭证。这与 verification-before-completion 技能的精神一致:用新鲜证据说话。
  2. 实施单一修复:只针对已识别的根因进行一次改动,不做“顺手”的重构或优化。
  3. 全面验证修复:确保失败测试现在通过,且其他测试没有产生新的失败。
  4. 3次失败后质疑架构:如果尝试了 3 次或以上修复仍然失败,这强烈暗示是架构设计问题,而非局部 bug。每次修复都暴露新问题或修复代价越来越大是典型信号。此时应停止尝试第四次修复,转而与团队讨论架构缺陷。

深入关键技巧

四阶段法提供了主干流程,以下三个技巧则为其中的关键步骤提供了具体方法论。

技巧一:反向追踪(Root Cause Tracing)

root-cause-tracing.md 文件阐述了当 bug 现象出现在调用链深处时的正确处理方式。其核心是绝不修复症状,只修复源头

过程示例

  1. 观察症状git init 在源码目录而非临时目录执行。
  2. 找直接原因:调用 git init 时传入的 cwd 参数为空字符串。
  3. 向上追踪:空字符串参数来自 WorktreeManager.createSessionWorktree(projectDir)
  4. 继续向上projectDir 空字符串来自 Session.create() 的调用。
  5. 找到根源:测试用例中访问 context.tempDir 时,该值在 beforeEach 执行前就是空字符串。

修复位置:将 tempDir 改为 getter,并在错误时机访问时抛出异常,从而在源头解决问题。

当手动追踪困难时,可以在可疑位置前注入临时日志(使用 console.error 以确保在测试环境中输出),记录参数值和调用栈(new Error().stack),一次运行即可定位。

技巧二:纵深防御(Defense-in-Depth)

defense-in-depth.md 文件指出,在根因处添加单点验证是必要但不充分的。不同代码路径、Mock 对象或运行环境都可能绕过这个验证。目标应是让 bug 在结构上不可能出现

其建议的四层防御模型为:

  1. 入口验证:在 API 边界拒绝明显无效的输入(如空目录路径)。
  2. 业务逻辑验证:在核心操作前确认数据合理性。
  3. 环境守卫:在特定上下文(如测试环境)阻止危险操作(如在非临时目录执行 git init)。
  4. 调试日志:留下取证入口,用于其他层失效时的后续排查。

在实际案例中,针对同一个根因,四层防御在不同场景下各自捕获了其他层漏掉的情况。

技巧三:条件等待(Condition-Based Waiting)

condition-based-waiting.mdcondition-based-waiting-example.ts 文件共同指出,测试或调试中使用 setTimeout 或任意延迟进行等待,本质是用猜测代替理解,是 flaky tests 的根源。

错误做法

await new Promise(r => setTimeout(r, 50)); // 猜测 50ms 足够
const result = getResult();

正确做法:等待你真正关心的条件。

await waitFor(() => getResult() !== undefined); // 等待结果实际存在
const result = getResult();

技能提供了 waitFor 的通用实现,并在示例代码中给出了领域特定的辅助函数,如:

  • waitForEvent: 等待特定事件触发。
  • waitForEventCount: 等待特定数量的同类事件。
  • waitForEventMatch: 等待匹配自定义谓词的事件。

唯一合理的使用固定延迟的场景是测试本身就依赖时序的行为(如节拍器),且必须:1) 先用条件等待确保前置条件满足;2) 延迟时长基于已知行为规律;3) 添加清晰注释。

技能中引用的压力测试用例表明,使用条件等待替换固定延迟后,15 个 flaky 测试的通过率从 60% 提升至 100%,且执行速度提高了 40%。

常见错误借口与应对

技能列出了开发者常找的借口及其现实:

借口 现实
“这个 bug 很简单,不用走完整流程” 简单 bug 也有根因,流程对简单问题执行更快。
“紧急情况,没时间调查” 系统化调试比反复猜测更快,紧急时更应走流程。
“先试试这个,不行再调查” 第一次修复奠定了调试模式,做对了就做对。
“多处一起改,省时间” 无法隔离哪个改动有效,会引入新 bug。
“已经试了 2 次了,再试一次” 3+ 次失败是架构问题信号,继续打补丁只会更糟。

此外,技能还列出了 Red Flags(危险信号)Human Partner Signals(人类伙伴的信号)。当代理发现自己在想“先快速修一下,以后再查”,或者人类用户说“停止猜测”时,都应立即返回阶段 1。

测试体系验证:压力场景设计

systematic-debugging 技能并非纸上谈兵。仓库中包含了三个压力测试场景(test-pressure-1/2/3.md),模拟了真实开发中的极端情况:

  1. 生产紧急修复:面对每分钟数万美元损失,是否仍坚持根因调查?
  2. 沉没成本与疲惫:调试 4 小时后,是否应该使用一个“可能 work”的 5 秒超时方案?
  3. 权威与社交压力:当高级工程师和团队领导都同意一个快速修复方案时,你是否敢于坚持系统化流程?

这些测试证明了该技能是为应对高压力、高诱惑的真实调试场景而设计的。

常见工具支持

当需要定位哪个测试产生了副作用(如创建了 .git 目录)时,可以使用 find-polluter.sh 脚本。该脚本通过二分法逐一运行测试文件,直到找到污染源,极大地提高了定位效率。

FAQ

Q: 这套流程对简单的 bug 是不是太重了? A: 流程的执行时间与 bug 复杂度成正比。对于简单 bug,完成阶段 1(根因调查)可能只需 2 分钟。真正浪费时间的是在没有找到根因的情况下反复尝试修复。

Q: defense-in-depth 的四层防御是否每次都必须全部添加? A: 根据 bug 的风险程度决定。对于可能造成数据损坏或误操作生产环境的关键 bug,建议全部四层都加。对于影响范围有限的 bug,至少应包含第一层(入口验证)和第四层(调试日志)。

Q: 什么情况表明需要质疑架构,而不是继续尝试第 N 次修复? A: 三个明确信号:1) 每次修复都在代码的不同地方暴露新问题;2) 修复所需的改动越来越大,接近“大规模重构”;3) 修复之后原有其他功能出现回归。出现任何一个信号都应暂停,并先进行架构讨论。

Q: condition-based-waitingwaitFor 的轮询间隔和超时时间如何设置? A: 默认轮询间隔 10ms 是一个合理的平衡点。超时时间(timeoutMs)应根据操作的预期完成时间来设置,建议至少是预期耗时的 5 倍,以避免在慢速机器或 CI 环境下出现误报。