| name | documentation-and-adrs |
| description | 记录决策和文档。在做出架构决策、修改公共 API、发布功能,或需要记录未来工程师和智能体理解代码库所需的上下文时使用。 |
文档与 ADR
概述
记录决策,而不仅仅是代码。最有价值的文档捕获的是为什么——导致了某个决策的上下文、约束和权衡。代码展示构建了什么;文档解释为什么以这种方式构建以及考虑了哪些替代方案。这一上下文对于未来在代码库中工作的人类和智能体至关重要。
何时使用
- 做出重要的架构决策
- 在竞争方案之间做选择
- 添加或修改公共 API
- 发布一个改变用户可见行为的功能
- 将新团队成员(或智能体)引入项目
- 当你发现自己在重复解释同一件事情时
何时不使用: 不要为显而易见的代码编写文档。不要添加重述代码已经表达的内容的注释。不要为一次性原型编写文档。
架构决策记录 (ADR)
ADR 捕获重要技术决策背后的推理。它们是你能编写的价值最高的文档。
何时编写 ADR
- 选择一个框架、库或主要依赖项
- 设计数据模型或数据库 schema
- 选择认证策略
- 决定复制与一致性协议(Raft vs. Paxos vs. 主从异步复制)
- 在构建工具、托管平台或基础设施之间做选择
- 任何逆转成本高昂的决策
首先匹配现有约定
在创建 ADR 之前,检查可用的仓库上下文以获取已建立的约定——现有的 ADR、项目指令以及与 ADR 相关的配置或工具(例如 .adr-dir 文件)。已建立的约定优先于以下默认值。匹配:
- 位置和格式——例如
docs/adr/*.md、Documentation/Decisions/*.rst、MADR 布局或 adr-tools 设置。匹配现有目录、文件扩展名和标记语言(Markdown vs reStructuredText)。
- 编号和命名——继续现有序列和文件名模式(
ADR-004-Title.rst、0004-title.md……);不要从 001 重新开始或引入第二种方案。
- 章节标题——复用项目的标题集,而非强加此模板的标题集。
如果现有证据冲突,提出冲突而非沉默地引入另一种方案。仅当无法确定任何约定时才应用以下默认值。
ADR 模板
将 ADR 存储在 docs/decisions/ 中,使用顺序编号(除非项目已使用其他位置——见上文):
# ADR-001:使用 Raft 作为元数据复制协议
## 状态
已接受 | 被 ADR-XXX 取代 | 已弃用
## 日期
2025-01-15
## 上下文
我们需要为分布式元数据服务选择复制协议。关键需求:
- 强一致性(元数据变更必须线性一致)
- 少数派故障下仍可写入(5 节点容忍 2 个故障)
- 团队能够正确实现并长期运维该协议
- 有生产环境验证过的实现可参考
## 决定
使用 Raft,基于 etcd raft library 实现。
## 考虑的替代方案
### Paxos
- 优点:理论成熟,学术界验证充分
- 缺点:难以正确实现,Multi-Paxos 的工程细节缺少权威描述
- 拒绝原因:实现和运维成本过高,团队无法长期维护自研 Paxos
### 主从异步复制
- 优点:实现简单,写入延迟低
- 缺点:主节点故障时可能丢失已确认的写入,故障切换依赖外部仲裁
- 拒绝原因:元数据服务不能接受已确认数据的丢失
### EPaxos
- 优点:无 Leader,多地域部署延迟更低
- 缺点:冲突处理复杂,社区实现不成熟
- 拒绝原因:当前单地域部署用不上,复杂度不值
## 后果
- 元数据写入需过半节点确认,延迟增加约一次跨机 RTT
- Leader 选举期间(秒级)写入不可用,客户端需要重试逻辑
- 团队需要理解 Raft 的日志复制与快照机制(有 etcd 等参考实现,风险可控)
- 读请求可通过 ReadIndex / Lease Read 优化,无需每次走日志复制
ADR 生命周期
提议 → 已接受 → (被取代 或 已弃用)
- 不要删除旧的 ADR。 它们捕获历史上下文。
- 当决策变更时,编写一个引用并取代旧 ADR 的新 ADR。
内联文档
何时注释
注释为什么,而非是什么:
counter += 1
if now.Sub(windowStart) > windowSize {
counter = 0
windowStart = now
}
何时不注释
func totalShards(replicas []*Replica) int {
total := 0
for _, r := range replicas {
total += r.Shards
}
return total
}
记录已知陷阱
func (n *Node) SetInitialConfig(cfg Config) {
}
API 文档
对于公共 API(gRPC、REST、内部库接口):
内联文档注释(Go 首选)
func (c *Client) CreateNamespace(ctx context.Context, req *pb.CreateNamespaceRequest) (*pb.Namespace, error) {
}
Protocol Buffers 用于 RPC 接口
service MetadataStore {
// 创建命名空间
rpc CreateNamespace(CreateNamespaceRequest) returns (CreateNamespaceResponse);
}
message CreateNamespaceRequest {
// 命名空间名称,必填,最长 200 字符
string name = 1;
// 副本数,0 表示使用集群默认值
int32 replicas = 2;
}
message CreateNamespaceResponse {
string id = 1;
int64 create_time_unix_ms = 2;
}
README 结构
每个项目都应该有一个涵盖以下内容的 README:
# 项目名称
一段话描述这个项目做什么。
## 快速开始
1. 克隆仓库
2. 安装依赖项:`go mod download`
3. 设置环境:`cp config.example.toml config.toml`
4. 启动本地开发集群:`make run-dev`
## 命令
| 命令 | 描述 |
|---------|-------------|
| `make run-dev` | 启动本地开发集群 |
| `make test` | 运行测试 |
| `make build` | 编译发布产物 |
| `make lint` | 运行 Linter |
## 架构
项目结构和关键设计决策的简要概述。
链接到 ADR 以获取详细信息。
## 贡献
如何贡献、编码标准、PR 流程。
Changelog 维护
对于已发布的功能:
# Changelog
## [1.2.0] - 2025-01-20
### 新增
- 快照增量传输:Follower 落后时按增量同步快照(#123)
- 节点扩缩容事件的 metrics 上报(#124)
### 修复
- Leader 快速切换时日志条目被重复应用(#125)
### 变更
- 单次复制批次上限调整为 50 条日志(原为 20),以提升高延迟链路的吞吐(#126)
面向智能体的文档
对 AI 智能体上下文的特殊考虑:
- CLAUDE.md / 规则文件 — 记录项目约定,使智能体遵循它们
- 规格文件 — 保持规格更新,使智能体构建正确的东西
- ADR — 帮助智能体理解过去决策的原因(防止重新决策)
- 内联陷阱 — 防止智能体落入已知陷阱
常见合理化借口
| 合理化借口 | 现实 |
|---|
| "代码是自文档化的" | 代码展示是什么。它不展示为什么、哪些替代方案被拒绝或适用哪些约束。 |
| "API 稳定后我们再写文档" | 当你编写文档时 API 稳定得更快。文档是设计的第一个测试。 |
| "没人看文档" | 智能体会看。未来的工程师会看。三个月后的你自己会看。 |
| "ADR 是额外开销" | 一个 10 分钟的 ADR 可以防止六个月后针对同一决策的两小时争论。 |
| "注释会过时" | 关于为什么的注释是稳定的。关于是什么的注释会过时——这就是为什么你只写前者。 |
红旗警告
- 架构决策没有书面推理
- 公共 API 没有文档或类型
- README 没有解释如何运行项目
- 被注释掉的代码而非删除
- 存在数周的 TODO 注释
- 有重要架构选择但没有 ADR 的项目
- 重述代码而非解释意图的文档
验证
编写文档后: