诊断 Bug (Diagnosing Bugs)
一套应对疑难 Bug 的规范化流程。只有在有充分明确理由的情况下,方可跳过某些阶段。
在探索代码库时,请阅读 CONTEXT.md(如果存在)以建立相关模块的清晰心智模型,并检查你所涉及领域的 ADR(架构决策记录)。
敏感信息脱敏 (Redact)
本技能需要你展示命令、输出和捕获的工件。首先必须对所有机密信息进行脱敏处理:在对应位置写入 <REDACTED>。基于环境变量构建闭环,确保凭据保留在运行环境中,而不是暴露在展示内容中。捕获的工件通常带有身份验证头:仅引用包含有效信号的行。
如果脱敏后的输出不足以诊断 Bug,请明确说明并向用户询问。
阶段 1:构建反馈闭环 (Build a feedback loop)
这是本技能的核心所在。 其余一切都只是机械化的操作。如果你能针对该 Bug 建立一个紧凑的成功/失败信号(一个能针对此 Bug 明确变红/失败的信号),你就一定能找到原因;二分排查、假设验证和插桩分析都只是在消费这个信号。如果你没有这样的信号,看再久的代码也无济于事。
在此阶段投入超常规的精力。要积极进取、富有创造力、绝不轻言放弃。
构建反馈闭环的方法(大致按推荐顺序排列)
- 失败的测试:在能够触及该 Bug 的任何切入点构建:单元测试、集成测试、端到端测试。
- Curl / HTTP 脚本:针对正在运行的开发服务器发起请求。
- CLI 调用:传入测试固件输入,并将标准输出与已知的正确快照进行比对 (diff)。
- 无头浏览器脚本 (Playwright / Puppeteer):驱动 UI 并对 DOM/控制台/网络请求进行断言。
- 重放捕获的追踪记录 (Trace):将真实的网络请求/负载/事件日志保存到磁盘;在隔离环境中通过特定代码路径进行重放。
- 一次性测试脚手架 (Throwaway harness):启动系统的最小子集(单个服务、模拟依赖项),通过单次函数调用来运行触发 Bug 的代码路径。
- 基于属性的测试 / 模糊测试闭环 (Property / fuzz loop):如果 Bug 表现为“偶发性错误输出”,运行 1000 次随机输入并寻找失败模式。
- 二分排查脚手架:如果 Bug 是在两个已知状态(提交、数据集、版本)之间出现的,自动执行“在状态 X 下启动 -> 检查 -> 重复”,以便使用
git bisect run。 - 差分闭环 (Differential loop):将相同的输入分别传入旧版本和新版本(或两种不同配置),并对比输出差异。
- 人机协同 (HITL) Bash 脚本:最后的手段。如果必须人工点击操作,使用
scripts/hitl-loop.template.sh来引导他们操作,以确保闭环依然结构化。捕获的输出将反馈给你。
构建出正确的反馈闭环,Bug 就已经解决了 90%。
收紧闭环 (Tighten the loop)
将闭环当作一个产品来对待。一旦有了一个闭环,就要收紧它:
- 能否让它更快?(缓存初始化设置、跳过无关初始化、缩小测试范围。)
- 能否让信号更敏锐?(针对具体症状进行断言,而不是仅仅断言“没有崩溃”。)
- 能否让它更具确定性?(固定时间、设定随机数种子、隔离文件系统、冻结网络请求。)
一个耗时 30 秒且不稳定的闭环几乎不比没有闭环好到哪去;而一个耗时 2 秒且结果确定的闭环则是极其紧凑的,堪称调试超能力。
非确定性 Bug (Non-deterministic bugs)
目标不是追求一次干净的复现,而是追求更高的复现率。循环执行触发操作 100 次、并发执行、施加压力、缩小时间窗口、注入 sleep 延迟。一个复现率为 50% 的偶发 Bug 是可调试的;而 1% 则不行,因此请不断提高复现率,直到其具备可调试性。
当你确实无法构建闭环时
停下来并明确说明。列出你已经尝试过的方法。向用户索取:(a) 可以复现该问题的环境访问权限,(b) 脱敏后的捕获工件(HAR 文件、日志转储、核心转储、带时间戳的屏幕录像),或 (c) 允许添加临时生产环境插桩。在没有闭环的情况下,切勿直接进入假设阶段。
完成标准:一个能够变红的紧凑闭环
当闭环满足紧凑且能够变红 (red-capable) 时,阶段 1 即告完成:你可以给出一个具体命令(脚本路径、测试调用、curl 命令),且该命令已经实际运行过至少一次(展示调用过程及其脱敏后的输出),并满足:
- 能够变红 (Red-capable):它能执行触发实际 Bug 的代码路径,并针对用户的确切症状进行断言,因此它能在存在此 Bug 时变红(失败),修复后变绿(成功)。不仅是“运行不出错”,还必须能够捕获该特定 Bug。
- 确定性 (Deterministic):每次运行得出相同结论(对于偶发 Bug:根据上述要求,具有固定的高复现率)。
- 快速 (Fast):耗时以秒计,而非以分钟计。
- Agent 可运行 (Agent-runnable):你可以无人值守地运行它;仅在通过
scripts/hitl-loop.template.sh时才需要人工介入。
如果在存在此命令之前,你发现自己正在通过阅读代码来建立理论,请停下来:直接跳到假设正是本技能所要防止的典型错误。 没有能够变红的命令,就绝不能进入阶段 2。
阶段 2:复现与最小化 (Reproduce + minimise)
运行闭环。观察它在 Bug 出现时变红。
确认:
- 闭环产生的失败模式与用户描述的完全一致,而不是刚好发生在附近的另一个无关失败。错误的 Bug = 错误的修复。
- 失败在多次运行中均可复现(或者对于非确定性 Bug,其复现率足够高以支持调试)。
- 你已捕获确切的症状(错误信息、错误输出、耗时过长),以便后续阶段能够验证修复是否真正解决了该问题。
最小化 (Minimise)
一旦测试变红,就将复现场景缩减为仍能触发红灯的最小场景。每次缩减一项输入、调用方、配置、数据和步骤,并在每次缩减后重新运行闭环,仅保留导致失败所必不可少的核心要素。
为什么要这么做:最小化复现场景缩小了阶段 3 中的假设空间(减少了需要怀疑的不稳定组件),并能成为阶段 5 中干净的回归测试。
当所有剩余要素都是必不可少的(移除其中任何一个都会使闭环变绿)时,本步骤完成。
在完成复现和最小化之前,切勿继续推进。
阶段 3:提出假设 (Hypothesise)
在测试任何假设之前,先生成 3–5 个按可能性排序的假设。仅生成单个假设容易让人固步自封在第一个看似合理的想法上。
每个假设都必须是可证伪的:明确说明该假设所做出的预测。
格式:“如果是 <原因 X> 导致的,那么 <修改 Y> 将使 Bug 消失 / <修改 Z> 将使 Bug 恶化。”
如果你无法给出预测,该假设就只是凭空猜测:请将其舍弃或进一步具体化。
在开始测试前,向用户展示排序后的假设列表。 用户通常具备能瞬间改变排序优先级的领域知识(“我们刚对第 3 项涉及的代码发布了变更”),或者知道哪些假设已经被排除。这是一个成本极低却能大幅节省时间的检查点。不要为此阻塞流程;如果用户暂时离开 (AFK),可按你自己的排序继续推进。
阶段 4:插桩分析 (Instrument)
每一次探测都必须对应阶段 3 中的某一个具体预测。每次只改变一个变量。
工具选择优先级:
- 调试器 (Debugger) / REPL 检查(如果运行环境支持)。一个断点胜过十条日志。
- 定向日志:打在能够区分不同假设的边界位置。
- 绝不要“打印所有日志然后去 grep”。
为每条调试日志添加唯一前缀标签,例如 [DEBUG-a4f2]。这样在最后清理时只需一次 grep 即可。无标签的日志容易被遗留;带标签的日志则能被彻底清除。
性能分支。 对于性能退化问题,打日志通常是不对的。正确的做法是:建立基准测量(计时脚手架、performance.now()、分析器 profiler、查询计划),然后进行二分排查。先度量,后修复。
阶段 5:修复与回归测试 (Fix + regression test)
在修复之前编写回归测试,但前提是必须存在合适的切入点 (seam)。
合适的切入点是指测试能够模拟真实调用场景下的实际 Bug 模式。如果唯一可用的切入点过于浅显(例如:当 Bug 需要多个调用方共同触发时只有单调用方测试,或者单元测试无法复现触发 Bug 的调用链),在该处编写的回归测试只会带来虚假的安全感。
如果不存在合适的切入点,这本身就是一个重要发现。 请记录下来。这意味着代码库的架构正在阻碍对该 Bug 的有效防护。请在下一阶段中对此进行标记。
如果存在合适的切入点:
- 将最小化复现场景转化为该切入点处失败的测试。
- 观察测试失败(变红)。
- 应用修复方案。
- 观察测试通过(变绿)。
- 针对原始(未最小化)场景重新运行阶段 1 的反馈闭环。
阶段 6:清理 (Cleanup)
在声明完成之前必须满足以下条件:
- 原始复现场景不再复现(重新运行阶段 1 的闭环)
- 回归测试通过(或已记录缺乏切入点的情况)
- 移除所有
[DEBUG-...]插桩代码(grep该前缀确认) - 删除一次性原型代码(或移动到明确标记的调试位置)
- 在 commit / PR 信息中说明被证实的正确假设,以便后续的调试人员学习参考