乐于分享
好东西不私藏

用Superpowers自动调试:让AI自己找bug、自己修、自己验证

用Superpowers自动调试:让AI自己找bug、自己修、自己验证

systematic-debugging逼AI先找根因再修bug,verification-before-completion要求AI跑完命令才能说"搞定了"。两个Skill配合,从"猜-改-猜-改"变成"定位-修复-验证"三步闭环

写在前面

你用AI修bug,大概率是这样的:

报了个错,把错误信息贴给AI,AI说"试试改这个",改了,还是报错。再贴,AI说"那试试改那个",改了,新错误出现了。来回几轮,bug没修好,倒是多出来3个新bug。

这不是AI能力不行,是AI缺少调试纪律。

没有纪律的调试就是猜。猜一次两次可能蒙对,但猜的效率远低于系统排查——根据实际使用经验:系统调试15-30分钟搞定的问题,猜-改-猜-改要2-3小时;首次修复率从40%提升到95%;系统调试引入新bug的概率接近零,猜-改-猜-改引入新bug是常事。

这篇文章讲Superpowers的调试体系:systematic-debugging(系统调试)和verification-before-completion(完成前验证)怎么配合,把"AI猜bug"变成"AI定位bug→修bug→验证修复"的闭环。我会用两个真实场景走一遍完整流程,每一步的输入输出都写出来。

一、为什么AI修bug容易越修越烂

AI修bug有3个天生的坏习惯:

1. 猜着改,不找根因

你贴了一行报错,AI直接给方案。它没有先读完整的错误信息、没有复现、没有追踪数据流,就是看到报错关键词就给修改建议。

结果:修了症状,根因还在。换个场景,同一个bug换个方式冒出来。

2. 一次改多个地方

AI觉得"既然在改,顺便把这几个相关的也优化一下"。一次改3个地方,有一个改对了,另外两个引入了新bug。但你不知道哪个改对了、哪个改错了,因为改了太多。

结果:旧bug没修好,新bug来了。

3. 没验证就宣布修好了

AI改完代码,说"应该没问题了"。"应该"两个字是关键——它没跑测试,没确认报错消失,只是觉得代码逻辑上没问题。

结果:你部署上去,发现bug还在。

Superpowers的systematic-debugging和verification-before-completion两个Skill就是针对这3个坏习惯的。三条铁律:

1
没找到根因之前,不允许提修复方案
2
一次只改一个地方
3
没跑过命令,不能说"搞定了"

二、systematic-debugging:4个阶段,不能跳步

systematic-debugging把调试拆成4个阶段,每个阶段必须完成后才能进入下一个:

阶段
做什么
成功标准
阶段1:根因调查
读错误、复现、查变更、多组件收集证据、追踪数据流
理解了"什么坏了"和"为什么坏了"
阶段2:模式分析
找正常代码、对比参考实现、对比差异、理解依赖
找到正常和异常的差异
阶段3:假设验证
提一个假设、做最小改动验证、确认后进阶段4或推翻重来
假设被确认或推翻
阶段4:实施修复
写失败测试、修根因、验证通过;修复失败则重新分析或质疑架构
bug修复、测试通过

注意阶段3和阶段4的区别:阶段3是验证假设(改了看效果),阶段4是正式修复(写测试、改代码、跑验证)。

阶段3验证后有两条路:

假设确认 → 进入阶段4正式修复
假设推翻 → 形成新假设,回到阶段3重新验证(不是在失败的修改上叠加更多修改)

还有一条原则很多人忽略:不知道就承认。Skill原文明确说"Say 'I don't understand X'"——不确定的时候,去问人、去研究,不要假装知道然后瞎猜。

什么时候最该用systematic-debugging?

不是闲的时候,是最急的时候。Skill原文写得很明确:"Use this ESPECIALLY when under time pressure."(在时间压力下尤其要使用这个Skill。)

紧急的时候,猜的诱惑最大。但猜的代价也最大——猜一次5分钟,猜错一次5分钟加回滚10分钟,3次猜错就1小时了。系统排查15-30分钟搞定,省下来的时间远超"先试试"省下的时间。

三、调试流程速查

在进入实战之前,先给一个完整的流程概览,方便后续对照:

发现bug时:

1
完整读错误信息,不要跳过
2
稳定复现,记录步骤
3
检查最近变更,看改了什么
4
多组件系统:在每个组件边界加诊断日志,跑一次收集证据
5
追踪数据流,从报错位置向上追到源头
6
找正常工作的类似代码,对比差异
7
如果在实现某个已知模式,完整阅读参考实现(不要略读)
8
理解依赖:组件依赖什么其他组件、配置、环境变量
9
提一个具体假设,做最小改动验证
10
假设确认后,写失败测试
11
修根因,跑测试
12
跑全量测试,确认没有回归

声称修好时:

1
什么命令能证明?
2
跑完整命令
3
读完整输出
4
输出确认了声明?如果没有,用证据说明实际状态;如果确认了,声明要附带证据
5
确认了,才能说"修好了"

修复失败时:

1
数一下试了几次
2
少于3次:回到阶段1,用新信息重新分析
3
3次或以上:停下来质疑架构——每次修复都在不同位置暴露新问题、修复需要大规模重构、每次修复在其他地方产生新症状,这些都是架构问题的信号,和人讨论后再决定

找不到根因时:

系统性调查后确实可能遇到环境因素、时序依赖或外部原因导致的bug。此时应该:记录调查过程、实现防御性处理(如重试机制、超时兜底、友好的错误提示)、加监控日志以备后续排查。但要注意:绝大多数"找不到根因"其实是调查不充分。

四、实战1:报错位置和根因很近

场景:React + TypeScript项目,用户反馈"提交表单后页面白屏"。

阶段1:根因调查

1.1 完整读错误信息

AI的第一反应不是给方案,而是先读错误:

TypeErrorCannot read properties ofundefined (reading 'map')  at OrderList (src/components/OrderList.tsx:23:18)  at renderWithHooks (react-dom.js:16175:18)  at mountIndeterminateComponent (react-dom.js:20919:18)

错误在 OrderList.tsx 第23行,对 undefined 调用了 .map()。这说明某个数组变量是 undefined,不是空数组。

1.2 稳定复现

AI尝试复现:

1
打开订单列表页 — 正常显示
2
提交新订单 — 页面白屏
3
刷新页面 — 订单列表恢复正常

复现条件确认:只有提交新订单后才会白屏,刷新后恢复。

1.3 检查最近变更

git diff HEAD~3 -- src/components/OrderList.tsx

发现最近一次提交把订单接口的返回类型从 Order[] 改成了 { orders: Order[]; total: number },但 OrderList 组件还在直接对整个响应做 .map()

1.4 多组件系统收集证据

这个bug涉及前端和BFF两层。AI在组件边界加诊断日志:

// 在API调用处console.error('DEBUG fetchOrders:', { url, params, response: data });// 在Hook处console.error('DEBUG useOrders:', { rawData: data, extractedOrders: data?.orders });// 在组件处console.error('DEBUG OrderList:', { orders, typetypeof orders, isArrayArray.isArray(orders) });

跑一次,收集到的证据:

DEBUG fetchOrders: { url:'/api/orders'params: {page:1}, response: {orders: [...], total:42} }DEBUG useOrders: { rawData: {orders: [...], total:42}, extractedOrders:undefined }DEBUG OrderList: { orders: {orders: [...], total:42}, type:'object'isArray:false }

证据显示:API返回格式正确,但useOrders没有从返回值中提取 .orders,直接把整个对象传给了组件。

1.5 追踪数据流

AI追踪数据从接口到组件的完整路径:

1
api/orders.ts 的 fetchOrders() 返回 { orders: Order[], total: number } ✓
2
hooks/useOrders.ts 的 useOrders() 把返回值直接存到 state ✗(应该提取 .orders
3
OrderList.tsx 从 useOrders() 拿到 state,直接对 state 做 .map() ✗

问题在第2步和第3步:Hook没有提取 .orders,组件对整个对象做 .map()

根因找到了:接口返回格式变了,Hook和组件没有同步更新。

到这一步,AI还没有改任何代码。

阶段2:模式分析

2.1 找正常工作的类似代码

AI搜索项目中其他使用了 fetchOrders 的地方:

grep -rn "fetchOrders" src/

发现 Dashboard.tsx 也调用了 fetchOrders,但它在渲染时用了 data.orders.map(),没有白屏。

2.2 对比参考实现

AI完整阅读了 Dashboard.tsx 中调用 fetchOrders 的部分,确认它正确地从返回值中提取了 .orders。这个参考实现就是"正常工作的代码"。

2.3 对比差异

维度
Dashboard.tsx
OrderList.tsx
调用方式
const { data } = useOrders()const orders = useOrders()
渲染方式
data.orders.map()orders.map()
是否白屏
不白屏
白屏

差异确认:Dashboard 正确地从返回值中取了 .ordersOrderList 没有。

2.4 理解依赖

AI检查了 useOrders Hook的依赖:它依赖 fetchOrders 的返回格式。当 fetchOrders 的返回格式变更时,所有调用方都需要同步更新。Dashboard 更新了,OrderList 漏了。

阶段3:假设验证

AI提出假设:"OrderList组件需要从useOrders返回值中取 .orders 属性"。

做最小改动验证:

// 修改前const orders = useOrders();// 最小改动const { orders } = useOrders();

只改了这一行,其他都不动。提交表单测试:白屏消失,订单正常显示。

假设确认。(这个case比较简单,阶段3的验证改动和阶段4的正式修复恰好一样。在复杂场景中,阶段3可能是临时加一行日志或hardcode一个值来验证假设,确认后阶段4才写正式的修复代码和测试。)

阶段4:实施修复

systematic-debugging要求先写失败测试,再修代码。

4.1 写失败测试

test('fetchOrders返回新格式时,OrderList正确渲染订单列表'() => {// 模拟新格式的API返回  mockFetchOrders.mockResolvedValue({orders: [{ id1name'测试订单' }],total1,  });render(<OrderList />);// 验证组件能正确从 { orders, total } 中提取 orders 并渲染expect(screen.getByText('测试订单')).toBeInTheDocument();});

运行测试:失败。因为组件还在对整个返回值做 .map()

4.2 修复根因

// 修改前const orders = useOrders();// 修复后const { orders } = useOrders();

运行测试:通过。

4.3 验证没有其他测试被破坏

npm test

全部通过。

到这里,这个bug修好了。但这个例子太简单了——看报错信息基本就能猜到根因。下面看一个报错位置和根因远离的场景,这才是systematic-debugging真正发挥价值的地方。

五、实战2:报错位置和根因隔了4层

场景:一个Node.js工具链项目,测试环境跑CI时,git init 在源码目录执行了,而不是在临时目录。这导致 .git 文件夹出现在了 packages/core/ 里,污染了源码仓库。

报错信息指向的是 git init 命令本身,但根因在4层调用之外。

阶段1:根因调查

读错误信息:git init 在 /Users/jesse/project/packages/core 执行了,而不是在临时目录。

复现: 每次跑测试都会在 packages/core/ 下生成 .git 目录。

检查最近变更: 最近改了 Session.create() 的初始化逻辑。

追踪数据流(5层):

1
git init 在 process.cwd() 执行 ← 空的 cwd 参数
2
WorktreeManager.createSessionWorktree(projectDir, sessionId) ← projectDir 是空字符串
3
Session.initializeWorkspace() ← 传了空字符串
4
Session.create() ← 传了空字符串
5
测试代码在 beforeEach 之前就访问了 context.tempDir ← setupCoreTest() 返回 { tempDir: '' }

根因找到了:不是 git init 本身有问题,是测试代码在初始化之前就访问了临时目录变量,拿到空字符串,一路传到底,git init 拿到空字符串作为工作目录,解析为 process.cwd()(源码目录)。

报错位置和根因隔了4层调用。如果只看报错位置修——给 git init 加个目录检查——能修掉这一个场景,但下次其他命令拿到空路径,同样的问题还会出现。修根因(让 tempDir 在被提前访问时抛错)才能一劳永逸。

阶段2:模式分析

AI找到项目中其他地方使用 tempDir 的代码,发现所有正常使用的地方都在 beforeEach 之后访问。只有这一个测试在 beforeEach 之前访问了。

阶段3:假设验证

假设:"把 tempDir 改成 getter,在 beforeEach 之前访问就抛错"。

最小改动:把 tempDir: '' 改成 get tempDir() { if (!this._tempDir) throw new Error('...'); return this._tempDir; }

跑测试:.git 不再出现在源码目录。假设确认。

阶段4:实施修复

写失败测试:访问 context.tempDir 在 beforeEach 之前,断言抛错。

修复根因:实现 getter。

跑测试:通过。

但systematic-debugging还要求一步:纵深防御。修完根因后,在数据流经过的每一层都加校验,让同类bug在结构上不可能发生:

1
入口校验Project.create() 校验 workingDirectory 不能为空、必须存在、必须是目录
2
业务逻辑校验WorkspaceManager 校验 projectDir 不能为空
3
环境守卫:测试环境下,git init 拒绝在临时目录之外执行
4
调试日志:在 git init 前记录目录、cwd、调用栈

为什么4层而不是1层?不同的代码路径可能绕过某一层。入口校验能拦住大多数情况,但mock和测试可能绕过;业务逻辑校验能拦住边缘情况,但跨平台差异可能绕过;环境守卫能拦住特定上下文。4层一起,同类bug极难再发生。

如果手动追踪走不通怎么办?

上面的例子是手动逐层追踪找到的根因。但有些调用链很深,手动追踪会断。这时候有两个技术可以用:

加栈追踪: 在问题操作前打印完整调用链:

asyncfunctiongitInit(directory: string) {const stack = newError().stack;console.error('DEBUG git init:', {    directory,cwd: process.cwd(),    stack,  });awaitexecFileAsync('git', ['init'], { cwd: directory });}

用 console.error 而不是 logger(logger在测试中可能被静默)。跑测试后搜索 DEBUG git init 就能看到完整调用链。

二分查找污染源: 如果某个测试污染了全局状态导致其他测试失败,但不知道是哪个测试,可以用二分法:逐个跑测试,找到第一个污染源。Superpowers提供了一个脚本 find-polluter.sh 来做这件事。

六、verification-before-completion:没跑命令,不能说"搞定了"

systematic-debugging修完bug后,怎么确认真的修好了?靠verification-before-completion。

这个Skill的铁律:

1
没跑过验证命令,不能声称完成
2
"刚才跑过了"不算——必须用当前代码重新跑
3
"应该没问题"不算——必须有命令输出作为证据

它定义了一个5步门禁函数:

1
IDENTIFY:什么命令能证明这个声明?
2
RUN:执行完整命令(新鲜的、完整的)
3
READ:读完整输出,检查退出码,数失败数
4
VERIFY:输出是否确认了声明?如果没有,用证据说明实际状态;如果确认了,声明要附带证据
5
ONLY THEN:才能做声明

常见的错误声明和正确声明对比:

错误声明
正确声明
"应该没问题了"
"运行 npm test,34/34通过,0个失败"
"测试应该能通过"
"运行 npm test -- --coverage,覆盖率87%,0个失败"
"bug修好了"
"运行 npm test -- OrderList,白屏测试通过,回归测试通过"
"代码改完了"
"运行 git diff,变更2个文件,+5行-3行"

"刚才跑过了"为什么不行? 因为你可能在两次运行之间又改了代码。上一次的测试结果不能证明当前代码没问题。verification-before-completion要求的是"新鲜的证据"。

容易踩的信号

Skill原文列举了几个典型的"快要违反铁律"的信号,看到这些信号就该停下来了:

使用了"应该"、"大概"、"好像"
在验证之前表达了满意("好了!""完美!""搞定!")
准备提交/推送/创建PR但还没跑验证
信任代理的报告(代理说"成功",你没自己验证)
只做了部分验证(lint通过了就以为build也没问题)
觉得"就这一次没问题"

Skill原文记录了多次违反验证铁律的失败案例,包括:代理报告"成功"但实际有未定义函数、功能不完整就声称完成、信任代理报告导致浪费时间返工。这些案例的共同点:没跑命令就声称完成,结果部署后出问题。

七、两个Skill怎么配合

systematic-debugging和verification-before-completion不是孤立的两个Skill,它们组成一个闭环:

1
systematic-debugging找到根因
2
systematic-debugging修根因(先写失败测试,再改代码)
3
verification-before-completion验证修复(跑测试,确认通过)
4
verification-before-completion验证无回归(跑全量测试,确认没有破坏其他功能)

少了第3步和第4步,修bug就少了闭环。你可能修了A但破坏了B,自己还不知道。

一个完整的调试闭环示例:

你:提交表单后页面白屏,帮我修一下AI(进入systematic-debugging阶段1):  读错误信息:TypeErrorCannot read properties ofundefined (reading 'map')  复现条件:提交新订单后白屏,刷新后恢复  检查变更:最近提交改了接口返回格式  多组件收集证据:在APIHook、组件边界加日志,确认数据在Hook层丢失  追踪数据流:接口返回{orders, total},Hook没有提取.ordersAI(进入阶段2):  找正常代码:Dashboard.tsx用了data.orders.map(),没问题  对比参考实现:完整阅读Dashboard的调用方式  对比差异:OrderList没有取.orders属性  理解依赖:useOrders依赖fetchOrders的返回格式AI(进入阶段3):  假设:OrderList需要从返回值取.orders属性  最小改动:const { orders } = useOrders()  验证:白屏消失  假设确认 → 进入阶段4AI(进入阶段4):  写失败测试:模拟新格式API返回,断言组件正确渲染  运行测试:失败(确认测试能捕获bug)  修复根因:改解构赋值  运行测试:通过AI(进入verification-before-completion):IDENTIFY:什么命令能证明bug修好了?RUN:npm testREAD34/34通过,0个失败VERIFY:全部通过,确认修复  声明:运行npm test,34个测试全部通过,0个失败。OrderList白屏bug已修复。你:确认,提交代码

注意最后一个声明。AI不是说"修好了",而是说"运行npm test,34个测试全部通过,0个失败"。前者是主观判断,后者是客观证据。

八、3个辅助技术

systematic-debugging带了3个辅助技术,它们不只是"特定场景才用"的可选工具——根因追踪是阶段1的核心技术,纵深防御是修复后的必要步骤,条件等待是修时灵时不灵bug的唯一可靠方法。

8.1 根因追踪(root-cause-tracing)

适用于:错误发生在调用栈深处,你不清楚数据是从哪里开始错的。

方法:从报错位置开始,逐层向上追踪,找到数据的源头。如果手动追踪走不通,加栈追踪(new Error().stack)打印完整调用链。如果是测试污染问题,用二分法逐个跑测试找到污染源。

核心原则:永远不要只修报错位置,要修源头。 报错位置是症状,源头是根因。修症状,同一个根因会换个方式冒出来。

8.2 纵深防御(defense-in-depth)

适用于:修完一个bug后,想防止同类bug再次出现。

方法:在数据流经过的每一层都加校验。4层模型:入口校验、业务逻辑校验、环境守卫、调试日志。

为什么需要4层而不是1层?不同的代码路径可能绕过某一层。入口校验能拦住大多数情况,但mock和测试可能绕过;业务逻辑校验能拦住边缘情况,但跨平台差异可能绕过;环境守卫能拦住特定上下文,但正常使用不会触发。4层一起,同类bug极难再发生。

8.3 条件等待(condition-based-waiting)

适用于:测试中有 setTimeoutsleep 等硬编码等待,导致测试时灵时不灵。

方法:用"等待条件满足"替代"等待固定时间"。

// 错误:猜时间awaitnewPromise(r =>setTimeout(r, 50));const result = getResult();// 正确:等条件awaitwaitFor(() =>getResult() !== undefined);const result = getResult();

3个常见错误:

1
轮询太快setTimeout(check, 1) 浪费CPU,应该10ms轮询一次
2
没有超时:条件永远不满足会死循环,必须设超时和明确的错误信息
3
缓存过期数据:在循环外缓存了状态,循环内读到的永远是旧值,必须在循环内调用getter

什么时候不该用条件等待:测试实际的时序行为(debounce、throttle间隔)时,应该用固定等待,但必须注释说明为什么需要固定等待。

根据实际使用反馈:一批时灵时不灵的测试改成条件等待后,通过率可以从60%左右提升到接近100%,执行时间也明显缩短。

九、AI修bug的借口,和Superpowers怎么堵

systematic-debugging的借口:

AI的借口
Superpowers的回应
"这个bug很简单,直接改就行"
简单的bug也有根因,走流程比猜更快
"我先改了再看"
没找到根因之前不允许提修复方案
"我同时改了3个地方,效率高"
一次改一个,不然分不清哪个改对了
"我试了2次都没修好,再试一次"
少于3次回到阶段1重新分析;3次以上停下来质疑架构
"参考实现太长了,我按理解来"
不完整理解保证出bug,必须完整阅读参考实现
"我不太确定,但应该是这个原因"
不确定就承认,去问人、去研究,不要假装知道

verification-before-completion的借口:

AI的借口
Superpowers的回应
"应该没问题了"
跑验证命令
"我很确定"
确定不等于证据
"就这一次不验证"
没有例外
"lint通过了"
lint通过不等于build通过
"代理说成功了"
独立验证,不信任代理报告
"太累了不想验证"
累不是跳过验证的理由
"部分验证就够了"
部分验证证明不了什么

第4条借口"我试了2次都没修好"特别值得注意。3次规则不是"试3次就放弃",而是:

少于3次:回到阶段1,用新信息重新分析,可以再试一次
3次或以上:停下来质疑架构。如果每次修复都在不同位置暴露新问题、修复需要大规模重构、每次修复在其他地方产生新症状——这些都是架构问题的信号,不是代码层面的bug,继续猜只会浪费时间

写在最后

调试的本质不是"改代码让报错消失",而是"找到根因,修掉根因,验证修复"。systematic-debugging逼AI走完"定位-修复-验证"的完整闭环,verification-before-completion逼AI用命令输出说话而不是用"应该没问题"糊弄。两个Skill配合,把修bug从猜谜游戏变成工程流程。

修bug的纪律比写代码的能力更容易被忽视,但大部分开发时间不是在写新代码,是在修旧bug。让AI修bug修得靠谱,比让它写代码写得快更有价值。