先复现再找根因:Agent 方法论框架 superpowers 的系统调试流程拆解

2026-07-29

本文基于 superpowers 仓库 commit 44c9b2d(2026-07-27)梳理,该项目仍在持续迭代,具体行为以仓库 https://github.com/obra/superpowers 最新代码与文档为准。

Agent 调试时反复空转,根子不在模型不够聪明,而在于没有任何东西拦住它在”还没搞清为什么”的状态下动手改代码。 superpowers 这个 MIT 许可的开源项目里,skills/systematic-debugging/ 就是专门造这道拦阻索的:它不教你怎么调试得更巧,它只干一件事——在 Agent 提出修改方案之前,先把”你到底查清楚没有”这个问题摆到台面上,并且用一整套抗辩解的措辞让它绕不过去。

站内的 AI 调试流程AI 修不好时的思维循环 讲的是通用方法论——你自己带着 AI 排错时该怎么想。本篇不重复那一层,只看一个具体项目把这套想法写成了什么样的文件:哪些话是硬约束,哪些文件负责哪一块,代价在哪里。

一、“换一种写法再试试”到底是怎么形成的

你大概见过这个场景:测试挂了,Agent 看一眼报错,说”可能是异步没等到”,加个 sleep;没好,改成更长的 sleep;再没好,换成 waitFor 之类的写法;还不行,就开始怀疑测试框架。四轮下来,代码里多了四处改动,没有一处基于证据,而最初那个报错信息从头到尾没被完整读过。

systematic-debugging/SKILL.md 把这类想法直接列成了 Red Flags,原文里的几条包括 “Quick fix for now, investigate later”、“Just try changing X and see if it works”、“It’s probably X, let me fix that”,以及 “Each fix reveals new problem in different place”。它给出的处置只有一个词:回到 Phase 1。

这份清单的写法本身值得琢磨。它拦的不是”错误的操作”,而是”操作前那一句自我辩解”。文档里另有一张 Common Rationalizations 表,左边是借口右边是现实,比如 “Emergency, no time for process” 对应的现实是 “Systematic debugging is FASTER than guess-and-check thrashing”,“Multiple fixes at once saves time” 对应的是 “Can’t isolate what worked. Causes new bugs.”。同目录下的 CREATION-LOG.md 把这一点说得更直白:作者认为最关键的加固手段就是把那些”当下感觉很合理的捷径”逐条写出来,让模型在想到这句话的瞬间看见它被标为错误,从而产生认知摩擦。

这是一个很工程的判断:模型不缺调试知识,缺的是在压力下不给自己找台阶的机制。

二、一条铁律和四个阶段

流程的核心只有一句,文档里用代码块单独框出来:

NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST

紧接着的一句是 “If you haven’t completed Phase 1, you cannot propose fixes.”——不是”建议先调查”,是没做完就不许提方案。

四个阶段的分工是这样的。

Phase 1 根因调查,五个动作:完整读报错和调用栈,注意行号、文件路径、错误码;稳定复现,如果不能可靠触发就继续收集数据而不是猜;检查最近改了什么,看 git diff、新依赖、配置变更、环境差异;在多组件系统里先加诊断埋点;追踪数据流,看坏值从哪来。第四步写得最具体:当系统跨了组件边界(文档举的例子是 CI → build → signing、API → service → database),要求在每个组件边界上记录进去什么、出来什么、环境和配置有没有传下去,然后跑一次专门用来收集证据,先定位哪一层断的,再去查那一层。这一步的价值在于把”哪儿坏了”和”为什么坏”拆成了两个问题,Agent 最容易犯的错就是把它们混在一起同时猜。

Phase 2 模式分析:在同一个代码库里找到相似的、能正常工作的代码,把参照实现完整读完而不是扫一眼,然后逐条列出坏的和好的之间的所有差异,哪怕小到你觉得”这不可能有影响”。文档专门有一条对应的辩解:“Reference too long, I’ll adapt the pattern”,现实是 “Partial understanding guarantees bugs.”

Phase 3 假设与验证:一次只提一个假设,写下来,句式是”我认为 X 是根因,因为 Y”;用最小的改动去验证,一次只动一个变量;验证不过就换新假设,而不是在旧改动上叠新改动。还有一条是允许说”我不懂 X”,不要装懂。

Phase 4 实施:先写一个能复现问题的失败测试(没有测试框架就写一次性脚本),文档要求这一步在修之前必须有,并指向同仓库的 superpowers:test-driven-development;然后只做一处改动,不许顺手重构、不许”既然来了就一起改了”;最后验证,并指向 superpowers:verification-before-completion

真正的机关在 Phase 4 的第 4、5 小步:修完不生效怎么办。文档要求你数一下已经试了几次。少于 3 次,回 Phase 1 带着新信息重新分析;到了 3 次及以上,停下来质疑架构,不允许在没有架构讨论的情况下尝试第 4 次修复。判断架构有问题的信号也给了:每次修复都在不同地方牵出新的共享状态或耦合、修复需要”大规模重构”才能落地、每个修复都在别处制造新症状。文档的原话是,这不是假设失败,这是架构选错了。

这个计数器是整套流程里最像”程序”的部分。它把一个模糊的判断(“是不是该换思路了”)变成了一个可执行的条件分支,而条件分支是模型能稳定执行的东西。

三、三件配套武器,各管一段

主文件末尾列了同目录下三个支撑技术,各自解决一类具体麻烦。下面这张表是这个技能目录的地图,路径都是仓库里实际存在的文件。

组成部分它负责什么对应仓库位置你什么时候会碰到它
四阶段主流程铁律、阶段划分、红旗清单、辩解对照表、3 次失败转架构讨论skills/systematic-debugging/SKILL.md任何 bug、测试失败、构建失败一开口就进这里
反向追踪从报错点沿调用链往上找原始触发点,教怎么加栈打印skills/systematic-debugging/root-cause-tracing.md错误发生在调用栈深处,坏值来源不明
分层校验找到根因后在数据流经的每一层加校验,让 bug 结构上不可能发生skills/systematic-debugging/defense-in-depth.md修完一处,担心别的代码路径绕过去
条件等待用轮询条件替换拍脑袋的固定延时skills/systematic-debugging/condition-based-waiting.mdcondition-based-waiting-example.ts测试时好时坏,并行跑就超时
污染源二分脚本逐个跑测试文件,停在第一个制造出脏文件/脏状态的那个skills/systematic-debugging/find-polluter.sh跑完测试目录里多了不该有的东西,不知道谁干的
压力测试用例在沉没成本、疲劳、权威压力等场景下检验流程扛不扛得住skills/systematic-debugging/test-pressure-1.mdtest-pressure-3.mdtest-academic.md你想验证自己那套提示词是不是真的拦得住
创建记录记录这个技能怎么从个人 CLAUDE.md 里提炼出来、怎么加固、怎么测skills/systematic-debugging/CREATION-LOG.md你要照着写自己团队的技能文件
技能调度总则规定流程类技能优先于实现类技能,修 bug 先进系统调试skills/using-superpowers/SKILL.md决定什么时候该把这套流程挂上去

root-cause-tracing.md 的主线是一个真实案例:症状是 .git 被建在了源码目录 packages/core/ 里。往上追了五层——git init 跑在了 process.cwd(),因为 cwd 参数是空的;WorktreeManager 收到的 projectDir 是空串;Session.create() 传下去的就是空串;测试在 beforeEach 之前访问了 context.tempDir;而 setupCoreTest() 初始时返回的正是 { tempDir: '' }。根因是顶层变量初始化时访问了尚未赋值的东西,修法是把 tempDir 改成 getter,在 beforeEach 之前访问就抛错。注意这里修的位置和报错的位置隔了五层,如果按”哪儿报错改哪儿”的思路,改的会是 git init 那一行。

文件里还有两条很实用的细节:手工追不动时,在危险操作之前打印 new Error().stack 加上目录、process.cwd()、相关环境变量;在测试里要用 console.error() 而不是 logger,因为 logger 可能被吞掉。

defense-in-depth.md 接在追踪之后。它的立场是单点校验不够——不同代码路径、重构、mock 都能绕过去,所以要在数据经过的每一层都拦一道,四层分别是入口参数校验、业务逻辑校验、环境守卫、调试埋点。第三层的例子最能说明什么叫”环境守卫”:

async function gitInit(directory: string) {
  // In tests, refuse git init outside temp directories
  if (process.env.NODE_ENV === 'test') {
    const normalized = normalize(resolve(directory));
    const tmpDir = normalize(resolve(tmpdir()));

    if (!normalized.startsWith(tmpDir)) {
      throw new Error(
        `Refusing git init outside temp dir during tests: ${directory}`
      );
    }
  }
  // ... proceed
}

这不是在校验数据对不对,而是在声明”这个上下文里就不该发生这种事”。文档给出的理由是:入口校验会被别的调用路径绕开,业务校验会被 mock 绕开,跨平台的边缘情况得靠环境守卫兜,而当前三层都失手时,只有埋点能告诉你结构上被误用了。

condition-based-waiting.md 处理的是本文开头那个 sleep 循环。它的主张是等你真正关心的条件而不是猜一个时长,配的 waitFor 实现里有两个细节:每 10ms 轮询一次(更快只是烧 CPU),以及必须带超时并在超时消息里写清等的是什么。它也没有一刀切禁掉固定延时——如果你测的就是防抖、节流这类定时行为,允许用,但要求先等到触发条件、时长基于已知的时序而不是猜、并且写注释说明为什么是这个数。

四、边界与代价:它不管什么

这套东西不是免费的,把代价说清楚比夸它有用。

它会让开发变慢,尤其是小改动。 一个明显的拼写错误、一处 import 写错,走完四个阶段并且先写失败测试,成本远高于直接改。文档的立场是”简单问题也有根因、流程对简单 bug 很快”,但这是个立场不是免检金牌——你得自己判断投入产出。

它会让 Agent 变啰嗦。 加诊断埋点、跑一次只为收集证据、把差异逐条列出来、把假设写下来,每一步都是实打实的 token 和轮次。上下文窗口紧张的时候,这些证据本身就会挤占空间。

它只管”怎么想”,不管”怎么改”。 整个目录里没有任何领域知识:不告诉你 React 的常见坑、不告诉你数据库连接池怎么配。using-superpowers/SKILL.md 把这层关系说明了——流程类技能定方法,实现类技能干活,修 bug 时先进系统调试,再进领域技能。

它默认你有可执行的验证手段。 Phase 4 要求先有失败测试,find-polluter.sh 直接调 npm test,条件等待的例子是 TypeScript。这些前提在有测试框架的工程里成立;面对只能靠人眼看界面的问题、只在生产环境偶发的问题、或者根本没有测试基建的老项目,流程的骨架还能用,配套脚本和例子就得自己翻译。

它明确承认存在查不到根因的情况,但门槛拉得很高:文档说如果系统调查后确认问题确实是环境、时序或外部因素造成的,那就记录调查过程、实现合适的处理(重试、超时、错误信息)、补监控和日志;紧接着一句是,这类”没有根因”的情况里绝大多数其实是调查没做完。这句话本身是项目作者的经验判断,不是测出来的统计。

它管不了人。 目录里那几个压力测试文件恰好暴露了这一点:test-pressure-2.md 设定的是你已经调了四小时、晚上八点、还有饭局,test-pressure-3.md 设定的是视频会上资深工程师以”这种模式我见过一百次”下判断、技术负责人以”这个会已经超时了”收口,直接拍板照资深工程师的方案改。写下这类场景,说明作者清楚流程的真正对手是社会压力和沉没成本,而文档能做的只是提前把选项和后果摆出来。

五、上手与避坑清单

别把它当”调试提示词模板”贴一次就完事。 会踩是因为它的效力来自被反复触发,而不是被读过一遍。using-superpowers/SKILL.md 的红旗表里就有一条 “I remember this skill”,对应的现实是技能会更新、要读当前版本。做法是把触发条件挂在”遇到任何 bug、测试失败、非预期行为”上,每次真正把文件读进上下文,而不是靠模型对流程的印象。

别跳过 Phase 1 第 4 步就去追数据流。 会踩是因为跨组件的问题里,你以为在追数据流,其实是在猜哪个组件坏了。做法是先在每个边界上打进出日志,专门跑一次只为看清楚哪一层断了,再进入那一层做反向追踪。这两步的顺序反了,追踪就变成了漫游。

别在追到根因后只修一处就收工。 会踩是因为修完当下那条路径确实能让测试变绿,看起来没问题。做法是照 defense-in-depth.md 把数据流经的每一个检查点列出来,逐层加校验,然后主动尝试绕过第一层,验证第二层能拦住——这一步文档里明确写了要测。

别让”再试一次”没有计数。 会踩是因为第三次和第四次修复在体感上没差别,都只是”再试一下”。做法是把失败次数当成显式状态记下来,到 3 次就切换议题:不再讨论怎么修,改成讨论这个结构是不是本来就不对。这是整套流程里最容易被省掉、也最值钱的一条。

别把固定延时全部机械替换掉。 会踩是因为看完条件等待那篇容易走另一个极端,把所有 sleep 都改成轮询,结果测防抖、节流的用例反而测不出东西。做法是区分”我在等一件事发生”和”我在测一段时长的行为”,后者保留固定延时,但先等到触发条件、时长有依据、注释写清理由。

别把它当成对人的授权。 会踩是因为流程写得斩钉截铁,容易让 Agent 在和人协作时变成”按流程你不能这么改”。using-superpowers/SKILL.md 里写了优先级:人的指令高于技能,技能高于默认行为。流程是给自己立的规矩,不是拿来压别人的。

收尾

这套流程的实际价值,可以用一个自检问题衡量:当你的 Agent 第三次说”我们换个方式试试”时,有没有任何机制会打断它? 如果答案是没有,那它迟早会把你的代码库改成一堆互相打补丁的痕迹。

想接着往下读,建议顺序是:先 skills/systematic-debugging/SKILL.md 通读一遍(它不长),再看 root-cause-tracing.md 里那个五层追踪的例子体会”修哪儿”的判断,最后翻 CREATION-LOG.md——如果你打算给自己团队写类似的流程文件,那份记录里关于如何提炼、如何加固、如何拿场景去测的部分,比流程本身更有参考价值。

具体到落地,还有两件事得你自己接:一是把这套流程和你的复现手段接上,参考 Agent 复现与回放 把”能稳定触发”从口头承诺变成可执行的东西;二是想清楚它在你的工具链里以什么形态存在——是技能文件、是规则文件还是别的,可以对照 Claude Code 技能机制 判断哪种挂载方式在你的环境里真的会被触发。

本文属于 superpowers 方法论专题(共 30 篇,含三篇与其它开源 Agent 项目的对照)。想看把资产铺满的另一种取向,见 ECC 开源 Agent 套件专题

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。