用户报来一个 bug:「点导出按钮偶尔没反应,大概十次里有一次。」
你现在有两条路。第一条:打开导出相关的代码,盯着看,在脑子里推演哪里可能出错——
这是大多数人(和大多数 agent)的本能,也是这个 skill 明文禁止的动作。
第二条:先造一条命令,跑一下就能让这「十次里的一次」变成「每次都失败」,
然后再开始想原因。diagnosing-bugs 就是把第二条路写成纪律的 skill:
六个相位(phase,阶段),第一相位是本体,其余五个都是机械执行。
它的原话是:没有能变红的命令,就没有资格进入第二相位。
学完这节课,你能说出六个相位各自的完成判据、它会往仓库里留什么、
修完该接哪个 skill,以及行为不对时该改哪个文件的哪一段。
0001 把 22 个已发布 skill 分成三层,diagnosing-bugs 在纪律层——
和 tdd、code-review、prototype 这些 model-invoked 的基本功坐在一起,
意思是它不像编排层那样交付一整条流水线,而是一项「随时掉进、修完退出」的纪律。
在 ask-matt 的地图里,它还占了另一个位置:三条 on-ramp(匝道)之一。
所谓 on-ramp,就是「工作不是从『我有个想法』开始,而是从半路并入主流程的入口」:
别人提的 bug 和需求堆积走 /triage;大到看不清的工程走 /wayfinder;
而「东西坏了」这个入口,就是 /diagnosing-bugs。
它给人看的文档(docs 页)用了第三种说法:reach-for-it-anytime standalone
(随时伸手就用的独立工具)——东西一坏你就进去,修复和回归测试一落地你就出来。
三种说法不矛盾:它不在主流程(想法 → 面试 → spec → 工单 → 实现)的传送带上,
但它是主流程之外最常用的岔路之一,而且它的出口可能指回系统内部——
复盘时如果发现「这个 bug 锁不住,根子是架构没有好接缝」,
就把具体情况移交给 improve-codebase-architecture(0017 那课讲的那条扫描线)。
diagnosing-bugs 是一条六相位的诊断纪律:
造反馈环 → 复现并最小化 → 列假设 → 插桩 → 修复加回归测试 → 清理复盘。
它的核心赌注是:诊断的速度不取决于你多会读代码,而取决于你多早造出一条
对这个 bug 能变红的命令。所以它把最大的力气花在第一相位,
并且明文规定:连变红命令都没有的时候,禁止读代码建立理论。
分层:0001 系统地图 · 匝道定位:ask-matt/SKILL.md 的 On-ramps 一节 · standalone 说法:docs/engineering/diagnosing-bugs.md 的 Where it fits 一节
这个 skill 是 model-invoked(模型可调用):它的 SKILL.md frontmatter
里没有 disable-model-invocation 这一行,
目录下的 agents/openai.yaml 也只写了显示名和一句话简介,
没有 policy.allow_implicit_invocation: false 这种「只许人启动」的封印。
所以两条路都通:你直接敲 /diagnosing-bugs;
或者你根本不用点名——只要你对 agent 说「diagnose 一下」「debug this」,
或者报告「什么东西坏了 / 抛错 / 失败 / 变慢」,frontmatter 里
description 字段的触发条件就会命中,agent 自己会把这个 skill 加载进来。
这也解释了为什么它是纪律层里使用频率最高的一档:bug 不需要你想起它,bug 自己会触发它。
docs 页给的范围是「难的那类」:第一眼看不穿的 bug、间歇性发作的 flake、 在两个已知良好状态之间悄悄溜进来的回归(regression:本来是对的,某次改动后变错了)。 一眼就能看懂的小毛病(明显的笔误、配置写错)直接修就行, 为一个拼写错误跑六相位是杀鸡用牛刀。真正的边界在它和邻居之间:
| 情境 | 该用 diagnosing-bugs 吗 |
更该去哪 |
|---|---|---|
| 东西坏了、抛错、失败、变慢,而且一眼看不出原因 | 该(这就是它的触发词覆盖的场景) | — |
| 想用一段可丢弃的代码回答一个设计问题(状态模型对不对、UI 该长什么样) | 不该(那是在探索「该建什么」,不是「建好的为什么坏」) | prototype(它的 docs 页明确把「已建好的东西行为不对」推回本 skill) |
| merge / rebase 正在进行,git 停在冲突上 | 不该(冲突还没解完,谈不到行为诊断) | resolving-merge-conflicts;但如果冲突解完、合并完成后行为不对了,它的 docs 页让你回本 skill |
| 别人往 Issue tracker(工单系统)里提了一堆 bug,还没分拣 | 不该(先决定哪些值得修、谁该修,再谈诊断) | triage;分拣出来标了 ready 的 issue 走 /implement |
| 修好了,但复盘发现是架构没有好接缝才锁不住这个 bug | 它的出口就是这里 | improve-codebase-architecture(复盘阶段带着具体情况移交) |
| 说不清现在该走哪条流程 | 不该 | ask-matt(路由器) |
diagnose,CHANGELOG 记录了一次改名:
现在只能用 /diagnosing-bugs 调用,旧名字已经作废。
如果你在老笔记、老文章里看到 /diagnose,不要去仓库里找这个目录——
找 skills/engineering/diagnosing-bugs/。
触发词:SKILL.md 的 frontmatter description · 封印缺失:agents/openai.yaml · 改名记录:CHANGELOG.md · 邻居互推:docs/engineering/prototype.md、docs/engineering/resolving-merge-conflicts.md
这个 skill 不搞独立词汇体系(那是 codebase-design 和 domain-modeling 的活), 但它的正文大量使用调试圈的黑话,第一次读容易卡住。 下面这张表把后面要用到的词一次性定义清楚,定义都按 SKILL.md 的用法写。
| 术语 | 平实定义 | 在本 skill 里的精确用法 |
|---|---|---|
| 反馈环(feedback loop) | 一条你能反复运行的命令——一个脚本、一次测试调用、一条 curl——运行完会给出「通过 / 失败」的判决 | 整个 skill 的地基。Phase 1 的全部工作就是造出它并把它调紧;没有它,后面五个相位都不许开始 |
| 变红 / 变绿(red / green) | 红 = 测试或检查失败,绿 = 通过 | 反馈环必须「red-capable」:对这个 bug 它有能力变红,修好后才变绿。只会说「没崩」的环不算数 |
| 紧(tight)的环 | 快(秒级)、结果确定(每次判决一致)、信号尖锐(断言的正是用户报的那个症状) | 原文:30 秒还抖动的环约等于没有环;2 秒且确定的环是「debugging superpower」(调试超能力) |
| 复现(repro) | reproduction 的缩写:让 bug 重新发生的一套操作 | Phase 2 先把环跑红确认复现的是用户报的那个病,再把这套操作削减到最小 |
| 非确定性 bug / flake | 时好时坏、不是每次都出现的 bug(十次里一次那种) | 目标不是「干净的复现」,而是把复现率抬高:50% 的 flake 可以调试,1% 的不行 |
| 假设(hypothesis)与可证伪(falsifiable) | 对病因的猜测;可证伪 = 它能说出一个「如果猜错了就能被观察推翻」的预测 | Phase 3 要求 3–5 个排好序的假设,每个都必须能写成「如果 X 是因,那么改 Y 会让 bug 消失」这种格式 |
| 插桩(instrumentation) | 往代码里临时放观测点——断点、日志、计时——来看清内部实际发生了什么 | Phase 4。纪律:每个探针对应 Phase 3 的一个预测,一次只改一个变量,日志必须打标记 |
| 接缝(seam) | 0005 的词:不用改那处代码就能换行为的位置,也就是模块接口所在的位置 | Phase 5 用它判断回归测试该写在哪:接缝太浅,测试给的是假信心;没有正确接缝,本身就是发现 |
| 回归测试(regression test) | 锁住这个 bug 的一条自动化测试:它在这个 bug 上失败,修复后通过,以后 bug 复活它会再报警 | Phase 5 要求在动手修之前先写好(前提是有正确接缝);Phase 2 的最小复现就是它的毛坯 |
| 二分(bisect) | 在「好的」和「坏的」两个已知状态之间对半查找,定位引入 bug 的那次改动 | Phase 1 的第 8 种造环法:把「在某个状态启动、检查、重复」自动化后交给 git bisect run 执行 |
| 复盘(post-mortem) | 修完之后回头总结:真正的病因是什么、什么能从一开始就防止它 | Phase 6。病因要写进 commit / PR 描述;预防方案若涉及架构,移交给 improve-codebase-architecture |
| HITL(human-in-the-loop) | 回路里必须有真人参与——比如必须有人亲手在界面上点那个按钮 | 造环手段的第 10 种、也是最后手段:用模板脚本驱动真人操作,把结果结构化地喂回给 agent |
词汇出处:SKILL.md 各相位正文 · seam 的精确定义:0005 codebase-design
SKILL.md 用加粗字体写着:「This is the skill. Everything else is mechanical.」 ——这就是本体,其余全是机械执行。它的推理是:只要你有一条对这个 bug 能变红的紧反馈环, 找到病因只是时间问题,因为二分、假设检验、插桩全都只是在「消费」这个信号; 反过来,没有这个信号,盯多久代码都救不了你。 所以它要求你在这一相位投入不成比例的力气,原文连用了三个祈使句: Be aggressive. Be creative. Refuse to give up.(要凶、要巧、别放弃。)
| # | 方法 | 什么时候合适 |
|---|---|---|
| 1 | 失败测试:在够得着 bug 的任意接缝上写一条测试——单元、集成、端到端都行 | 首选。接缝够得着 bug 路径时,这是最便宜也最耐久的环(修好后就地变成回归测试) |
| 2 | curl / HTTP 脚本:对着跑起来的开发服务器打请求,断言响应 | bug 在 HTTP 接口层,起个 dev server 比配测试框架还快 |
| 3 | CLI 调用:给命令行工具一个固定输入(fixture,样本数据),把标准输出和已知正确的快照做 diff | bug 在命令行工具的输出里,差异肉眼可见但手工比对太累 |
| 4 | 无头浏览器脚本(Playwright / Puppeteer):程序驱动真实界面,断言 DOM、控制台报错、网络请求 | bug 只在真实浏览器交互里出现,比如「点导出按钮没反应」 |
| 5 | 重放抓取的轨迹:把真实的网络请求、负载(payload)、事件日志存到磁盘,隔离地重放进代码路径 | bug 依赖一份线上才有的真实数据,本地编不出来 |
| 6 | 一次性 harness(临时台架):启动系统的最小子集——一个服务、依赖用 mock(仿冒实现)——一次函数调用就走到 bug 路径 | 系统太大起不来整体,但 bug 路径其实只穿过一小块 |
| 7 | 属性 / fuzz 循环:跑 1000 个随机输入,找失败模式 | bug 的形状是「有时输出不对」,但你不知道什么样的输入会触发 |
| 8 | 二分 harness:把「在状态 X 启动、检查、重复」自动化,交给 git bisect run |
bug 出现在两个已知状态之间(两个 commit、两版数据、两个版本) |
| 9 | 差分循环:同一份输入分别跑旧版和新版(或两套配置),diff 两份输出 | 你怀疑「升级之后行为变了」,想让差异自己现身 |
| 10 | HITL bash 脚本(最后手段):必须真人点击时,用模板脚本驱动真人,把他的观察结构化地打印出来喂回给 agent | 前面九种都够不着——比如必须在登录后的真实界面里手动操作 |
原文给这一节的评语是:「Build the right feedback loop, and the bug is 90% fixed.」
——环造对了,bug 就修好了九成。第十种方法(HITL)有现成模板:
scripts/hitl-loop.template.sh,复制一份改改步骤就能用,
它提供两个辅助函数——step(给真人看一句指令,等他按回车)和
capture(问真人一个问题,把回答存进变量),
最后把所有答案以 KEY=VALUE 的形式打印出来,方便 agent 解析。
原文要求把环「当产品对待」,从三个方向收紧:
判据很直白:30 秒还抖动的环只比没有环好一点点;2 秒且确定的环才叫紧——那是调试超能力。 环每快一倍,你每一轮假设检验的成本就降一半,这就是为什么「 tightening 」值得花掉整段诊断里最大块的预算。
对十次里发作一次的 flake,别追求「一次必现」——原文给的目标是 higher reproduction rate(更高的复现率): 把触发动作循环 100 次、并行跑、加压力、收窄时间窗口、注入 sleep。 判据同样直白:50% 的 flake 可以调试,1% 的不行—— 没达到可调试的复现率之前,就继续抬。
如果环真的造不出来,原文的要求是明确说出来,而不是硬往下走。 列出你试过的方法,然后向用户要三样东西之一:
Phase 1 不是「我觉得差不多了」就能过的。完成时你必须能说出一条命令—— 脚本路径、测试调用或 curl——而且至少真的跑过一次,把调用方式和输出贴出来。 这条命令要通过四项检查:
| 检查项 | 含义(写完整) |
|---|---|
| Red-capable (能变红) |
它驱动的是真实的 bug 代码路径,断言的是用户报的那个确切症状——所以它对这个 bug 能变红、修好后能变绿。「运行没报错」不算数,它必须能抓住这只特定的虫 |
| Deterministic (结果确定) |
每次运行判决一致;对 flake,则是按上一节把复现率钉在一个足够高的固定水平上 |
| Fast (够快) |
以秒计,不以分钟计 |
| Agent-runnable (agent 能自己跑) |
不需要人盯着就能运行;回路里唯一的真人只能是通过 hitl 模板脚本被驱动的那个 |
本节全部出自 SKILL.md 的 Phase 1 各小节 · HITL 模板:scripts/hitl-loop.template.sh
相位二做两件事:把环跑红,再把复现削到最小。跑红之后先确认三件事, 原文是三条勾选项,每一条都对应一类常见翻车:
确认变红之后,把复现削减到仍然会变红的最小场景: 输入、调用方、配置、数据、操作步骤,一次砍一个,每砍一刀重跑一次环, 只留对失败「承重的」部分——承重(load-bearing)的意思是:砍掉它,环就不红了。 完成判据:剩下的每个元素都是承重的,再砍任何一个环都会变绿。
出处:SKILL.md 的 Phase 2 一节
相位三的纪律只有三条,但每条都在反本能:
出处:SKILL.md 的 Phase 3 一节
相位四回答「现在往代码里放什么观测点」。总原则:每个探针必须对应 Phase 3 清单里的一个具体预测,不是「到处看看」。执行时一次只改一个变量—— 同时改两处,你就不知道是哪处让症状变了。
每条调试日志都带一个唯一前缀,比如 [DEBUG-a4f2]。
收尾时清理就是一条 grep:搜这个前缀,全删。
原文的对仗句值得背:「Untagged logs survive; tagged logs die.」
——没标记的日志会活下来(混进代码库成为永久居民),打了标记的才会死。
对性能回归(变慢,而不是算错),日志通常是错的工具。
正确的动作是先建立基线测量(baseline):计时 harness、performance.now()、
profiler(性能分析器,按函数统计耗时)、query plan(数据库查询计划),
然后在测量值上二分。原文八个字:Measure first, fix second.
出处:SKILL.md 的 Phase 4 一节
相位五的第一句话就是纪律:回归测试写在修复之前——但有一个前提, 存在正确的接缝。这里用的是 0005 那套词:接缝是模块接口所在的位置, 测试该站在接缝上,不该捅进内部。
正确接缝的标准:测试在那里能按 bug 在调用现场的真实发生方式,复现出真正的 bug 模式。 反例是太浅的接缝:这个 bug 需要多个调用方按特定顺序调用才出现, 你却只写了一个单调用方的单元测试;或者触发 bug 的是一整条调用链, 单元测试根本拼不出那条链。在太浅的接缝上写「回归测试」, 给的是假信心——测试绿了,bug 换个路径照样回来。
improve-codebase-architecture 的具体材料(那边的「加深候选」正好处理这种病)。
出处:SKILL.md 的 Phase 5 一节 · 接缝词源:0005 codebase-design 的「接口就是测试面」
宣布「修完了」之前,相位六有五条强制勾选项,少一条都不算完:
[DEBUG-...] 插桩已删除(用前缀 grep 验证)。
然后问复盘的核心问题:「什么能从一开始就防止这个 bug?」
如果答案涉及架构改动——没有好的测试接缝、调用方缠成一团、藏着看不见的耦合——
就带着具体情况移交给 /improve-codebase-architecture。
原文特意规定了时点:在修复落地之后提这个建议,而不是之前——
因为修完之后你掌握的病因信息,比刚动手时完整得多。
六相位总览(每个闸门都是硬性的)
Phase 1 造反馈环 闸门:能点名一条已跑过、red-capable 的命令
Phase 2 复现+最小化 闸门:复现的是用户的病;每个剩余元素都承重
Phase 3 列假设 纪律:3–5 个、排序、可证伪、先给用户看
Phase 4 插桩 纪律:探针对应预测、一次一变量、日志打标记
Phase 5 修复+回归测试 纪律:测试先于修复;无正确接缝=发现本身
Phase 6 清理+复盘 闸门:五条勾选;病因进 commit;架构问题移交
出处:SKILL.md 的 Phase 6 一节
| 动作 | 落在哪 | 说明 |
|---|---|---|
探索代码库时读 CONTEXT.md(如果存在)和相关区域的 ADR |
只读 | SKILL.md 开篇的要求:先建立相关模块的心智模型再动手。这两个文件是 grill-with-docs / domain-modeling 那条线维护的,本 skill 只消费 |
| Phase 1 的反馈环(脚本、curl、临时测试) | 多数是临时的 | 若是失败测试形态,Phase 5 会把它转正为回归测试;其余形态在 Phase 6 删除或挪到调试专用位置 |
[DEBUG-...] 标记的日志 |
临时,必须死 | Phase 6 用前缀 grep 全删;漏网的就是 bug |
| 回归测试 | 留在仓库的测试套件里 | Phase 2 的最小复现改写而来,写在正确接缝上 |
| 修复本身 | 改业务代码 | 0001 的副作用矩阵把「业务代码 + 测试 + commit」归给 implement 和 diagnosing-bugs 的修复路径 |
| 病因结论 | commit / PR 描述 | Phase 6 勾选清单的最后一项 |
| Issue tracker、CONTEXT.md / ADR | 不写 | 它不碰工单系统,也不直接维护领域词汇——复盘发现架构问题时是「移交」,由 improve-codebase-architecture 那条线决定要不要落 ADR |
/improve-codebase-architecture(0017)。那边扫描出的加深候选可以生成新想法,并入主流程的 /grill-with-docs。 ┌──────────────────────────┐
│ CONTEXT.md / ADR │ grill-with-docs + domain-modeling 维护
└────────────┬─────────────┘
│ 读(建立模块心智模型)
▼
触发词命中 ──► ┌──────────────────┐ 复盘移交(无好接缝 / 纠缠 / 隐藏耦合)
或 /diagnosing-bugs │ diagnosing-bugs │ ──────────────────────────► improve-codebase-architecture
└──────┬───────────┘
│ 用词与纪律
┌────────────┼────────────────┐
▼ ▼ ▼
codebase-design tdd 邻居互推:
(seam 词汇, (回归测试就是 prototype(设计问题 vs 东西坏了)
Phase 5 判据) 红绿纪律) resolving-merge-conflicts(合完行为不对 → 来这里)
CONTEXT.md 和 ADR(读);codebase-design 的接缝词汇(Phase 5 的判据);tdd 的红绿纪律(回归测试先红后绿)。读 CONTEXT/ADR 与移交:SKILL.md 开篇与 Phase 6 · 邻居互推:prototype / resolving-merge-conflicts 的 docs 页 · 矩阵:0001 系统地图
这个 skill 的全部行为几乎都在一个文件里:skills/engineering/diagnosing-bugs/SKILL.md,
134 行,六个相位各一节。改行为之前先想清楚你要改的是「触发」「纪律」还是「工具」:
| 症状 | 去改哪 | 不要误改 |
|---|---|---|
| agent 没造环就开始讲病因理论 | SKILL.md Phase 1 的「Completion criterion」一节,特别是「No red-capable command, no Phase 2」那句闸门 | Phase 3 的假设格式(它是下一道闸门,管不了没环就起跑) |
| 环选得不合适,或顺序总乱(上来就写 HITL 脚本) | Phase 1「Ways to construct one」那张十项清单及其「roughly this order」的顺序 | hitl 模板脚本本身(它只是第 10 项的载体) |
| 对间歇 bug 轻易宣布「无法复现」然后放弃 | Phase 1「Non-deterministic bugs」一节(目标是抬高复现率:50% 可调,1% 不行) | 「When you genuinely cannot build a loop」一节(那是真的造不出环时的协议,不是 flake 的退路) |
| 跳过最小化,带着几十行噪音场景进假设 | Phase 2 的「Minimise」小节:一次砍一个、每砍重跑、承重判据 | Phase 5 的五步(测试毛坯来自 Phase 2,不在那里补砍) |
| 只抛一个假设就开测,或者假设说不出来怎么验证 | Phase 3:3–5 个排序、可证伪格式、先给用户看但不阻塞 | Phase 4 的工具顺序(工具没问题,是没东西可测) |
| 满屏无标记日志、修完还留在代码里 | Phase 4 的 [DEBUG-...] 打标规则 + Phase 6 清理勾选项 |
「Never log everything and grep」一句(那是方法问题;这是卫生问题) |
| 性能问题靠加日志猜 | Phase 4 的「Perf branch」:先基线测量再二分 | Phase 1 的环清单(性能 bug 同样需要环,但插桩工具不同) |
| 在太浅的接缝上写「回归测试」,bug 换条路又回来 | Phase 5 的「correct seam」定义和两个反例 | tdd 的测试风格细则(问题不在风格,在接缝位置) |
| 修完 commit 里没写病因,下一个调试者重新猜 | Phase 6 勾选清单最后一项 | 仓库的 commit message 风格约定(这是内容缺失,不是格式问题) |
| 触发太灵(小笔误也启动六相位)或太钝(报告了错误没反应) | SKILL.md frontmatter 的 description 字段:「Use when…」那段触发词 |
plugin.json(它只是发布清单,不管触发灵敏度) |
| HITL 脚本想改交互方式 | scripts/hitl-loop.template.sh 的 step / capture 两个辅助函数 |
SKILL.md 的 Phase 1 正文(它只规定「最后手段用模板」,不规定模板长什么样) |
| Codex 侧的显示名或一句话简介不对 | agents/openai.yaml 的 display_name / short_description |
SKILL.md 的 H1 标题(两边各管各的展示) |
全部指向 SKILL.md、 hitl-loop.template.sh、 agents/openai.yaml 三处原文
先别往回翻,凭记忆答。选项的长度刻意对齐,不会从版式泄题。判据以本课和 SKILL.md 的原文为准。
本课主一手材料(请打开原文读,不要只背本页摘要):
skills/engineering/diagnosing-bugs/SKILL.md
—— 全文只有 134 行,六个相位、十项造环清单、四道闸门都在里面,值得通读。
…/scripts/hitl-loop.template.sh
—— 第 10 种造环法的模板:step / capture 两个辅助函数,复制改造即用。
docs/engineering/diagnosing-bugs.md
—— 给人看的叙事版:「It's working if」四条观察信号适合验收 agent 的诊断行为
(aihero.dev/skills-diagnosing-bugs)。
速查页(本课同步): reference/diagnosing-bugs.html
导航: 上一课 0014 triage (另一条 on-ramp:别人的 bug 先分拣;分拣排期之外、眼下就烧起来的那个,才进本课的诊断循环)。 相邻课程: 0012 tdd(回归测试的红绿纪律)、 0005 codebase-design(接缝词汇)、 0008 prototype(设计问题 vs 东西坏了)、 0017 improve-codebase-architecture(复盘移交的去处)、 0018 resolving-merge-conflicts(合完代码行为不对时的下一站是本课)。
建议下一课(0016): 0016 wayfinder——三条 on-ramp 里最重的一条: 工程大到或模糊到一个会话装不下时,不交付代码,先在 Issue tracker 上画一张决策地图。 和本课的交接点很自然:diagnosing-bugs 处理「目的地清楚、路断了」, wayfinder 处理「连路在哪都看不见」。
SKILL.md / hitl-loop.template.sh / docs 页的原文,不会临场编造。
做完检索练习后,回复「练习结果 / 哪里卡住 / 开 0016 或先补 0014」,我们安排下一课。