ARTICLE · 1270570
AI Coding 时代的源码仓库最佳实践
发布时间:2026-10-28 23:52:39 最近访问:2026-10-28 23:52:40
AI Coding 时代的源码仓库最佳实践
AI Coding 把代码生产的成本压低了,但没有压低验证和评审的成本,也没有改变一个更基本的事实:执行者不再提问。人遇到含糊会停下来确认,Agent 会补一个看起来合理的解释,然后把它写成代码,而且语气确定。于是仓库里两件事变得致命——没写下来的意图,会被现存代码的样子自动填补;没被机器检查的约束,会在生成过程中被静默绕过。这份文档围绕这两件事展开。它不是一份合规清单,而是一组论证:每条实践都说明它防的是什么、为什么选这个载体、缺了它会怎样。实践按"违反的代价"分成三级,因为把所有条目等权罗列,执行者最后往往只做了最容易的那几条。
0. 怎么读这篇文档
0.1 三个级别
"最佳"不等于"全部都要"。最小集是"缺了就会失控"的那部分;在它之上,是按收益与成本分层引入的做法。原因:把所有实践等权罗列,执行者无法取舍,最后往往是最容易的几条被做了,最关键的反而没做。判据是违反的代价,不是"它听起来重不重要"。一条规则如果违反了没人发现、发现了也容易改,就不该进最小集。0.2 必须项速览
以下十三项构成最小集,缺任何一项都会产生结构性问题:
1. 原则
1.1 【必须】四条判据
少于这个集合,问题迟早出现;多于这个集合,维护成本会超过收益。收益:后续每条建议都可以用这四条检验——不满足的,要么补上机制,要么降级为建议,而不是靠语气强调。1.2 歧义不再被社交吸收
人读"接口要规范",会用自己的判断补全,不确定就问一句。Agent 不问,它会给出一个看起来合理的结果,而且语气确定。原因:人与人的协作里,含糊由沟通兜底;人与 Agent 的协作里,含糊直接变成代码。收益:把要求写成可判定的断言后,错误从"上线后发现"提前到"提交时被拦住"。1.3 上下文是稀缺资源
Agent 每次进入仓库,都要用有限的上下文重建对项目的理解。文件越大、约定越分散,重建越慢、越容易漏。原因:窗口大小是硬约束,不能靠"多读几遍"解决。收益:入口文件短、目录结构可预测、约定按范围分层,能让新会话在几分钟内进入可工作状态。1.4 产出速度超过评审速度
代码生成的成本在下降,评审的成本没有。差距会变成"未评审的改动在主干上堆积"。原因:瓶颈从"写"转移到"验",而人工评审的吞吐是固定的。收益:把机械检查交给流水线,人只审设计意图,评审注意力花在最不可替代的地方。1.5 改动可批量生成,回滚半径必须变小
一次提示词可以改二十个文件。如果这二十个文件跨了三个主题,出问题时你无法只撤掉其中一部分。原因:生成速度放大了"改动粒度不当"的代价。收益:一次提交一个主题,回滚半径等于一个提交,定位问题靠 git log 就够。1.6 代码会成为事实上的规格
人读代码会带着怀疑:这里有个捷径,可能不对。Agent 不会——它从现有形态中归纳模式,然后把模式当作规范复制到新代码里,包括其中的偏差。原因:对生成式执行者来说,"现状"是最强的先验,它不会区分这是设计还是历史遗留。收益:明确写下架构意图后,至少存在一个能与代码冲突的东西;冲突一旦出现,就有机会被发现。反之,漂移不只是可能发生,而是会被下一次生成主动放大。
2. 仓库形态
2.1 【必须】一个产品一个仓库
把同一产品的前端与后端放进同一个仓库,用顶层目录区分。原因:一次需求经常同时改契约和消费方。两个仓库意味着两个提交、两条流水线、两种合并顺序,必然产生一个窗口:后端已合并而前端未跟进,或反过来。这个窗口里主干是坏的,而且没有单次 revert 能恢复一致——你需要 revert 两个仓库的两次提交并保证顺序。跨仓库的历史二分也无法定位"这两个提交彼此不兼容"。收益:一致窗口为零;一个提交可以同时包含契约变更、消费方变更和两侧测试;一次评审能看到线缆的两端;出问题时一次 revert 回到一致状态。边界:同仓不等于自动原子,还缺三件事——契约校验、一次提交覆盖两侧的规则、按路径过滤的流水线。缺了这三样,只是把两个不同步的仓库放进了同一个目录。2.2 【视规模】分仓的正当理由
两侧有真正独立的发布主体(不同产品负责人、不同客户、独立发版节奏);单仓体积已经影响日常操作(通常要到数十 GB 级别才成立)。原因:分仓的唯一正当理由是组织边界,不是技术偏好。用"后端同学不关心前端"当理由,代价由整个团队承担。收益:避免"因为习惯而分仓",也避免为了合并而做无意义的组织调整。2.3 【必须】同仓之后,用路径过滤而不是拆仓
流水线按路径触发:改后端才跑后端作业,改前端才跑前端作业。原因:同仓最常见的抱怨是"改一行前端要等全部测试"。这是流水线配置问题,不是仓库形态问题。收益:保留原子性的同时拿回反馈速度。2.4 【视规模】不要引入构建编排工具
一个后端服务加一个前端应用——也就是两个可独立构建的单元——不需要 Nx、Turborepo、Bazel 这类工具。原因:这类工具解决的是:几十个包之间的依赖图、任务编排、增量构建、远程缓存、统一工具链。它们的价值随包的数量增长,而不是随代码量增长。两个包没有依赖图可算,这里的"增量"就是"改后端就别跑前端",一条路径过滤就能做到。引入的代价是多一层配置、多一个需要升级的依赖、每个参与者(包括 Agent)都得先理解构建系统。收益:少一层抽象,少一处会过期的配置,命令是明的而不是推导出来的。什么时候才划算:前端拆出五到十个共享包(设计系统、工具库、多应用共用代码)时,轻量任务编排开始有意义;包数量到十几个且有真实依赖关系时,带依赖图的工具才有价值;更重的构建系统只在多语言、强可复现构建的大规模场景下才考虑。
3. 目录与命令
3.1 【必须】最小目录骨架
repo/├── AGENTS.md Agent 入口:地图、命令、铁律、禁区├── README.md # 人的入口:这是什么、怎么跑起来├── Makefile 或 scripts/ # 唯一的命令入口(见 3.2)├── docs/│ ├── architecture.md # 系统现状:分层、依赖方向、主链路、改动指南│ ├── adr/ # 架构决策记录,只追加│ ├── specs/ # 需求与能力规格,带状态字段│ ├── changes/ # 进行中的变更计划,合并后移除(见 4.9、9.5)│ ├── domain/ # 领域共同语言:业务共识,手写(见 4.7)│ │ ├── model.md # 术语表与业务不变量│ │ └── states.md # 业务状态机与迁移│ ├── generated/ # 生成视图,禁止手改(见 4.7、7.3)│ │ └── erd.md # 物理数据模型,由数据库 schema 生成│ ├── guides/ # 操作手册:本地起服务、排障、发布│ └── archive/ # 过期文档,显式标注"不作依据"├── contracts/│ ├── openapi.json # 由后端生成,不手写│ └── events.md # 异步事件或流式消息的约定├── backend/│ ├── pyproject.toml # 依赖、测试、检查工具的唯一配置源│ ├── app/│ │ ├── api/ # 路由与请求/响应模型│ │ ├── domain/ # 领域模型与纯逻辑(最该有测试)│ │ ├── services/ # 用例编排│ │ ├── adapters/ # 外部系统、队列、模型调用│ │ └── core/ # 配置、鉴权、错误封装│ ├── migrations/ # 数据库变更的唯一入口│ └── tests/│ ├── unit/ contract/ api/ integration/│ └── conftest.py└── frontend/ ├── package.json ├── src/ │ ├── api/ # 与 contracts 对齐的请求层 │ ├── pages/ components/ store/ utils/ └── tests/ 或与源码同目录的 *.test.ts
原因:目录本身就是最便宜的一条约束——它让"该动哪一层"可以被看见,而不是靠记忆。收益:Agent 能通过目录名判断改动边界,从而不去改无关代码;评审时也容易发现"改错了层"。目录树里最容易放错的是生成物:docs/domain/ 只放领域共同语言(术语、业务不变量、业务状态机),全部手写;ER 图放在 docs/generated/,因为它是从 schema 生成的物理数据模型视图,既不是业务共识,也不属于跨组件契约。生成物的目录必须与手写物分开,否则"禁止手改"这条规则没有清晰的适用边界。3.2 【必须】两条命令原则
仓库必须提供且只提供两个稳定入口——一条启动(make dev),一条验证(make verify)。verify 依次跑完所有本地可跑的检查。原因:Agent 每次进入仓库都要判断"该怎么验证"。如果答案散落在文档、历史提交和同事记忆里,它就会跳过验证或者猜一条。收益:会话开头不用探索,直接 make verify 拿到确定结论;人和其他工具复用同一个入口,避免"我这里能过"的争议;流水线调用同一个脚本,保证本地与 CI 的判定一致。3.3 【推荐】按变更原因分层
api/ 因契约变化而变,domain/ 因业务规则变化而变,adapters/ 因外部系统变化而变,services/ 因用例编排变化而变。原因:按文件类型分层(controllers、models、utils)会把一次需求拆散到各处,且无法回答"这次该动哪一层"。收益:改动的边界可预测;依赖方向可以被检查;测试可以按层定向运行。3.4 【必须】不要建的东西
每个目录一个 README——空文档比没有更糟:它不会覆盖上级 README 的内容(README 没有继承或"就近生效"的语义),却会让读者和 Agent 认为"这个目录的文档已经补齐了",从而取消掉"该写真正文档"这个动作;原因:这些东西的共同问题是没有过期机制,而"看起来已经补齐"比"明显缺失"更难纠正。收益:仓库里每一处存在都能被一句话解释清楚。
4. 文档体系
4.1 【必须】四类文档,四种寿命
| | | | |
|---|
| | | | |
| | 、docs/domain/、docs/generated/、guides/ | | |
| | | | |
| | | | |
原因:四类文档的"过期速度"差了一个数量级。混放会让读者无法判断手上这份是当前的还是三年前的,而错误的依据比没有依据更危险。收益:看到路径就知道该不该信、该信多久;归档有明确去处,不需要靠删除来"治理"。领域共同语言是"描述"里的特例:它跨端复用、必须唯一、且只能由业务共识产出,所以单独占一个目录,见 4.7。ER 图不在此列——它由 schema 生成,是物理数据模型的视图,归入 docs/generated/。"Markdown 是唯一信源"只在文本类文档的范围内成立。项目里的事实各有权威载体:接口形状以代码生成的契约为准,数据库结构以迁移脚本为准,依赖版本以锁文件为准,行为以测试为准。文档写意图、边界与取舍,不复述这些能被机器读出的事实——复述一份就多一处会漂移的副本。单一信源的准确含义是"每个事实只有一处权威",不是"所有事实都写进 Markdown"。4.2 【必须】需求文档必须带状态
每份需求或规格的开头写三行——状态、负责人、取代者(仅在已被取代时填写)。可选状态:评审中(草稿与生效之间的卡点,仅在有正式评审流程时需要)、已撤回(需求取消,且没有继任者)、已归档(移入 docs/archive/,并显式标注"不作依据")。可选状态不是必填项——加上它们只在这套流程真的存在时才有意义。迁移规则:草稿 → 评审中 → 生效;草稿 → 已撤回;生效 → 已被取代(必须填继任者);生效 → 已撤回(需求取消,无继任者)。"已被取代"与"已撤回"的区别是有没有继任者,不是语气强弱。生效之后不要原地大改。澄清措辞、补例子可以;改变已验收的行为、改写验收标准,应当产生一份新文档,把旧文档标为"已被取代"并填写 superseded_by。原因:实现和测试已经按旧标准写下,原地改写会让"文档说的"和"代码做的"同时存在两套历史,而且没有提交能说明哪一套先发生。小步澄清与行为变更的分界,就看验收标准是否改变。实现进度不是文档状态。"未开始 / 进行中 / 已验收 / 已上线"变化以天计,属于任务跟踪系统(issue、看板);文档状态变化以月计,回答的是"这份文件还算不算数"。两者混在一个字段里,读者就无法区分"已上线但已过时"和"生效但还没做",状态字段也就失去了筛选作用。原因:需求文档往往多代并存,且文件名不表达代际关系。没有状态字段时,读者只能按修改时间猜,而"最近改的"不等于"现在生效的"。收益:消除"哪份算数"的猜测;废弃时不必删除,只需改一行状态并指明继任者,历史可追溯;Agent 在入口处就能排除掉不该读的版本。4.3 【必须】验收标准要能落到检查上
需求里的每条验收标准,要么对应一个自动化测试,要么对应 docs/guides/ 里一段可复现的手工步骤。两者都做不到的条目,明确标为"不验收"。原因:不可核验的验收标准在实现阶段会被自由解释,最后变成"看起来做了"。收益:需求、实现、验证三者可以对照;需求变更时,能立刻看出哪些测试要跟着改。4.4 【必须】文本格式,交付物用生成
规格、设计、决策用 Markdown 入库;对外交付的 docx/PDF 由脚本生成。生成物不进入人工评审——人评审的是 Markdown 源和生成脚本。原因:二进制文件无法 diff、无法行内评论、合并时只能二选一。而"生成物"与"源文件"同时存在时,人们往往会改错那一份。收益:变更可审查、可追溯;交付格式变化时只改脚本,内容不会分叉。"不参与评审"的准确边界:它指不作为人工评审对象,不是不参与任何验证。流水线仍要验证两件事——能从源重新生成,且生成结果与提交的版本一致;交付前再由人抽查一次排版、分页与图表渲染。分工是:人看内容与脚本(错误源头),机器看可重复性(生成过程),交付时人看成品(渲染问题)。三者缺一,都会出现"源是对的、发出去的是错的"。例外:若 docx/PDF 本身是唯一法定载体(需要签章、监管报送、对外具有法律效力的文件),它就不是生成物,必须作为正式文件单独评审、单独存档,不能套用这一条。图:用 Mermaid 写在 Markdown 里,或保留 SVG 源文件。只留导出的 PNG,等于把设计变成了不可编辑的截图。4.5 【推荐】设计按生命周期拆成三份
设计空间并没有因为 AI 而消失,只是变集中了。表单、列表、CRUD 这些地方,需求加契约已经把可能性压得很窄,直接写代码是合理的;而数据模型、状态机、模块边界、并发语义、迁移路径这些地方,决策空间原封不动,只是数量少——风险全部堆在那里。所以正确的做法是"少量高密度设计 + 大量无设计直接实现",不是"没有设计"。收益:设计成本与风险成正比——高密度区得到评审,低密度区不被形式拖慢;变更设计不进入长期文档,结构描述与决策记录各自只维护一次。为什么不要一个docs/design/:单独的设计目录没有更新触发点,半年后全是过期图,而读者以为那是现状。设计不是一种文档,而是三种寿命不同的东西,混放就无法标注"这份还准不准"。谁产出与谁批准是两件事:让 AI 写设计没问题,但如果它从规格直接跳到代码,设计决策就被埋进实现,只有写完后才可评审,而那时推翻很贵。高密度区要在代码之前可评审,低密度区代码本身即设计。4.6 【必须】architecture.md:现状的唯一描述
它回答五件事:系统边界在哪、怎么分层、谁可以依赖谁、请求怎么流、新东西该放哪。为什么它是必需而不是可选:没有它不等于没有架构模型,只等于模型不可证伪。模型仍然存在——活在代码的隐含模式里、活在"我们一直这么做"里。过期的文档至少提供争论的入口:有人能把它和代码对照,然后说这里不对。而只存在于头脑中的架构,连这句话都没地方说——它不会错,因为它从未被表述;它也不会被修正,同理。更关键的是 1.6 那条:对 Agent 来说,代码就是事实上的规格。你不写意图,先验就自动取值为"现存代码的样子",瑕疵一并继承。它的风险是静默过期,而这不是省略的理由。架构很少变,但漂移的来源不是架构变了,而是代码变了、架构没变——文档宣称存在干净的约束,而代码里没有。低变更频率反而让偏离更难被发现,因为没有"更新事件"来触发审查。:新增模块、新增跨层依赖、新增外部集成、改变请求主链路,这四种改动必须同时改文档,否则评审不通过;:让依赖规则的报错消息直接写明"违反第 2 节,若为有意变更请更新文档并补 ADR"。文档因此是被调用的,而不是靠自觉维护;收益:新会话与评审者有一个可被反驳的现状模型;偏离一旦出现,有触发点和报错指向它,而不是被下一次生成当作规范复制。写法:两页以内,只写已在主干上成立的东西——混入目标态就无法区分已实现与计划。五节即可:上下文与部署拓扑;分层与依赖方向(含由什么检查强制);请求主链路;结构不变量;改动指南(加一个接口、加一个页面、加一个外部集成分别动哪里)。最后一节对执行者价值最高:它把"该动哪一层"从推理变成查表。一份还是多份:根目录一页写跨端上下文与调用方向;每个可部署单元一份写内部结构。分界是"读者是否需要同时理解两侧"。4.7 【推荐】领域语言手写,ER 图生成
把术语表、状态机、ER 图统称为"领域模型"并放在同一个目录,是一个常见错误。领域共同语言由业务共识产出,ER 图由 schema 产出;前者生成不出后者,后者也表达不了前者。为什么必须分开:领域语言回答"订单的'已锁定'是什么意思、哪些状态迁移合法",它不描述表名、列名、外键;从 schema 里读不出这些含义。反过来,ER 图回答"现在库里实际有什么",它不解释业务语义。把两者放在一个目录、一个名字下,后果是有人会以为其中一个能生成另一个——要么把业务术语硬塞进注释冒充领域语言,要么手改生成的图去"补充业务含义"。为什么 ER 图不放进contracts/:contracts/ 的判据是"跨组件边界 + 被机器消费"(前端按 openapi.json 生成类型、按事件约定解析消息)。ER 图是给人看的现状视图,前端构建不消费它。多个服务共享一个库,属于数据所有权问题,应在 architecture.md 或 ADR 里说明,而不是把 ER 图升级成契约。它和architecture.md的分工:architecture.md 写分层、边界、依赖方向与"为什么这样设计";ER 图只写"物理结构现在是什么"。数据模型的取舍理由属于前者或 ADR,ER 图不承载意图。判断是否散落的标准:改一处 schema,需要手工同步几个地方?答案超过一,就说明有视图被当成了源。图是视图,不是源——手绘的 ER 图在第一次加字段时就已经错了,而且错得没人发现。这条标准只适用于 ER 图这类生成视图:领域语言本来就不随加字段而变,它不变是正确的,不是失同步。原因:手绘图没有任何机制保证与实现同步,而错误的数据结构视图会直接导致错误的查询和错误的迁移。收益:schema 变更必然带动图变更,流水线校验"重新生成后无 diff"(见 7.3);术语表保证前后端用同一套词,而不是各自翻译;两者分开后,各自的更新触发点都很清楚。4.8 【必须】ADR:只记有替代方案的决策
为什么只追加:ADR 的价值是保留当时的判断现场。允许改写,就无法区分"我们改主意了"和"我们一直这么想"——前者说明约束变了,值得重新审视;后者只是在掩盖摇摆。Git 历史理论上有痕,但读者读的是当前文件,不是 git log。改主意就写新的一条,把旧的状态标为"已被取代"并链接过去。边界:草稿可以随便改,只有"已接受"之后才只追加;ADR 只记决策,不记实现细节;后果一节必须写清"放弃了什么",否则它退化成宣传稿。它不是架构元素清单。逐个元素写,得到的是清单,那属于 architecture.md 的职责,而且需要持续维护。ADR 记录的是存在过替代方案、并且选择有代价的点——一个 ADR 常常覆盖多个元素。正常项目几年下来十几到几十条;如果是几百条,说明把"记录"当成了"清单"。双向门(可逆、代价低):选哪个 lint 工具、日志库、目录叫 utils 还是 shared——不需要 ADR,改回来就是。单向门(不可逆或逆转昂贵):数据模型、鉴权模型、部署拓扑、契约风格、供应商锁定——必须有记录。怎么判断一扇门是不是单向:问四个问题,任一为"是"就按单向门处理。删字段、改主键、合并表、改变字段语义(例如金额单位从元改成分)——已经写入的旧数据无法还原,或者还原需要一次专门的数据修复。已发布的接口、对方已接入的回调、第三方账号体系、客户侧的导出格式——回滚时机不完全由你控制。能双写、能灰度、能按开关回退,多半是双向门;只能整体切换、切换瞬间只有一种版本可用,就是单向门。一个提交就能撤,是双向门;需要停机窗口、数据修复、客户通知和多方排期,就是单向门。"重要"不是判据。选主数据库听起来很重,但若能双写迁移、能从备份重建,它可能只是一扇比较贵的双向门;反过来,一个不起眼的字段语义变更(时间戳从本地时区改为 UTC)一旦写进历史数据就不可逆,它就是单向门。判据是撤销的成本,不是决策的分量。门型会随时间和影响范围改变。同一个决策在项目早期可逆(没有数据、没有外部消费者),上线后不可逆;只影响一个模块时可逆,一旦成为跨团队契约就不可逆。所以评估要按"现在的影响范围"做,并把评估时的范围写进 ADR——否则后人无法判断这条记录是否仍然适用。ADR 至少写四样:当时有哪些备选;为什么选它、放弃了什么;逆转成本高在哪一步;什么信号出现时应该重新评估(数据量、调用方数量、供应商涨价、延迟指标越过某条线)。最后一项让 ADR 从"墓志铭"变成"触发器"。"改干净了就不用记"这个想法只对了一半。以更换数据库为例:迁移完成、验证通过、没有残留,历史确实可以不写长。但有三件事在最终状态里看不见——触发原因(不记下来,十八个月后会有人提议换回去,而没人说得出当初为什么走)、迁移中的语义损失(类型映射、排序规则、空值语义、时间精度,迁移后完全不可见,却会以偶发缺陷回来)、以及"验证完善"的范围(这本身是个需要被记录的主张,没写就等于默认全都验过)。ADR 的价值不在保存历史,而在降低下一次同类决策的成本。后来者能区分"改主意"与"一直如此",也能看到迁移中那些在最终状态里消失的语义损失。4.9 【推荐】计划合并时:先抽取,残值进提交
计划是过程文档,寿命等于一次变更。合并时先做一次抽取:文档里任何一条判断,如果没有被代码、契约、规格、ADR 或测试接住,就内联写进对应载体——取舍与备选进 ADR,需求变化进 specs,结构变化进 architecture.md,不变量进测试。长期文档必须自足,不依赖指向计划的指针;指针只是"想深挖再看这里"的便利项。抽取完成后,计划不再承载任何唯一价值,于是树里不留它。原则是:树存现状,历史存变更;计划描述的是"这次变更打算怎么发生",天然属于历史。它的残值(判断现场)写进合并提交的 message body:意图、关键取舍、被否的备选、回滚方式、更新了哪些长期文档。提交消息是最可靠的归宿——与变更永久绑定、随 clone 传播、在 log 与 blame 里可见,而且永远不会冒充现状。若计划太长不适合塞进 message:用 merge commit 保留分支历史(不要 squash),计划文件的增删仍在历史中可查;或用 git notes 给提交挂附件,但 notes 默认不随 fetch/clone 传播、多数工具不显示,只有团队愿意维护传播时才用。原因:过程文档是意图快照,需求一变即失效且不会自动作废;留在 HEAD 的过期计划会被 Agent 当成现状依据,比没有计划更糟。收益:HEAD 只留现状与长期文档;考古入口收敛到 git log;长期文档自足,不随计划删留而残缺。feat(order): 支持订单取消意图:允许未发货订单自助取消,释放库存。取舍:选同步释放而非异步补偿,因为库存服务已有幂等接口;被否:事件驱动(引入最终一致复杂度,收益不抵)。回滚:revert 本提交;迁移 0042 提供 downgrade。文档:architecture.md §3、specs/order.md、ADR-0017 已同步。
5. 把要求变成可核验的
5.1 【必须】三个层次
"可执行优于可阅读"容易被误读成"什么都写成测试"。更准确的三层是:——跑一条命令得到通过/失败:测试、类型检查、lint、schema 校验;——不一定能跑,但有确定真值,能被脚本或 diff 判定;——拿到就能动手,不需要追问:确切路径、确切命令、确切分支条件。核心动作是:把一个需要解释的形容词,换成一句有确定真值的断言,再配一条能验证它的命令。收益:一条要求该写成测试、写成脚本检查,还是写成可操作步骤,不再靠感觉;"可执行"也不会被误读为"必须有测试"。5.2 【推荐】规则的载体有强弱之分
推论:规则的强度应该匹配"违反的代价",而不是匹配"它有多重要"。违反代价高且难以事后发现的,放到最上面几层;违反代价低、改回来很容易的,写在文档里就够了。把所有重要的事都塞进流水线,会让每次改动的摩擦大到人们开始绕过它——那本身就是一种失败。容易被忽略的一个载体是数据库约束:唯一索引、外键、检查约束表达的是"系统中不可能出现的状态",比任何文档都可靠,而且不需要维护。5.3 后端示例
5.4 前端示例
| | |
|---|
| | |
| 请求/响应类型由 contracts/openapi.json 生成 | |
| | |
| | |
5.5 【推荐】不可核验的怎么写
"为什么选 A 不选 B"没有真值,只有权衡。这类内容写成 ADR:背景、决策、后果、备选。原因:强行把它做成测试,会得到编码了偶然行为的脆弱测试,一重构就红,最终被集体忽略。收益:决策有据可查,且不会伪装成必须通过的检查。5.6 【推荐】生成测试与手写测试的分工
自动生成的测试擅长锁定当前行为(防回退),不擅长表达应有行为(防误解)。它能告诉你"改这里会破坏什么",不能告诉你"这里本来该是什么"。原因:生成测试的输入是现有实现,它天然把现状当作正确。收益:生成测试铺覆盖面,少量手写测试守住不变量与业务规则。全部依赖生成,得到的只是"当前实现被永久固化",包括其中的错误。5.7 【推荐】检查循环的前提
以"检查通过"为终止条件的执行循环,是收敛还是震荡,取决于检查的质量。检查太弱,它会学会绕过去——删断言、加跳过、把类型放宽;检查太慢,循环的成本高到没人用;检查有误报,它开始怀疑检查而不是修代码。原因:循环的终止条件是检查,而不是人的判断,所以检查的质量直接成为产出质量的上界。收益:这类循环要配两样东西才可靠——合并层不可绕过的检查,以及 diff 评审。因为"变绿"和"修好"永远是两件事。
6. 闸门
6.1 【必须】最小四道 + 【推荐】完整七道
闸门不必一次装齐,但要区分哪些可以推迟:最小集是四道,其余三道按风险补。原因:这四项覆盖"行为对不对、能不能交付、两侧对不对得上、有没有泄密"。再少的项会留下无法承受的盲区;5 到 7 项的价值随团队规模上升,可以在最小集稳定后再加。收益:主干的可发布性由机器保证,人只需要判断设计是否合理。6.2 【推荐】lint 用基线,不要一次修完
如果存量已有错误,先记录当前数量为上限,流水线只拦"新增"。原因:一次性清零成本高、周期长,而在这期间完全没有保护,是最差的组合。收益:闸门当天就能生效;存量随重构自然消化;新代码不再增加负债。6.3 【必须】没有流水线时的退化方案
至少提供一个 verify 脚本,并在提交前自动执行。原因:闸门的价值在于"不可绕过"。没有流水线时,本地钩子是次优但有效的替代。收益:在引入流水线之前问题就不会流入主干;脚本本身可直接复用为流水线作业。6.4 【推荐】分支保护与合并队列
主干应禁止直接推送,合并前必须通过闸门;多人同时合并时启用合并队列,避免"各自通过、合起来坏"。原因:闸门只在"无法绕过"时才有效;合并队列解决的是并发合并导致的组合回归。收益:主干始终处于可发布状态;不再需要人工盯"谁先合"。
7. 契约与生成物
7.1 【必须】接口契约一处生成,两处消费
后端框架能自动产出接口描述(FastAPI 天然提供),就把它导出为仓库内的契约文件;前端类型由此生成;流水线校验生成结果与提交内容一致。手写接口文档一律不做。原因:手写文档必然与实现背离,因为没有任何机制强制它们同时更新。而背离的接口文档会让消费方按错误假设开发,错误在集成时才暴露。收益:契约不可能是错的——它与代码同源同提交;前端类型无需人工维护;接口变更会在合并前同时暴露两侧影响。7.2 【必须】数据库变更只走迁移
schema 变更只通过迁移脚本,且迁移历史必须是一条线(单头)。"单头"是什么:每个迁移都记录自己的前驱(Alembic 里是 down_revision)。把迁移按前驱关系连成图,没有后继的那个迁移就是头(head)。正常情况只有一个头,所有迁移串成一条线。若两个人从同一个旧迁移各自新增一个迁移再合并,就会出现两个头——两条线都声称自己是最新,部署时只执行其中一条,另一条里的表结构在目标环境根本不存在。怎么检查:alembic heads 只应列出一个;alembic history 应是一条没有分叉的线。出现多头时,用一个空的合并迁移把它们接起来(alembic merge -m "merge heads" ),而不是删掉其中一个——删掉会让已经执行过它的环境与迁移记录对不上。流水线还应从空库执行一次 alembic upgrade head:单头只证明图是连通的,空库升级才证明整条线能跑通。原因:手工改表会让本地、测试、生产三套结构悄悄分叉。而多头的迁移历史意味着两个人的变更互不知情,部署顺序决定了结果。收益:任何环境都能从空库重建出一致结构,部署失败可定位到具体迁移;多头在合并前就被拦住,而不是等到发布日才发现。7.3 【推荐】类型与 ER 同样是生成视图
前端类型从 contracts/openapi.json 生成;ER 图从 schema 生成到 docs/generated/erd.md;二者都加入"重新生成后无 diff"的校验。这条校验具体做什么:在干净工作区运行生成命令,然后对生成物执行 git diff --exit-code。diff 为空则通过;非空说明提交的生成物与当前源不一致——要么源改了忘了重新生成,要么有人手改了生成物。它不判断"内容是否合理",只判断"是不是由当前源生成的那一份"。——同样的输入必须得到逐字节相同的输出。生成器固定版本、字段与条目稳定排序、不写时间戳、不写绝对路径、固定换行符与 locale。任何一处不确定,diff 就会长期为红,这条检查随即被人为关闭;make gen 生成契约、前端类型、ER 图git diff --exit-code -- contracts/openapi.json frontend/src/api/types.gen.ts docs/generated/erd.md
确定性本身只让差异可见,不阻止修改。完整链条有四段:生成确定 → 生成物入库 → 流水线对 diff 判失败 → 分支保护使这个检查不可绕过。少了最后一段,本地改完生成物可以直接推送;少了第二段,流水线没有基线可比。它防的是"无意的漂移"和"顺手改生成物",不防恶意篡改——有写权限的人总能改源或改生成器,那要靠评审与权限控制。它抓不到什么:生成器升级导致的整体格式变化(会一次性产生大量 diff,应当作为独立提交);源本身写错(生成物会忠实地跟着错);生成物被路径过滤排除在流水线之外(检查根本没跑)。所以路径过滤规则必须覆盖生成器脚本与源定义,而不只是生成物本身。原因:凡是"同一事实的第二个表述",只要靠手工同步,就一定会漂移。收益:图与类型永远不是"另一份需要维护的文档";diff 一旦出现,问题在合并前暴露,而不是在消费方集成时才发现。7.4 【推荐】测试库与生产同引擎
原因:方言差异(类型、约束、函数、并发行为)会把问题藏到上线。收益:迁移和关键查询在上线前就经过真实引擎验证。
8. Agent 入口文件
8.1 【必须】只写五类内容
仓库根放一个入口文件,长度控制在 150 行以内,只写:禁区:明确不能做的事(写凭据、改无关代码、把演示当已交付等)。原因:入口文件是每次会话唯一必读的文件,它越长,单位篇幅的价值越低;而把会过期的信息放进去,会让它整体失信——一旦发现有一条过时,读者就会怀疑全部。收益:新会话能在几分钟内定位到正确位置;约束集中在一处,不会散落在各工具的私有配置里。8.2 【必须】它是渐进式披露的起点
披露路径是三层:根文件(全局)→ 每个可部署单元一份(命令与铁律不同)→ 代码与文档。冲突时"离被修改文件最近的那份优先",所以模块级文件可以覆盖根文件的局部约定。再往下按功能目录铺通常是浪费——部分工具默认不读嵌套文件,稳妥的上限就是三层。原因:上下文有限,必须让"必读"和"按需读"分层。收益:常见任务只读根文件即可开工,边缘任务才深入模块文件。8.3 【必须】文件名优先用 AGENTS.md
它已是跨工具的开放约定,多数编码工具会从仓库根自动读取。有些工具仍以自己的文件名为准(如 CLAUDE.md、.cursor/rules/*.mdc),给它们放一行指针即可。只维护一份内容源,不要复制——复制的那一刻就开始漂移。原因:多份真相同步没有机制保障。收益:换工具不用重写约定;一处修改处处生效。8.4 【必须】明确不写什么
完整目录树、依赖清单、最近改了什么、通用编程建议、以及"要写高质量代码"这类无法核验的话。原因:前四项可以直接读出来或必然过期,最后一项不改变任何行为。收益:文件保持短小,每一条都真的被使用。
9. 日常协作
前八章解决"仓库长什么样",这一章解决"每天怎么用它"。漂移往往发生在日常操作里,而不是架构评审里。9.1 【必须】一次提交一个主题
原因:生成速度让一次改动可以横跨多个主题;混合提交无法单独回滚,也无法用作二分定位的单元。收益:回滚半径等于一个提交;出问题时 git log 直接指向原因;评审者能一次看完一个完整意图。9.2 【必须】依赖锁定
锁文件必须入库;安装依赖时必须使用锁定模式(而非"解析最新兼容版本");升级依赖是一个显式提交,不是安装时的副产品。"依赖锁定文件入库"是什么意思:依赖有两层表述。第一层是声明——pyproject.toml / package.json 里写"需要某库不低于某版本"或"允许某大版本内的更新",它给的是一个范围;第二层是解析结果——uv.lock / poetry.lock / package-lock.json / pnpm-lock.yaml,它记录这次实际选中了哪个版本、每个传递依赖选了哪个版本,通常还带哈希。入库的是第二层。只入库声明,等于每次安装都重新求一次解,两台机器、两个时间点就可能得到不同结果。安装必须走锁定模式:uv sync --locked、npm ci、pnpm install --frozen-lockfile。普通 install 在声明与锁文件不一致时会顺手更新锁文件,把"环境不一致"变成"看起来装好了"。升级是一个独立提交:锁文件的变更应当只包含升级本身,提交信息写明升了什么、为什么。这样出问题时能单独回滚,也能用二分定位"是哪次升级引入的"。安装时被动产生的锁文件变化,不要混进功能提交。应用与库的边界:可部署的应用必须提交锁文件;要发布给别人安装的库通常不提交(或只保留一份供 CI 用),因为库的依赖范围必须由使用方求解,把解析结果强加给下游会制造冲突。只锁直接依赖不够:传递依赖同样会变,锁文件的价值恰恰在于固定整张依赖图,而不只是顶层几行。原因:没有锁文件时,同一天在两台机器上装出的依赖可能不同;Agent 尤其容易在"能跑就行"的目标下拉进新版本。可复现构建是一切验证的前提——本地过、CI 不过,多数时候是依赖差异。收益:本地、CI、生产三处一致;问题可复现;升级的影响范围是一个可以回滚的提交。9.3 【必须】配置与密钥分离
仓库里只放 .env.example(键名 + 说明 + 安全默认值),真实值来自环境变量或密钥管理;应用启动时校验必需变量,缺失即失败,而不是使用隐藏默认值。原因:密钥进入 Git 历史是不可逆的;而"缺失时悄悄用默认值"会让错误配置一路跑到生产才发现。收益:新环境十分钟内可起;配置错误在启动时就暴露;密钥泄露面收敛到运行环境。9.4 【推荐】测试分层与定向运行
按 unit / contract / api / integration 分层,并提供"只跑某一层"的命令。原因:全量测试一旦变慢,执行者(人和 Agent)就会跳过它;而不同层的失败含义完全不同,混在一起无法快速定位。收益:日常反馈在秒级,提交前跑全量;失败信息直接指向"逻辑错"还是"接口错"。9.5 【推荐】高密度改动先写实施计划
触及数据模型、鉴权、跨模块边界、外部依赖、并发语义的改动,先写一页实施计划再动代码;其余改动直接做。计划放在 docs/changes/<日期>-.md,一个变更一个文件,目录里只放进行中的变更。它在 AI 协作里的角色是外部记忆。大改动跨多个会话,而 Agent 没有跨会话记忆;计划是每个会话恢复上下文的唯一依据——同一意图、同一边界、同一剩余步骤。没有它,每个会话各自推导意图,推导结果还会彼此漂移。一页内写六样:意图与明确不做什么;触及哪些长期载体(specs、契约、迁移、architecture.md 的哪一节);分解成原子步骤,每步配一条验证命令;回滚方式;完成定义;收尾时要更新哪些长期文档。最后一项保证变更结束时文档更新不被遗忘。进行中的修改规则:步骤勾选可以原地更新,那是过程状态,不是事实;但方案本身若变(换设计、换边界),不要静默改写,在计划里追加一条决策记录——为什么换、放弃了什么,否则后续会话会把被否的方案再提一遍。若这个变化其实是单向门决策,升级为 ADR,而不是留在计划里。边界:计划引用 specs、ADR、architecture.md,不复述它们;一旦开始复述需求或架构,说明它在漂移,该删的是复述部分。合并时的抽取与归宿见 4.9。原因:这些地方的决策一旦埋进实现,只有写完才能评审,而那时推翻很贵。收益:在成本最低时对齐方向;跨会话拿到同一意图;计划不进入长期文档树,不会冒充现状。9.6 【推荐】评审只看意图与风险
机械问题交给闸门,人的注意力放在四问:意图是否清楚?边界是否被跨过?失败路径是否处理?有没有引入新的隐式约定?原因:评审是唯一不可自动化的环节,注意力必须花在机器判断不了的地方。收益:评审时间下降,结论更稳定;不会出现"格式争论三小时、设计问题没人看"。9.7 【推荐】定期回滚演练
每季度挑一次最近发布,在预发环境真实走一遍回滚;数据库迁移要么提供可执行的降级,要么明确声明不可逆并配套备份策略。原因:回滚路径平时不走,真出事时才发现它不可用——迁移不可逆、配置有顺序依赖、旧版本读不懂新数据。收益:把"可回滚"从假设变成已验证的事实;迁移设计阶段就会考虑不可逆的代价。9.8 【视规模】提交模板与变更日志
团队超过五人,或需要对外发版时,再统一提交格式并自动生成变更日志。原因:格式统一的价值随人数增长;两人团队手工记录更快。收益:发版说明可自动生成,追溯成本下降。
10. 【必须】落地顺序
顺序本身有讲究:先让现状可信,再装自动闸门,最后才写约定。反过来做,约定会写在流沙上。顺序上的一个陷阱:不要在仓库结构调整(例如合并前后端、移动目录)之前写入口文件与流水线,否则路径要改两遍,而失效路径比没有路径更糟——它看起来像权威。
11. 反模式
| | |
|---|
| 换一个执行者就读不到,且容易写出只有某工具认识的路径 | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
12. 【视规模】什么时候才加东西
"最佳"不等于一次装完。下面是明确的升级阈值,达到再加,避免提前设计。
附录 A:检查清单
A.1 必须(最小集)
前后端在同一仓库,顶层按 backend/、frontend/ 分开两条命令:一条启动、一条验证全部,且本地与流水线执行同一套检查入口文件不超过 150 行,只写地图、命令、权威表、铁律、禁区A.2 推荐(有效可行)
分层与依赖方向由检查强制(如 import 规则),而不只写在文档里领域共同语言(术语、不变量、业务状态机)集中在 docs/domain/,全部手写ER 图由 schema 生成到 docs/generated/erd.md,校验重新生成后无 diff前端类型由 contracts/openapi.json 生成设计按生命周期三分,不存在 docs/design/ 坟场测试分层:unit / contract / api / integration,可定向运行高密度改动先写一页实施计划(docs/changes/),合并时抽取结论、残值进提交 message
附录 B:入口文件骨架
工程约定## 仓库地图| 路径 | 职责 | 技术栈 || --- | --- | --- |## 命令- 启动:make dev- 验证:make verify- 单层测试:make test-unit / make test-api## 文档权威表| 路径 | 内容 | 状态 || --- | --- | --- |## 铁律1. 新接口必须声明响应模型2. 数据库变更只走迁移脚本3. 一次提交一个主题,不改无关代码4. 契约由代码生成,不手写5. 不写凭据,不读凭据6. 演示数据不得进入正式链路7. 诊断类需求只输出结论,不改代码## 禁区- 不修改生成物- 不在仓库内保存密钥附录 C:ADR 模板# NNNN. 标题- 状态:提议 / 已接受 / 已废弃(被 NNNN 取代)- 日期:YYYY-MM-DD## 背景约束是什么,为什么现在必须决定。## 决策选了什么,具体到可执行。## 后果变简单的、变复杂的、放弃了什么。## 备选还考虑过什么,为什么不选。