系统性调试
概述
随机修复浪费时间还会制造新 bug。快速补丁掩盖底层问题。
核心原则: 必须先找到根因再尝试修复。治标等于失败。
违反这个流程的字面意思,就是违反调试的精神。
铁律
未经根因调查,禁止修复
未完成阶段一,不得提出修复方案。
使用场景
适用于任何技术问题:
- 测试失败
- 生产环境 bug
- 意外行为
- 性能问题
- 构建失败
- 集成问题
以下情况尤其要用:
- 时间紧迫时(紧急情况让人忍不住猜)
- "就改一下试试"看似显而易见时
- 已经试过多次修复
- 上一次修复没用
- 你并不完全理解问题
以下情况不能跳过:
- 问题看起来简单(简单 bug 也有根因)
- 赶时间(赶工必然返工)
- 领导要求立刻修好(系统化比瞎折腾更快)
四个阶段
每个阶段必须完成,才能进入下一阶段。
阶段一:根因调查
在尝试任何修复之前:
仔细阅读错误信息
- 不要跳过错误或警告
- 它们通常包含确切的解决方案
- 完整阅读堆栈跟踪
- 记录行号、文件路径、错误码
稳定复现
- 能可靠触发吗?
- 确切步骤是什么?
- 每次都发生吗?
- 无法复现 → 收集更多数据,不要猜
检查近期变更
- 什么改动可能导致这个问题?
- Git diff、最近的提交
- 新依赖、配置变更
- 环境差异
多组件系统中收集证据
当系统有多个组件时(CI → 构建 → 签名,API → 服务 → 数据库):
在提出修复方案之前,先添加诊断插桩:
对每个组件边界: - 记录进入组件的数据 - 记录离开组件的数据 - 验证环境/配置传播 - 检查每层状态 运行一次,收集证据显示哪里出错 然后分析证据,定位故障组件 然后调查该具体组件示例(多层系统):
# Layer 1: Workflow echo "=== Secrets available in workflow: ===" echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}" # Layer 2: Build script echo "=== Env vars in build script: ===" env | grep IDENTITY || echo "IDENTITY not in environment" # Layer 3: Signing script echo "=== Keychain state: ===" security list-keychains security find-identity -v # Layer 4: Actual signing codesign --sign "$IDENTITY" --verbose=4 "$APP"由此可揭示: 哪一层出了问题(secrets → workflow ✓,workflow → build ✗)
追踪数据流
当错误深埋在调用栈中时:
完整的反向追踪技术见本目录下的
root-cause-tracing.md。快速版本:
- 错误值从哪里产生?
- 谁用错误值调用了这个?
- 持续向上追踪直到找到源头
- 在源头修复,而非在症状处修复
阶段二:模式分析
修复前先找到模式:
找到可工作的示例
- 在同一代码库中找到类似的正常代码
- 有什么类似的代码是正常工作的?
对照参考实现
- 如果在实现某个模式,完整阅读参考实现
- 不要略读——逐行阅读
- 完全理解模式后再应用
识别差异
- 正常的和出错的有什么不同?
- 列出每一个差异,无论多小
- 不要假设"这个应该没关系"
理解依赖关系
- 这需要哪些其他组件?
- 需要什么设置、配置、环境?
- 它做了什么假设?
阶段三:假设与验证
科学方法:
形成单一假设
- 明确表述:"我认为 X 是根因,因为 Y"
- 写下来
- 具体而非模糊
最小化测试
- 做尽可能小的改动来验证假设
- 每次只改一个变量
- 不要同时修多个东西
验证后再继续
- 有效?→ 阶段四
- 无效?→ 形成新假设
- 不要在上面叠加更多修复
不知道的时候
- 说"我不理解 X"
- 不要假装知道
- 寻求帮助
- 继续研究
阶段四:实施
治本而非治标:
创建失败测试用例
- 尽可能简单的复现
- 尽可能用自动化测试
- 没有框架就用一次性测试脚本
- 修复前必须先有测试
- 使用
superpowers:test-driven-development技能编写规范的失败测试
实施单一修复
- 针对已识别的根因
- 每次只改一处
- 不做"顺手改一下"的改进
- 不捆绑重构
验证修复
- 测试通过了吗?
- 其他测试没有被破坏?
- 问题真的解决了?
如果修复无效
- 停下
- 数一数:试过几次修复了?
- 如果 < 3:回到阶段一,用新信息重新分析
- 如果 ≥ 3:停下并质疑架构(见下方步骤 5)
- 未经架构讨论,不要尝试第四次修复
如果 3 次以上修复失败:质疑架构
表明架构问题的模式:
- 每次修复都暴露出新的共享状态/耦合/不同位置的问题
- 修复需要"大规模重构"才能实现
- 每次修复都在其他地方制造新症状
停下并质疑根本设计:
- 这个模式从根本上是对的吗?
- 我们是否"纯粹因为惯性在坚持"?
- 应该重构架构还是继续修症状?
在尝试更多修复之前,先与你的人类搭档讨论
这不是假设失败——这是架构错误。
危险信号 - 停下并回到流程
如果你发现自己在想:
- "先修了再说,以后再查"
- "就试试改 X 看看行不行"
- "加几个改动,跑一下测试"
- "跳过测试,我手动验证"
- "大概是 X 吧,修一下"
- "我不完全理解但这样应该能行"
- "模式说 X 但我换个方式适配"
- "主要问题是这些:[列出修复但没调查]"
- 还没追踪数据流就提出解决方案
- "再试一次修复"(已经试过 2 次以上)
- 每次修复都暴露出不同位置的新问题
以上任何一种情况都意味着:停下。回到阶段一。
如果 3 次以上修复失败: 质疑架构(见阶段四.5)
人类搭档的错误信号
留意这些纠正:
- "那不是在发生吗?" —— 你在没有验证的情况下做了假设
- "能不能让我们看到...?" —— 你本应添加证据收集
- "别猜了" —— 你在不理解的情况下提出修复
- "深入思考一下" —— 质疑根本设计,而非只看症状
- "我们卡住了?"(沮丧)—— 你的方法不管用
看到这些信号时: 停下。回到阶段一。
常见自欺欺人
| 借口 | 现实 |
|---|---|
| "问题很简单,不需要流程" | 简单问题也有根因。流程对简单 bug 来说很快。 |
| "紧急情况,没时间走流程" | 系统性调试比瞎猜乱试更快。 |
| "先试试这个,不行再查" | 第一次修复定下基调。从一开始就做对。 |
| "等确认修复有效再写测试" | 未经测试的修复站不住。先测试才能证明。 |
| "同时修多个省时间" | 无法隔离哪个有效。还会制造新 bug。 |
| "参考太长了,我直接适配模式" | 一知半解必出 bug。完整阅读。 |
| "我看到问题了,直接修" | 看到症状 ≠ 理解根因。 |
| "再试一次"(已失败 2 次以上) | 3 次以上失败 = 架构问题。质疑模式,不要再修。 |
快速参考
| 阶段 | 关键活动 | 成功标准 |
|---|---|---|
| 1. 根因 | 读错误信息、复现、检查变更、收集证据 | 理解"是什么"和"为什么" |
| 2. 模式 | 找可工作的示例、对比 | 识别差异 |
| 3. 假设 | 形成理论、最小化测试 | 确认或产生新假设 |
| 4. 实施 | 创建测试、修复、验证 | Bug 解决,测试通过 |
当流程揭示"没有根因"
如果系统性调查发现问题确实源于环境、时序或外部因素:
- 你已经完成了流程
- 记录你调查了什么
- 实施适当的处理(重试、超时、错误信息)
- 添加监控/日志以便后续调查
但是: 95% 的"没有根因"案例都是调查不彻底。
辅助技术
这些技术是系统性调试的一部分,在本目录下可用:
root-cause-tracing.md— 通过调用栈反向追踪 bug,找到原始触发点defense-in-depth.md— 找到根因后,在多层添加验证condition-based-waiting.md— 用条件轮询替代任意超时
相关技能:
- superpowers:test-driven-development — 用于创建失败测试用例(阶段四,步骤 1)
- superpowers:verification-before-completion — 在宣称成功前验证修复有效
实际效果
来自调试会话的数据:
- 系统化方法:15-30 分钟修复
- 随机修复方法:2-3 小时瞎折腾
- 首次修复率:95% vs 40%
- 引入新 bug:几乎为零 vs 频繁
局限性
- 仅在任务明确匹配上述范围时使用此技能。
- 不要将输出视为环境特定验证、测试或专家审查的替代品。
- 如果缺少所需输入、权限、安全边界或成功标准,请停下来请求澄清。