Home
avatar

.Sam

【AI Native开发-15】同一个 Bug 反复修,真正该回到哪一层?

30 秒看懂这篇

同一个权限 Bug,第一次修一行 if,第二次再修一行,第三次换个接口继续修——代码越来越多,根因却从没离开过。

真正省时间的做法不是更快修绿,而是先问:这次失败究竟在惩罚哪一层的错误? Spec 没说清、Plan 映射错、实现漏做、测试误报,止血动作看似相同,后续回写完全不同。

flowchart TB
    F["失败"] --> C["证据归因:Spec / Plan / 实现 / 测试 / 环境"]
    C --> H["最小止血与回归验证"]
    H --> W{"根因回写"}
    W --> S["Spec:补齐业务规则"]
    W --> P["Plan / ADR:修正技术方案"]
    W --> T["回归集:守住具体缺陷"]
    W --> K["pitfalls / CI:复用教训"]
    S --> N["下一次变更"]
    P --> N
    T --> N
    K --> N

核心论点:

  1. 代码是契约的下游。 业务决定不能偷偷塞进一个临时分支,技术方案也不能伪装成需求规则。
  2. 止血与根治是两个动作。 可以先恢复服务,但必须保留证据、补回归并回到根因层。
  3. 失败只有回写才会产生复利。 Spec、Plan、测试集和知识库,各自接收不同类型的教训。

本篇给出归因地图、决策树和回写飞轮。

一个典型场景

还是删除功能。代码评审发现:普通用户直打接口时可能绕过权限;而现有接口测试只覆盖管理员路径,没有捕获这个风险。

AI 的修法很快:在删除接口里加了一个判断——if (user.role !== 'admin') return 403。新补的普通用户用例从红变绿,看起来结束了。但回溯发现:

  • Spec 只写了“服务端必须校验权限”,没说角色、拒绝响应以及“拒绝时无副作用”;
  • Plan 也只写“加权限判断”,没有决定复用统一权限中间件;
  • 实现手写浅判断,只让状态码正确,没有证明文件未被删;
  • 原有测试设计漏了普通用户直打接口和副作用断言,所以缺陷没有被旧用例捕获。

这里有时序上的区别:旧测试全绿不代表没有 Bug,只代表它没测这个场景;新增回归用例先在旧实现上变红,再用于证明修复有效。

根因横跨 Spec、Plan 和测试设计。只修一行 if,下个接口仍可能重演。正确做法是:代码止血;Spec 补业务权限与无副作用规则;Plan/ADR 决定统一中间件;回归测试同时断言 401/403 和文件仍在。

核心问题:为什么就地修绿是最贵的

“修到绿色”的诱惑在于它当下最快。但它的真实成本是延后和放大的:

就地修绿的代价链:

失败 → 改代码/改测试压绿 → 根因(Spec/Plan/测试)没动
  → 同类缺陷在另一个功能里重现(因为含糊的 Spec 还在)
  → 再就地修一次 → 再重现……
  → 代码里堆满"Spec 没提过的判断",没人知道为什么在那
  → 测试被改宽松,失去拦截能力
  → 最终一个压绿的失败在生产爆炸(越权/数据丢失)
  → 排查成本 = 100+

沿用第 01 篇的教学相对成本示意(Spec 阶段 1 → Plan 3 → 实现 8 → 测试 20 → 生产 100+),可以更直观地看到:**就地修绿往往把本可在上游处理的问题,推向下游成本更高、牵涉范围更大的阶段。**这些数字不是行业测量值,真实成本应由团队自己的缺陷记录估算。

而回流修复的收益是复利的:把根因修在 Spec,所有基于这个 Spec 做的功能都受益;把结论写进 pitfalls,所有 AI 和人下次都会避开。

失败分层归因地图

一个失败出现后,它的根因可能在六个不同的层。用这张表定位”该回到哪里”:

失败表现最可能根因层该回到哪里关键判据
规则矛盾、目标不清、验收含糊SpecSpec 重写规则两个合理的人会对”正确行为”给出不同答案
技术方案不可行、理解错架构PlanPlan 重做方案Spec 清楚,但 Plan 的技术路径走不通
代码没按 Plan 做、漏了异常路径Implement修实现Plan 对、代码没落实,或代码写错
文本用例遗漏场景、预期写错测试设计补/改用例代码行为符合 Spec,但用例预期和 Spec 矛盾
自动化脚本选择器错、mock 错、断言错自动化用例修测试代码文本用例对,但代码化时走样(如 mock 掉权限)
环境/数据/CI 不稳定测试执行/环境修环境基建本地绿 CI 红、偶发、报错指向依赖/数据
生产出现从未设想的新场景Spec + 回归集补 Spec + 回归用例Spec 压根没覆盖这类情况

用法:不要默认”失败 = 代码错”。 先问”这个失败暴露的是哪一层的缺陷”。判据列给了快速区分的方法。

归因决策树

按顺序走这六步(第 02 篇归因决策树的完整版):

失败出现,逐步判断:

第 1 步:这是产品失败还是测试失败?
  - 系统行为违背 Spec → 产品失败,进第 2 步
  - 系统符合 Spec,但测试报错 → 测试失败,跳到第 5 步

第 2 步:Spec 对这个行为的规定,清楚且可测吗?
  - 不清楚/含糊/矛盾/没规定 → 根因在【Spec】,回 Spec
    (这是最容易被跳过、也最值钱的一步)
  - 清楚 → 进第 3 步

第 3 步:Plan 正确理解了 Spec 和代码现状吗?
  - Plan 漏了这步 / 方案错 / 理解错架构 → 根因在【Plan】,回 Plan
  - Plan 正确 → 进第 4 步

第 4 步:代码按 Plan 实现了吗?
  - 没按 Plan / 漏异常路径 / 写错 → 根因在【Implement】,修代码
  - 按 Plan 做了但还是错 → Plan 本身有盲区,回第 3 步或第 2 步

第 5 步(测试失败):文本用例对吗?
  - 用例预期和 Spec 矛盾 / 漏了场景 → 根因在【测试设计】,改用例
  - 文本用例对,但自动化走样 → 根因在【自动化用例】,修测试代码
    (选择器过期、mock 掉了被测逻辑、断言错对象)

第 6 步:是环境或数据问题吗?
  - 本地绿 CI 红、偶发、依赖缺失 → 根因在【环境/数据】,修基建
  - 偶发失败可能是并发/幂等产品 bug → 别当环境问题压掉,回第 4 步

第 2 步是整套归因里最容易被跳过、又最关键的一步。 人(和 AI)拿到失败的本能是”代码哪儿错了”,很少回头问”Spec 到底说清了没有”。但大量反复出现的失败,根因都在 Spec 的含糊。一个强制动作:任何产品失败,先把对应的 Spec 条目找出来读一遍,确认它是否真的把这个行为规定清楚了。

那条经验法则

贯穿全系列的一条判据,在归因里特别好用:

如果新增分支改变了 Spec 未规定的业务语义,先回 Spec 补规则;如果只是实现既定契约所需的技术细节,更新 Plan 和代码即可。

例子:

  • 在删除接口加 if (published) reject——Spec 里状态规则写清了吗?没写清,回 Spec;
  • 加一个”管理员例外”分支——Spec 里角色权限定义了吗?没定义,回 Spec;
  • 加一个重试逻辑处理重复提交——Spec 里幂等要求写了吗?没写,回 Spec。

这些判断不是不能加,而是它们代表一个业务决定,这个决定应该在 Spec 阶段被显式做出、经过评审,而不是在调试时由人或 AI 临场拍脑袋。临场拍的决定没有经过”这会不会影响别的场景”的审视,往往顾此失彼。

失败回写:让一次失败变成永久资产

归因完成、止血修复之后,还有最关键的一步——回写。失败只有沉淀下来,才能防止复发。按根因选择去处,不必每次四处全写:

1. 回写 Spec:业务规则缺口

Spec 只描述可观察契约,不指定“统一中间件”这种实现方案:

REQ-002(修订后):
- 普通用户调用删除返回 403;未登录返回 401
- 拒绝时不得执行文件操作或其他副作用
- 即使前端隐藏入口,直打接口同样拦截

2. 回写 Plan / ADR:技术方案缺口

“统一权限中间件”属于技术决策,应写进 Plan;若它是跨功能长期约束,再形成 ADR:

Plan / ADR:删除等敏感操作复用 src/lib/auth 统一权限中间件;
业务函数不得另写一套角色判断。实现必须保留拒绝路径的副作用测试。

3. 回写回归测试集:漏测或断言无效

新用例先在旧实现上确认能红,再在修复后确认变绿:

回归用例 REG-xxx:普通用户直打删除接口
- 断言 403 + 文件仍在 + 无副作用
- 永久保留,防止权限校验被重构掉

线上缺陷应至少留下一种防复发资产:可自动化的转 CI 回归;难以稳定自动化的,转人工验收、监控告警或运行手册,并说明原因。

4. 回写知识库 / 检查表:可复用的认知教训

## 权限拒绝路径必须验证副作用

历史教学案例:接口手写浅判断,只断言 403,
漏掉“文件仍在”,可能掩盖先删除后拒绝的实现。

配套:复用统一中间件;拒绝路径必须断言数据未变。

知识库给后续 AI 与人提供线索,但不能替代 Spec、Plan 和自动化约束;还要避免把未经核实的“历史事故”写成事实。

线上失败的特殊处理

流到生产的失败,说明现有验证链存在缺口,需要更严格地归因和回写:

线上缺陷出现后,必答五个问题:
1. 直接根因在哪一层?
2. Spec 是否遗漏或写错了业务规则?
3. 测试为什么没拦住:漏用例、陪跑、层级还是环境?
4. Verify 为什么没暴露这个缺口?
5. 怎样让同类缺陷下次更早被抓住?

动作:
- 保留证据并止血修复
- 按根因补/改 Spec、Plan 或 ADR
- 可稳定自动化的缺陷转 CI 回归;其余转人工验收、监控或运行手册
- 更新 pitfalls / 评审检查表 / CI 规则
- 复盘流程,而非只怪个人

线上失败已经发生成本,能追回的价值就是让它推动契约、测试、监控或机制变得更强。

归因反模式

反模式一:默认代码错。 拿到失败就改代码,从不回头看 Spec。导致上游缺陷反复在不同功能里重现。对策:强制走决策树第 2 步,先读对应 Spec 条目。

反模式二:改测试压绿。 产品失败被当成测试失败,放宽断言让它过。这是最贵的一种——它主动关掉了能抓 bug 的警报。对策:任何断言改动必须引用 Spec 依据,人确认。

反模式三:修完不回写。 止血了就翻篇,Spec 还含糊、用例还缺、pitfalls 没更新。同一个坑几周后再踩。对策:止血 + 回写是一个完整动作,不回写不算修完。

反模式四:只怪个人。 “谁让 AI 写错的 / 谁评审没看出来”。个人会换、AI 会更新,靠追责不能防复发。对策:把教训固化进流程(检查表、CI、pitfalls),让机制防复发。

反模式五:把并发 flaky 当环境问题。 偶发失败一律重试压掉,其中可能藏着幂等/并发产品 bug。对策:偶发失败先排查产品并发缺陷,排除后再归环境。

失败回退总表(贴在工作区)

Spec 层缺陷   → 改 Spec,重过 Gate 1,下游 Plan/代码/测试跟着更新
Plan 层缺陷   → 改 Plan,重过 Gate 2
实现层缺陷    → 改代码,补回归用例
测试设计缺陷  → 改文本用例,重新代码化
自动化缺陷    → 修测试代码(mock/选择器/断言),不碰产品代码
环境/数据缺陷 → 修基建,不为迁就坏环境改产品代码
线上新场景    → Spec 补规则 + 回归用例 + pitfalls + 检查表/CI

人审重点

人裁决失败分层,凭 Spec 确认测试修改,并核对 Spec、回归集与知识库是否按根因回写;线上复盘聚焦流程改进而非个人追责。

结语

SDD 不承诺没有失败,它承诺的是:每个失败都有一个明确的归属层,并且不会白白发生——它要么让 Spec 更严密,要么让测试集更密,要么让知识库更深。修到绿色是把失败清零,归因回写是把失败变现。


到这里,个人视角的 SDD 全流程——上下文、Spec、Spike、Plan、评审、Implement、代码评审、文本用例、自动化、调试、执行、Verify、归因——就完整了。但一个人用得好,不等于一个团队用得起来。最后一篇讲规模化:怎么把这套个人方法,变成团队共享的 AI Native 研发系统——统一模板、CI 强制追踪、测试集治理、多 Agent 并行的隔离,以及线上反馈如何驱动整个系统持续进化。

AI Native SDD 失败归因 回退 根因 软件工程