| name | adr |
| description | 记录与读取架构决策记录(Architecture Decision Records, ADR)。在做出、提议或推翻任何"架构重要"的决策时务必使用本 skill——例如选型或替换数据库、框架、编程语言、通信协议(REST/gRPC/消息队列),引入或移除重要依赖或服务,拆分/合并服务,或确立某种贯穿全局的约定(鉴权、错误处理、数据一致性、API 版本策略)。同样地,在开始任何会触及架构的任务之前,先用本 skill 读取已有 ADR,理解当前设计背后的约束与理由,再动手修改。即使用户没有明说"ADR",只要某个决策难以逆转、或未来维护者会问"当初为什么这么设计",就应当触发本 skill 来记录或查阅。 |
架构决策记录(ADR)
ADR 的唯一目的是记录"为什么这么决定",而不是"决定了什么"。代码和架构图本身能体现"是什么";"为什么"一旦不写下来,几个月后连决策者自己都会忘,后人就只能靠猜,或者错误地推翻一个其实有充分理由的决策。
作为编程 agent,你在两个时机使用本 skill:改架构之前先读,做完重要决策之后再写。 下面分别说明。
工作流一:动手前先读 ADR(READ-first)
在开始任何会修改系统结构的任务之前(改数据访问层、换通信方式、动核心依赖、调整服务边界等),先读已有 ADR。原因很实在:很多看似"不合理、该重构"的设计,其实是当初在某个约束下刻意做出的权衡。不读 ADR 就改,很可能推翻一个有理由的决策,引入它当初正是要避免的问题。
执行步骤:
- 定位 ADR 目录。按优先级查找:
docs/adr/、doc/adr/、docs/architecture/decisions/、adr/。找不到就在仓库里搜 *adr* 或含 ## Status / ## Context / ## Decision 结构的 Markdown。
- 读
README.md(索引,见下文)快速扫一遍所有决策的标题和状态。
- 重点精读与当前任务相关、且状态为 Accepted 的 ADR。注意被标记为 Superseded 的,它记录的是"已经被推翻的旧决策",别照它做,但要看它链接到的新 ADR。
- 把读到的约束当作硬性前提带入后续工作。如果你的改动会违背某条 Accepted 的 ADR,不要默默绕过——明确告诉用户"这会与 ADR-XXXX 的决策冲突",并按工作流二写一条新 ADR 来取代它。
如果仓库里完全没有 ADR,而你即将做一个架构重要的决策,主动建议用户引入 ADR,并直接按工作流二写下第一条。
工作流三:关键决策前产多候选决策集(COMPARE-candidates)
当面临一个关键架构决策且存在多个可行方案时(典型触发方:planner 铁律6——技术选型 / 引入新中间件 / 影响其他任务的共享 schema),不要私自拍板,先产出候选决策集交用户选择。这是 generate-and-filter 模式在架构层的落地:多生成、按统一维度评分、给推荐,人拍板。
输入: 决策问题 + 2-3 个候选方案。每个候选含:简述、Context(在什么约束下成立)、Pros、Cons。
评分维度(按决策性质取 3-5 个): 实现成本 / 可维护性 / 性能 / 可逆性 / 团队熟悉度 / 与现有架构契合度 / 外部依赖风险。各维度给候选打分(高/中/低 或 1-5),不靠主观一句话。
输出: 一张对比表 + 明确推荐(含"为什么推荐这个、为什么没选另外两个")。
## 决策:<问题一句话>
| 维度 | 候选A:<名> | 候选B:<名> | 候选C:<名> |
|------|-----------|-----------|-----------|
| 简述 | ... | ... | ... |
| 实现成本 | 低 | 中 | 高 |
| 可维护性 | 中 | 高 | 高 |
| 可逆性 | 高 | 中 | 低 |
| <其他维度> | ... | ... | ... |
| **Pros** | ... | ... | ... |
| **Cons** | ... | ... | ... |
**推荐:** 候选B —— <理由:权衡了什么,为什么压过 A/C>
流转: 用户拍板后 → 按工作流二把选定方案写成一条正式 ADR(Context 段纳入"曾考虑 A/C 及为什么没选",保留决策溯源)。候选集本身是决策过程产物,不替代 ADR。
多候选场景由调用方(如 planner)在关键决策点显式触发;非关键 / 单方案决策直接走工作流二即可。
工作流二:做完重要决策后写 ADR(WRITE-after)
第一步:判断这个决策值不值得记录
不是所有决策都该写,写太多会让 ADR 沦为噪音。判断只有一个核心词:架构重要性(Architecturally Significant)——这个决策是否影响了系统的结构、非功能特性、依赖关系、接口或构建方式。
该写(满足任意一条即可):
- 决策难以或代价高昂地逆转(选数据库、选单体 vs 微服务、定通信协议)。
- 在多个备选方案之间做了取舍,且"为什么没选另一个"不直观。
- 决策跨越多个团队或模块,需要别人理解约束。
- 引入或移除了重要的技术依赖(框架、中间件、云服务、关键 SDK)。
- 确立了贯穿全局的约定:鉴权、错误处理、数据一致性、API 版本、日志/可观测性策略。
- 决策当下有明显争议或权衡,未来很可能有人质疑。
不该写:容易逆转的局部决定(命名、用哪个工具类)、纯实现细节、行业默认共识。
经验法则:如果半年后有新人问"这里为什么要这样设计",而你需要花十分钟解释背景和当时的权衡,那它就该是一条 ADR。
判断不确定时,问用户一句:"这个决策我打算记一条 ADR,可以吗?"——但当用户已经明确要求记录、或决策显然重要时,直接写,不必每次都问。
第二步:生成 ADR
优先用脚本来保证编号连续、索引同步(无需任何第三方依赖):
python3 scripts/adr.py new "使用事件溯源处理订单状态变更" --dir docs/adr
脚本会找到下一个编号、基于模板生成文件(初始状态 Proposed)、并打印出文件路径。然后你只需填充正文内容(Context / Decision / Consequences)。
如果环境里不方便跑脚本,就手动操作:ls 一下目录看现有最大编号,复制 assets/template.md,文件名形如 0007-use-event-sourcing-for-order-state.md(四位数字编号 + 短横线小写英文短语)。
要取代一条旧决策时,用:
python3 scripts/adr.py new "改用单库 + 读写分离" --dir docs/adr --supersedes 7
脚本会在新 ADR 里自动写入"Supersedes ADR-0007",并把旧的 0007 状态改成 Superseded by ADR-XXXX。
第三步:写好内容(关键)
严格使用以下五段式结构(Nygard 格式),完整模板见 assets/template.md:
- Title — 编号 + 简短名词短语。例:
ADR-0007: 使用事件溯源处理订单状态变更。
- Status — 取值之一:
Proposed(提议)/Accepted(已接受)/Deprecated(已弃用)/Superseded by ADR-XXXX(被取代)。新建时默认 Proposed,经用户/团队确认后改 Accepted。
- Context — ADR 最有价值的部分。陈述当时面对的问题和相互冲突的"力(forces)"。只摆事实和压力,不下结论。
- Decision — 用主动语气直接说决定做什么:"我们将采用……"。
- Consequences — 这个决策带来的结果,正面和负面都必须写,包括引入的新风险、新约束、后续要做的工作。
第四步:更新索引并交给用户确认
写完后重新生成索引:
python3 scripts/adr.py index --dir docs/adr
然后把新写的 ADR 内容呈现给用户,请其确认是否可以从 Proposed 改为 Accepted。不要替用户擅自决定 Accepted——状态的推进是一个需要人参与的确认动作。
两条最容易被忽略、但最重要的规则
1. 业务背景必须和技术背景一起写进 Context
很多技术决策单看技术层面讲不通,只有放回业务约束里才合理。一条"选了昂贵的托管数据库而非自建"的 ADR,如果只写技术理由会显得很蠢;但补上"距上线只剩 8 周、团队没有专职 DBA、首要目标是快速验证市场",决策瞬间就成立了。
凡是驱动或约束了这个决策的业务因素都要写:时间/上线压力、预算、团队规模与技能、合规与监管要求(数据驻留、GDPR、等保)、业务规模预期(预计用户量/并发)、对客户的 SLA 承诺、战略方向。
判断方法:删掉这段业务描述后,如果技术决策看起来变得"莫名其妙"或"显然错误",就该留;删掉后决策依然站得住,它就是噪音,不写。
注意分寸:只写与本决策直接相关的业务背景,不要复述整份业务规划。 ADR 通常和代码同仓,会被很多人看到——具体财务数字、未公开商业计划、客户合同细节要脱敏("预算有限"而非"只有 X 万")或不写进版本库。
2. ADR 不可变;决策变了不改旧的,而是 supersede
永远不要修改一条已 Accepted 的 ADR 的 Context / Decision / Consequences。 决策改变时,写一条新的 ADR,把旧的状态标记为 Superseded by ADR-XXXX 并互相链接。
唯一允许修改旧 ADR 的地方,就是把它的 Status 那一行改成 Superseded/Deprecated。
这样做的价值在于保留决策的演化史:回头看就能复盘"当初预期用户 10 万,基于此选了单库;后来实际 500 万,故被 ADR-0015 取代"。这段证据如果靠改旧文件就会永远丢失。
文件与目录约定
docs/adr/
├── README.md # 索引,由 scripts/adr.py index 自动生成
├── 0001-record-architecture-decisions.md # 通常第一条:"我们决定采用 ADR"
├── 0002-use-postgresql-as-primary-database.md
└── 0007-use-event-sourcing-for-order-state.md
- 编号四位数,连续递增,永不复用(即使某条被取代,编号也保留)。
- 文件名:
NNNN-小写英文短横线短语.md。即便正文用中文,文件名也用英文 slug,便于跨工具和命令行处理。
- 默认目录
docs/adr/;若仓库已有其他约定目录,沿用现有的,不要新建第二套。
示例:一条写得好的 ADR(节选)
注意 Context 里业务"力"与技术"力"是交织在一起的,以及 Consequences 诚实地写了负面后果。
# ADR-0007: 使用事件溯源处理订单状态变更
## Status
Accepted
## Context
订单状态频繁流转(下单→支付→拣货→发货→签收/退款),且财务和客服强需求是
能够审计"某笔订单在任意时刻处于什么状态、由谁、因何变更"——这是合规与对账的
硬性要求(业务力)。当前用单表 status 字段直接覆盖,历史状态被冲掉,已多次
导致退款纠纷无法举证(技术力 + 业务痛点)。团队对事件驱动有经验,且消息中间件
已在生产使用(技术现状)。距下个合规审计约 3 个月(时间约束)。
## Decision
我们将对订单聚合采用事件溯源(Event Sourcing):订单的每次状态变更以不可变
事件追加写入事件存储,当前状态由事件回放得到,并维护一个读模型供查询。
## Consequences
正面:获得完整、不可篡改的状态变更审计轨迹,直接满足合规与对账需求;天然支持
按时间回溯任意历史状态。
负面:引入读写模型分离的复杂度,团队需理解最终一致性;事件 schema 演进需要版本
策略,后续须专门写一条 ADR;查询当前状态需经读模型,简单 CRUD 心智模型不再适用。
新增运维成本:事件存储的备份与重放演练。
快速自检
写完一条 ADR 前,确认: