系统化调试
使用时机
当你需要采用四阶段调试方法论并进行根因分析时使用本技能。在调查 bug、修复测试失败或排查意外行为时使用。强调未经根因调查不得修复。
核心原则
未经根因调查,不得修复。
切勿使用针对症状的补丁掩盖潜在问题。在尝试修复之前,先理解失败的原因。
四阶段框架
阶段一:根因调查
在动手修改任何代码之前:
- 仔细阅读错误信息——每个字都至关重要
- 稳定复现问题——如果无法复现,就无法验证修复
- 检查最近的变更——问题开始出现之前做了什么改动?
- 收集诊断证据——日志、堆栈跟踪、状态转储
- 追踪数据流——沿调用链向上回溯,定位无效值的来源
根因追踪技巧:
1. 观察症状——错误在何处显现?
2. 找出直接原因——哪段代码直接产生了该错误?
3. 追问"谁调用了它"——向上绘制调用链
4. 持续向上追踪——沿调用栈反向追踪无效数据
5. 定位最初触发点——问题真正从何处开始?
关键原则: 切勿仅在错误显现处修复问题——务必追溯到最初的触发点。
阶段二:模式分析
- 定位可正常工作的示例——找出表现正确的同类代码
- 完整对比实现——不能只是粗略浏览
- 识别差异——正常代码与故障代码有何不同?
- 理解依赖关系——这段代码依赖哪些东西?
阶段三:假设与测试
运用科学方法:
- 提出一个清晰的假设——"错误发生的原因是 X"
- 设计最小化测试——每次只改变一个变量
- 预测结果——若假设正确,应当发生什么?
- 执行测试——运行并观察
- 验证结果——表现是否与预测一致?
- 迭代或推进——假设错误则修正,假设正确则实施
阶段四:实施
- 创建失败的测试用例——捕获 bug 的行为
- 实施单一修复——针对根因而非症状
- 验证测试通过——确认修复有效
- 运行完整测试套件——确保无回归
- 若修复失败,立即停止——重新评估假设
关键规则: 若连续三次或以上修复失败,立即停止。这表明存在需要讨论的架构问题,而非再加一层补丁。
危险信号——流程违规
若察觉自己有以下念头,立即停下:
- "先快速修一下,稍后再调查"
- (在多次失败后)"再试一次修复"
- "这个应该能行"(却未理解原因)
- "我就试试……"(未提出假设)
- "在我机器上是好的"(却未调查差异)
深层问题的警示信号
连续修复在不同区域暴露新问题,表明存在架构层面的问题:
- 停止打补丁
- 记录已发现的情况
- 与团队讨论后再继续
- 思考是否需要重新审视设计
常见调试场景
测试失败
1. 阅读完整的错误信息与堆栈跟踪
2. 定位是哪条断言失败及其原因
3. 检查测试设置——测试环境是否正确?
4. 检查测试数据——mock/夹具是否正确?
5. 追溯到异常值的来源
运行时错误
1. 捕获完整的堆栈跟踪
2. 定位抛错的代码行
3. 检查哪些值为 undefined/null
4. 向上回溯,找到异常值的来源
5. 在源头添加校验
"之前是正常的"
1. 使用 git bisect 定位引入问题的提交
2. 将当前变更与之前可工作的版本对比
3. 识别哪些假设发生了变化
4. 在假设被违反的源头进行修复
间歇性失败
1. 排查竞争条件
2. 检查共享的可变状态
3. 检查异步操作的执行顺序
4. 排查时序依赖
5. 添加确定性等待或适当的同步机制
调试检查清单
在声称 bug 已修复之前:
- 已识别并记录根因
- 已形成并测试假设
- 修复针对根因而非症状
- 已创建可复现 bug 的失败测试
- 应用修复后测试已通过
- 完整测试套件通过
- 未使用"快速修复"的自我开脱
- 修复最小化且聚焦
成效指标
系统化调试的首修成功率约为 95%,相比之下临时拼凑式约为 40%。
做得对的标志:
- 修复不会引发新 bug
- 能够解释 bug 发生的原因
- 类似 bug 不会再次出现
- 修复后代码更佳,而非仅"可用"
与其他技能的集成
- testing-patterns:先创建能复现 bug 的测试,再进行修复
局限性
- 仅当任务明确匹配其上游来源与本地项目上下文时使用本技能。
- 在应用变更前,验证命令、生成的代码、依赖、凭证以及外部服务的行为。
- 切勿将示例视为环境特定测试、安全审查或用户对破坏性/高成本操作授权的替代。