核心问题

系统犯过的错,如何变成以后不再重复的知识?

软件工程里的认识论,不只是发现一次错误。

更重要的是:

把一次错误转化成系统和团队的长期学习能力。

如果一个事故解决后,只留下几句聊天记录:

已经修了。
下次注意。

那团队几乎没有真正变强。

真正有价值的复盘要回答:

  1. 我们原来相信什么?
  2. 事实证明哪里错了?
  3. 为什么现有测试、监控、流程没有提前发现?
  4. 系统要增加什么证据或约束?
  5. 以后类似问题如何更早暴露?

复盘不是追责

坏复盘会变成:

谁写的?
谁 review 的?
为什么没注意?

这种复盘会让人隐藏信息、避免承担、减少冒险。

好的复盘问:

什么系统条件让这个错误能够发生?
为什么它没有被更早发现?
我们如何让下一次更难发生、更早发现、更容易恢复?

这不是说个人责任不存在,而是说:

工程复盘的目标是改进系统,不是寻找替罪羊。

一次事故暴露的是认知缺口

事故常常说明团队某个判断错了。

例如:

我们以为退款后访问权会立即撤销。
实际上只有 purchase-based grant 被撤销,subscription-based grant 没处理。

这不是单纯代码漏了一行。

它暴露的是模型缺口:

我们没有明确区分不同访问权来源的撤销规则。

所以复盘要追到认知层:

  • 哪个概念没定义清楚?
  • 哪个契约是隐式的?
  • 哪个 source of truth 被误用?
  • 哪个缓存过期承诺没说清楚?
  • 哪个状态流转没有被约束?

Postmortem 的基本结构

一份轻量但有效的复盘可以包含:

1. Summary

一句话说明发生了什么。

退款流程没有撤销部分课程访问权,导致 37 个已退款账号仍可访问课程。

2. Impact

说明影响范围。

影响时间:2026-06-10 14:20 - 16:05
影响用户:37 个账号
影响资源:12 门课程
用户可见影响:退款后仍可访问课程内容

3. Timeline

按时间记录事实,不要写猜测。

14:20 新退款逻辑上线
14:43 course_access_denied_count 无异常
15:12 客服收到第一例反馈
15:30 工程开始排查
15:48 定位到 subscription grant 未处理
16:05 关闭新逻辑 feature flag
16:40 完成数据修复

4. Root Cause

区分触发条件和根因。

触发条件:新退款逻辑只撤销 purchase grant。
根因:系统没有显式建模不同 access grant source 的撤销契约,也没有测试覆盖多来源访问权。

5. Detection Gap

为什么没有更早发现?

缺少 refund -> access revocation ratio 指标。
缺少多 grant source 的集成测试。
客服后台没有显示访问权来源和撤销状态。

6. Action Items

行动项必须具体、可验证、有 owner。

1. 增加 refund revocation integration test。
2. 为 CourseAccessGrant 增加 source-specific revocation policy。
3. 增加 refund_completed_count / access_grant_revoked_count 指标。
4. 客服后台展示 grant source、validUntil、revokedAt。

Action Item 要改变系统

坏行动项:

以后更仔细。
加强 review。
注意测试。

这些不可验证,也不会改变系统。

好行动项:

新增测试:退款后撤销 purchase、subscription、organization_assignment 三类 grant。
新增告警:refund_completed_count 正常但 access_grant_revoked_count 低于预期。
新增约束:所有 grant revocation 必须通过 revokeCourseAccessGrant。

好的行动项会改变反馈回路、契约、类型、测试、监控或工具。

原则:

如果行动项不能被验证,它就不是行动项。

知识沉淀到哪里

复盘后的知识不能只留在会议纪要里。

不同知识应该沉淀到不同地方:

知识类型应该沉淀到
业务规则领域文档、规格、测试
架构取舍ADR
操作流程Runbook
常见排查路径Debugging guide
系统约束类型、lint、schema、状态机
监控信号Dashboard、alert
历史事故Incident log

复盘的目标不是写一篇漂亮文档,而是:

把学到的东西放到未来最可能被用到的地方。

ADR:记录为什么这样设计

ADR 是 Architecture Decision Record。

它适合记录重要架构决策:

Decision: 使用 Postgres outbox 处理支付后的异步副作用。

Context:
当前订单量较低,但需要确保 purchase 和 outbox event 在同一事务中写入。

Alternatives:
Kafka, Redis Stream, direct async call.

Decision:
选择 Postgres outbox。

Consequences:
简化运维,保证事务一致性;高吞吐和多消费者场景下未来可能迁移。

ADR 的价值是让未来的人知道:

当时为什么这样选,而不是只看到现在的代码。

Runbook:记录如何处理

Runbook 适合记录操作流程。

例如:

Course access grant missing after payment

1. 用 paymentId 查询 payment record。
2. 检查 purchase 是否存在。
3. 检查 course_access_grant 是否存在。
4. 如果 payment succeeded 但 purchase missing,运行 reconciliation。
5. 如果 grant missing,检查 outbox event 和 worker logs。
6. 不要手动直接写 grant,除非 incident commander 批准。

Runbook 的价值是:

事故中减少临场发明。

Blameless 不等于 Toothless

无责复盘不是没有要求。

它不是:

没人有责任,所以什么都不用改。

它是:

不把人当根因,但必须改变系统。

如果同类事故反复发生,而 action items 没有完成,那就是管理和工程系统的问题。

复盘要温和对人,严格对系统。

复盘检查清单

每次事故后问:

  1. 事实时间线是什么?
  2. 用户或业务受到了什么影响?
  3. 触发条件是什么?
  4. 根因是什么?
  5. 哪个测试本应发现它?
  6. 哪个指标本应发现它?
  7. 哪个契约没有写清楚?
  8. 哪个 source of truth 被误解?
  9. 是否有数据需要修复?
  10. 哪些行动项会让同类问题更难发生?
  11. 这些行动项是否有 owner 和验证方式?
  12. 学到的规则应该沉淀到文档、测试、ADR、runbook 还是工具?

小结

  1. 复盘的目标是把错误转化成长期知识。
  2. 好复盘改进系统,不寻找替罪羊。
  3. 事故暴露的是认知缺口,不只是代码缺陷。
  4. Action item 必须具体、可验证、有 owner。
  5. 知识要沉淀到未来最可能被使用的地方。
  6. ADR 记录为什么这样设计,Runbook 记录出事时怎么处理。
  7. 无责复盘不是没有要求,而是温和对人、严格对系统。