系统化调试
目标:先证明根因,再做最小修复。任何“先试一下”“大概是”“改了再看”都算失败。
硬门禁
除紧急止血例外外,在满足下面 5 个门禁前,不要提出业务修复方案、不要改业务代码、不要加 workaround:
- 复现门禁:能说明复现命令/步骤、实际结果、期望结果;不能复现时先补日志或收集证据。
- 证据门禁:阅读完整的错误上下文、堆栈、失败断言和相关日志,覆盖能够影响根因判断的范围,并记录关键文件、行号、错误码或异常值。
- 根因门禁:能用一句话说明“根因是 X,因为证据 Y 指向 Z”,而不是只描述症状。
- 影响门禁:修复前评估要改的符号/接口/配置会影响谁;有 GitNexus 索引时必须先跑影响面分析。
- 验证门禁:修复前先有失败用例、最小复现或可重复验证命令;修复后必须运行它。
如果任一门禁缺失,先补门禁,不进入修复。
执行流程
0. 侦察代码地图(有 GitNexus 索引时)
优先用 GitNexus 建立 bug 周围的执行流地图:
- 用错误关键词或业务术语查询相关流程与符号。
- 查看疑似符号上下文,确认调用者、被调用者、所属执行流。
- 需要精确链路时再用自定义图查询追踪调用链。
通过标准:能说出 bug 涉及的符号、执行流、直接上游和直接下游。
如果索引过期,先运行 npx gitnexus analyze --index-only;没有索引则跳过本阶段。工具参数见 ../gitnexus/references/toolkit.md。
1. 根因调查
按顺序完成:
- 读完整错误:错误信息、堆栈、断言 diff、日志上下文、触发位置。
- 稳定复现:记录最小复现命令/步骤;不稳定时记录出现频率和条件。
- 查近期变更:检查当前 diff、最近提交、依赖/配置/环境变化。
- 追踪数据流:从错误值向上追踪到最早产生处;不要只在报错行修症状。
- 跨边界取证:多组件系统要在组件边界记录输入、输出、配置和状态;优先使用日志、调试器或请求捕获等观测手段,不改变业务行为。必须增加诊断代码时,应保持可逆、限制范围,并在定位后移除。
通过标准:能解释错误值从哪里来、在哪一层变坏、为什么会到达失败点。
反向追踪方法见 references/auxiliary-techniques/root-cause-tracing.md。
2. 模式对比
在修复前找一个正常样本:
- 找同仓库或参考项目中相同模式的正常实现。
- 阅读与当前执行流相关的完整实现,不要只看相似行。
- 列出正常实现与失败实现的差异。
- 标出依赖条件:配置、时序、缓存、权限、环境、第三方行为。
通过标准:能说出“正常路径满足 A/B/C,失败路径缺少或破坏了 D”。
3. 单假设验证
一次只验证一个假设:
- 写出假设:
我认为根因是 X,因为证据 Y;如果成立,观察到 Z。 - 用最小实验验证:一条命令、一个日志点、一个断言或一个临时隔离改动。
- 验证失败就撤回该假设,回到第 1 阶段补证据;不要在失败假设上叠加更多改动。
通过标准:假设被证据确认,且能指向一个明确修复点。
4. 修复前影响面
修复前先评估爆炸半径:
- 有 GitNexus 索引:对要修改的函数、类、方法、路由或关键符号做上游影响分析。
- 无索引:手动列出调用方、配置消费者、接口消费者和相关测试范围。
风险处理:
| 风险 | 判断 | 要求 |
|---|---|---|
| LOW | 少量调用方,非关键流程 | 最小修复 + 相关验证 |
| MEDIUM | 多个调用方或 2 个以上流程 | 补回归测试 + 跑相关测试 |
| HIGH | 大量调用方、公共工具、核心状态 | 先说明风险和方案,再修复 |
| CRITICAL | 认证、支付、数据写入、权限、生产配置 | 先和用户确认方案 |
5. 最小修复与验证
- 涉及可测试行为的 Bug 修复,先按
test-driven-development创建并验证失败的回归测试;其他场景使用最小复现脚本或明确可重复的验证命令,并记录原因。 - 只修已确认根因,不顺手重构,不捆绑无关优化。
- 运行复现验证、相关测试和必要的 lint/typecheck/build。
- 修复后进入最终验证阶段;若当前流程已由 TDD 或上层工作流衔接
verification-before-completion,由其统一执行,不重复启动;只能根据实际命令输出声明成功。 - 有 GitNexus 索引时,修复后运行变更检测,确认影响范围符合预期。
如果修复失败:
- 修复验证失败:停止叠加补丁,回到第 1 阶段补充证据;若新证据与当前根因假设矛盾,重新评估数据流、模型和影响面。
紧急止血例外
只有生产环境全面中断、数据持续损坏或安全风险正在扩大时,才允许止血与根因调查并行。
止血必须同时满足:
- 可逆:有开关、回滚路径、超时上限或熔断边界。
- 不冒充修复:明确标记为临时缓解。
- 记录决策:写下采取了什么、为什么、待查根因是什么。
- 立即继续调查:止血后回到第 1 阶段。
- 有跟踪:设定后续验证或告警,避免临时方案永久化。
部分降级、单用户问题、有 workaround、普通测试失败不适用该例外。
常见绕过方式
出现这些想法时,立刻回到门禁:
- “先临时修一下,之后再排查。”
- “试着改 X 看看。”
- “一次改多个地方,跑了再说。”
- “看起来很简单,不用复现。”
- “我不完全理解,但应该能行。”
- “测试晚点补。”
- “参考实现太长,凭经验改。”
- “已经失败两次了,再试一次。”
- “有 GitNexus 但先不用。”
- “影响面分析太麻烦。”
团队压力下的做法
硬门禁是独处时的纪律。在真实团队中,你不可能每次都硬刚到底——以下做法不是"退而求其次",而是技能设计的正确用法。
核心原则:你不必赢下争论,但你必须在妥协前完成自己能控制的部分。 即使团队决定先合入未经根因验证的变更,也必须明确记录其未验证状态,不得将其描述为已修复。
你的三件事(在妥协前必须完成)
- 请求一个短时间盒,不要争论。"给我 10 分钟验证 token 在 middleware 前后是否变化。" 把流程从"对错之争"变成"小实验"。
- 用具体问题替代反对。"这个状态在哪一层从 completed 变成 pending?" "middleware 是验签失败还是主动清除了 token?" 具体问题比"我们应该调查根因"更有说服力。
- 记录风险。"我担心在报错点兜底会掩盖上游字段丢失,建议至少跑 X 验证。" 即使现在不改,也让风险可见。
如果团队决定先合入
你已经完成了当前能做的短时间盒、具体问题和风险记录。现在:
- 接受团队决定,不继续僵持。
- 明确标记这个变更为"临时缓解,根因待查",不得称为已修复。
- 创建跟踪 ticket,继续验证结果和根因补查。
这就是技能的正确用法。 你不是在"妥协"——你是在有纪律地走完自己能控制的部分,然后把团队决策交给团队。
输出格式
调试过程中对用户保持这个结构,简短即可:
复现:<命令/步骤> → <实际结果>
证据:<关键日志/堆栈/差异>
根因假设:<X,因为 Y;预期观察 Z>
验证:<已跑/要跑的最小实验>
影响面:<要改的符号/文件,调用方与风险>
修复:<确认根因后的一句话方案>
验证结果:<命令与结果>
相关资料
- GitNexus 工具参数:
../gitnexus/references/toolkit.md - 反向根因追踪:
references/auxiliary-techniques/root-cause-tracing.md - 防御式修复:
references/auxiliary-techniques/defense-in-depth.md - 条件等待替代固定 sleep:
references/auxiliary-techniques/condition-based-waiting.md