# Diagnosing Bugs

> 难调的 bug 和性能回归的诊断流程。当用户说"diagnose"/"debug 一下"，或报告某东西 broken / throwing / failing / slow 时使用。

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

---


# 诊断 bug

一套对付难调 bug 的纪律。只有在明确说出理由时才能跳过阶段。

探索代码库时，先读 `CONTEXT.md`（若存在）建立相关模块的心智模型，并查你将要改动区域的 ADR。

## 脱敏

本 skill 会让你展示命令、输出和抓取到的产物。**先给每个密钥脱敏**——在它原位写 `<REDACTED>`。回路对着环境变量搭，让凭证留在环境里，而不是留在你展示的东西里。抓取的产物带 auth header：只引用承载信号的那几行。

如果脱敏后的输出不足以诊断 bug，说明情况，问用户。

## 阶段 1 — 建立反馈回路

**这一步就是本 skill 的全部。** 其余都是机械操作。如果你有一个**紧**的通过/失败信号——一个在*这个* bug 上会变红的信号——你一定能找到根因；二分、验证假设、加打点，全都只是在用它。没有这样一个信号，盯着代码看到天亮也没用。

在这里投入不成比例的精力。**积极点。有创意点。别放弃。**

### 怎么搭一个回路——大致按这个顺序试

1. **失败的测试**，落在能触达 bug 的任意接口——单元、集成、端到端。
2. **curl / HTTP 脚本**，打本地 dev server。
3. **CLI 调用**，喂一个 fixture 输入，把 stdout 和一个已知正确的快照做 diff。
4. **无头浏览器脚本**（Playwright / Puppeteer）——驱动 UI，对 DOM / console / network 做断言。
5. **回放一条抓到的 trace。** 把一个真实的网络请求 / payload / 事件日志存到磁盘，单独把它喂回代码路径。
6. **一次性脚手架。** 起一个系统的最小子集（一个服务、依赖打桩），用一次函数调用走通出 bug 的代码路径。
7. **性质测试 / fuzz 回路。** 如果 bug 是"偶尔输出错"，跑 1000 个随机输入找失败模式。
8. **二分脚手架。** 如果 bug 出现在两个已知状态之间（commit、数据集、版本），把"启动到状态 X、检查、再来"自动化，好让 `git bisect run` 接管。
9. **差分回路。** 同一输入跑老版本 vs 新版本（或两套配置），diff 输出。
10. **HITL bash 脚本。** 最后手段。如果非得人手点，用 `scripts/hitl-loop.template.sh` 驱动*他们*，让回路仍然有结构。抓到的输出再喂回给你。

搭对反馈回路，bug 就修好了九成。

### 收紧回路

把回路当成一个产品来对待。一旦有了*一个*回路，就**收紧**它：

- 能不能更快？（缓存 setup、跳过无关初始化、收窄测试范围。）
- 信号能不能更锋利？（断言具体的症状，而不是"没崩"。）
- 能不能更确定？（钉住时间、固定 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`。

如果你发现这条命令还不存在、自己就开始读代码构建理论，**停下——直接跳到假设，正是本 skill 要防止的那个失败。** 没有能变红的命令，就没有阶段 2。

## 阶段 2 — 复现 + 缩小

跑回路。看它变红——bug 出现了。

确认：

- [ ] 回路产出的是**用户**描述的那个失败模式——不是恰好挨着的另一个失败。搞错 bug = 修错地方。
- [ ] 多次跑都能复现（或：偶发 bug 以足够高的复现率出现，能拿来调）。
- [ ] 已经抓下了精确的症状（错误信息、错误输出、慢的耗时），后面阶段才能验证修复确实对上了。

### 缩小

一旦变红，把复现缩到**仍然会变红的最小场景**。一次砍掉一项——输入、调用方、配置、数据、步骤，每砍一项就重跑一次回路——只留下对失败真正承重的。

为什么要费这个劲：最小复现缩小了阶段 3 的假设空间（剩下的可疑动件更少），并在阶段 5 成为干净的回归测试。

完成于**剩下的每一项都承重**——去掉任何一项，回路都会变绿。

没有复现**且**没有缩小之前，别往下走。

## 阶段 3 — 假设

测试之前，先给出 **3–5 条排好序的假设**。只生成一条假设，会把你锚在第一个听起来靠谱的念头上。

每条假设必须**可证伪**：说出它做出的预测。

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

如果说不清预测，这条假设只是个感觉——丢掉，或磨利它。

**测试之前，把排好序的清单给用户看一眼。** 他们常有领域知识能立刻重排（"我们刚把 #3 那块部署上去了"），或知道哪些假设已经被排除。便宜的检查点，省大把时间。用户不在就别等——按你的排序继续。

## 阶段 4 — 打点

每一处探针对应阶段 3 的一条具体预测。**一次只改一个变量。**

工具优先级：

1. **调试器 / REPL 检查**，环境支持的话。一个断点抵十条 log。
2. **针对性的 log**，打在能区分各假设的边界上。
3. 永不"什么都 log 再 grep 筛选"。

**给每条 debug log 打一个唯一前缀**，比如 `[DEBUG-a4f2]`。收尾时一次 grep 就能清干净。没打标签的 log 会活下来；打了标签的会死掉。

**性能分支。** 对性能回归，log 多半是错的。改成：建立基线测量（计时脚手架、`performance.now()`、profiler、query plan），然后二分。先量，再修。

## 阶段 5 — 修复 + 回归测试

**先于修复**写回归测试——但前提是存在一个**正确的接口**。

正确的接口，是测试能在调用点真正演习 bug 发生模式的接口。如果手头唯一的测试面太浅（bug 需要多个调用方、却只有单调用方测试；bug 需要整条调用链、单元测试却复现不了），在那里加回归测试只会给虚假信心。

**如果没有正确的接口，这本身就是一条发现。** 记下来。代码库的架构正在挡住这个 bug 被锁住。标给下一阶段。

存在正确的接口时：

1. 把缩小后的复现变成那个接口上的失败测试。
2. 看它挂。
3. 上修复。
4. 看它过。
5. 用阶段 1 的回路，对着原始（未缩小的）场景再跑一遍。

## 阶段 6 — 收尾 + 复盘

宣布完成之前必做：

- [ ] 原始复现不再复现（重跑阶段 1 的回路）
- [ ] 回归测试通过（或：接口缺失已被记录）
- [ ] 所有 `[DEBUG-...]` 打点已移除（`grep` 前缀）
- [ ] 一次性原型已删（或挪到一个明确标注的 debug 位置）
- [ ] 最终被验证为对的那条假设，写进了 commit / PR message——让下一个调它的人有得学

**然后问一句：什么本可以预防这个 bug？** 如果答案涉及架构改动（没有好的测试接口、调用方纠缠、隐藏的耦合），把这个建议留在修复落地**之后**再给，不是之前——你现在的信息比开工时多。

