# Diagnosing Bugs

> 针对棘手 bug 和性能回归的诊断循环。当用户说"诊断"/"调试这个"，或报告某处崩溃/报错/不正常/缓慢时使用。

- Skill: `devcxl/diagnosing-bugs` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add devcxl/diagnosing-bugs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/devcxl/diagnosing-bugs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: devcxl (https://skillmd.com/u/devcxl)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/devcxl/diagnosing-bugs

---


# 诊断 Bug

针对棘手 bug 的一条纪律：只有显式说明正当理由时才能跳过某个阶段。

在探索代码库时，读取 `CONTEXT.md`（如果存在）以获得相关模块的清晰心智模型，并查看你所触及区域的 ADR。

## 脱敏

本技能会让你展示命令、输出和捕获的产物。**先脱敏所有秘密**：用 `<REDACTED>` 替换。针对环境变量构建循环，这样凭证留在环境中而不是出现在你展示的内容里。捕获的产物可能携带认证头：只引用携带信号的若干行。

如果脱敏后的输出不足以诊断 bug，要明确说明，并请用户提供更多材料。

## Phase 1：构建反馈循环

**这才是这个技能本身。** 其它一切都只是机械动作。如果你对这个 bug 有一条**紧密**的通过/失败信号（一条会针对_这个_ bug 变红的信号），你就能找到根因；二分、假设检验、插桩都只是这条信号的消费者。如果你没有这条信号，盯着代码看到天荒地老也救不了你。

在这一步投入不成比例的精力。**要激进。要有创意。绝不放弃。**

### 构建反馈循环的若干方式（大致按此顺序）

1. **失败测试**：在能触及 bug 的任何 seam 上写——unit、integration、e2e。
2. **Curl / HTTP 脚本**：针对正在运行的 dev server。
3. **CLI 调用**：使用固定输入，把 stdout 与已知正常快照做 diff。
4. **无头浏览器脚本**（Playwright / Puppeteer）：驱动 UI 并断言 DOM/console/network。
5. **重放已捕获的 trace。** 把真实的网络请求 / payload / 事件日志落盘，单独通过代码路径重放。
6. **一次性 harness。** 拉起系统最小子集（一个服务、mock 掉依赖），用一次函数调用就能触发 bug 代码路径。
7. **属性 / fuzz 循环。** 如果 bug 是"有时输出不对"，跑 1000 个随机输入，观察失败模式。
8. **二分 harness。** 如果 bug 出现在两个已知状态（commit、数据集、版本）之间，自动化"以状态 X 启动、检查、重复"，便于 `git bisect run`。
9. **差分循环。** 把同一输入分别跑过老版本和新版本（或两种配置），对比输出。
10. **HITL bash 脚本。** 最后的手段。如果必须由人来点击，就用 `scripts/hitl-loop.template.sh` 来驱动_他们_，这样循环仍是结构化的。捕获到的输出再反馈给你。

把反馈循环做对了，bug 已经解决了 90%。

### 收紧循环

把循环当作产品。一旦你有了_一条_循环，就**收紧**它：

- 能不能让它更快？（缓存初始化、跳过无关 init、缩小测试范围。）
- 能不能让信号更尖锐？（针对具体症状做断言，而不是"没有崩溃"。）
- 能不能让它更确定？（固定时间、播种 RNG、隔离文件系统、冻结网络。）

30 秒的 flaky 循环只比没有循环强一点点；2 秒、确定性的循环才是真正紧凑的——是调试的超能力。

### 非确定性 bug

目标不是干净的复现，而是**更高的复现率**。把触发条件循环跑 100 轮，并行化、增加压力、收紧时窗、注入 sleep。一个 50% 复现率的 flaky bug 是可调试的；1% 不行，所以持续把复现率抬到可调试为止。

### 当你真的建不出循环时

停下来，并明确说出来。列出你尝试过的所有办法。请用户提供：(a) 能复现该 bug 的环境的访问权限，(b) 一份脱敏后的捕获产物（HAR 文件、日志 dump、core dump、带时间戳的录屏），或 (c) 允许你在生产环境加临时插桩的授权。**不要**在没有循环的情况下进入空谈理论。

### 完成判据：一条紧凑、能变红的循环

Phase 1 完成的标志是循环**紧凑**且**能变红**：你能点出**一条命令**（脚本路径、一次测试调用、一条 curl），并**至少已经实际跑过一次**（给出调用与已脱敏的输出），并且它满足：

- [ ] **能变红（Red-capable）**：驱动真正的 bug 代码路径，并对**用户描述的精确症状**做断言——所以它能对这个 bug 变红，而修复后变绿。不是"不报错"；它必须能_抓住这个具体 bug_。
- [ ] **确定性**：每次跑都得到同样的判定（flaky bug：按上文固定到高复现率）。
- [ ] **快速**：秒级，不是分钟级。
- [ ] **Agent 可跑**：你可以在无人值守时跑；只有通过 `scripts/hitl-loop.template.sh` 时才在环里放一个人。

如果你在写出这条命令之前就已经开始读代码、构建理论，**停下：跳过假设直接行动正是本技能要防止的失败模式。** 没有能变红的命令，就没有 Phase 2。

## Phase 2：复现 + 最小化

跑循环。看着它因 bug 出现而变红。

确认：

- [ ] 循环产生的是**用户**所描述的失败模式，而不是恰好在附近的另一种失败。找错 bug = 修错 bug。
- [ ] 该失败在多次运行中可复现（或对非确定性 bug 而言，复现率高到可以基于它调试）。
- [ ] 你已经捕获到精确症状（错误信息、错误输出、慢的耗时），以便后续阶段可以验证修复确实对症。

### 最小化

一旦它变红，就把复现例子收缩到**仍然会变红的最小场景**。逐个裁剪输入、调用者、配置、数据和步骤，每次裁剪后重新跑循环，只保留对失败承重的部分。

为什么要做：最小化的复现例子压缩了 Phase 3 的假设空间（剩下来需要怀疑的活动部件更少），同时在 Phase 5 中又成为干净的回归测试。

完成的标志是**剩下的每个元素都承重**：移除任何一项都会让循环变绿。

在复现**并**最小化都完成之前，不要进入下一阶段。

## Phase 3：列假设

在测试任何假设之前，生成**3–5 个排序后的假设**。只生成单个假设会锚定在第一个看似合理的想法上。

每个假设必须**可证伪**：明确陈述它做出的预测。

> 格式："如果 <X> 是原因，那么 <改变 Y> 会让 bug 消失 / <改变 Z> 会让它更严重。"

如果说不清预测，那这只是 vibe：丢掉或重新打磨它。

**在测试之前把排序后的列表展示给用户。** 他们经常拥有些能瞬间重新排序的领域知识（"我们刚部署了一个改动到 #3"），或者知道他们已经排除掉的假设。这是个廉价的检查点，但能省下大量时间。别阻塞在用户身上；如果用户 AFK，就按你自己的排序继续推进。

## Phase 4：插桩

每一次探测都必须对应 Phase 3 中某个具体的预测。**一次只改一个变量。**

工具偏好：

1. **Debugger / REPL 检查**：环境支持的话就用。一个断点胜过十条日志。
2. **针对性日志**：放在能区分假设的边界处。
3. 永远不要"全部打日志再 grep"。

**为每条调试日志打上唯一前缀**，例如 `[DEBUG-a4f2]`。最后的清理就变成一次 grep。不带前缀的日志会活下来，带前缀的会死掉。

**性能分支。** 对性能回归，日志通常不合适。改为：先建立基线测量（计时 harness、`performance.now()`、profiler、查询计划），再做二分。先测，再修。

## Phase 5：修复 + 回归测试

把回归测试**写在修复之前**，但前提是存在**正确的 seam**。

正确的 seam 是指测试能在调用现场触发的位置**真实复现 bug 模式**。如果唯一可用的 seam 太浅（单调用方测试，但 bug 需要多个调用方；unit 测试无法复现触发 bug 的整条链路），那里的回归测试只会带来虚假信心。

**如果不存在正确的 seam，这就是发现本身。** 记下来。代码库架构正在阻止 bug 被锁定。把这标记到下一阶段。

如果存在正确的 seam：

1. 把最小化的复现例子变成该 seam 上的一个失败测试。
2. 看着它失败。
3. 实施修复。
4. 看着它通过。
5. 重新跑 Phase 1 反馈循环，验证原始（未最小化的）场景。

## Phase 6：清理

宣布完成前必须做的事项：

- [ ] 原始复现已不再复现（重跑 Phase 1 循环）
- [ ] 回归测试通过（或者 seam 缺失已记录在案）
- [ ] 所有 `[DEBUG-...]` 插桩已移除（`grep` 该前缀）
- [ ] 一次性原型已删除（或移到显式标记为 debug 的位置）
- [ ] 真正成立的假设写进了 commit / PR 信息，方便下一个调试者学习

