| name | diagnosing-bugs |
| description | 针对疑难 bug 和性能回归的诊断循环。当用户说"诊断"/"调试这个",或报告有东西损坏/抛出异常/失败/变慢时使用。 |
诊断 Bug
针对疑难 bug 的规程。仅在明确有理由时才跳过阶段。
探索代码仓时,阅读 CONTEXT.md(如果存在)以获取相关模块的清晰心智模型,并检查你接触区域的 ADR。
阶段 1 — 构建反馈回路
这就是核心技能。 其他一切都是机械性的。如果你针对 bug 有一个紧凑的通过/失败信号 — 一个在_此_ bug 上会变红的信号 — 你就会找到原因;二分查找、假设检验和插桩都只是消费它。如果你没有一个这样的信号,再盯着代码看也救不了你。
在此投入不成比例的精力。要激进。要有创意。拒绝放弃。
构建方式 — 按大致顺序尝试
- 失败测试,位于能触及 bug 的任意缝合点 — 单元、集成、端到端。
- Curl / HTTP 脚本,针对正在运行的开发服务器。
- CLI 调用,带固定输入,将 stdout 与已知良好快照进行 diff。
- 无头浏览器脚本(Playwright / Puppeteer)— 驱动 UI,对 DOM/控制台/网络进行断言。
- 回放捕获的追踪数据。 将真实网络请求 / 负载 / 事件日志保存到磁盘;在隔离环境中通过代码路径回放。
- 一次性测试夹具。 启动系统的最小子集(一个服务、模拟依赖),用单个函数调用来驱动 bug 代码路径。
- 属性 / 模糊测试循环。 如果 bug 是"有时输出错误",运行 1000 次随机输入,寻找故障模式。
- 二分查找夹具。 如果 bug 出现在两个已知状态之间(commit、数据集、版本),自动化"在状态 X 启动、检查、重复"以便
git bisect run。
- 差分循环。 通过旧版本 vs 新版本(或两种配置)运行相同输入,对输出进行 diff。
- HITL bash 脚本。 最后手段。如果必须由人工点击,用
scripts/hitl-loop.template.sh 驱动_他们_,使循环仍然结构化。捕获的输出反馈给你。
构建了正确的反馈回路,bug 就修复了 90%。
收紧回路
将回路视为产品。一旦你有了_一个_回路,收紧它:
- 能更快吗?(缓存设置,跳过无关初始化,缩小测试范围。)
- 信号能更清晰吗?(针对具体症状断言,而非"没崩溃"。)
- 能更确定性吗?(固定时间、种子随机数、隔离文件系统、冻结网络。)
一个 30 秒的抖动回路比没有回路好不了多少;一个 2 秒的确定性回路才是紧凑的 — 这是调试的超能力。
非确定性 bug
目标不是干净的复现,而是更高的复现率。循环触发 100 次,并行化,增加压力,缩小时间窗口,注入 sleep。50% 抖动的 bug 是可调试的;1% 则不行 — 持续提高复现率直到可调试。
当确实无法构建回路时
停下来,明确说明。列出你尝试过的方法。向用户请求:(a) 访问能复现的任何环境,(b) 一个捕获的产物(HAR 文件、日志转储、核心转储、带时间戳的屏幕录制),或 (c) 添加临时生产环境插桩的许可。不要在没有回路的情况下进入假设阶段。
完成标准 — 一个会变红的紧凑回路
阶段 1 完成的条件是回路紧凑且具备变红能力:你能说出一条命令 — 一个脚本路径、一个测试调用、一个 curl — 你已经至少运行过一次(粘贴调用及其输出),并且该命令满足:
如果你发现自己在回路存在之前阅读代码来构建理论,停下来 — 直接跳到假设正是本技能要防止的确切失败模式。 没有变红能力的命令,就没有阶段 2。
阶段 2 — 复现 + 最小化
运行回路。看着它变红 — bug 出现。
确认:
最小化
一旦变红,将复现场景缩小到仍能变红的最小场景。逐个削减输入、调用者、配置、数据和步骤,每次削减后重新运行回路 — 只保留对故障有负载作用的部分。
为什么费这个劲:最小复现场景缩小了阶段 3 的假设空间(值得怀疑的移动部件更少),并成为阶段 5 的干净回归测试。
完成条件是每个剩余元素都有负载作用 — 移除其中任何一个都会使回路变绿。
在复现且最小化之前不要继续。
阶段 3 — 提出假设
在测试任何假设之前生成 3-5 个排名假设。单一假设生成会锚定在第一个看似合理的想法上。
每个假设必须是可证伪的:陈述它的预测。
格式:"如果 是原因,那么 <改变 Y> 会使 bug 消失 / <改变 Z> 会使它更糟。"
如果你无法陈述预测,这个假设只是感觉 — 丢弃或精炼它。
在测试之前向用户展示排名列表。 他们通常拥有能立即重新排名的领域知识("我们刚刚部署了对第 3 项的改动"),或知道他们已经排除的假设。低成本检查点,大幅节省时间。如果用户 AFK,不要等待 — 按你的排名继续。
阶段 4 — 插桩
每个探测必须映射到阶段 3 中的一个具体预测。每次只改变一个变量。
工具偏好:
- 调试器 / REPL 检查,如果环境支持。一个断点胜过十行日志。
- 针对性日志,在能区分假设的边界处。
- 永远不要"记录一切然后 grep"。
用唯一前缀标记每条调试日志,例如 [DEBUG-a4f2]。最后的清理只需一次 grep。未标记的日志保留;已标记的日志删除。
性能分支。 对于性能回归,日志通常是错误的。替代方案:建立基线测量(计时夹具、performance.now()、分析器、查询计划),然后二分查找。先测量,后修复。
阶段 5 — 修复 + 回归测试
在修复之前编写回归测试 — 但仅当存在正确的缝合点时才这样做。
正确的缝合点是指测试能在调用点处驱动真实的 bug 模式。如果唯一可用的缝合点太浅(bug 需要多个调用者时却只有单调用者测试,单元测试无法复现触发 bug 的调用链),在那里的回归测试会给出虚假的信心。
如果不存在正确的缝合点,这本身就是发现。 记录下来。代码仓架构正在阻止锁定此 bug。将此标记给下一阶段。
如果存在正确的缝合点:
- 将最小复现转为该缝合点处的失败测试。
- 看着它失败。
- 应用修复。
- 看着它通过。
- 对原始(未最小化的)场景重新运行阶段 1 的反馈回路。
阶段 6 — 清理 + 事后分析
宣布完成前必须完成:
然后问:什么本可以预防这个 bug? 如果答案涉及架构变更(没有好的测试缝合点、纠缠的调用者、隐藏的耦合),将具体情况移交给 /improve-codebase-architecture 技能。在修复之后提出建议,而非之前 — 你现在比开始时拥有更多信息。