诊断 Bug
针对疑难 bug 的纪律。只有在明确有正当理由时才跳过阶段。
探索代码库时,读取 CONTEXT.md(如果存在),以便对相关模块建立清晰的心智模型,并检查你正在触及区域的 ADR。
脱敏
此技能会让你展示命令、输出和捕获的产物。先对所有机密进行脱敏:用 <REDACTED> 替换它们。针对环境变量构建循环,以便凭据留在环境中,而不是出现在你展示的内容里。捕获的产物会携带认证头:只引用带有信号的行。
如果脱敏后的输出不足以诊断该 bug,请明确说明并询问用户。
阶段 1:构建反馈循环
这才是该技能的核心。 其他一切都是机械性的。如果你为该 bug 拥有一个紧凑的通过/失败信号(一个会在 _这个_ bug 上变红的信号),你就会找到原因;二分法、假设测试和插桩都只是在消费它。如果你没有这样的信号,再怎么盯着代码看也救不了你。
在这里投入不成比例的大量精力。要激进。要有创造力。拒绝放弃。
构建反馈循环的方法,大致按此顺序
- 在能触及该 bug 的任何接缝处编写失败测试:单元测试、集成测试、e2e。
- 针对运行中的开发服务器编写 Curl / HTTP 脚本。
- 使用固定输入进行 CLI 调用,将 stdout 与已知良好快照比较差异。
- 驱动 UI 并对 DOM/控制台/网络进行断言的无头浏览器脚本(Playwright / Puppeteer)。
- 重放捕获的跟踪。 将真实网络请求 / 载荷 / 事件日志保存到磁盘;在隔离环境中通过代码路径重放它。
- 一次性测试装置。 启动系统的一个最小子集(一个服务、模拟依赖),通过单次函数调用执行 bug 代码路径。
- 属性测试 / 模糊测试循环。 如果 bug 表现为“有时输出错误”,运行 1000 个随机输入并寻找失败模式。
- 二分法测试装置。 如果 bug 出现在两个已知状态之间(提交、数据集、版本),自动化“在状态 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:如上文所述,固定且较高的复现率)。
- [ ] 快速:秒级,而不是分钟级。
- [ ] 可由 Agent 运行:你可以无人值守地运行它;只有当通过
scripts/hitl-loop.template.sh时,才有人参与循环。
如果在这条命令存在之前,你发现自己正在阅读代码以构建理论,停下:直接跳到假设正是该技能要防止的确切失败。 没有可变红命令,就没有阶段 2。
阶段 2:复现 + 最小化
运行循环。观察它在 bug 出现时变红。
确认:
- [ ] 循环产生用户描述的失败模式,而不是碰巧在附近的另一种失败。错误的 bug = 错误的修复。
- [ ] 该失败在多次运行中可复现(或者,对于非确定性 bug,复现率高到足以进行调试)。
- [ ] 你已捕获确切症状(错误消息、错误输出、耗时变慢),以便后续阶段能够验证修复确实解决了它。
最小化
一旦变红,将复现缩小到仍然会变红的最小场景。一次一项地削减输入、调用方、配置、数据和步骤,每次削减后重新运行循环,只保留对失败起支撑作用的内容。
为什么要这样做:最小复现会缩小阶段 3 的假设空间(留下更少的可疑活动部件),并成为阶段 5 中干净的回归测试。
当每个剩余元素都起支撑作用时才算完成:移除其中任何一个都会使循环变绿。
在已经复现并最小化之前,不要继续。
阶段 3:提出假设
在测试任何假设之前,生成3–5 个排序后的假设。单一假设生成会锚定在第一个看似合理的想法上。
每个假设都必须可证伪:说明它所做出的预测。
格式:“如果 <X> 是原因,那么 <更改 Y> 会让 bug 消失 / <更改 Z> 会让它变得更糟。”
如果你无法说明预测,那么该假设只是一种感觉:丢弃它或使其更清晰。
在测试之前,将排序后的列表展示给用户。 他们通常拥有能立即重新排序的领域知识(“我们刚刚部署了 #3 的更改”),或者知道他们此前已经排除的假设。这是一个便宜的检查点,却能节省大量时间。不要因此阻塞;如果用户 AFK,请按你的排序继续。
阶段 4:插桩
每个探测点都必须映射到阶段 3 中的某个具体预测。一次只更改一个变量。
工具偏好:
- 如果环境支持,则使用 调试器 / REPL 检查。一个断点胜过十条日志。
- 在能够区分假设的边界处添加针对性日志。
- 绝不“记录所有日志然后 grep”。
为每条调试日志打上标签,使用唯一前缀,例如 [DEBUG-a4f2]。最后的清理会变成一次 grep。未打标签的日志会留存;已打标签的日志会被删除。
性能分支。 对于性能回归,日志通常是错误的。相反:先建立基线测量(计时测试装置、performance.now()、性能分析器、查询计划),然后进行二分。先测量,后修复。
阶段 5:修复 + 回归测试
在修复之前编写回归测试,但仅当存在适合它的正确接缝时才这样做。
正确的接缝是指测试能够在调用点重现真实 bug 模式。如果唯一可用的接缝太浅(在 bug 需要多个调用方时只测试单个调用方,或单元测试无法复制触发该 bug 的链条),在那里添加回归测试会给出虚假信心。
如果不存在正确的接缝,这本身就是发现。 记录下来。代码库架构正在阻止该 bug 被锁定。将这一点标记到下一阶段。
如果存在正确的接缝:
- 将最小化后的复现转换为该接缝处的失败测试。
- 观察它失败。
- 应用修复。
- 观察它通过。
- 针对原始(未最小化)场景重新运行阶段 1 反馈循环。
阶段 6:清理
在声明完成之前必须满足:
- [ ] 原始复现不再复现(重新运行阶段 1 循环)
- [ ] 回归测试通过(或已记录不存在接缝)
- [ ] 所有
[DEBUG-...]插桩均已移除(grep该前缀) - [ ] 一次性原型已删除(或移动到明确标记的调试位置)
- [ ] 在 commit / PR 消息中说明最终正确的假设,以便下一位调试者学习