Diagnosing Bugs(诊断 Bug)
一套面向困难 Bug 的诊断纪律。只有在明确说明理由时才可跳过阶段。
在探索代码库时,如果存在 GLOSSARY.md,先阅读它以建立相关模块的清晰心智模型,并查看所触及领域的 ADR(架构决策记录)。
脱敏(Redact)
该技能要求展示命令、输出和捕获的产物。先对每个秘密脱敏:用 <REDACTED> 替代。围绕环境变量构建循环,让凭据留在环境中,而不是出现在展示内容里。捕获的产物可能带有认证头:只引用携带信号的那几行。
如果脱敏后的输出不足以诊断 Bug,应说明这一点并向用户询问。
阶段 1:构建反馈循环
这就是本技能的核⼼。 其余都是机械操作。如果拥有针对该 Bug 的紧致通过/失败信号(能对这个 Bug 变红),你就能找到原因;二分、假设检验、插桩都只是消费这个信号。如果没有,光盯着代码也救不了你。
在此投入不成比例的精力。要激进。要有创意。拒绝放弃。
构建循环的方式,大致按以下顺序
- 在能触及 Bug 的任何接缝处写失败测试:单元、集成、端到端。
- 对运行中的开发服务器使用 Curl / HTTP 脚本。
- 用固定输入做 CLI 调用,将 stdout 与已知正确的快照做 diff。
- 无头浏览器脚本(Playwright / Puppeteer)驱动 UI,并对 DOM/控制台/网络断言。
- 重放捕获的 trace。 把真实的网络请求/载荷/事件日志保存到磁盘;在隔离状态下经代码路径重放。
- 一次性测试台(harness)。 启动系统的最小���集(一个服务、mock 依赖),用单次函数调用走 Bug 代码路径。
- 属性/模糊循环。 如果 Bug 是“有时输出错误”,跑 1000 个随机输入寻找失败模式。
- 二分测试台。 如果 Bug 出现在两个已知状态(commit、数据集、版本)之间,自动化“在状态 X 启动、检查、重复”,以便用
git bisect run运行。 - 差分循环。 把同一输入分别跑旧版本与新版本(或两套配置),diff 输出。
- 人机协同(HITL)bash 脚本。 最后手段。如果必须由人来点击,用
scripts/hitl-loop.template.sh驱动他们,使循环仍保持结构化。捕获的输出回传给你。
构建正确的反馈循环,Bug 就解决了 90%。
收紧循环
把循环当作产品。一旦拥有一个循环,就收紧它:
- 能否更快?(缓存准备,跳过无关初始化,收窄测试范围。)
- 能否让信号更锐利?(对具体症状断言,而非“没有崩溃”。)
- 能否更确定?(固定时间、给 RNG 播种、隔离文件系统、冻结网络。)
30 秒且不稳定的循环比没有循环好不了多少;2 秒且确定性的循环是紧致的,是调试的超能力。
非确定性 Bug
目标不是干净复现,而是更高的复现率。把触发循环 100 次、并行化、加压力、收窄时序窗口、注入 sleep。50% 概率的 flake Bug 可调试;1% 则不能,所以持续提高复现率直到可调试。
当确实无法构建循环时
停下来并明确说明。列出你尝试过的内容。向用户请求:(a) 访问能复现它的任何环境,(b) 脱敏后的捕获产物(HAR 文件、日志转储、core dump、带时间戳的录屏),或 (c) 允许添加临时的生产插桩。不要在没有循环的情况下进入假设阶段。
完成标准:一个能变红的紧致循环
阶段 1 在循环紧致且能变红时结束:你能说出一条命令(脚本路径、测试调用、curl),你已经至少运行过一次(展示调用及其输出,已脱敏),并且它满足:
- [ ] 能变红:驱动真实的 Bug 代码路径,并断言用户的确切症状,从而能对这个 Bug 变红、修复后变绿。不是“运行无错误”;它必须能捕获这个特定 Bug。
- [ ] 确定性:每次运行判词一致(flake Bug:按上述固定且高复现率)。
- [ ] 快速:以秒计,而非分钟。
- [ ] 智能体可运行:你能无人值守运行;人机协同仅通过
scripts/hitl-loop.template.sh。
如果发现自己在这条命令存在之前就去读代码构建理论,停下:直接跳到假设正是本技能要防止的失败。 没有能变红的命令,就没有阶段 2。
阶段 2:复现 + 最小化
运行循环。观察它随 Bug 出现而变红。
确认:
- [ ] 循环产生的是用户描述的失败模式,而非附近碰巧出现的其他失败。错的 Bug = 错的修复。
- [ ] 该失败在多次运行中可复现(或对非确定性 Bug,可复现率达到足以调试)。
- [ ] 你已捕获确切症状(错误消息、错误输出、慢的耗时),以便后续阶段验证修复确实针对它。
最小化
一旦变红,把复现缩小到仍会变红的最小场景。一次只削减一个输入、调用方、配置、数据和步骤,每次削减后重新运行循环,只保留对失败有支撑作用的元素。
为何要做:最小复现会在阶段 3 缩小假设空间(剩余可疑的活动部件更少),并在阶段 5 成为干净的回归测试。
当每个剩余元素都有支撑作用时完成:移除其中任何一个都会让循环变绿。
在完成复现且最小化之前,不要继续。
阶段 3:假设
在测试任何假设之前,先产生 3–5 个排序假设。单一假设会让思路锚定在第一个看似合理的想法上。
每个假设必须可证伪:说明它所做的预测。
格式:“如果 <X> 是原因,那么 <改变 Y> 会让 Bug 消失/<改变 Z> 会让它更严重。”
如果无法陈述预测,该假设只是一种感觉:丢弃或使其更锐利。
在测试前把排序列表展示给用户。 他们常拥有能立即重排的领域知识(“我们刚部署了 #3 的改动”),或知道你已排除的假设。廉价的检查点,省下大量时间。不要阻塞等待;如果用户不在,就按你的排序继续。
阶段 4:插桩
每个探针必须映射到阶段 3 的特定预测。一次只改变一个变量。
工具偏好:
- 如果环境支持,使用调试器/REPL 检查。一个断点胜过十条日志。
- 在能区分假设的边界处做定向日志。
- 绝不“记录全部再 grep”。
给每条调试日志打标签,使用唯一前缀,例如 [DEBUG-a4f2]。最后清理就变成一次 grep。未打标签的日志会存活;打了标签的日志会消亡。
性能分支。 对性能回归,日志通常是错的。改为:先建立基线测量(计时测试台、performance.now()、profiler、查询计划),然后二分。先测量,后修复。
阶段 5:修复 + 回归测试
在修复之前写回归测试,但前提是存在正确的接缝。
正确的接缝是指测试能复现调用点上发生的真实 Bug 模式。如果唯一可用的接缝太浅(Bug 需要多个调用方时只测单个调用方、单元测试无法复现触发 Bug 的调用链),在此写回归测试会带来虚假的信心。
如果不存在正确的接缝,这本身就是发现。 记下来。代码库架构阻止了该 Bug 被锁定。为下一阶段标记这一点。
如果存在正确接缝:
- 把最小复现转成该接缝处的失败测试。
- 观察它失败。如果你通过改动代码或夹具强行变红,与原始副本做
diff,证明改动确实生效后再信任它。 - 应用修复。
- 观察它通过。
- 针对原始(未最小化)场景重新运行阶段 1 的反馈循环。
阶段 6:清理
在宣布完成前必须做到:
- [ ] 原始复现不再复现(重新运行阶段 1 循环)
- [ ] 回归测试通过(或记录接缝缺失)
- [ ] 所有
[DEBUG-...]插桩被移除(grep 该前缀) - [ ] 一次性原型已删除(或移到明确标记的调试位置)
- [ ] 在 commit/PR 信息中说明最终正确的假设,让下一个调试者从中学习