如何写出真正能防止复发的事后复盘
12 年来,我写过或参与过大约 40 份事后复盘。大多数都失败了。不是那种轰轰烈烈的失败——只是没能防止下一次事故。行动项模糊,没人认领,六周之内大家就把它忘了。同类故障会再次出现,有时就在同一个服务里,然后有人会说"我们不是写过复盘吗?"确实写过。但没有用,因为复盘变成了仪式而不是工具。
为什么大多数复盘都失败了
我反复看到三种失败模式,它们会相互叠加。
伪装成无责文化的甩锅文化。文档顶部写着"无责",但时间线读起来像一份起诉书。"工程师 X 没看仪表盘就发布了。""值班人员 47 分钟后才上报。"人们学会在未来的复盘中保护自己,而不是坦诚还原真相。下一次事故有着相同的促成因素,只是时间线措辞更加谨慎了。
模糊的行动项。"改进监控。""增加更多测试。""发布时更加小心。"我收集了一批来自真实复盘的这类表述。没有一条是可执行的,没有一条有验收标准。如果你无法判断行动项是否完成,它就永远不会完成。如果你搜索团队的复盘存档,我敢打赌你会发现相同的模糊行动项隔几个月就重复出现。
没有跟进机制。复盘会议结束了。文档进了 wiki。没人检查行动项是否完成。三个月后,同类事故再次发生。有人翻出老复盘,发现行动项从未关闭,于是团队又开了一次同样的会。我见过这种循环在同一个团队、同一种故障模式上重复三次。
我曾亲眼看到一个团队在 18 个月内为本质上相同的故障写了三份独立的复盘:一个超时触发了重试,重试创建了重复状态。第一份复盘写着"增加重试限制"。第二份写着"改进超时配置"。第三份才终于问为什么操作不是幂等的。每一份复盘都停在了舒适的答案上——不需要重新思考系统设计的那种。前两份复盘的行动项从技术上看是完成了(有人确实改了一个配置值),但没有触及根因。幂等性缺口一直没人动,因为没有人将事故追溯到一个缺失的规格决定。
有效复盘的构成
一份有用的复盘有四个真正重要的部分。其他都是仪式。
时间线。不是叙事——是带时间戳的事件序列。"14:03 UTC:commit abc123 开始部署到生产环境。14:07:第一个告警触发(p95 延迟 > 2s)。14:12:值班人员确认。14:23:根因定位。14:31:开始回滚。14:34:回滚完成。14:41:指标确认恢复。"如果你的时间线里有主观判断,请重写。时间线是证据,分析放在后面。
5 个为什么(诚实地做)。5 Whys 技术在你抵制住停在舒适答案的冲动时非常强大。大多数团队停在近因:"配置变更没有测试过。"继续追问。为什么没测试?"我们没有配置变更的预发环境。"为什么没有?"配置一直被当作低风险的。"为什么?"配置系统的规格文档从未定义哪些配置键影响用户可见行为。"现在你到了规格层面的缺口,这是可以在系统层面修复的,而不是"以后更小心"的层面。
促成因素 vs. 根本原因。这个区分极其重要。根本原因是你愿意修复的最深层系统性问题。促成因素是其他一切让事故变得更严重或更可能发生的东西。告警晚触发 8 分钟是促成因素。缺失的幂等性规格是根本原因。如果你只修复告警时间,你会得到同样的事故但响应更快。如果你修复规格缺口,你从根本上防止了事故。我见过团队把所有复盘精力花在促成因素上,忽视根本原因——因为根本原因需要更难的工作。
规格缺口分析。这是大多数复盘模板缺失的部分。对于每个事故,问:什么决定从未被明确做出?不是"代码哪里出了问题",而是"在实现开始之前,什么问题从未被书面回答过?"根据我的经验,大约 60% 的生产事故可以追溯到一个从未被规格化的行为——超时策略、错误处理策略、重试语义、数据一致性保证。我之前写过的计费事故复盘就是教科书式的例子:规格文档从未涉及重试安全性,所以实现做了一个隐含的决定,结果证明是错的。
将复盘发现关联回规格缺口,是大多数团队断链的地方。他们找到了出问题的代码,找到了缺失的测试,但没有问那个真正能防止复发的问题:规格文档应该写什么?我开始在复盘中做一个具体练习:让每个人独立写下一句话——那句如果存在就能防止此次事故的规格描述。大家写出的答案通常不一样,这恰恰说明团队从一开始就没有就预期行为达成一致。那种分歧就是规格缺口。边界用例检查清单是一个好的起点,可以帮助在事故逼迫对话之前就把这类问题挖出来。
缺失的规格如何导致事故
我调查过的每一次事故,在因果链的某处,都有一个隐含的决定。某个人必须选择一种行为——超时时怎么办、下游服务宕机时怎么办、输入在技术上合法但语义上荒谬时怎么办——他们在实现过程中临时做了决定,没有和任何人商量。
这不是工程师的失败,是流程的失败。如果规格文档不涉及某个行为,工程师有三个选择:阻塞等答案(在迭代压力下没人这么做)、用自己的最佳判断(实际发生的情况)、或者跳过这个边界情况(也经常发生)。三个选项中有两个会产生可能是错误的隐含决定,而没人会评审隐含的决定,因为它们在代码评审中是不可见的。
以下是我在复盘中看到的模式:
事故:缓存失效失败后,用户看到过时数据长达 3 小时。 近因:缓存 TTL 设为 6 小时;写入时的失效静默失败。 规格缺口:缓存层的规格文档写着"为提升性能缓存响应"。 它没有指定: - 每种数据类型的最大可接受过时时间 - 失效策略(仅 TTL?直写?事件驱动?) - 失效失败时的行为 - 缓存命中率/未命中率的监控要求 工程师选择了仅 TTL 缓存加即发即忘的失效策略。 这是一个合理的默认值。但对这个场景来说是错的。
复盘行动项不应该是"修复缓存失效"。应该是"编写缓存规格文档,定义每种数据类型的过时预算、失效策略和失败行为。"这个行动项能防止下一次缓存事故,而不只是这一次。这与 Spec-First 错误处理模式的原则相同——如果你没有规格化故障模式,你就没有设计好系统。
一个真正有效的复盘模板
这个模板是我在四个团队、七年时间中迭代出来的。它有明确的观点。这些观点来自于看到其他方式失败。
## 事故标题 一句话。包含用户影响。"由于队列消费者重启竞态条件, 用户在 4 小时内收到重复邮件通知。" ## 严重等级与影响 - 严重等级:S1/S2/S3/S4(使用团队标准) - 持续时间:从开始到完全恢复(UTC) - 受影响用户:数量和范围 - 收入影响:如有 - 数据影响:任何数据丢失或损坏 ## 时间线(UTC,无主观判断,无归咎) - HH:MM 事件 - HH:MM 事件 - (持续到确认恢复) ## 5 个为什么 1. 为什么发生了 [用户可见影响]? 因为 [近因]。 2. 为什么发生了 [近因]? 因为 [更深层原因]。 3-5.(继续直到你找到一个从未被明确做出的决定) ## 根本原因 一段话。陈述最深层的系统性问题。如果开头是 "工程师 X"或"发布流程",说明你还没有挖得够深。 ## 规格缺口分析 - 什么行为从未被规格化? - 这个决定应该在哪里被记录? - 规格评审时提出什么问题本可以防止此事故? ## 促成因素 项目符号列表。让事故变得更严重或更持久的因素, 但不是事故的原因。告警延迟、缺失的运维手册、 不清晰的上报路径。 ## 行动项(SMART 格式) | # | 行动 | 负责人 | 截止日期 | 验证标准 | 工单 | |---|------|--------|---------|---------|------| | 1 | ... | ... | ... | ... | ... | 每个行动项必须有: - 单一负责人(不是团队——是一个人) - 截止日期(不是"下个迭代"——是日历日期) - 验证标准(怎么知道完成了?) - 在团队追踪器中关联工单
规格缺口分析部分是这个模板区别于标准 Google/PagerDuty 模板的地方。它迫使团队将事故关联到一个缺失的决定,而不只是缺失的测试或告警。它直接输出更新规格文档的行动项,而不只是修改代码。
示例:使用此模板的真实复盘
以下是我在一次涉及破坏性 API 变更的事故后写的复盘的精简版。(我另外写了完整案例研究。)
## 事故:v2.3 部署后合作伙伴集成中断 6 小时 ## 严重等级与影响 - S2:12 个合作伙伴集成返回错误,持续 6 小时 - 340,000 次失败的 API 调用 - 3 个合作伙伴升级至客户经理 ## 时间线 - 09:15 v2.3 开始部署(金丝雀) - 09:22 金丝雀升级至 100% - 09:41 第一个合作伙伴工单:"/v2/users 端点返回 401" - 10:03 值班人员调查,发现 auth header 解析发生变化 - 10:15 确认:v2.3 将 header 匹配从大小写不敏感改为大小写敏感 - 10:30 决定回滚(非热修复) - 10:38 回滚完成 - 15:20 热修复部署,恢复大小写不敏感匹配 - 15:35 所有合作伙伴集成确认恢复 ## 5 个为什么 1. 为什么合作伙伴集成中断? v2.3 中 auth header 解析变为大小写敏感。 2. 为什么解析行为发生了变化? 一个库升级改变了默认解析模式。 3. 为什么部署前没有发现? 没有契约测试验证 header 解析行为。 4. 为什么没有相关的契约测试? API 规格文档没有定义 header 是否大小写敏感。 5. 为什么规格文档没有涉及 header 敏感性? header 处理从未成为规格评审检查清单的一部分。 ## 根本原因 /v2/users 的 API 规格文档没有定义 header 解析语义。 大小写敏感性是一个隐含行为,在依赖升级时发生了变化。 ## 规格缺口 API 规格文档应该包含:"所有 HTTP header 必须按照 RFC 7230 Section 3.2 进行大小写不敏感匹配。"这一句话就能让库升级的 行为变化在契约测试中变得可见。
注意 5 Whys 如何从代码层面的问题(库升级)经过测试缺口(无契约测试)追溯到规格缺口(header 处理从未被规格化)。修复不是"添加大小写敏感性的测试"。修复是"更新 API 规格文档定义 header 处理语义,然后从规格文档生成契约测试。"一个防止这个具体事故,另一个防止整个类别。上线与回滚设计指南涵盖了这个等式的部署侧——当规格缺口确实漏过时,如何构建发布来控制影响范围。
真正能完成的行动项
复盘能否防止复发,最大的预测指标是行动项是否完成。听起来显而易见。确实显而易见。然而大多数团队在 30 天内完成的复盘行动项不到一半。
以下是我关于能真正落地的行动项的经验:
一个负责人,不是一个团队。"平台团队改进监控"不是行动项。"Sarah 为支付服务添加 p99 延迟告警,阈值 500ms,截止 2026-05-02,工单 PLAT-4421"才是行动项。区别在于责任归属。当一个团队拥有某件事时,等于没人拥有。
日历日期,不是迭代引用。"下个迭代"是个移动目标,会滑动。日历上的日期不会滑动——要么按时完成,要么明确重新协商,这会创造一个可见的决策点。我对大多数复盘行动项默认使用 14 天。足够完成工作,又短到仍与事故的紧迫感相连。
可以机械化检查的验证标准。"提高测试覆盖率"没有验证标准。"为所有 v2 端点添加契约测试,覆盖 header 解析、请求体 schema 和错误响应格式;如果缺少任何契约测试,CI 流水线失败"——这是可验证的。可以用 grep 查。机器人可以检查。完成与否没有歧义。
规格更新作为第一优先级行动项。这是大多数团队跳过的部分。代码修复出得快,因为事故还热乎。规格更新被降优先级,因为感觉像文档而不是工程。但规格更新是防止该类别下一次事故的行动项,而不只是这个特定事故的下一个实例。我把规格更新作为第一个行动项而不是最后一个,因为它迫使团队在写修复代码之前定义"修好了"意味着什么。
在复盘中获得真诚的贡献,不是在文档标题写上"无责"就够了。我发现最有效的技巧是将数据收集阶段与分析阶段分开。在第一个阶段,异步收集时间线和原始事实——让大家在小组会议之前以书面形式、私下贡献内容。人们在文档中比在经理注视的会议室里更加诚实。在分析阶段,将小组讨论聚焦在系统和流程上,而不是个人决策。如果有人说"我没看仪表盘",后续问题应该是"为什么系统在仪表盘需要检查时没有发出告警?"——而不是暗含评判的沉默。持续这样做的团队能获得更丰富的时间线,发现更深层的根因,因为最接近事故的人不再把精力花在自我防卫上。
| 差的行动项 | 好的行动项 |
|---|---|
| 改进监控 | 为支付服务添加 p99 延迟告警,阈值 500ms。负责人:Sarah。截止:2026-05-02。工单:PLAT-4421。 |
| 增加更多测试 | 为所有 /v2 端点编写契约测试,覆盖 header 解析和错误 schema。负责人:James。截止:2026-05-09。工单:API-1187。 |
| 更新规格文档 | 在 API 规格文档中添加 header 处理章节,定义按 RFC 7230 进行大小写不敏感匹配。负责人:Daniel。截止:2026-04-25。工单:SPEC-302。 |
| 发布时更小心 | 在 CI 流水线中添加部署前契约测试门禁;如果任何契约测试失败则阻止升级。负责人:Priya。截止:2026-05-09。工单:PLAT-4430。 |
衡量复盘是否有效
如果无法衡量,就无法改进。以下是我跟踪的四个指标:
30 天行动项完成率。复盘后 30 天内有多少百分比的行动项标记为完成?如果这个数字低于 80%,你的复盘正在产生无法落地的工作。解决方案通常是更小、更具体、截止日期更紧的行动项——而不是更多的意志力。
复发率。同一类别的事故在 90 天内复发的频率是多少?不是完全相同的事故——是同一类别。如果你有过一次缓存事故,三个月后又有一次不同的缓存事故,这算复发。通过为复盘打上故障类别标签来跟踪(缓存、认证、数据一致性、部署等)。拥有健康复盘实践的团队,在已做过复盘的类别中应该看到接近零的复发率。
规格缺口识别率。有多少百分比的复盘识别出了具体的规格缺口?根据我的经验,持续在复盘中发现规格缺口的团队,总体事故率更低,因为他们在修复系统性问题而不是打补丁。如果你的复盘很少识别出规格缺口,你可能把 5 Whys 停得太早了。
从事故到规格更新的时间。事故发生后,缺失的规格章节需要多长时间才能写好?这应该比部署代码修复的时间更短。规格更新定义了"修好了"意味着什么。没有它,你在部署一个修复但没有定义它满足的需求,这意味着下一个接触该代码的工程师无法知道它为什么这样工作。
我合作过的一个团队在一个每周更新的仪表盘上跟踪这四个指标。六个月后,他们看到了明显的趋势:行动项完成率从 45% 提升到 88%,在已完成复盘的类别中复发率降至接近零,规格缺口识别成为了常规操作。仪表盘本身并没有直接改变复盘的质量——它改变的是团队对"复盘是否重要"的认知。当你能向工程总监展示某次具体的复盘催生了某次具体的规格更新,而该更新在接下来九个月里阻止了某一类事故的发生时,复盘流程就不再是开销,而成为了基础设施。这种可信度,才是在下一次截止日期压力来临时让实践得以存续的关键。
让复盘成为工具而非仪式
最难的部分不是写好复盘,而是在事故的紧迫感消退后维持这个实践。以下是对我有效的方法:
在固定会议中评审行动项。不是专门的复盘评审会——嵌入到已有的会议中。我们在每周团队同步会中加了 5 分钟的"未关闭复盘项"环节。有人打开追踪器,读出未完成的项,问进展。就这样。连续三周报告"未开始"的社会压力足以推动进展。
公开闭环。当一份复盘的所有行动项完成时,公告出来。"3月3日合作伙伴集成事故的所有行动项已完成。规格已更新,契约测试已纳入 CI,告警阈值已设置。"听起来微不足道。它很重要,因为它为复盘流程创造了一个可见的终点。没有它,复盘感觉永远拖不完,这会让人害怕复盘。
将复盘与规格评审联系起来。复盘中识别的每一个规格缺口都应该成为规格评审检查清单上的一个问题。在我之前描述的计费事故之后,"这个端点重试是否安全?"成为永久的规格评审问题。在 API 变更事故之后,"HTTP header 语义是否已定义?"成为另一个问题。你的规格评审检查清单应该是一个从复盘中生长的活文档。这就是复盘如何产生复利效应:每一次复盘让下一次规格评审稍微好一点,让下一次事故稍微不那么可能发生。
为险些发生的事故做复盘。最好的复盘是针对差点发生的事故。在金丝雀阶段被抓住的部署。在预发环境失败的数据迁移。在压力测试中发现的超时。这些"险些"与真实事故有相同的因果结构,但没有压力、归咎压力和紧迫感——而这些正是让真实事故复盘难以做好的因素。如果你的团队只在客户可见事故后才做复盘,你错过了最便宜的学习机会。
目标不是零事故。目标是同一类别的事故绝不发生第二次。每一次复发都是复盘流程有缺口的证据——通常在跟进环节,有时在分析深度。修复流程,事故自然就少了。
专题阅读路径
这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
编辑说明
本文面向软件交付团队,介绍如何写出真正能防止复发的事后复盘。示例均为工程场景说明,不构成法律、税务或投资建议。
- 作者信息:Daniel Marsh
- 编辑政策:文章审阅与更新方式
- 纠错:联系编辑