诊断 Bug
适用场景
当此工作流匹配用户请求时使用:按本文档记录的工作流使用该技能。
来源:mattpocock/skills (MIT).
一套用于处理棘手 Bug 的纪律。只有在明确合理时才可跳过某个阶段。
在探索代码库时,请阅读 CONTEXT.md(若存在)以建立相关模块的清晰心智模型,并查阅你所触及领域的 ADR。
阶段 1 —— 建立反馈循环
这才是技能的核心。 其余一切只是机械执行。如果你针对这个 Bug 拥有紧的通过/失败信号——一个能就此 Bug 变红的信号——你就能找到根因;二分查找、假设检验、插桩都只是对它的消耗。如果没有,再怎么盯着代码也救不了你。
在这一阶段投入不成比例的精力。要激进。要有创意。绝不放弃。
构建反馈循环的方法——大致按此顺序尝试
- 失败测试——在能触达 Bug 的任意接缝处:单元、集成、端到端。
- Curl / HTTP 脚本——对运行中的开发服务器发起请求。
- CLI 调用——使用固定输入(fixture),将 stdout 与已知正常的快照做 diff。
- 无头浏览器脚本(Playwright / Puppeteer)——驱动 UI,对 DOM/控制台/网络做断言。
- 回放已捕获的 trace。将真实的网络请求 / payload / 事件日志保存到磁盘,在隔离环境中通过代码路径回放。
- 临时测试夹具(Throwaway harness)。启动系统的一个最小子集(一个服务、模拟依赖),用单次函数调用跑通 Bug 代码路径。
- 属性 / 模糊循环。如果 Bug 是"有时输出错误",跑 1000 次随机输入寻找失败模式。
- 二分夹具(Bisection harness)。如果 Bug 出现在两个已知状态(commit、数据集、版本)之间,将"在状态 X 启动、检查、重复"自动化,以便用
git bisect run。 - 差分循环。将同一输入在旧版本与新版本(或两种配置)上各跑一遍,对比输出差异。
- HITL bash 脚本。最后手段。若必须由人工点击,用
scripts/hitl-loop.template.sh驱动用户,让循环保持结构化。捕获的输出会反馈给你。
构建正确的反馈循环,Bug 等于解决了 90%。
收紧循环
把循环当作一个产品。一旦你拥有了某个循环,就去收紧它:
- 能让它更快吗?(缓存初始化、跳过无关启动、缩小测试范围。)
- 能让信号更锋利吗?(断言到具体症状,而不是"没有崩溃"。)
- 能让它更确定吗?(固定时间、播种 RNG、隔离文件系统、冻结网络。)
一个 30 秒且不稳定的循环,几乎等同于没有循环;一个 2 秒且确定的循环就是紧——一种调试超能力。
非确定性 Bug
目标不是干净的复现,而是更高的复现率。循环触发 100 次,并行、加压、缩窄时间窗口、注入 sleep。50% 闪退的 Bug 是可调的,1% 则不可——持续提升比率直到可调。
当你真的无法建立循环时
停下来明确说明。列出你尝试过的事情。请用户提供:(a) 可复现该问题的环境访问权限,(b) 已捕获的工件(HAR 文件、日志转储、core dump、带时间戳的录屏),或 (c) 在生产环境临时加埋点的授权。没有循环就绝不进入假设阶段。
完成标准——能变红的紧循环
阶段 1 完成时,循环必须紧且能变红:你能指出一条命令——一个脚本路径、一个测试调用、一段 curl——你已经至少跑过一次(粘贴该调用及其输出),并且它满足:
- 能变红——它驱动真实的 Bug 代码路径,并断言用户描述的精确症状,因此能就此 Bug 变红,并在修复后变绿。不是"运行不报错"——它必须能_捕获这个特定 Bug_。
- 确定性——每次运行结论一致(对闪退 Bug:按上文固定高复现率)。
- 快速——秒级,而非分钟级。
- 智能体可跑——可无人值守运行;只有通过
scripts/hitl-loop.template.sh时才有人参与。
如果你发现自己在该命令出现之前就开始读代码、构建理论——停下来——直接跳到假设正是本技能要防止的失败。 没有能变红的命令,就没有阶段 2。
阶段 2 —— 复现并最小化
运行循环。看到它变红——Bug 出现。
确认:
- 循环产生的失败模式是用户描述的那个——不是附近恰好出现的另一个失败。Bug 找错,修复也会错。
- 该失败在多次运行间可复现(或对非确定性 Bug 而言,复现率高到可以调试)。
- 已捕获精确症状(错误信息、错误输出、慢的耗时),以便后续阶段验证修复确实针对它。
最小化
一旦变红,就把复现压缩到仍能变红的最小场景。逐个裁剪输入、调用方、配置、数据和步骤,每次裁剪后重跑循环——只保留对失败承重的部分。
为什么要做:最小复现能压缩阶段 3 的假设空间(剩下的可疑活动部件更少),并自然成为阶段 5 的干净回归测试。
当剩余的每个元素都是承重的时算完成——移除任一元素都会让循环变绿。
在完成复现并最小化之前,不要进入下一阶段。
阶段 3 —— 假设
在测试任何假设之前,先提出 3–5 个有排序的假设。单一假设生成会锚定在第一个看似合理的想法上。
每个假设必须可证伪:明确说出它所做出的预测。
格式:"如果 是原因,那么改变 会让 Bug 消失 / 改变 会让它更严重。"
如果你无法陈述预测,那么这只是一个 vibe——丢弃或打磨它。
在测试之前向用户展示排序后的列表。 他们往往具备能瞬间重排的领域知识("我们刚改了 #3"),或知道已被排除的假设。这是廉价的检查点,却能省大量时间。不要阻塞——如果用户离线,按你的排序继续即可。
阶段 4 —— 插桩
每个探针必须对应阶段 3 的某条具体预测。一次只改变一个变量。
工具偏好:
- Debugger / REPL 检查,若环境支持。一个断点胜过十条日志。
- 定向日志,放在能区分假设的边界处。
- 绝不要"全打日志再 grep"。
给每条调试日志打上唯一前缀,例如 [DEBUG-a4f2]。收尾时清理只需一次 grep。没打标签的日志会活下来,打了标签的日志会被清掉。
性能分支。 对性能回归,日志通常没用。取而代之:先建立基线测量(计时框架、performance.now()、profiler、查询计划),再做二分。先测量,再修复。
阶段 5 —— 修复 + 回归测试
先于修复写回归测试——但前提是存在一个正确的接缝(correct seam)。
正确接缝是指:测试在调用点位置,以真实 Bug 模式的方式运行。如果唯一可用的接缝太浅(Bug 需要多调用方却只有单调用方测试;单元测试无法复现触发 Bug 的整条链路),在此处放回归测试只会带来虚假信心。
如果不存在正确接缝,这就是发现本身。 记下来。代码库的架构正在阻止 Bug 被锁定。请在下一阶段标记此项。
若存在正确接缝:
- 将最小复现转为在该接缝处失败的测试。
- 观察它失败。
- 实施修复。
- 观察它通过。
- 针对原始的(未最小化的)场景,重跑阶段 1 的反馈循环。
阶段 6 —— 收尾 + 事后复盘
宣布完成前必须满足:
- 原始复现已不再复现(重跑阶段 1 的循环)
- 回归测试通过(或已记录接缝缺失)
- 所有
[DEBUG-...]埋点已移除(grep该前缀) - 临时原型已删除(或移至明确标记的调试位置)
- 最终被证实的假设已写进 commit / PR 描述——让下一位调试者学到东西
然后追问:怎样才能从一开始阻止这个 Bug? 若答案涉及架构层面变更(没有好的测试接缝、调用方纠缠、隐藏耦合),将具体情况交接给 /improve-codebase-architecture 技能。待修复落地之后再给建议——此时你掌握的信息比开始时更多。
局限性
- 当工作流点名需要上游工具、账号、API key 或本地环境时,需要相应配置。
- 未经用户明确授权,不会执行破坏性、生产环境、付费或对外消息类的操作。
- 在将生成的工件或建议视为最终结论前,请对照用户的真实来源做校验。