小黄鸭调试法

本页提供可直接复制的 5W1H 调试模板、最小复现(MRE)构建步骤、给 Agent 描述 Bug 的标准模板、常见思维陷阱克服方法,以及有效调试日志记录格式。

SKILL 应提供提问模板:输入是什么、期望输出是什么、实际在哪一步偏离、曾排除过哪些原因。下面分节说明节奏、与 Agent 的分工,以及如何把口述压成可复制的短脚本。

何时使用 + 5W1H 分析模板

适合逻辑分支多、状态隐含在脑子里、或「感觉不对但说不清」的场景;不适合已通过日志明确断言失败且栈已指到行号的情况(那时直接对着证据改更快)。

信号:你在脑内跳步、或口头解释与 IDE 里选中代码不一致——这就是小黄鸭要打断的地方。

5W1H 分析模板(可直接复制到任何调试会话开始时填写):

## Bug 5W1H 分析

**What(期望行为)**:
用户点击「提交订单」后,应该跳转到订单确认页面,并展示订单号

**What(实际行为)**:
页面显示空白,控制台报错:TypeError: Cannot read properties of undefined (reading 'id')
网络面板:POST /api/orders 返回 201,响应体包含 orderId

**When(什么时候发生)**:
只在「优惠码」字段填写后提交时复现;不填优惠码则正常

**Where(哪个模块/文件/行)**:
checkout/OrderConfirm.tsx 第 42 行:
  const { id } = order.coupon;   ← 当 coupon 为 null 时崩溃

**Why(目前的假设)**:
order.coupon 在没有使用优惠码时是 null,而不是 {},
但代码没有做 null check

**How(复现步骤)**:
1. 登录账号
2. 添加任意商品到购物车
3. 在结账页输入优惠码「TEST10」
4. 点击提交订单 → 白屏

**已尝试的方案**:
- 已检查 API 响应:coupon 字段在无优惠码时确实返回 null(不是 {})
- 已尝试 optional chaining:order?.coupon?.id → 不崩溃但跳转逻辑还有问题

口述与首个分歧点

每段只覆盖少量语句或调用链;先说出「读入什么、期望什么」,再一行行往下,直到第一个与预期不符的中间状态。不要一次讲完整个模块。

  [ 陈述:输入 / 期望输出 / 实际现象 ]
                    │
                    ▼
              [ 逐行或逐调用向下 ]
                    │
           ┌────────┴────────┐
           ▼                 ▼
    [ 中间状态一致? ]   [ 发现不一致 ]
           │                    │
           │ yes                ▼
           │            [ 停:这是首个分歧点 ]
           │                    │
           └──────────► [ 写假设 + 最小验证 ]
                                    │
                                    ▼
                            [ 证实 / 证伪 → 更新心智模型 ]
  • 发现假设错误时,把原结论记进注释或 issue,避免下次再踩。
  • 并发问题:口述必须显式线程、锁、内存顺序,不能省略「谁先谁后」。

与 Agent 协作

可配置两种模式:只听不问——Agent 只复述听到的逻辑并标出跳跃;只问不猜——在信息不足时提问,不输出大段补丁。若口述与代码不一致,优先指出不一致处,再讨论是否改代码。

对 Agent:在对方口述时禁止急于给补丁;先复述理解并标出逻辑跳跃。人类方若发现 Agent 在脑补,应打断并要求引用具体符号或行范围。

最小复现(MRE)构建步骤

建议与「Bug 复现」类技能串联:先口述复现路径,再口述代码路径,最后才缩小到最小用例。顺序错了容易在错误分支上「小黄鸭」很久。

最小复现(MRE)构建步骤:

步骤 1:确认 Bug 在完整应用中可稳定复现
  → 录制操作步骤或写自动化测试用例复现

步骤 2:移除与 Bug 无关的外部依赖
  → 把 API 调用替换为 mock/stub(使用 msw 或 jest.fn)
  → 移除认证层(用 hardcoded 测试用户)
  → 移除其他 feature flag

步骤 3:最小化数据量
  → 把 100 条测试数据减少到 1-2 条能触发 bug 的数据
  → 找出触发 bug 的最少字段(如:只有填了 coupon 才触发)

步骤 4:隔离为单个函数测试
  → 写一个 unit test,直接调用出问题的函数
  it('should handle null coupon', () => {
    const order = { id: '123', coupon: null };
    expect(() => renderConfirmPage(order)).not.toThrow();
  });

步骤 5:若仍无法复现,对比环境差异
  → Node 版本(node -v)
  → 依赖版本(package-lock.json diff)
  → 环境变量(process.env.FEATURE_FLAG)
  → 浏览器版本/操作系统

给 Agent 描述 Bug 的标准模板(可直接复制使用):
---
我遇到了一个 Bug,请帮我分析原因。

**环境**:Node 20.x / React 18 / TypeScript 5.x
**文件**:src/checkout/OrderConfirm.tsx,第 42 行
**错误信息**:TypeError: Cannot read properties of undefined (reading 'id')
**最小复现代码**:
  const order = { id: '123', coupon: null };
  const { id } = order.coupon;  // ← 这里崩溃

**期望行为**:当 coupon 为 null 时,id 应该是 undefined 而不是抛出异常
**实际行为**:直接崩溃,TypeError
**我的假设**:需要 null check,但我不确定应该在哪一层处理
**已尝试**:optional chaining (`order?.coupon?.id`) 解决了崩溃,但后续跳转逻辑仍有问题
---

常见思维陷阱及克服方法

陷阱 1:确认偏见(只看支持自己假设的证据)
  症状:「我觉得是缓存问题,所以只去查缓存日志」
  克服:先列出 3 个互相排斥的假设,再分别找证据证伪
  检验:能找到否定当前假设的证据吗?

陷阱 2:环境差异(本地好的以为全局好)
  症状:「我本地能跑,CI 挂了肯定不是我的代码问题」
  克服:用 docker 复现 CI 环境:docker run --rm -v $(pwd):/app node:20 sh -c "cd /app && npm ci && npm test"
  检验:把环境变量完整列出对比

陷阱 3:先入为主(认定是 A 模块的问题,不看 B 模块)
  症状:花了 2 小时调 A 模块,最后发现是 B 模块传入了错误数据
  克服:从错误栈最底层往上追,不要从「最可疑的地方」开始
  检验:在错误发生的精确行号上打断点,而不是猜测位置

调试日志的有效记录格式

// 有效的调试日志(可定位问题):
console.log('[validateCoupon] input:', {
  couponCode: code,        // 精确输入值
  userId,                  // 上下文
  timestamp: Date.now()    // 时间戳(排查竞态)
});
console.log('[validateCoupon] db result:', {
  found: !!coupon,
  expired: coupon?.expiresAt < Date.now(),
  usageCount: coupon?.usageCount  // 关键状态
});

// 无效的调试日志(无法定位):
console.log('here');       // 无上下文
console.log(data);         // 整个对象,不知道关注哪里
console.log('error!');     // 没有错误类型和值

口述脚本拼装

填写下面三行,点击生成,可得到一段可直接粘贴到对话或 PR 的短脚本;内容仅保存在本页浏览器(localStorage),不上传。


            
---
name: rubber-duck-debug
description: 5W1H 调试分析、MRE 构建、思维陷阱识别与 Agent 协作
---

# 步骤 1:填写 5W1H 模板(开始前必须完成)
What(期望行为): ___
What(实际行为): 含错误信息/错误码/异常堆栈
When(触发条件): 只在 ___ 条件下发生
Where(精确位置): 文件:行号
Why(当前假设): 最多 3 个互斥假设
How(复现步骤): 最少步骤的操作序列

# 步骤 2:确认 Bug 可稳定复现
连续复现 3 次后再开始分析
记录复现成功率(100% / 偶发 / 仅特定用户)

# 步骤 3:构建 MRE(最小复现)
从完整应用开始,逐步移除无关依赖:
a. 替换 API 调用为 mock(msw / jest.fn)
b. 移除认证层,用 hardcoded 测试数据
c. 最小化数据:找到触发 bug 的最少字段
d. 写 unit test 直接测出问题的函数

# 步骤 4:逐行口述(找第一个分歧点)
从函数入口开始,每步说出:
「我期望 X,实际得到 Y,因为 Z」
遇到第一个不一致时停下来,不要往后走

# 步骤 5:排除思维陷阱
检查确认偏见:我有没有只看支持假设的证据?
检查环境差异:在 CI/Docker 环境中能复现吗?
检查先入为主:我从错误栈底层往上追了吗?

# 步骤 6:记录结论
根本原因(一句话): ___
修复方案: ___
预防措施(测试/lint规则): ___
写入 PR 描述或 issue,避免同类 bug 再次出现

返回技能库 更多技能入口