关于 diagnosing-bugs
面向编码 Agent 的调试技能,要求先建立快速、可重复的反馈循环,再进行假设、排查、修复与清理。
主要功能
- 01构建明确的通过/失败循环
- 02复现并缩减故障场景
- 03排列可证伪的假设
- 04使用定向检测手段
- 05添加回归测试并清理
适用场景
- 01调查间歇性缺陷
- 02追查已报告故障的原因
- 03用原始复现验证修复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nstaab1/ nathan-agent-skills --skill diagnosing-bugs安装后在 Claude Code 中输入/diagnosing-bugs确认已加载。
附带文件agents/openai.yamlscripts/hitl-loop.template.sh
详细介绍
诊断 Bug
针对疑难 Bug 的一套方法。只有在明确说明理由时才可跳过某个阶段。
探索代码库时,若存在 GLOSSARY.md,请先阅读它,以便清晰理解相关模块;同时检查你所修改区域中的 ADR。
脱敏
本技能会要求你展示命令、输出和捕获的产物。先对所有机密信息进行脱敏:用 <REDACTED> 替换它们。基于环境变量构建循环,使凭据保留在环境中,而不出现在你展示的内容里。捕获的产物中包含认证请求头:只引用携带关键信号的行。
如果脱敏后的输出不足以诊断该 Bug,请明确说明并询问用户。
阶段 1:构建反馈循环
这是本技能的核心。 其余步骤都是机械性的。如果你拥有针对该 Bug 的严格通过/失败信号(只在_这个_ Bug 出现时才变红),就能找到根因;二分法、假设验证和插桩都只是消费这一信号。如果没有这样的信号,再怎么盯着代码看也无济于事。
在这里投入远超其他步骤的精力。要大胆,要有创造力,绝不放弃。
构建方式,大致按以下顺序排列
- 失败测试:在能触达该 Bug 的任意切入点编写:单元测试、集成测试或端到端测试。
- Curl / HTTP 脚本:针对正在运行的开发服务器。
- 命令行调用:使用测试夹具输入,将标准输出与已知正确的快照进行比对。
- 无头浏览器脚本(Playwright / Puppeteer):驱动 UI,并对 DOM、控制台或网络请求进行断言。
- 重放捕获的追踪记录。 将真实的网络请求、负载或事件日志保存到磁盘;在隔离环境中通过相应代码路径重放。
- 一次性测试框架。 启动系统的最小子集(单个服务,模拟依赖),通过一次函数调用执行该 Bug 的代码路径。
- 属性测试 / 模糊测试循环。 如果 Bug 表现为「偶尔输出错误」,运行 1000 组随机输入,寻找失败模式。
- 二分测试框架。 如果 Bug 出现在两个已知状态(提交、数据集、版本)之间,将「在状态 X 启动、检查、重复」自动化,以便使用
git bisect run执行。 - 差分循环。 将同一输入分别通过旧版本与新版本(或两种配置)运行,并比对输出。
- 人工介入(HITL)Bash 脚本。 最后手段。如果必须由人点击,则使用
scripts/hitl-loop.template.sh引导其操作,使循环仍然有结构。捕获的输出会反馈给你。
构建正确的反馈循环,Bug 就已经修复了 90%。
收紧循环
把循环当作一个产品来对待。一旦有了_一个_循环,就要收紧它:
- 能否让它更快?(缓存初始化、跳过无关的初始化、缩小测试范围。)
- 能否让信号更精准?(断言具体症状,而不是「没有崩溃」。)
- 能否让它更具确定性?(固定时间、设定随机种子、隔离文件系统、冻结网络。)
一个耗时 30 秒且不稳定的循环,几乎不比没有循环好;而一个耗时 2 秒且确定性的循环,才是严密的、强大的调试利器。
非确定性 Bug
目标不是获得干净的复现,而是提高复现率。将触发操作循环 100 次,并行化,增加压力,缩小时间窗口,注入 sleep。复现率为 50% 的 Bug 可以调试,1% 则不行,因此应持续提高复现率,直到可以调试为止。
确实无法构建循环时
请明确停下并说明情况。列出你已尝试的方法。向用户请求:(a) 访问能够复现该问题的环境;(b) 经过脱敏的捕获产物(HAR 文件、日志转储、核心转储、带时间戳的屏幕录像);或 (c) 添加临时生产环境插桩的许可。在没有循环的情况下,不要继续提出假设。
完成标准:一个能稳定触发失败的紧凑循环
当循环足够紧凑且能够触发失败时,Phase 1 就完成了:你可以指出一条命令(脚本路径、测试调用或 curl),并且至少运行过一次(展示调用和输出,注意脱敏),且该命令满足以下条件:
- 能够触发失败:它会驱动实际的 Bug 代码路径,并断言用户遇到的确切症状,因此遇到这个 Bug 时能失败,修复后能通过。不是“运行时没有报错”就够了;它必须能_捕获这个特定 Bug_。
- 确定性:每次运行结果都相同(对于不稳定的 Bug,按照上述说明,需固定并达到较高的复现率)。
- 快速:耗时以秒计,而不是分钟。
- 可由 Agent 运行:可以无人值守地运行;只有通过
scripts/hitl-loop.template.sh才需要人工介入。
如果你发现自己在这条命令还不存在时,就开始读代码构建理论,请停下来:直接跳到假设阶段,正是这项技能要避免的问题。 没有能触发失败的命令,就不能进入 Phase 2。
Phase 2:复现并缩小范围
运行循环。观察它在 Bug 出现时失败。
确认:
- 循环产生的是用户描述的故障模式,而不是碰巧相关的另一种故障。Bug 找错了,修复也会错。
- 多次运行都能复现故障(对于非确定性 Bug,复现率必须足够高,才能据此调试)。
- 已捕获确切症状(错误消息、错误输出、耗时过长),以便后续阶段验证修复是否确实解决了问题。
缩小范围
确认循环会失败后,将复现缩减为仍能触发失败的最小场景。每次只删减一项输入、调用方、配置、数据或步骤,并在每次删减后重新运行循环,只保留对故障起关键作用的内容。
这样做的原因:最小复现能在 Phase 3 缩小假设范围(可疑因素更少),并在 Phase 5 成为干净的回归测试。
当剩下的每个元素都起关键作用时,即可完成:移除其中任意一个,循环都会通过。
完成复现并缩小范围之前,不要继续。
Phase 3:提出假设
在测试任何假设之前,先生成 3–5 个按优先级排序的假设。只生成一个假设会让人锚定在第一个看似合理的想法上。
每个假设都必须可证伪:说明它会给出什么预测。
格式:“如果 是原因,那么 <改变 Y> 将使 Bug 消失/<改变 Z> 将使问题恶化。”
如果无法说明预测,这个假设就只是凭感觉:舍弃它或进一步明确。
测试假设之前,先向用户展示排序后的列表。 用户通常掌握领域知识,可以立即重新排序(“我们刚部署了与 #3 相关的改动”),或者知道哪些假设已经被排除。这是成本低、收益大的检查点。不要因此停滞;如果用户暂时不在线,就按自己的排序继续。
Phase 4:加设探针
每个探针都必须对应 Phase 3 中的具体预测。每次只改变一个变量。
工具优先顺序:
- 如果环境支持,优先使用调试器/REPL 检查。一个断点胜过十条日志。
- 在能够区分不同假设的边界处添加定向日志。
- 绝不要“记录所有内容再用 grep 筛选”。
为每条调试日志添加唯一前缀,例如 [DEBUG-a4f2]。最后只需一次 grep 就能清理干净。未加标签的日志会残留;加了标签的日志则会被清除。
性能分支。 对于性能回退,日志通常不是正确的办法。相反,应先建立基准测量(计时测试框架、performance.now()、性能分析器、查询计划),然后进行二分排查。先测量,再修复。
第 5 阶段:修复 + 回归测试
先编写回归测试,再进行修复,但前提是存在合适的切入点。
合适的切入点是指测试能在调用点复现真实的缺陷模式。如果唯一可用的切入点过于浅显(例如,缺陷需要多个调用方,但测试只涉及单个调用方;或者单元测试无法复现触发缺陷的调用链),那么在那里编写回归测试会造成虚假的信心。
**如果不存在合适的切入点,这本身就是发现的问题。**请记录下来。代码库架构导致无法将该缺陷固定下来。请在下一阶段标记此问题。
如果存在合适的切入点:
- 将最小复现转化为该切入点上的失败测试。
- 确认测试失败。
- 应用修复。
- 确认测试通过。
- 针对原始(未经最小化的)场景,重新运行第 1 阶段的反馈循环。
第 6 阶段:清理
宣布完成之前,必须:
- 原始复现不再能复现该问题(重新运行第 1 阶段的反馈循环)
- 回归测试通过(或已记录不存在合适的切入点)
- 已移除所有
[DEBUG-...]插桩(使用grep搜索该前缀) - 已删除临时原型(或将其移至明确标记为调试用途的位置)
- 在提交 / PR 消息中说明最终证实的假设,让后续调试人员能够从中吸取经验