ADR(Architecture Decision Record)是一份简短文档,用来保存一个重要架构决策及其理由。多份 ADR 按时间组成项目的决策日志。

代码能告诉后来者“系统现在怎样实现”,却不一定能解释“为什么这样实现”。ADR 补的是这个缺口:当约束、参与者和技术环境发生变化时,未来的开发者可以判断当前决定仍然成立,还是应该被新决定取代。

ADR 记录什么

一份 ADR 的中心是决策,而不是技术知识或讨论过程。它至少要让读者看懂三件事:

  • 当时有哪些事实、约束或力量需要平衡;
  • 项目选择了什么,以及为什么;
  • 这个选择接受了哪些成本或后果。

例如:

公开文章阅读和直接搜索不依赖 AI。模型服务能增强数字分身体验,但成本与上游可用性不可控,因此模型或额度不可用时降级为直接搜索,而不是让博客整体不可用。

这段话同时包含背景、决定和权衡,已经是一份有效的 ADR。表格、流程图和长模板都不是必要条件。

什么时候值得写

不要为每次技术选择都写 ADR。一个决定同时满足以下三个条件时,才值得进入决策日志:

  1. 难以撤销:未来改变它会产生明显迁移成本、兼容风险或组织影响。
  2. 缺少背景会令人意外:后来者只看代码,很可能误以为当前设计是疏忽并试图“修正”。
  3. 存在真实权衡:至少有一个合理替代方案,而项目基于具体约束作出了选择。

常见候选包括:

  • 单体、模块化单体还是微服务;
  • 数据由哪个业务边界拥有;
  • 同步调用还是异步事件;
  • 数据库、身份提供商或部署平台等高迁移成本选择;
  • 公共 API 的兼容和弃用策略;
  • 不容易从代码看出的合规、隐私或性能约束;
  • 有意偏离团队惯例的方案。

通常不需要 ADR 的内容包括:

  • 普通缺陷修复和局部重构;
  • 容易替换的小型工具库;
  • 已由编码规范明确规定的常规写法;
  • 没有合理替代方案的必然选择;
  • 具体任务拆分和实施进度。

改动行数不是判断标准。一个只有几行代码的数据所有权边界,可能比一次大规模但机械的重命名更值得记录。

ADR 与其他文档的边界

文档主要回答的问题
README如何了解、运行或使用项目
架构说明系统当前由哪些部分组成,它们如何协作
CONTEXT / 术语表项目中的业务词语分别是什么意思
ADR为什么选择当前方案,并接受了什么代价
Issue / 计划接下来由谁完成哪些工作
Runbook运行故障发生时怎样处理

ADR 不是会议纪要。讨论记录可以作为附件,但 ADR 应提炼最终决定,而不是保存所有发言。

ADR 也不是规范的替代品。假如“金额以最小货币单位存储”已经形成系统契约,ADR 解释为什么选择它;数据模型和 API 文档仍要准确规定字段单位与校验规则。

从最小模板开始

最轻量的 ADR 只需要标题和一段完整的话:

# 使用模块化单体与独立索引 Worker
 
第一版由模块化单体承载 Web、认证和数字分身入口,并使用独立
Worker 执行知识摄取与索引任务。该形态保留模块边界和异步任务
隔离,同时避免在产品边界尚未稳定时承担微服务的网络契约、部署
和故障处理成本。

当一段话不足以消除歧义时,再增加真正有价值的字段:

---
status: proposed
---
 
# 实测某供应商作为模型候选
 
[背景、决定和理由]
 
## Considered Options
 
[只有被拒绝方案将来可能被再次提出时才保留]
 
## Consequences
 
[只有非显而易见的正面、负面或中性影响才单列]

常见状态可以包括:

  • proposed:尚待责任人接受;
  • accepted:当前有效;
  • deprecated:不再推荐,但未必存在直接替代;
  • superseded by ADR-NNNN:已被指定的新决定取代。

状态不是必填字段。若仓库只保存已接受决定,省略状态往往比每份文件重复写 accepted 更清楚。

怎样写得短而有用

标题直接表达选择

使用“使用 PostgreSQL 保存持久任务”而不是“数据库讨论”。读者在目录中就应知道决定是什么。

背景写事实与约束

说明为什么现在需要决定,以及哪些力量相互冲突。不要为了证明结论而把偏好伪装成事实。

决定使用主动语态

“我们将……”比“建议考虑……”更明确。若尚未接受,用 proposed 表达状态,而不是模糊决策句。

理由必须与项目有关

“业界流行”“性能更好”通常不够。写清当前规模、预算、团队能力、隐私边界、兼容要求或测量结果。

后果同时包含所得与所失

真实决策一定有代价。只写优点的文档更像方案推销,而不是决策记录。

链接详细材料,不复制它们

基准测试、调研表和长设计稿可以链接到 ADR。这样既保留证据,又不让决策本身淹没在材料中。

文件位置与编号

常见做法是在代码仓库中建立 docs/adr/,使用单调递增的编号:

docs/adr/
├── 0001-separate-public-and-owner-knowledge.md
├── 0002-git-content-is-the-article-source.md
└── 0003-reading-and-search-do-not-depend-on-ai.md

编号只表达记录顺序,不代表优先级,也不应复用。文件名中的短语应稳定描述决定,避免依赖临时 Issue 标题。

仓库不是唯一选择,但 ADR 应靠近其约束的实现,并进入版本控制和正常评审流程。跨多个仓库的系统级决定,则需要一个所有受影响团队都能发现的权威位置。

决策变化时不要抹掉历史

上下文变化并不表示旧 ADR 写错了。若已接受并实施的决定发生变化:

  1. 保留旧记录;
  2. 创建新 ADR,说明新上下文和新决定;
  3. 将旧记录标记为被新 ADR 取代;
  4. 在新旧记录之间建立双向链接。

这样可以区分“当时基于旧约束作出的合理选择”与“当前有效选择”。拼写修正和链接维护可以直接修改,但不应静默改写历史决定,让它看起来像从未变化。

ADR 如何进入开发流程

  1. 在高影响方案尚可调整时起草,而不是实现完成后补写胜利叙事。
  2. 让受影响边界的责任人审查事实、替代方案和后果。
  3. 决定接受后,与相关实现一起或在其之前合并。
  4. 在架构说明、模块文档或 Pull Request 中链接 ADR,不在代码注释里复制全文。
  5. 当约束变化时,重新评估并用新 ADR 取代旧决定。

CI 最多检查编号重复、链接和格式。它无法判断一项权衡是否真实,也不能代替责任人接受高影响决定。

与 AI Agent 协作

ADR 对 Agent 的价值不是增加更多上下文,而是提供代码中看不见的意图。Agent 在修改模块边界、数据所有权、安全策略或基础设施前,应先检索相关 ADR。

Agent 可以协助:

  • 从代码、配置和讨论材料中整理事实;
  • 找出与现有 ADR 的冲突;
  • 提出替代方案和反例;
  • 起草背景、决定与后果;
  • 检查实现是否符合已接受决定。

Agent 不应未经授权接受高影响决定,也不应因为新方案看起来更现代就绕过现有 ADR。若代码与 ADR 不一致,应先确认是实现偏离、文档过时,还是上下文已经变化。

发布前检查

  • 是否只记录一个核心决定?
  • 三个门槛是否同时成立?
  • 标题能否直接看出选择?
  • 背景是否包含项目特有的事实和约束?
  • 是否写清选择理由与至少一个真实代价?
  • 是否避免复制教程、会议纪要和实施清单?
  • 若取代旧决定,是否保留历史并互相链接?
  • 实现、架构说明和 ADR 是否一致?

一句话总结:ADR 不是架构文档的重量级模板,而是写给未来开发者的一份最小解释——我们面对什么约束、选择了什么、为什么,以及为此接受了什么。

参考资料:Michael Nygard:Documenting Architecture DecisionsADR GitHub Organization:Motivation and DefinitionsADR Templates