AI 协作项目需要 CLAUDE.md、AGENTS.md、CONSTRAINTS.md 和 SESSION_HANDOFF.md 吗?本文说明如何设计指令加载链、自动化检查与 CI 门禁,并维护不过期的会话交接。
用 AI 做过几个项目之后,很容易得出一个相似的结论:应该在起步时就把规则写下来。常见的方案是三份文档——一份宪章式的文件,声明开发过程该遵循哪些范式;一份 CONSTRAINTS.md,列出具体的约束条件;一份 SESSION_HANDOFF.md,供下一次会话接手未完成的工作。
这个方向是对的。写下来的规则确实比每次重新交代要省力,也确实能减少 agent 在项目里横冲直撞。但我见过不少项目认真写了这三份文档,效果却和没写差不多。问题通常不出在写得好不好,而出在一个更前置的地方:一份文档要真正影响 agent 的行为,它得先被加载进上下文,再在具体动作发生时被遵守,而且它描述的世界还得和当前代码一致。
这三道关各有各的失效方式,也各自对应一条值得遵守的纪律。文件数量本身不在其中。
第一道关最朴素,也最容易被忽略:文档得先进入上下文,才谈得上其他。
现在的编码 agent 大多有一套约定的指令发现机制,CLAUDE.md、AGENTS.md 或类似的文件通常是其中的入口。但这套机制并不总是「只读一个文件」:有的工具会沿目录层级加载指令,有的支持导入、规则目录或按路径加载。反过来,一个任意命名的 CONSTRAINTS.md 即使躺在仓库根目录,也不会仅仅因为位置显眼就自动进入上下文。
更稳妥的说法是:只有落在当前工具明确支持的发现或导入链上的文档,才值得期待它会被加载。拆文件之前,先确认工具究竟会读哪些文件、何时读;否则这类失败很难察觉,因为它不报错。输出看上去是正常的代码,只是某几条约束从来没被纳入考虑。
由此可以得到一个判断标准:**如果两份文档每次都要一起读,它们就应该处在同一条可验证的加载链上。**对小项目来说,直接合并成一个文件通常最省心;但如果维护者、更新频率或目录作用域不同,分成多个文件再显式导入或按路径加载也完全合理。关键不是物理上只能有一个文件,而是不能指望 agent 自己猜到该去哪里找。
如果项目确实大到需要分文件,更可靠的分界线是加载时机和作用域,而不是内容类型。默认加载链只放每次都必须知道的东西:项目是什么、怎么 build 和 test、哪些是绝对红线。其余按需查阅的深度文档——架构说明、部署流程、历史决策记录——单独放,在入口里留一行指针,或者交给工具的路径规则在相关文件被访问时加载。这样拆出来的边界是可执行的:一部分默认加载,一部分按需加载,不存在「这条规则算范式还是算约束」这种没有答案的问题。
过了第一道关,文档进了上下文,接下来的问题是它会不会被真的遵守。
这里有一个我认为最值得强调的判断:写进 Markdown 的约束,性质上更接近建议。
「不要直接改 main 分支」写在 CONSTRAINTS.md 里,和写成一个 pre-commit hook,效果相差很远。前者依赖 agent 读到并在行动时照做,模型对这类上下文指令的遵循并没有硬保证。后者能在本地提交时多拦一道,但也不是硬门禁:它阻止不了 agent 在 main 的工作区里直接改文件,可以被 git commit --no-verify 绕过,而且换一个没有安装 hook 的 clone 就可能完全不存在。真要禁止共享仓库里的 main 被直接更新,需要在远端配置受保护分支或 ruleset,要求通过 PR 和必要检查。
同样的迁移适用于相当多的条目。「函数不要超过 80 行」可以是 lint 规则;「这个模块不许引入新依赖」可以是 CI 里的一条检查;「改完必须能跑通测试」可以由 CI 调用 make test,再把结果设成合并所需的状态检查;「提交信息要带 issue 号」可以先用 commit-msg hook 做本地反馈,再用服务端规则兜底。只有当检查接上一个不能随手跳过的门禁时,约束才真正从「希望它记得」变成「它记不记得都一样」。
这里最好分清三层。make test 这类一键命令是可执行的验证,能快速给出反馈,但仍然要有人或 agent 去运行;本地 hook 是自动触发的护栏,更及时,却可能没安装或被绕过;受保护分支、必需的 CI 检查和权限策略才是共享仓库里的硬门禁。三者都有价值,只是约束力不同。
所以写约束清单时,值得对每一条先问两句:它能不能变成 lint 规则、测试、hook 或 CI 检查?变成之后,又会在什么时机被运行或强制执行?能自动化的就迁过去,Markdown 里只留一行指针,说明这条约束在哪里验证、属于反馈还是硬门禁——留这行指针是有用的,它让 agent 知道有这么一道关卡存在,从而不会白白写出一版必然被拒的代码。剩下真的没法自动化的——架构取向、技术选型偏好、某个模块的历史包袱、「这块代码正在重构,别顺手改」——才值得用文字承载。一份三十条的约束文档,往往有二十条本该是代码。
与此相关的另一个观察是:**一条能一键跑通的测试命令,对 agent 行为的约束力常常超过一整份规范文档。**原因不难理解。文档是单向告知;只要这条命令被 agent 或 CI 真的运行,验证回路就能双向纠错,让错误尽早暴露,而不需要你在场盯着。在这个意义上,把项目的构建和测试整理到「一条命令跑通」,再把它接进默认工作流,可能是所有文档工作里回报最高的一项前置投入。
这条经验有它的边界,值得说清楚:验证回路管得住「能不能跑通」,管不住「架构对不对」。测试全绿的实现,可能在用一种你完全不想要的方式解决问题。这类判断仍然需要人来做,也正因为如此,文字承载的部分才更应该留给这种没法用断言表达的东西。
第三份文档——SESSION_HANDOFF.md——面对的是另一类问题。它要解决的是会话之间的断裂:上一次做到哪、为什么这么做、下一步是什么。这件事确实需要有人记录,因为它恰好是最容易在上下文截断中丢失的部分。
我最认同这份文档的必要性,也最担心它的失效方式:**过期的交接文档比没有更糟。**没有交接文档,下一次会话会去读代码、跑测试、问你;有一份过期的交接文档,它会直接采信,然后自信地走错路。而它几乎一定会过期——上下文用尽被截断的那次,或者单纯忘记更新的那次。
要让它活下去,有三条规矩值得遵守。
第一,只写仓库里推导不出来的东西。改了哪些文件、加了哪些函数,git diff 和 git log 说得比你清楚,抄进来只是制造第二个真相来源,而且是会先过期的那个。该写的是意图、已经试过并否掉的方案及原因、当前卡在哪、下一步为什么是这个。「否掉的方案」尤其值得记——它是最容易被重复踩的坑,因为代码里不留痕迹。
第二,覆盖写而不是追加。它是当前状态的快照,不是工作日志。一旦变成日志,长度会先超过阅读意愿,再超过上下文预算,最后没有人会读它,包括 agent。
第三,在顶部标上日期和对应的 commit。下一次会话至少能判断它有多旧,以及它描述的状态和当前代码差了多远。这一条成本极低,也是让读者主动检查文档新鲜度最简单的机制之一。
顺带一提,Claude Code 这类工具同时带有跨会话记忆和会话内的上下文压缩。两者不要混为一谈:自动压缩是在同一次会话接近上下文上限时总结旧内容,并不能替代新会话所需的交接;跨会话记忆才会在后续会话中重新加载,功能上和持久文档有部分重叠。可以按稳定程度分工:记忆适合放长期不变的事实,比如个人偏好、项目背景、协作习惯;交接文件适合放短期易变的状态。两者混用容易互相污染——稳定的事实被反复覆盖,易变的状态又留得太久。
把三道关连起来看,三条纪律指向的其实是同一件事:文档的成本不在写,而在维护。
每多一份文件,就多一个会随代码漂移的东西。而漂移之后它不只是失效——它会主动误导,因为读者没有理由怀疑一份写得很认真的文档。从这个角度看,「起步时写三份文档」的直觉需要一点修正:起步时最该做的,是把规则尽量放到不需要维护的地方去,剩下确实需要人维护的,尽量少而准。
这也意味着上面的建议有适用范围。单人项目、单个仓库,一份入口文件加一份交接文件通常就够。参与的人一多、模块一多,按目录拆分入口文件反而更合适——那时「每次都要一起读」的前提已经不成立,前端目录的约定和数据管道的约定本来就不该互相占用上下文。判断标准仍然是加载时机,只是加载的单位从项目变成了目录。
这套做法与从编码者到约束设计者讨论的能力重心是同一件事的两个尺度:那篇讲的是工程师的任务组合正在往定义问题和建立保证证据的方向移动,这篇讲的是这种移动在一个具体仓库里长什么样。
如果需要现成的起点,我把入口文件和交接文件的骨架整理成了两段可复制的模板。
一个还没有答案的问题是:当越来越多的约束迁移到 lint、hook 和 CI 里,文字文档最后会剩下什么?
目前看来剩下的是判断——为什么选这条路、放弃了什么、代价是什么、什么条件下应该推翻它。这部分之所以留在文字里,不是因为还没来得及自动化,而是因为它本身没有可执行的形式:它不描述「必须怎样」,只解释「当时为什么这样」。也许这才是那份宪章式文件真正该写的内容,而不是一堆本该由工具强制执行的规则。