技术架构文档的写作规范:ADRs(Architecture Decision Records)从原理到实践
技术架构文档的写作规范ADRs(Architecture Decision Records)从原理到实践一、架构知识流失——为什么口口相传的架构决策必然失败技术团队的架构知识流失是一种慢性病。一个关键的架构决策——为什么选择了消息队列A而非B、为什么数据库表设计采用反范式化、为什么微服务拆分以这个边界为准——如果只存在于当事人的头脑中或某次会议的纪要里随着人员流动和时间推移这些决策的为什么将彻底消失。后果是双重的新人困惑新成员看到代码中的非直觉设计无法判断这是刻意的架构权衡还是历史的遗留债务。前者需要维护后者需要重构。决策反复当原有决策的条件发生变化如流量增长10倍、数据量突破阈值团队缺乏上下文来判断原决策的前提是否依然成立导致在同样的问题上重复讨论。ADRsArchitecture Decision Records正是为解决这一问题而生的轻量级文档范式。它不追求架构文档的系统性和完整性而是聚焦于记录单个决策的上下文、方案对比、选择理由和后果。一个ADRs文件短则一页长则三四页但信息密度极高——读者能在5分钟内理解一个关键决策的来龙去脉。二、ADRs的核心结构——从Michael Nygard模板到团队自定义规范ADRs 的经典模板由 Michael Nygard 在2011年提出包含五个核心章节。一个良好的 ADR 文档具有极高的信息密度和自包含特性。标题规范采用ADR-NNNN: 决策的简要描述格式。编号使用自增序列不重新编排。标题应当概括做什么决策如ADR-0003: 采用PostgreSQL的JSONB列存储非结构化元数据而非关于数据库选型的讨论。状态机是ADR的生命周期管理核心。状态流转规则Draft → Proposed初稿完成提交团队评审Proposed → Accepted团队达成共识ADR成为正式记录Accepted → Superseded新决策取代旧决策ADR头部引用新ADR编号Accepted → Deprecated决策因架构演进自然过期如微服务下线背景章节要回答为什么现在做这个决策和如果不做决定会怎样。不是描述技术方案的细节而是描述面临的具体技术约束和业务需求。比如当前的MySQL单表已增长到5000万行按月分区策略导致跨月查询需要扫描所有分区p99延迟已恶化到3秒。决策章节需要阐述我们决定做什么以及为什么是这个方案。关键不是罗列方案特性而是说明权衡trade-off的逻辑——为什么接受A的成本换取B的收益。如果列出了备选方案需要逐一说明为什么不选。后果章节是ADR最有价值的部分。它分为两个方向正向哪些问题被解决了什么变得更容易负向引入了什么新成本什么变得更困难需要注意哪些风险三、ADR管理工具——模板驱动的规范化生成与检索以下代码实现了一套ADR生命周期管理工具确保团队以统一格式生成和管理架构决策记录 ADRArchitecture Decision Records管理工具 支持模板生成、状态管理、关联检索和Markdown渲染 from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Optional from enum import Enum class ADRStatus(str, Enum): ADR状态枚举 DRAFT draft PROPOSED proposed ACCEPTED accepted SUPERSEDED superseded DEPRECATED deprecated property def label(self) - str: labels { draft: 草案, proposed: 提议中, accepted: 已接受, superseded: 已取代, deprecated: 已废弃, } return labels[self.value] dataclass class ADR: ADR数据模型 id: int title: str status: ADRStatus context: str # 背景 decision: str # 决策 consequences: str # 后果 alternatives: list[str] field(default_factorylist) superseded_by: Optional[int] None supersedes: Optional[int] None created_at: datetime field(default_factorydatetime.now) updated_at: datetime field(default_factorydatetime.now) tags: list[str] field(default_factorylist) def filename(self) - str: 生成标准文件名 slug ( self.title.lower() .replace( , -) .replace((, ) .replace(), ) ) return fADR-{self.id:04d}-{slug}.md class ADRGenerator: ADR文档生成器从数据模型生成Markdown格式文档 TEMPLATE # {title} - **状态**: {status} - **日期**: {date} - **标签**: {tags} {superseded_section}{supersedes_section} ## 背景与上下文 {context} ## 决策 {decision} ## 备选方案 {alternatives} ## 后果与影响 {consequences} def generate(self, adr: ADR) - str: tags_str , .join(adr.tags) if adr.tags else — superseded if adr.superseded_by: superseded f- **已被取代**: ADR-{adr.superseded_by:04d}\n supersedes if adr.supersedes: supersedes f- **取代**: ADR-{adr.supersedes:04d}\n alternatives_text if adr.alternatives: items [ f- **备选{i1}**: {alt} for i, alt in enumerate(adr.alternatives) ] alternatives_text \n.join(items) else: alternatives_text 未记录备选方案 return self.TEMPLATE.format( titleadr.title, statusadr.status.label, dateadr.updated_at.strftime(%Y-%m-%d), tagstags_str, superseded_sectionsuperseded, supersedes_sectionsupersedes, contextadr.context.strip(), decisionadr.decision.strip(), alternativesalternatives_text, consequencesadr.consequences.strip(), ) class ADRRepository: ADR仓库管理器 负责ADR的CRUD操作、索引管理和关联查询 INDEX_HEADER # 架构决策记录索引 | 编号 | 标题 | 状态 | 日期 | |------|------|------|------| def __init__(self, root_dir: str docs/adr): self.root Path(root_dir) self.root.mkdir(parentsTrue, exist_okTrue) self.generator ADRGenerator() self._adrs: dict[int, ADR] {} self._load_existing() def create(self, adr: ADR) - str: 创建新的ADR if adr.id in self._adrs: raise ValueError(fADR-{adr.id:04d} 已存在) # 分配编号 if adr.id 0: adr.id max(self._adrs.keys(), default0) 1 content self.generator.generate(adr) filepath self.root / adr.filename() filepath.write_text(content, encodingutf-8) self._adrs[adr.id] adr self._rebuild_index() return str(filepath) def supersede(self, old_id: int, new_adr: ADR) - tuple[str, str]: 用新ADR取代旧ADR 自动更新旧ADR的状态和引用 if old_id not in self._adrs: raise ValueError(fADR-{old_id:04d} 不存在) old_adr self._adrs[old_id] old_adr.status ADRStatus.SUPERSEDED old_adr.superseded_by new_adr.id new_adr.supersedes old_id # 重新生成旧ADR self._save_adr(old_adr) # 创建新ADR new_path self.create(new_adr) self._rebuild_index() return (str(self.root / old_adr.filename()), new_path) def get_active_adrs(self) - list[ADR]: 获取所有活跃已接受的ADR return [a for a in self._adrs.values() if a.status ADRStatus.ACCEPTED] def search_by_tag(self, tag: str) - list[ADR]: 按标签检索ADR return [a for a in self._adrs.values() if tag in a.tags] def get_decision_chain(self, start_id: int) - list[ADR]: 追踪决策链从一个ADR出发找出所有取代它的后续ADR chain [] current_id start_id visited set() while current_id and current_id not in visited: visited.add(current_id) current self._adrs.get(current_id) if not current: break chain.append(current) current_id current.superseded_by return chain def _load_existing(self) - None: 加载已有ADR文件 for md_file in sorted(self.root.glob(ADR-*.md)): content md_file.read_text(encodingutf-8) adr self._parse_adr(content, md_file.stem) if adr: self._adrs[adr.id] adr def _parse_adr(self, content: str, filename: str) - Optional[ADR]: 从Markdown内容解析ADR lines content.split(\n) if not lines: return None title_line lines[0].strip(# ) adr_id 0 try: parts filename.split(-) if len(parts) 2: adr_id int(parts[1]) except ValueError: pass return ADR( idadr_id, titletitle_line, statusADRStatus.ACCEPTED, context, decision, consequences, ) def _save_adr(self, adr: ADR) - str: 更新已有ADR文件 adr.updated_at datetime.now() content self.generator.generate(adr) filepath self.root / adr.filename() filepath.write_text(content, encodingutf-8) self._adrs[adr.id] adr return str(filepath) def _rebuild_index(self) - None: 重建README索引 rows [] for adr in sorted(self._adrs.values(), keylambda a: a.id): date_str adr.updated_at.strftime(%Y-%m-%d) rows.append(f| ADR-{adr.id:04d} | {adr.title} | {adr.status.label} | {date_str} |) index_content self.INDEX_HEADER \n.join(rows) \n (self.root / README.md).write_text(index_content, encodingutf-8) # 使用示例 if __name__ __main__: repo ADRRepository(root_dir./docs/adr) # 创建第一条ADR adr1 ADR( id0, # 自动分配 titleADR-0001: 采用PostgreSQL的JSONB列存储非结构化元数据, statusADRStatus.ACCEPTED, context( 产品中的每个订单有10-50个动态扩展属性如定制参数 传统EAV模式需要3张表联表查询查询复杂度高。 业务要求单次查询延迟 200ms。 ), decision( 使用PostgreSQL 14的JSONB数据类型存储动态扩展属性 配合GIN索引加速JSON路径查询 使用CHECK约束确保JSON Schema合规。 ), alternatives[ EAV实体-属性-值模式灵活但查询复杂不满足延迟要求, MongoDB需要引入新数据库组件运维成本过高, MySQL 8.0 JSON类型支持不完善性能弱于PG, ], consequences( 正向单表查询即可获取订单全量数据延迟满足要求。\n 负向JSONB索引占用较多存储空间约原始数据的30% 需要建立JSON Schema治理规范 开发团队需了解PG的JSON操作符。 ), tags[database, postgresql, data-modeling], ) path1 repo.create(adr1) print(f已创建: {path1}) # 获取所有活跃ADR active repo.get_active_adrs() print(f活跃ADR数量: {len(active)}) # 检索标签 db_adrs repo.search_by_tag(database) print(f数据库类ADR数量: {len(db_adrs)})四、ADR实践中的反模式——什么不算ADR什么不该记录为ADR不要将ADR写成架构文档的替代品。ADR记录的是决策不是系统全景图。架构文档C4 Model等描述系统是什么样ADR描述为什么是这个样。两者互补不是竞争关系。不要记录所有技术决策。只有以下类型的决策值得写入ADR影响系统外部接口或行为API契约、协议选择产生显著的技术债务或运维成本引入新技术栈、数据存储方案会约束未来的架构选择微服务拆分边界、Event Schema存在多个合理的方案且团队有分歧不要在ADR中堆砌无关细节。一个ADR应该在300-800字内完成核心内容的阐述。如果超过1500字说明决策本身需要进一步拆解为多个更小的决策。不要忘记更新ADR的状态。ADR不是一次性的交付物。当架构演进导致原有决策不再适用时必须通过superseded或deprecated状态显式标记确保读者看到的是当前有效的约定而非历史的考古记录。五、总结ADRs是一种轻量级架构决策记录格式通过背景-决策-后果的三段式结构将关键架构决策的为什么固化到代码仓库中。其核心价值不在于文档本身而在于迫使团队在做出决策时显式记录上下文、评估方案、声明后果——这个过程本身就是架构决策质量的保障机制。落地要点从零开始选最近一次有分歧的技术决策写出第一篇ADR与代码同行ADR存放在代码仓库的docs/adr/目录中随PR提交轻量渐进不追求全覆盖每月新增1-2条高质量的ADR比一次性补齐所有历史决策更有价值状态维护定期回顾已接受的ADR评估其是否依然适用ADRs解决的不是文档怎么写的问题而是团队如何积累和传承架构判断力的问题——这是技术团队从短期生存走向长期演进的必经之路。

相关新闻

用8小时,找到了200张可视化大屏模板,你可以直接用

用8小时,找到了200张可视化大屏模板,你可以直接用

示例中提到的数据大屏模板分享给大家—— https://s.fanruan.com/gtpby 零基础快速上手,还能根据需求进行个性化修改哦 2020年了,还有人不会可视化大屏? 我以前不止一次给各位看过可视化大屏,无论是什么做的,最终的意…

2026/7/16 18:09:36阅读更多 →
革命性AI视觉语言模型:mlx-community/gemma-4-e4b-it-nvfp4全面解析与快速上手指南

革命性AI视觉语言模型:mlx-community/gemma-4-e4b-it-nvfp4全面解析与快速上手指南

革命性AI视觉语言模型:mlx-community/gemma-4-e4b-it-nvfp4全面解析与快速上手指南 【免费下载链接】gemma-4-e4b-it-nvfp4 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/gemma-4-e4b-it-nvfp4 mlx-community/gemma-4-e4b-it-nvfp4是一款专为…

2026/7/16 18:09:36阅读更多 →
AnimeGen-I2V-A14B-Lightning-int4部署指南:从本地到云端的最佳实践

AnimeGen-I2V-A14B-Lightning-int4部署指南:从本地到云端的最佳实践

AnimeGen-I2V-A14B-Lightning-int4部署指南:从本地到云端的最佳实践 【免费下载链接】AnimeGen-I2V-A14B-Lightning-int4 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/AnimeGen-I2V-A14B-Lightning-int4 AnimeGen-I2V-A14B-Lightning-int4是…

2026/7/16 18:09:36阅读更多 →
长征十号乙实现全球首创海上网系回收:箭船协同强实时管控背后系统底座能力解析

长征十号乙实现全球首创海上网系回收:箭船协同强实时管控背后系统底座能力解析

7 月 10 日,长征十号乙运载火箭完成首飞任务,一子级依托全球首创海上网系回收模式,在南海海域由 “领航者” 号回收平台柔性网精准捕获,标志我国可重复使用运载火箭技术开辟全新技术路线,为商业航天高频次、低成本星座…

2026/7/16 19:09:39阅读更多 →
DNABERT-2:基因组智能分析的新一代基础模型,重新定义DNA序列理解

DNABERT-2:基因组智能分析的新一代基础模型,重新定义DNA序列理解

DNABERT-2:基因组智能分析的新一代基础模型,重新定义DNA序列理解 【免费下载链接】DNABERT_2 [ICLR 2024] DNABERT-2: Efficient Foundation Model and Benchmark for Multi-Species Genome 项目地址: https://gitcode.com/gh_mirrors/dn/DNABERT_2 …

2026/7/16 19:09:39阅读更多 →
LINE Bot SDK PHP实战项目:构建一个完整的电商客服机器人

LINE Bot SDK PHP实战项目:构建一个完整的电商客服机器人

LINE Bot SDK PHP实战项目:构建一个完整的电商客服机器人 【免费下载链接】line-bot-sdk-php LINE Messaging API SDK for PHP 项目地址: https://gitcode.com/gh_mirrors/li/line-bot-sdk-php LINE Bot SDK PHP是一款强大的LINE Messaging API开发工具&…

2026/7/16 19:09:39阅读更多 →
Luckysheet导出实战指南:3步告别数据混乱,高效保存Excel和PDF

Luckysheet导出实战指南:3步告别数据混乱,高效保存Excel和PDF

Luckysheet导出实战指南:3步告别数据混乱,高效保存Excel和PDF 【免费下载链接】Luckysheet Luckysheet upgraded to Univer 项目地址: https://gitcode.com/gh_mirrors/lu/Luckysheet 你是不是经常遇到这样的困境?在Luckysheet上精心制…

2026/7/16 19:09:39阅读更多 →
Jackett终极指南:如何用一个工具搞定所有种子资源搜索

Jackett终极指南:如何用一个工具搞定所有种子资源搜索

Jackett终极指南:如何用一个工具搞定所有种子资源搜索 【免费下载链接】Jackett API Support for your favorite torrent trackers 项目地址: https://gitcode.com/GitHub_Trending/ja/Jackett 你是否曾在十几个不同的种子网站之间来回切换,只为寻…

2026/7/16 19:09:39阅读更多 →
Signature Pad性能优化:压缩JSON输出的实用技巧

Signature Pad性能优化:压缩JSON输出的实用技巧

Signature Pad性能优化:压缩JSON输出的实用技巧 【免费下载链接】signature-pad A jQuery plugin for assisting in the creation of an HTML5 canvas based signature pad. Records the drawn signature in JSON for later regeneration. 项目地址: https://gitc…

2026/7/16 19:04:39阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/16 8:28:11阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/16 6:53:04阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/16 12:02:41阅读更多 →
A--10 Codex Review与GitHub PR工作流实战指南:从代码审查到安全合并

A--10 Codex Review与GitHub PR工作流实战指南:从代码审查到安全合并

摘要:本文系统讲解如何利用Codex App的Review功能与GitHub PR工作流,实现从代码修改到安全合并的完整流程。涵盖Review面板深度使用、/review命令实战、GitHub Connector配置、PR描述撰写技巧,以及常见问题排查方法。通过多个实战案例和流程图,帮助开发者建立高效的AI辅助代…

2026/7/16 0:00:38阅读更多 →
遗传算法解5皇后问题:从Hello World到工业优化的进化实验室

遗传算法解5皇后问题:从Hello World到工业优化的进化实验室

1. 项目概述:为什么用遗传算法解5皇后问题,而不是直接回溯?我带过十几届算法课,也给不少初创团队做过AI架构咨询。每次讲到组合优化问题,学生和工程师的第一反应永远是“写个回溯试试”。这没错——55棋盘上找所有合法…

2026/7/16 0:00:38阅读更多 →
5.1V稳压管输出为何只有4.7V?工作电流与负载影响分析

5.1V稳压管输出为何只有4.7V?工作电流与负载影响分析

前几天调试一个简单的电源模块,用到了5.1V稳压管。电路接好,上电测试,万用表一量——输出居然只有4.7V。第一反应是稳压管坏了,换了一个新的,结果还是4.7V。这让我想起很多初学者都会遇到的困惑:明明标称5.…

2026/7/16 0:00:38阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/15 15:50:47阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/16 8:58:42阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/16 17:10:26阅读更多 →