# Systematic Debugging

> 遇到 bug、测试失败、行为异常时使用。先定位根因,再改代码,禁止瞎试。

- Skill: `wade-devcode/systematic-debugging` (Agent Skill)
- Install (CLI): `npx skillmds@latest add wade-devcode/systematic-debugging`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wade-devcode/systematic-debugging/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Wade-DevCode (https://skillmd.com/u/wade-devcode)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/wade-devcode/systematic-debugging

---


# 系统化调试

## 何时用

- 运行测试时出现 `FAILED` / `ERROR`，或 CI 红了。
- 程序行为与预期不符：输出值错误、接口返回异常状态码、UI 渲染出错。
- 改了一处代码后原本正常的功能突然挂掉（回归）。
- 看到报错信息、异常堆栈、日志中的 `Exception` / `panic` / `Segfault`，不知道从哪里下手。

## 核心规则

### 1. 先复现

**规则：** 在动任何代码之前，先稳定复现问题，写下复现步骤与"期望行为 vs 实际行为"。

**为什么：** AI 拿到 bug 描述后会立刻联想到"可能是 X 原因"并直接改代码——但如果问题根本无法稳定复现，改动就是在打空拳。更常见的事故是：AI 改了某处，恰好该次运行没触发 bug，就宣称"已修复"，下次复现时问题依然存在。

**怎么做：**
- 明确记录触发条件：输入数据、环境变量、调用顺序、并发时序等。
- 写一个最小复现脚本或测试用例，能稳定触发问题再继续。
- 若问题无法稳定复现，先补充日志/断言，下一次触发时收集更多信息，而非盲猜。

---

### 2. 读真实报错

**规则：** 逐字阅读错误信息与完整堆栈，定位第一处出错的文件行号，不跳过、不脑补。

**为什么：** AI 经常只看错误的最后一行（如 `NullPointerException`），然后凭直觉猜"是不是哪个对象没初始化"并随意修改。真正的根因往往藏在堆栈中部——例如某个中间件吞掉了原始异常、某个工厂方法返回了错误类型——只读最后一行会让修复方向完全偏离。

**怎么做：**
- 从堆栈的**最顶层**（第一次抛出点）开始读，而不是从底层的框架代码开始。
- 遇到"Caused by"或"wrapped"链式异常，追到链条的根源。
- 把报错的关键词（函数名、行号、错误码）直接复制到搜索或代码跳转，不凭记忆定位。

---

### 3. 二分缩小范围

**规则：** 用打印/断点/注释二分法，把问题缩小到最小代码段，再下结论。

**为什么：** AI 倾向于在读了几十行代码后就"觉得问题在这里"，跳过验证直接改。这种直觉经常错：真实 bug 往往在你以为不可能出错的地方。不二分就不改，是避免"修了半天发现改错位置"的唯一可靠方法。

**怎么做：**
- 把可疑范围一分为二：注释掉后半段，确认前半段输出正确，再检查后半段。
- 在关键中间点插入 `print` / `console.log` / `assert`，确认数据在此时的真实状态。
- 重复缩小，直到能用不超过 10 行代码稳定触发问题，再动手修复。

---

### 4. 找根因，不贴补丁

**规则：** 能解释"为什么会错"之后再动手改代码。禁止用 `try/except` 吞异常、随机调参、加 `|| null` 等掩盖症状的做法。

**为什么：** AI 面对报错时最常见的逃生路线就是在外层套一个 `try/except`，让异常不再抛出，然后声称"问题解决了"。但根因没消除：数据仍然损坏、状态仍然不一致，只是沉默了。这类"修复"在生产环境会演变成更难排查的数据问题或静默错误。

**怎么做：**
- 改代码前用一句话写下根因假设：「变量 `user` 在首次调用时为 `None`，因为 `db.find()` 在记录不存在时返回 `None` 而非抛出异常。」
- 修复根因（加校验、修初始化逻辑），而不是在调用处加 `try/except` 掩盖。
- 若确实需要捕获异常，必须在 `except` 块里做有意义的处理（记录、回滚、向上重抛），绝不能空块或仅 `pass`。

---

### 5. 改完验证

**规则：** 用最初的复现步骤逐一确认问题已修复，并确认没有引入新的失败。

**为什么：** AI 改完代码后习惯性地说"应该好了"，但没有真正跑一遍。或者只跑了新加的测试，没跑已有的回归测试集，结果修了一个 bug、破了三个已有功能。

**怎么做：**
- 重新执行第 1 步写下的复现步骤，确认实际行为与期望行为一致。
- 跑完整测试套件（不只是相关测试），确认无新的 `FAILED`。
- 若引入了新失败，视为新 bug，回到第 1 步重新走流程，不要在同一次修改里叠加多个"顺手修复"。

---

## 正例 / 反例

### 反例：用 try/except 吞异常声称"已修复"

```python
# 反例 — AI 用空 except 消灭报错，根因未解决
def get_user_age(user_id: int) -> int:
    try:
        user = db.find_user(user_id)
        return user["age"]          # user 为 None 时会 TypeError
    except Exception:
        pass                        # ❌ 吞掉异常，调用方收到 None，数据链路静默损坏
```

```python
# 正例 — 定位根因：db.find_user 在记录不存在时返回 None，修复初始化逻辑
def get_user_age(user_id: int) -> int:
    user = db.find_user(user_id)
    if user is None:
        raise ValueError(f"用户 {user_id} 不存在")   # ✅ 明确报错，调用方能感知并处理
    return user["age"]
```

---

### 反例：不读堆栈、凭直觉改错位置

```python
# 错误日志：
# AttributeError: 'NoneType' object has no attribute 'email'
#   File "app/views.py", line 42, in send_welcome
#     msg = build_email(request.user)
#   File "app/email.py", line 17, in build_email
#     return Template(user.email)           # ← 第一次出错点在这里

# 反例 — AI 只看到 AttributeError 就在视图层加了防御
def send_welcome(request):
    if request.user is None:               # ❌ request.user 其实从不为 None，
        return                             #    根因是 build_email 接收了未登录状态下的匿名对象
    ...
```

```python
# 正例 — 读完堆栈，定位到 build_email 第 17 行，找到根因：
# 匿名用户对象没有 email 属性，而非 request.user 为 None
def build_email(user):
    if not hasattr(user, "email") or user.email is None:
        raise ValueError("无法为匿名用户构建邮件")   # ✅ 在真正出错点做校验
    return Template(user.email)
```

---

## 自查清单

- [ ] 已写下稳定的复现步骤，并能在改代码前稳定触发问题。
- [ ] 已逐字读完完整堆栈，定位到第一处出错的文件与行号。
- [ ] 已通过二分或打印确认问题所在的最小代码段，而非凭直觉猜测。
- [ ] 能用一句话解释根因（"因为 X，所以 Y"），而不只是描述症状。
- [ ] 修复方式针对根因，没有用空 `except`、`|| null`、`if err != nil { return }` 等手段掩盖问题。
- [ ] 改完后重新执行了最初的复现步骤，确认实际行为与期望一致。
- [ ] 运行了完整测试套件，确认没有引入新的失败。

