本文是「Agent-first 大型项目文档分层」系列的第一篇。素材来自某企业级支付审批系统 7 个月的 AI 协作开发:我和 Claude Code、Codex、Kimi 一起写代码、写测试、写文档。系列只讲文档这条腿,测试方法论另开专题。
一、两次封板,和一份早就写好的规范
9 月 16 日,某企业级支付审批系统在同一天打了两个版本的 tag。这两个版本的更新日志和交接说明当时都没有写。
两天后,仓库里出现了一个提交,一次做了三件事:新建一份独立的封板规范(封板:冻结版本、跑全量测试、打 tag、出包);在入口文件里加一张「操作 → 必读」表;补写这两个版本漏掉的 release 文档------给业务用户的更新日志和给运维的交接说明。表上方的引言把教训写进了给 Agent 的入口规则:「封板未读全量定义、漏写 release 文档」。
可「全量」的定义并不是这一天才有的。规范早就有:「全量测试固定包含以下六套,缺一不可」早已写在项目测试规范里;文档总入口的目录说明提到过「六套全量纪律」;更早两次封板的变更计划也都要求过六套测试证据。规范本身是够用的,缺的是从入口走到它的那条路。
再看立规之前的入口文件------每次会话都会被自动加载的那份 Markdown------410 行,关于发版只有 Git 分支约定里夹着的一段打 tag 规则,「全量」「封板」两个词一次都没出现。从入口出发,没有任何一处路标把封板引向那份测试规范。
新的封板规范写了六套全量定义、封板 checklist 和 release 两件的必含项;入口文件那张表的第一行是这样的(节选,已脱敏):
| 操作 | 必读规范 |
|---|---|
| 封板 / 出包 / 打 tag / 说「全量绿」 | 封板规范(六套全量定义 + checklist + release 两件模板) |
注意第一列最后一项:说「全量绿」。说一句话也被登记成了一个操作------Agent 想说出这三个字之前,必须先去读那份规范。(第二列写的是「模板」,规范正文其实是让 Agent 照一份历史文档仿写,这个坑留到第③篇。)截至 10 月 14 日,立规后 v2.0.5 至 v2.3.1 这 9 个版本,更新日志和交接说明每版齐全,第②篇细说。
所以「Agent 老是忘事」这个说法找错了对象。Agent 没有失忆,它本来就没有跨会话的记忆,每次会话都是一个从入口走进来的新人;问题在于入口没有把它带到该去的地方。这个系列的核心论点只有一句:在 Agent 写大部分代码的项目里,文档的第一读者是 Agent,文档体系本质上是一套给 Agent 用的路由系统。 本篇讲总模型,后三篇分别讲路由、寻址和生命周期。
二、文档的第一读者换了
先看量级。这个项目 7 个月里共 3614 次提交,触及文档或入口文件的有 1373 次,其中至少 547 次带着 Agent 署名(按署名统计的下限)。文档目录下截至 10 月 14 日有 832 份 Markdown、51 个结构化变更 bundle(一次变更的需求、设计、实现记录、用例同放一个目录,称为 bundle),还有 301 份早期的单文件 plan。
这些文档谁在读?每次会话开始,Agent 自动加载入口文件,开工前按入口指引读规范,进入模块时翻计划和交接记录。我的立场是:文档现在首先写给 Agent,人读的是 Agent 在决策点上产出的东西,而不是从头通读。
开场那句论点要落到 Agent 的特性上:整套机制都从下表推出来。后文每条原则都会标出它对应哪一个特性。
| Agent 特性 | 对文档的要求 | 对应机制 |
|---|---|---|
| 无跨会话记忆 | 每次从入口进,入口必须是路由表 | L0 + 「操作 → 必读」表 |
| 上下文有限且有成本 | token 是第一成本,按需下钻 | L0--L3 分层、薄索引 |
| 按字面执行、不会意会 | 状态、替代关系、适用范围显式写出;「建议参考」改为「执行前必须先读」 | 状态机、Replaced by、必读表 |
| 能 grep / glob / 跑脚本 | 命名可模式匹配、元信息可解析、规则可 lint | 编号体系、元信息块、doc-lint |
| 同时是主要作者 | 约定靠自觉必漂移,必须机器强制 | hook / CI 校验 |
| 多个 Agent 并存 | 单一入口;教训进 repo 而非个人记忆 | 唯一入口文件(AGENTS.md) |
后两行多说一句。「同时是主要作者」:写规范的是 Agent,违反规范的也可能是下一次会话里的同一个 Agent,所以能机器检查的约定最终都要变成检查(hook / CI 是通用形态,我们项目选了另一种强制方式,留到第④篇)。「多个 Agent 并存」:Claude Code、Codex、Kimi 轮流上,只存在某个 Agent 个人记忆里的教训,换一个 Agent 就等于没有。
代码、测试、文档,三条腿
代码、测试、文档三者同等重要,缺一不可。 代码由 Agent 写,测试证明代码是对的,文档告诉 Agent 该怎么写、写完往哪回写。三者互相咬合:路由表把 Agent 引向测试规范;用例索引把测试反向关联到需求;doc-lint 就是「文档的测试」。开场那次事故里,测试和测试规范一直都在,缺的是文档体系里把 Agent 引到测试规范面前的那一跳。
三条推论
推论 1:人的角色从读者变为裁决者。(对应「同时是主要作者」)Agent 既然是主要作者,人就不必再是文档的日常读者和维护者。人出场的地方是裁决点:批准 plan、拍板、封板验收、确认交付物;人读 Agent 为裁决点准备的摘要、diff 和对话总结,不翻文档树,也不维护文档。这也意味着裁决的质量取决于 Agent 摆上桌的东西:Agent 没读到的规范,不会出现在它准备的摘要和 checklist 里。让 Agent 读到该读的那份,是人能裁决的前提。
推论 2:交付物 ≠ 工程文档。(对应「上下文有限且有成本」)用户手册、更新日志、部署交接说明的读者是业务用户和运维,属于产品交付物。它们由 Agent 维护,但不进 Agent 的路由体系,否则只会白白消耗上下文;路由表只登记「什么操作之后必须更新它」。不登记的代价我们付过:某条审批流程 7 月 7 日上线,用户手册 7 月 15 日才补上,中间 8 天手册里的流程清单是错的。
推论 3:一份文档只服务一种读者。(对应「上下文有限且有成本」)同时服务人和 Agent 的文档必然变胖。我们的 plan 索引本该只回答「这个变更的文档在哪」,却被当成进度看板来写,「下一步」一列平均 98 字,最长一行 1128 字,Agent 每查一次索引都要先吞下一整面进度叙述。索引只给 Agent;人看进度,由 Agent 现场汇总或脚本生成视图。
三、六个症状
「Agent 老是忘事」是个笼统的抱怨。拆开看是六种不同的失败,每一种在这个项目里都有现场。
症状 1:不知道该读哪份,就不读。 开场的封板事故是典型:规范在,路径不在。Agent 不会因为「好像应该有个规范」就去全库搜索,路径不在入口里,它就只能凭常识开工。
症状 2:读到过期的计划,当成当前事实。 文档模型规范开头的 AI Quick Path 写着:文档显示已被替代时,必须先跳到 Replaced by 指向的新文档。可 51 个 bundle 里有 35 个没写 Replaced by,16 个连 Stable ID(文档的固定编号)都没有。规则在,字段不在,规则就空转。对按字面执行的 Agent 来说,没写「已被替代」就等于「仍然有效」。状态本身也不可靠:规范定义 8 个状态,索引里实际出现 9 种写法。
症状 3:读得太多,被历史淹没。 入口文件涨到 457 行,混着大量实案叙事;roadmap 顶部堆了 34 条日期横幅(以日期开头的进展公告,一条压一条),成了第二份更新日志。Agent 为一件小事开工,先要装下几百行无关的故事,真正相关的约束反而被稀释了。
症状 4:用例找不到来源需求。 用例主索引自称「唯一事实源」,实际是 28 段按批次写的叙述,另有两个派生视图靠人工同步,三份共 909 行。Agent 想知道「这条用例为哪个需求而写」「这个需求有没有用例覆盖」,都只能通读叙述去猜。
症状 5:干完不回写。 18 个已完成的 bundle 没有 implementation.md------实际怎么做的、哪里偏离了计划,只存在于当时那次会话里。下一个接手的 Agent 只读得到计划,不知道计划没有被照做。推论 2 里那份滞后的用户手册,是同一个症状落在交付物上。
症状 6:多个 Agent 各读各的入口。 9 月 3 日之前入口文件叫 CLAUDE.md,按 Claude Code 的约定命名,换个工具未必会被自动加载。另有一批项目级通用教训存在某个 Agent 的个人记忆里,其他 Agent 看不见。同一个仓库,三个 Agent 看到的「项目规则」不是同一套。
把症状和 Agent 特性对一下,它们并不神秘:
| 症状 | 背后的 Agent 特性 |
|---|---|
| 不知道读哪份就不读 | 无跨会话记忆 |
| 读到过期 plan 当事实 | 按字面执行、不会意会 |
| 被历史淹没 | 上下文有限且有成本 |
| 用例找不到来源需求 | 能 grep,却没有可 grep 的关联字段 |
| 干完不回写 | 同时是主要作者 |
| 各读各的入口 | 多个 Agent 并存 |
四、根因:入口文件成了百科全书
六个症状追到底,是两组区分没有做。
第一组:常驻与按需。 入口文件每次会话都被自动加载,是整个文档体系里最贵的位置,它的每一行在每一次会话里都要付 token。治理前的入口文件里放着 9 条「Code review 检查项」、7 处「立规」标注和它们背后的实案故事;一个只改按钮文案的会话,也要把代码审查清单、数据库迁移的坑统统装进上下文。反过来,真正该常驻的路由------比如「封板前去读哪份」------当时(9 月 18 日之前)却不在。
第二组:当前事实与历史。 当前事实回答「现在是什么样」,要信;历史回答「当时为什么这样」,要查证。我们项目里两者混在一起:项目概览停在 9 月 3 日,之后发了好几个版本都没改;重构专题的 roadmap 里,8 项标题打着 ✅,正文却写着「待启动」。人读到这种矛盾会去问,Agent 只会在两句话里挑一句信。
两组区分都没做,结果就是:入口文件成了百科全书,而不是路由表。 百科全书回答「是什么」,路由表回答「去哪读」。百科全书式的入口越尽职就越长,越长就越难找到要紧的那一条;开场那次事故,正是百科全书没收录、路由表又不存在的结果。
五、三条原则
从根因倒推,总模型建立在三条原则上,每条都能一句话复述,括号里是对应的 Agent 特性。
原则 1:入口只路由,不承载。(对应「无跨会话记忆」)入口是 Agent 唯一确定会读到的东西。除少数硬约定外,它的职责是告诉 Agent「这类工作去读哪份」,而不是把那份的内容搬进来------搬进来的内容会过期、会膨胀,最后把路由本身淹掉。
原则 2:常驻层只放违反了就来不及的约定,每条带一句理由,其余按需下钻。(对应「上下文有限且有成本」;理由那半句对应「按字面执行、不会意会」)判断一条内容该不该常驻,只问:如果 Agent 这次没读到它,会不会在被发现之前就造成损害?会,就常驻;不会,就放到 L1 及以下,由路由表在需要时引过去。理由不能省:只有命令没有理由,字面一不匹配 Agent 就会放过;有了理由,它才能在规则没覆盖到的边界情况下推断出正确做法。完整实案下沉到 L2 / L3,入口只留链接。
原则 3:当前事实和历史分开放,状态与替代关系写成字段。(对应「按字面执行、不会意会」)人能从语气里读出「这份好像是旧的」,Agent 不能。一份文档是否有效、被谁替代,必须是可解析的字段,而不是需要意会的叙述。写成字段还顺带让 Agent 能 grep、脚本能校验,这在第③④篇展开。
六、总模型:L0--L3 四层
三条原则落到文档结构上,就是四层:
| 层 | 内容 | 何时读 | 约束 |
|---|---|---|---|
| L0 常驻 | 入口文件硬约定 + 路由表 | 每次会话自动加载 | 必须短 |
| L1 按工作类型 | 规范(开发/测试/DB/封板/手册/部署) | 开工前按路由表只读一份 | 通用层 + 项目层 |
| L2 按模块/变更 | plan bundle、长期专题、交接文档 | 进入该模块时 | 有状态、有替代关系 |
| L3 证据 | 封板测试报告等执行报告、审计输出 | 需要证据时下钻 | 不承担入口职责 |
L0 常驻 只放两类内容:违反了就来不及的约定(每条一句规则、一句理由、一个链接),以及告诉 Agent 每类工作该去 L1 哪一份的路由表。它每次都被加载,所以唯一的约束是短。多个 Agent 并存时 L0 只能有一份:唯一入口文件 AGENTS.md,其他工具约定的入口文件只做转发(对应「多个 Agent 并存」)。
L1 按工作类型:开发、测试、数据库、封板、用户手册、部署,每类工作一份规范,开工前按路由表只读对应那一份。L1 分通用层和项目层:项目层声明继承哪份通用规范,只写本项目的填充、现状、例外和坑。
L2 按模块 / 变更 :一次变更的 plan bundle、一个长期专题、一份交接文档,进入该模块时才读。L2 天然会过期,所以必须有状态、有替代关系。
L3 证据 :封板测试报告、审计输出、事故报告,需要证据时才下钻。L3 不承担入口职责------规则如果只写在某份报告的结论里,就只在有人碰巧读到它时才生效。
一个 L0 入口文件的骨架大致如下(只放关键部分,完整模板留给系列末尾的 skill):
markdown
# AGENTS.md ------ 唯一入口(每次会话自动加载,保持短)
## 硬约定(始终生效)
- <一句话规则>。理由:<一句话>。细则:<L1 链接>
## 操作 → 必读
| 操作 | 必读规范 | 一句话要点 |
|---|---|---|
| 封板 / 出包 / 打 tag / 说「全量绿」 | <封板规范> | <全量口径的一句话> |
| DB 结构 / 跨环境数据变更 | <项目数据库规范> | <最关键的一条约束> |
## 分层阅读原则
L0 = 本文件,始终生效;L1 = 上表按工作类型定址,开工前只读对应行;
L2 = 模块的 plan / 专题 / 交接文档,进入该模块时再读;L3 = 证据,需要时下钻。
判断不了读哪份时,先读上表,再到对应规范开头找下一层索引。
L2 文档头部则一律带一段可解析的元信息,这是原则 3 的落点:
markdown
> Status: approved
> Stable ID: <T>-YYYYMMDD-NNN
> Replaces: none
> Replaced by: none
Status 只能取固定词表里的值,不许自由发挥;文档被替代时,Replaced by 填新文档的 Stable ID,Agent 读到就跳转。
这个模型留下三个问题,正好是后面三篇:
| 问题 | 落在哪 | 篇目 |
|---|---|---|
| 该读哪几份? | L0 → L1 的路由:路由表、统一入口、两层继承 | ② 路由 |
| 去哪找、信哪份? | 目录、编号、bundle、索引、状态与替代链 | ③ 寻址 |
| 写到哪、怎么不烂? | 写回闭环、机器强制、记忆分层、新鲜度 | ④ 生命周期 |
七、这套体系是被事故逼出来的
我得坦白:上面的四层模型不是一开始设计好的,而是 7 个月里一次次事故之后,回头才看清的形状。
3 月 4 日项目初始化,入口文件 CLAUDE.md 128 行;当天就加上了后来影响最大的规矩「Plan 驱动开发」:功能先写 plan,确认后才动代码,完成后回填实际执行记录,同时维护 roadmap。文档从此跟着开发节奏生长------那种早期单文件 plan 到今天有 301 份,其中 218 份回填了「实际执行记录」。
5 月 26 日入口文件 302 行,8 月 20 日 406 行。增长的部分有很大一块是实案叙事:踩了坑,就在入口文件里加一段,写清楚发生了什么、以后怎么做。每段单独看都有道理,合起来,入口文件成了一本不断加厚的事故手册。
9 月 3 日是第一个转折点:一次文档信息架构重整动了 235 个文件。散在四处的测试资料合成一处;plan 收成结构化变更 bundle、长期专题、早期单文件 plan 三类;改为 AGENTS.md 唯一入口,CLAUDE.md 只做转发。这次缓解了「各读各的入口」和「放在哪」,但没解决「该读哪份」。
9 月 18 日,开场那次封板事故之后,「操作 → 必读」表第一次出现,入口文件开始有了路由的形状。9 月 29 日,数据库迁移撞号,两条同号迁移都已在生产执行,搬移返工后立了「取号前先查所有并行分支和 worktree」。9 月 30 日,同名旧包被误部署,此后包名强制带时分秒。10 月 13、14 日,两次把浏览器自动化工具(MCP)的操作当成验收,立了「MCP 只定位不验收」并写进路由表。
10 月 14 日,路由表扩成 L0 / L1 / L2 的分层阅读原则------本文模型的前身;此时入口文件 457 行。同一天,项目立了一个专门的文档治理变更(编号 D-20261014-001),这个系列也同时动笔,治理结果见第③④篇。
排开来看,模式很清楚:后来加的规则,几乎每一条背后都站着一次事故。 体系是被事故逼出来的;写这个系列,是希望下一个项目不必再拿事故去换。
八、每类文档在哪一层、谁读、人做什么
总模型必须对所有文档类型一视同仁。下表把十四类常见文档放进四层。在 Agent-first 的体系里,人不是任何一类文档的日常读者,但每一类文档都有一个人必须出场的裁决点。
| 文档类型 | 属于哪层 | 谁读 | 何时读 | 人的角色 |
|---|---|---|---|---|
| 需求 | L2 | 实施、审查该变更的 Agent | 进入该变更时第一份读 | 批准前确认「要什么」,对范围拍板 |
| 设计 | L2 | 实施该变更的 Agent | 动手实现之前 | 批准 plan,对方案取舍拍板 |
| 实现记录 | L2 | 接手、返工、追溯的 Agent | 接手时;追问「当时为什么」时 | 读偏差摘要,确认偏离可接受 |
| 开发规范 | L1 | 所有写代码的 Agent | 开工前按路由表只读对应一份 | 实案立规时拍板新规则 |
| 重构专题 | L2 | 改动相关模块的 Agent | 进入该模块时先读历史教训 | 对专题优先级、搁置与收口拍板 |
| 测试规范 | L1 | 写、改、审查测试的 Agent | 动测试之前 | 拍板测试口径,如「全量」含哪几套 |
| 用例索引 | L2 | 改模块、查覆盖的 Agent | 改模块前查已有用例及来源需求 | 验收时确认覆盖缺口是否可接受 |
| 封板规范 | L1 | 执行封板的 Agent | 封板、出包、说「全量绿」之前 | 封板验收:按 checklist 看证据摘要放行 |
| 更新日志 | 交付物 | 业务用户 | 新版本上线后了解改了什么 | 确认交付物:范围、措辞与实际一致 |
| 用户手册 | 交付物 | 业务用户 | 使用系统、查操作流程时 | 确认交付物:抽查流程与界面一致 |
| DB 规范 | L1 | 改表结构、跨环境数据的 Agent | 写迁移脚本之前 | 对兼容口径与例外拍板 |
| 迁移索引 | L2 | 新增迁移、准备部署的 Agent | 取号前;部署前列执行清单时 | 部署前确认 Agent 给出的执行清单 |
| 部署 runbook(操作手册) | L1 | 执行部署的 Agent | 部署之前 | 批准上生产;运维读另一份交接说明 |
| 事故报告 | L3 | 排查同类问题、立规的 Agent | 需要证据时下钻 | 确认根因,决定是否立规、立到哪层 |
三点值得注意。第一,「何时读」一列没有一格是「随时」,每类文档都绑在一个触发时刻上,这就是路由的含义。第二,更新日志和用户手册不在 L0--L3 里,Agent 维护它们但不靠它们获取工作知识。Agent 追溯「某版改了什么」时,读的是 L3 的封板测试报告等执行报告,不是写给业务用户的更新日志;同样,部署 runbook 给 Agent 读,运维读交接说明------一份文档只服务一种读者。第三,「人的角色」一列没有一格是「维护」或「通读」,全是批准、拍板、验收、确认。
九、这个模型会怎么坏
下面都不是假设,都是我们项目治理前的现场。
坏法一:L0 膨胀
入口文件的行数:128 → 302 → 406 → 457。注意最后一段:9 月 3 日改成唯一入口、9 月 18 日加了路由表之后,它仍然在涨。
原因很朴素:每次事故之后,最省事的动作就是在入口文件里加一段,根因一节数过的检查项和立规标注就是这样攒起来的。每段单独看都对,但 L0 的代价是乘法:行数乘以会话数。更糟的是,膨胀的 L0 会淹没路由表自己,让开场那次事故在路由表所在的文件里重演。还有个细节:入口文件里一个标题写着「四坑」,下面列了 5 条------这种不一致能一直留在每次都被加载的文件里,说明它已经长到没人逐行核对了。
对策方向:L0 每条只留一句规则、一句理由、一个链接(原则 2),实案下沉;入口层的链接要有机器检查。路由表本身怎么写才不会长成入口文件,是第②篇的内容。
坏法二:层级边界模糊
四层的边界没人守,就会互相渗透:
- L1 混进了模板和项目适配。 文档模型规范本身 1123 行,通用规范、项目适配和约 460 行模板混在一份 L1 文档里,恰恰违反了它自己定的「通用 → 项目」分层。
- L2 长期专题装进了短期 plan。 重构专题本应只做主题导航,却装了 145 份单文件 plan 和一份 2613 行、带状态标记的 roadmap;根因一节那种「标题 ✅、正文待启动」的打架就出在这里。
- L2 和 L3 不分。 roadmap 顶部的日期横幅记的是一次次发版的历史,本该下沉到实现记录和 L3 的执行报告,却堆在当前事实文档的开头;同一组封板数字在索引横幅、索引行、release、roadmap 四处重复,一处没更新,Agent 面对的就是四个版本的「事实」。
最刺眼的是第一个:定义分层的那份规范,自己没有遵守分层。
坏法三:写文档的 Agent 也会失忆
上面这些膨胀和越界,很多是 Agent 照着规范写出来的。它在写的那一刻遵守了读到的规则,但不记得上周另一个会话定下的约定,也不会回头检查整个体系是否一致。Agent 作为读者会「失忆」,作为作者同样会,靠自觉维持的分层一定会漂移。这是第④篇的主题:写回是变更的一部分,约定由机器强制、人只裁决,入口层零容忍、深层设基线只降不升。
十、小结
- Agent 不是失忆,它本来就没有跨会话记忆;每次会话都是从入口进来的新人。要修的是入口,不是 Agent。
- 文档的第一读者是 Agent,人从读者变为裁决者;交付物另算,一份文档只服务一种读者。
- 入口只路由不承载;常驻层只放违反了就来不及的约定;当前事实和历史分开,状态与替代关系写成字段。
- 四层总模型:L0 常驻(必须短)、L1 按工作类型(只读一份)、L2 按模块 / 变更(有状态、有替代)、L3 证据(不当入口)。
- 体系是被事故逼出来的,它自己也会坏:L0 会胖,边界会糊,写文档的 Agent 也会失忆。
系列导航
- Agent 不是失忆,是找不到入口------大型项目文档分层设计(一)(本篇)
- 让 Agent 只读该读的------文档路由设计(二)
- 让每份文档都有唯一的位置------文档寻址设计(三)
- 让文档不烂------Agent 时代的文档生命周期(四)