用 AI 长期开发一个项目,我想先把文档减到两份
AI 写代码很快,需求文档、分支和 Worktree 却越来越难管理。我想用两份长期文档保存目标与现状,把每次迭代的理由、取舍和验证留在 PR 中。
和 Codex、Claude 这些 Coding Agent 打交道差不多一个月,我对 AI 写代码的兴奋还在。一个需求,只要把输入、输出、操作过程和页面说清楚,它往往就能做出一个可以运行的东西。
如果代码只用一次,这已经很好了。麻烦出现在第二轮、第三轮开发之后。
第一次讨论需求,AI 写了一份方案,拆成几个模块,再交给不同的 Agent 并行实现。做完以后,仓库里有架构说明,也有讨论留下的设计文档。第二次,我调整需求,它又写了一份。后来的方案可能推翻前面的方案,几份文档甚至相互矛盾。只看文件,我很难确认哪些想法已经进入代码,哪些仍停留在讨论里。
与此同时,分支和 Worktree 越来越多。我开始很难回答几个原本应该简单的问题:现在到底实现到哪里了?哪份文档还有效?这个目录里的代码已经合并了吗?下一轮开发应该接着哪一个版本往下走?
AI 可以很快写出下一版代码,我却越来越难判断,整个项目现在是什么状态。
这篇文章记录的是我和 AI 围绕这个问题的一次讨论:最初考虑用更完整的规范和决策记录管理开发,后来又把方案收缩成两份长期文档,加上以 PR 为中心的迭代流程。它是我准备采用的方法,还没有经过长期项目验证。
最初的方案很完整,但我不想再维护那么多东西
我一开始想找的是具体实践:长期用 AI 开发的人,怎样保存文档,怎样让新会话接上上下文,又怎样管理一轮接一轮的迭代。
最初讨论的方向包括 OpenSpec 这样的规范驱动流程,以及 ADR,也就是架构决策记录。前者把变更的意图、行为要求、设计和任务拆开;后者把一次重要选择的背景、备选方案和后果单独记下来。
这些做法有明确的用途。比如 OpenSpec 会区分当前规范和正在提议的变更,让“现在系统是什么样”与“这次准备改什么”有各自的位置。它的流程也允许回头调整,并非所有事情都必须按同样的重量处理。OpenSpec 概念说明
不过,当我把这些内容放回自己的开发过程里,还是觉得重。每次需求调整,除了检查代码,我还得确认几份文档之间是否一致、哪些决策已经过期、哪些任务还没收尾。
AI 生成这些文件很便宜,阅读、判断和纠正它们的工作却要由我承担。如果一套流程让我每轮都多出几个需要长期照顾的地方,它可能解决了记录问题,却没有减轻我的负担。
于是我把问题缩小了:对于个人或小团队维护的中小型项目,能不能只长期维护两份文档?
两份文档,各自只回答一个问题
我的设想是,在做出第一个最小可运行版本后,整理出 PRODUCT.md 和 ARCHITECTURE.md。后续每次开发的需求、方案和取舍,放到对应的 Pull Request,也就是 PR 中。
它们的分工可以很简单:
| 位置 | 回答的问题 | 什么时候更新 |
|---|---|---|
PRODUCT.md |
这个产品最终要帮助谁完成什么事? | 产品目标或边界发生实质变化时 |
ARCHITECTURE.md |
当前版本怎样组织代码、完成主要流程? | 模块、依赖、数据流或边界发生变化时 |
| PR | 这一次为什么改、选了什么、怎样验证? | 随本次迭代推进,在合并前整理好最终结论 |
这里说的两份文档,指的是帮助人和 Agent 重新进入项目的长期入口。项目确实需要的安装说明、接口契约或操作手册,仍然要有。没有必要为了凑足“两份”,把所有内容塞进两个大文件。
PRODUCT.md:保存目标,给实现留下调整空间
PRODUCT.md 记录的是产品最终服务的场景。我希望它足够短,能让一个刚接手项目的人知道:用户遇到什么问题,用完这个产品以后,应该能完成什么。
它也可以交代少量重要边界,帮助我判断一个新需求是否仍在产品范围内。但我不想把未来所有功能、每轮任务和技术选型都写进去。那些内容变化很快,放进来之后,这份文件很容易变成不断膨胀的待办清单。
目标应该在开始时认真讨论,不因某个库不好用、某种实现不顺利就跟着摇摆。路径可以换,用户最终要完成的事应该相对稳定。
“相对稳定”也不意味着永远不能变。如果后来发现最初理解错了用户的问题,就应该明确修改目标,并在对应 PR 中说明原因。否则,旧目标只会逼着代码和文档一起假装没有变化。
ARCHITECTURE.md:保存当前实现,让人能顺着它找到代码
ARCHITECTURE.md 负责介绍当前版本的主要模块、关键入口、调用关系、数据流和依赖边界。读完之后,我应该知道一项操作大致经过哪些地方,以及下一步该去看哪些文件。
比如用户提交一个任务,从接收请求、保存状态,到后台处理、返回结果,中间经过什么组件,失败由谁处理。一个简短的流程图,加上对应的代码路径,通常比把目录树完整复制一遍更有用。
我最初希望它“实时反映所有代码变化”。但如果把这句话理解为每改一个函数都要更新文档,维护成本又会回来。更可行的约定是:改变了读者理解系统所需的信息,就在同一个 PR 里更新架构说明。
局部重命名未必需要改,增加一个后台执行组件通常需要改。文档描述它所属版本已经实现的结构;尚未落地的设计留在 PR 中,不能提前写成现状。查看主分支文档时,也就能知道已经合入的结构。
另外,代码里存在一条路径,不代表它已经在真实环境里运行成功。没有验证的部署条件或运行假设,应该在相关位置说明。发现文档和代码冲突时,也要先核对实现与验证结果,再判断该修文档还是修代码。
有了反复使用的规则,再加一个规则入口
如果确实存在跨任务的约束,例如哪些检查必须通过、哪些接口需要保持兼容、架构变化怎样同步文档,可以再放一份简短的规则文件。
主要供 Agent 阅读时,可以使用 AGENTS.md;项目已经有 Conduct.md 这样的约定,也可以沿用。关键是选择清楚的入口,避免把相同规则抄到几处,日后改了一份却漏掉另一份。
一条只针对本次修改的限制,先留在本次 PR 中。只有后续任务也需要遵守时,才把它提为长期规则。
把每轮开发收进一个 PR
文档减少以后,迭代过程仍然需要留下来。我们最后收敛的做法,是让 PR 成为每次变更的主要记录位置。我把它叫作 PR-Centric Development,也就是以 PR 为中心组织开发。
PR 本来就能把说明、讨论、代码差异和检查结果放在一起。GitHub 的 PR 页面也正是按这些内容组织的,方便审阅者理解一次变更。GitHub PR 文档
但只有标题和文件差异,仍然解释不了为什么这样改。对我来说,一个能够在几个月后重新读懂的 PR,至少要说清楚四件事:
- 原来有什么问题,这次想得到什么结果。
- 最终采取什么办法,关键取舍是什么。
- 实际验证了什么,结果怎样,还有哪里没有覆盖。
- 这次修改有哪些影响、已知限制,必要时怎样撤回。
这些内容不必很长。小修复可能几句话就够,涉及数据迁移或公共接口时才需要展开。也不必复制整段聊天记录,未来的读者需要的是足以理解最终决定的上下文。
举个假设的例子:一个导出接口总是因为处理时间太长而超时,这次准备改成异步任务。
PR 可以说明,用户现在会在导出完成前断开连接;这次接口先返回任务标识,后台完成导出后,用户再获取结果。讨论中考虑过只延长超时时间,但这次选择异步,是为了让请求不必等待整个处理过程。验证则要覆盖任务提交、成功、失败,以及重复操作的行为。
如果新增了后台执行组件,ARCHITECTURE.md 就应补上新的处理路径。PRODUCT.md 可能完全不用改,因为用户最终还是要拿到导出结果。假如这次暂不支持跨机器执行,这个限制也应写清楚。
这样,同一次修改的理由、实现和验证就能一起检查。以后方案再变,也可以从新 PR 连接到旧 PR,交代哪一项决定被替代了。
还有一个容易漏掉的动作:讨论结束后,要把最终结论整理回 PR 描述。评论里先说 A,后来改成 B,最后又选 C,如果描述还停在 A,读者依然要把整场讨论重新读一遍。
下一次开发,具体怎么接着做
这套方法要解决的,正是打开新会话后不知道从哪里开始的问题。我准备按下面的顺序使用它。
先确定起点。 第一个最小可运行版本完成后,检查代码,整理产品目标和实际架构,把这个版本留在主分支上。早期方案中仍然有用的结论归入相应位置;准备移走旧文件前,先确认有没有未完成工作依赖它们。
每轮先理解当前状态。 新会话先读产品目标、架构和已有规则,再查看本次涉及的代码。只有遇到无法理解的选择,才去查相关历史 PR。没有必要让 Agent 每次从头阅读项目的全部历史。
动手前说清楚本次变化。 先用几段话写明问题、范围和完成标准;有了可提交的初始改动,就可以建立 Draft PR,后续沿着这个入口继续讨论和实现。对于跨多个模块的任务,先确认接口和合并顺序,再决定是否并行。
合并前一起检查代码、结论和文档。 验证完成后,把最终方案与实际结果整理进 PR。目标是否改变、架构是否改变,各自检查一次;需要更新的内容随代码一起合并。一个 PR 尽量围绕一个可以独立解释和验证的变更,避免一次塞进几件互不相关的事。
结束时收尾分支和 Worktree。 合并了什么、放弃了什么、还有什么继续做,要有明确去向。确认没有未保存修改和独有提交后,再移除不需要的工作目录。暂停中的工作则留下对应 PR 或其他明确入口,避免只剩一个看不懂名字的文件夹。
Worktree 给并行任务提供了不同的工作目录,但它们仍然属于同一个仓库。目录分开,不会自动解决两个任务对同一个接口作出不同假设的问题。Git Worktree 文档
我现在更愿意按自己能审阅和合并的速度决定并行量。能同时启动多少个 Agent,并不能说明我能同时接住多少份结果。
想知道“为什么”,再沿着代码找回去
把决策放进 PR 后,回查的路径大致是:
读到一段不理解的代码
↓
用 git blame、文件历史或搜索定位相关提交
↓
通过提交中的 PR 编号或链接,找到对应变更
↓
查看当时的问题、最终取舍和验证结果
这里需要修正我最初的一个期待:git blame 默认标注的是当前每一行最后一次被修改的提交,并不会直接告诉我,这个设计最早为什么出现。一次格式调整或重构,就可能让眼前的提交离最初决策很远。文件改名可以被追踪,跨文件移动或复制的代码还可能需要额外选项。Git Blame 文档
所以,Blame 是入口。遇到这样的情况,还要继续看文件历史、差异,或者搜索相关符号的变化。提交和 PR 之间也需要保留可追溯的编号或链接。
如果采用 Squash Merge,把一轮开发压成一个提交,合并时就要检查提交信息是否留下了足够线索。GitHub 可以配置 Squash 提交信息使用 PR 的标题和描述,但具体采用哪种内容取决于仓库设置及合并时的编辑。GitHub Squash 配置说明
还要记住,PR 的描述和评论属于托管平台的数据,不会因为克隆了 Git 仓库就一起下载。非常关键的决策,至少应该在合并提交里保留简短理由和 PR 链接。需要迁移或备份项目时,也要考虑这些平台记录的去向。
轻量能不能成立,取决于几件不太轻松的事
这套方法减少了长期文档的数量,却没有消除整理工作。它要求我在每次变更结束时,判断哪些信息成为当前状态,哪些只属于这次历史。
PR 如果写成“完成需求,测试通过”,以后依然看不懂。架构变化如果总说“下次再补”,两份文档也一样会过期。新 Agent 如果访问不到 PR,就需要提供相关结论,不能指望历史放在平台上,它就会自动知道。
两份文档也覆盖不了所有项目。涉及支付、安全、复杂迁移、多人长期协作,或者必须独立审计的决策时,可能需要单独的设计、迁移方案或 ADR。等真实问题出现后再增加相应记录,比为了保持文件数量而省略必要信息更合理。
我仍然想从这个小版本开始:两份长期文档,一个明确的迭代入口,每次合并都留下能读懂的理由。