Agent不是失忆,是找不到文档入口:大型项目文档分层实践

Agent 不是失忆,而是缺少从入口到规范的路径——文档应为 Agent 重构为 L0–L3 路由系统。
文章基于一个企业支付审批系统七个月的 AI 协作开发经验,反驳"Agent 总是忘事"这一流行抱怨。作者指出,Agent 本就没有跨会话记忆,每个会话都是从入口文件进入的新人;问题不在 Agent,而在入口文件没有把它带到该去的地方。全文围绕一个论点展开:在 Agent 写大部分代码的项目里,文档的第一读者是 Agent,文档系统就是为它服务的路由系统。文章从六种真实症状(不读文档、把过时计划当真、淹死在历史里、用例无法追溯、不写回、各读各的入口)出发,追到两个未被做出的区分——常驻与按需、当前事实与历史,进而提出三条原则与 L0–L3 四层模型,并坦陈模型的每一层都是被事故逼出来的,最后列出它必然崩坏的三种方式。

这是系列文章《面向 Agent 的大型项目文档分层》的第一篇。素材来自一个企业支付审批系统七个月的 AI 协作开发:作者与 Claude Code、Codex、Kimi 轮流协作,共同产出代码、测试和文档。系列只讨论文档这条腿,测试方法论是另一个话题。
两次发布、一份早已写好的规范
9 月 16 日,一个企业支付审批系统在同一天打了两次版本标签,两个版本都没有 changelog,也没有交接说明。
两天后的一次提交同时做了三件事:新增独立的"发布封版"规范(封版=冻结版本、跑全量测试、打标签、产出交付包)、在入口文件加入"操作 → 必读文档"对照表、补写那两个版本跳过的发布文档。对照表上方的说明把教训写进了 Agent 的入口规则:"未阅读全量测试定义就封版发布,且跳过了发布文档。"
真正值得注意的地方在于:"全量"的定义并非当天才有。规范早就存在。 "全量运行永远包含以下六个测试套件,缺一不可"早就在项目测试规范里;文档首页提到过"六套件全量纪律";更早两次封版的变更计划也要求过六套件测试证据。
规范本身足够,缺的是从入口文件到规范的那条路径。事发前的入口文件共 410 行,关于发布的唯一文字是藏在 Git 分支约定里的一条打标签规则——"全量套件"和"发布封版"这些词从未出现。从入口出发,没有任何线索把一次封版指向那份测试规范。
所以"Agent 总是忘事"这句话怪错了对象。Agent 不会失忆,它从来就没有跨会话记忆。每个会话都是一个从入口走进来的新人,问题是入口没有把它带到该去的地方。整个系列只有一个论点:在 Agent 写大部分代码的项目里,文档的第一读者是 Agent,文档系统就是为这个 Agent 服务的路由系统。
文档的第一读者变了
先看规模。七个月里项目累计 3,614 次提交,其中 1,373 次触及文档或入口文件,至少 547 次带有 Agent 归属标记(下限,按归属行统计)。截至 10 月 14 日,文档树有 832 个 Markdown 文件、51 个结构化变更包、301 份早期单文件计划。
谁在读这些文档?每个会话开始时,Agent 自动加载入口文件;动工前,顺着入口找到规范;进入某个模块时,打开该模块的计划与交接记录。作者的立场是:文档现在首先是写给 Agent 的。 人类读的是 Agent 在决策点产出的摘要,而不是从头到尾翻完整棵文档树。
这个论点必须落到 Agent 的属性上。整个机制由下面这张表推导而来,系列里每一条原则都会标注它对应哪一条属性。
| Agent 属性 | 对文档的要求 | 对应机制 |
|---|---|---|
| 无跨会话记忆 | 每次会话都从入口进入,入口必须是路由表 | L0 + "操作 → 必读"对照表 |
| 上下文有限且花钱 | Token 是第一成本,按需下钻 | L0–L3 分层、薄索引 |
| 按字面文本执行、不推断意图 | 状态、替换、范围必须显式写出;"你可以参考"要改成"动作前必读" | 状态机、Replaced by、必读对照表 |
| 能 grep、glob、跑脚本 | 名称可模式匹配、元数据可解析、规则可校验 | ID 方案、元数据块、doc-lint |
| 同时也是主要作者 | 靠自觉维持的约定必然漂移,必须机器强制 | Hook / CI 检查 |
| 多个 Agent 同时在场 | 单一入口;经验沉淀在仓库,不在个人记忆里 | 唯一的 AGENTS.md 入口 |
最后两行值得单独展开。"也是主要作者"意味着:写规范的那个 Agent,下一个会话里可能正是违反规范的那个 Agent,所以凡是机器能检查的约定最终都会变成检查项。"多个 Agent 同时在场"意味着 Claude Code、Codex、Kimi 轮流上阵,只存在于某一个 Agent 个人记忆里的经验,换个 Agent 就没了。
代码、测试、文档:三条腿
代码、测试、文档三者同等必要,缺一条另外两条都站不住。 Agent 写代码,测试证明代码正确,文档告诉 Agent 怎么写、把结果写回哪里。三者互相咬合:路由表把 Agent 指向测试规范,用例索引把测试指回需求,doc-lint 就是"文档的测试套件"。开头那次事故里,测试和测试规范都在,缺的只是文档系统内部那一跳。
三条推论
推论一:人的角色从读者变成裁决者。 如果 Agent 是主要作者,人类就不必每天阅读或维护文档,只在决策点出现:批准计划、拍板取舍、验收封版、确认交付物。人类读的是 Agent 为这次决策准备的摘要、diff 和会话纪要。决策质量因此取决于 Agent 端上桌的内容——它没读过的规范,不会出现在摘要和清单里。
推论二:交付物不是工程文档。 用户手册、changelog、部署交接说明是给业务方和运维读的,属于产品交付物。Agent 负责维护它们,但它们不进入 Agent 的路由系统,否则只会白白消耗上下文。路由表只记录"哪个操作之后必须更新它"。漏掉这一行的代价是真实的:7 月 7 日一条审批流上线,用户手册到 7 月 15 日才更新,八天里手册上的流程列表是错的。
推论三:一份文档只服务一个读者。 想同时服务人和 Agent 的文档一定会变胖。项目的计划索引本应只回答"这次变更的文档在哪",却被写成了进度看板,"下一步"一列平均 98 个字符、最长 1,128 字符。Agent 每次打开索引都得先吞下一整页进度叙事。索引是给 Agent 的;人要进度,让 Agent 现场总结,或者用脚本生成视图。
六种症状
"Agent 总是忘事"是个模糊的抱怨,拆开看是六种不同的失败,每一种都在这个项目里发生过。
症状一:不知道读哪份文档,就干脆不读。 开头的封版事故就是典型。规范在,路径不在。Agent 不会因为"应该有份规范"就去搜遍整个仓库;入口里没有路径,它就从常识开始。
症状二:读到过时计划,当成当前事实。 文档模型规范顶部的"AI 快速路径"写着:当文档显示已被取代时,先跳到 Replaced by 指定的新文档。而 51 个变更包里有 35 个没写 Replaced by,16 个连 Stable ID 都没有。规则在,字段不在,规则原地空转。对按字面执行的 Agent 来说,没有"已被取代"就等于"仍然有效"。状态本身也不可靠:规范定义了 8 种状态,索引实际用了 9 种写法。
症状三:读得太多,淹死在历史里。 入口文件涨到 457 行,混入大量事故叙事。路线图顶部堆了 34 条日期横幅,俨然成了第二个 changelog。启动一个小任务,Agent 要先加载几百行无关故事,真正重要的约束被稀释。
症状四:用例无法追溯到需求。 用例索引自称"唯一事实来源",实际是 28 段分批写成的叙事,加上两份手工同步的派生出视图,三个文件合计 909 行。Agent 想查"这个用例对应哪条需求",只能读叙事然后猜。
症状五:活干完了,什么也没写回。 18 个已完成的变更包没有 implementation.md。实际怎么做的、哪里偏离了计划,只存在于那个会话里。下一个 Agent 能读到计划,却看不出计划没被遵守。
症状六:每个 Agent 各读各的入口。 9 月 3 日之前入口文件叫 CLAUDE.md,沿用 Claude Code 的约定,其他工具不一定会加载它。另一批项目级经验存在于某个 Agent 的个人记忆里,对其他 Agent 不可见。同一个仓库、三个 Agent、三套"项目规则"。
把症状和 Agent 属性摆在一起,答案就不再神秘:不知道读哪份文档对应"无跨会话记忆";把过时计划当真对应"按字面执行不推断";淹死在历史里对应"上下文有限且花钱";找不到需求关联字段,是"能 grep 但没有关系字段可 grep";不写回对应"也是主要作者";各读各的入口对应"多个 Agent 同时在场"。
grep 与 glob 是两类最基本的文件检索原语。grep(Global Regular Expression Print)按内容检索:给一段正则或字面量,它扫过文件内容,返回命中的行。glob 按路径检索:用 **/*.md、docs/**/plan-*.md 这类通配模式匹配文件名与目录结构。对 Agent 而言,这两者是它探查未知仓库时几乎唯一可靠的传感器——它没有"记忆里知道那份文档在哪"的能力,只能靠名称模式与文本内容去定位。这也解释了为什么文档系统必须让名称可模式匹配、让元数据可解析:如果一份文档叫"关于封版的一些想法(终稿)",globs 抓不到;如果状态写在散文里,grep 也无从判断。文中反复出现的"ID 方案""元数据块""doc-lint",本质都是在把文档仓库改造成 grep 和 glob 能有效工作的形状。缺少这些字段时,Agent 不是不愿意查,而是没有可查的抓手。
根因:入口文件变成了百科
顺着六种症状挖到底,是两个始终没有被做出的区分。
第一个区分:常驻与按需。 入口文件每个会话都加载,是文档系统里最贵的位置,每一行都在每一次会话里付费。清理前,入口里塞了 9 条"代码审查检查项"和 7 条"规则固化"标记,外加它们背后的事故故事。一个只改按钮文案的会话,也要加载代码审查清单和数据库迁移的坑。而真正属于那里的路由——"封版前先读这份"——反而缺席。
第二个区分:当前事实与历史。 当前事实回答"现在什么是真的",可信;历史回答"当时为什么那样",需要核对。这个项目里两者混在一起:项目概览停在 9 月 3 日,之后好几个版本发布都没改它;重构轨道的路线图里 8 个标题标着 ✅,正文却写"未开始"。人读到矛盾会发问,Agent 会挑其中一句话相信。
两个区分都不做,结果就是:入口文件变成了百科,而不是路由表。 百科回答"它是什么",路由表回答"我该去哪读"。百科式入口会随着尽责程度越来越长,越长越难找到关键的那一行。开头那次事故,就是百科从未记录该事实、而路由表根本不存在时发生的事。
入口文件(AGENTS.md / CLAUDE.md) 是 Agent 工具约定俗成的项目级配置文件。以 Claude Code 的 CLAUDE.md 为例,它在每个会话启动时被自动读入上下文,作为该会话的"项目须知";Codex、Kimi 等工具各有自己的等价文件(如 AGENTS.md),加载规则并不互通。这带来两个直接后果。其一,入口内容是每次会话都付费的:写进去的每一行都会消耗每一次会话的上下文预算,成本随会话数线性放大。其二,入口是 Agent 唯一必然读到的东西:L1 规范、L2 计划再重要,只要入口没有指向它,Agent 就可能永远不读。因此入口的定位应是"路由表 + 少量硬规则",而不是知识仓库。当仓库里存在多个工具的入口文件时,必须选一个作为唯一真源(通常是 AGENTS.md),其余文件只做一行转发,否则就会出现文中"同一个仓库、三个 Agent、三套项目规则"的分裂。
三条原则
从根因倒推,模型立在三条原则上,每条都能用一句话复述,括号里是它对应的 Agent 属性。
原则一:入口只路由,不承载内容。(对应"无跨会话记忆")入口是 Agent 一定会读的唯一东西。除了少数硬规则,它的职责是告诉 Agent"这类活请读那份文档",而不是把文档内容搬进来。被搬进来的内容会过时、会膨胀,最后淹没路由本身。
原则二:常驻层只保留"事后才发现就太晚了"的规则,每条规则附一句话理由,其余全部按需下钻。(对应"上下文有限且花钱",理由那半句对应"按字面执行不推断")判断一条内容是否常驻,只问一件事:如果 Agent 这个会话不读它,会不会在没人察觉之前造成损害?会,就常驻;不会,就下沉到 L1 以下,由路由表在需要时指过去。理由不能省——没有理由的命令,一旦字面措辞对不上就会被丢弃;有理由,Agent 才能在规则未覆盖的边界情形里推断出正确动作。完整事故下沉到 L2 / L3,入口只留链接。
原则三:当前事实与历史分开放,状态和替换写成字段。(对应"按字面执行不推断")人能听出语气里"这条好像旧了",Agent 听不出。一份文档是否有效、被什么取代,必须是可解析的字段,而不是需要推断的叙事。写成字段还能让 Agent grep、让脚本检查。
模型:L0–L3 四层
三条原则落到文档结构上,就是四层:
| 层 | 内容 | 何时读 | 约束 |
|---|---|---|---|
| L0 常驻 | 入口文件里的硬规则 + 路由表 | 每个会话自动加载 | 必须短 |
| L1 按工作类型 | 规范(开发 / 测试 / 数据库 / 封版 / 手册 / 部署) | 动工前,只读路由表指定的那一个 | 共享层 + 项目层 |
| L2 按模块 / 变更 | 计划包、长期轨道、交接文档 | 进入该模块时 | 带状态与替换链接 |
| L3 证据 | 封版测试报告、审计输出等执行报告 | 需要证据时下钻 | 不作为入口 |
L0 常驻放两类内容:事后才发现就太晚的规则(一句规则、一句理由、一个链接),以及告诉 Agent 每类工作该打开哪份 L1 文档的路由表。它每次都会加载,唯一约束就是保持短。多个 Agent 共用仓库时,L0 只能有一个:唯一的入口文件 AGENTS.md,其他工具要求的入口文件只做转发。
L1 按工作类型:开发、测试、数据库、封版、用户手册、部署,每类工作一份规范。动工前只读路由表指定的那一份。L1 分共享层与项目层,项目层声明继承哪份共享规范,只写本项目的填空、当前状态、例外和坑。
L2 按模块 / 变更:一次变更的计划包、一条长期轨道、一份交接文档,只在进入该模块时读。L2 天然会过时,所以必须带状态和替换链接。
L3 证据:封版测试报告、审计输出、事故报告,只在需要证据时下钻。L3 不作为入口——只活在某份报告结论里的规则,要等有人碰巧读到才生效。
L0 入口文件的骨架大致如下(只保留关键部分):
**Stable ID** 指的是一份文档在生命周期内永不变更的稳定标识符,形如 `<T>-YYYYMMDD-NNN`,例如 `D-20261014-001`。它与文件名、标题、路径都解耦:文档改名、移动目录、重写标题,ID 都不变。这一点对 Agent 尤其关键——按字面执行的 Agent 无法像人那样"猜出这两份文件其实是同一份文档的前后版本",它需要的是一个可以精确引用、可以 grep 的锚点。Stable ID 支撑起三件事:一是 `Replaces` / `Replaced by` 字段能指向确定对象,取代关系形成可遍历的链;二是变更包、计划、报告之间的引用不会因为文件搬迁而断裂;三是脚本与 doc-lint 可以据此校验引用完整性,发现悬空链接或重复取号。文中"16 个变更包连 Stable ID 都没有"之所以是问题,正是因为缺失 ID 后,这些文档在机器视角里成了无法被稳定寻址的孤岛。
# AGENTS.md — 唯一入口(每次会话加载;保持简短)
## 硬规则(始终有效)
- <一句话规则>。理由:<一句话>。详情:<L1 链接>
## 操作 → 必读
| 操作 | 必读规范 | 一句话要点 |
|---|---|---|
| 封版 / 出包 / 打标签 / 说出"全量套件绿灯" | <封版规范> | <全量定义的一句话> |
| DB schema / 跨环境数据变更 | <项目数据库规范> | <最重要的一条约束> |
## 分层阅读
L0 = 本文件,始终有效。L1 = 上表,按工作类型寻址;动工前只读匹配的那一行。
L2 = 模块的计划 / 轨道 / 交接;进入该模块时读。L3 = 证据;需要时下钻。
分不清该读哪份文档时,先读上表,再在匹配规范顶部找下一级索引。
每份 L2 文档顶部带一个可解析的元数据块,这就是原则三的落点:
> Status: approved
> Stable ID: <T>-YYYYMMDD-NNN
> Replaces: none
> Replaced by: none
Status 只能取固定词表里的值,不允许自由发明。文档被取代时,Replaced by 填入新文档的 Stable ID,Agent 读到即跳转。
模型是怎么被事故逼出来的
必须坦白:四层模型不是一开始设计出来的,而是七个月里一次次事故之后回看才显形的形状。
3 月 4 日项目初始化,入口文件 CLAUDE.md 是 128 行。当天加入的"计划驱动开发"规则后来变得最重要:先写计划,确认后才动代码,完成后回填实际执行记录,保持路线图最新。此后文档随开发节奏生长,早期单文件计划如今有 301 份,其中 218 份回填了"实际执行记录"。
5 月 26 日入口文件 302 行,8 月 20 日 406 行。增长的一大部分是事故叙事:踩一个坑,就在入口加一节,写下发生了什么、下次怎么办。每一节单独看都合理,合起来,入口文件变成了一本越来越厚的事故手册。
9 月 3 日是第一个转折点。一次文档架构重整触及 235 个文件,散在四处的测试材料归为一处,计划归成三类,AGENTS.md 成为唯一入口,CLAUDE.md 只做转发。这缓解了"各读各的入口"和"东西该放哪",但没有回答"该读哪份文档"。
9 月 18 日,开头那次封版事故之后,"操作 → 必读"对照表第一次出现,入口文件开始有路由的样子。9 月 29 日一次数据库迁移编号撞车,两个同号迁移都已在生产执行过,重整之后立下规则"取号前检查所有并行分支和 worktree"。9 月 30 日误部署了一个同名旧包,此后包名必须带上时分秒。10 月 13 日和 14 日,浏览器自动化(MCP)动作两次被当作验收,"MCP 只定位、不验收"被写进规则并加入路由表。
10 月 14 日,路由表演化为 L0 / L1 / L2 的分层阅读原则,也就是本文模型的前身,入口文件 457 行。同一天项目开启了专门的文档治理变更(ID D-20261014-001),这个系列也同时开工。
按顺序摊开,规律非常明显:后来加的几乎每一条规则,背后都站着一次事故。 这个系统是被事故逼出来的。系列写出来,是希望下一个项目不必再用事故来换。
这套模型会怎么崩
以下都不是假设,每一条都曾是清理前项目的真实状态。
失败一:L0 膨胀
入口文件行数:128 → 302 → 406 → 457。注意最后一段:9 月 3 日成为唯一入口、9 月 18 日加入路由表之后,它还在继续长。
原因很直白:每次事故之后,最省事的做法就是往入口文件加一节,审查检查项和"规则固化"标记就是这么堆积起来的。每一节单独都正确,但 L0 的成本是乘法:行数 × 会话数。更糟的是,膨胀的 L0 会淹没自己的路由表,开头那次事故会在持有路由表的文件内部重演。有个细节:入口文件里一个标题写着"四个坑",下面却列了 5 条——这种不一致能在一个每次加载的文件里长期存在,说明它已经膨胀到没人逐行核对了。
修复方向:每条 L0 条目只保留一句规则、一句理由、一个链接,事故下沉;入口层的链接需要机器检查。路由表怎么写才不至于自己长成入口文件,是第二篇的主题。
失败二:层级边界模糊
没人看守,四层就会互相渗漏。
- L1 混入了模板和项目适配。 文档模型规范自身 1,123 行,共享规范、项目适配和约 460 行模板挤在一份 L1 文档里,违反了同一份文档自己定义的"共享 → 项目"分层。
- L2 长期轨道吞掉短期计划。 重构轨道本应只是主题导航,却装着 145 份单文件计划和一份 2,613 行、带状态标记的路线图。"标题 ✅、正文未开始"的矛盾就住在这里。
- L2 与 L3 没有分开。 路线图顶部的日期横幅记录了一个又一个版本的发布历史,它们属于执行记录和 L3 报告,却堆在"当前事实"文档的顶部。同一组封版编号在索引横幅、索引行、发布文档、路线图四处重复,漏更新一处,Agent 看到的就是四个版本的"事实"。
最扎心的是第一条:定义分层的规范自己没遵守分层。
失败三:写文档的 Agent 也会失忆
上面大部分膨胀和越界,都是某个 Agent 照着规范写出来的。写的那一刻它遵守了读到的规则;它不记得另一个会话上周定的约定,也不会回头检查整个系统是否一致。作为读者它"失忆",作为作者它也"失忆"。靠自觉维持的分层必然漂移——这是第四篇的主题:写回是变更的一部分,约定由机器强制、人只做裁决,入口层零容忍,更深的层有一条只能降不能升的基线。
doc-lint 这一概念借自代码领域的 lint 工具(如 ESLint)。它的思路是把"文档约定"从人的自觉翻译成可执行的检查项:状态字段是否取自固定词表、Replaced by 指向的 ID 是否存在、L0 入口行数是否超阈值、每个操作是否在路由表里有对应条目、Stable ID 是否唯一且符合格式。doc-lint 可以挂在 CI 或提交 Hook 上,在合并前拦截违规。它之所以在 Agent 协作里格外重要,是因为文档的读者与作者常常是同一个 Agent 的不同会话:写规范的 Agent 下一个会话就可能违反规范,而它既不会记得约定,也不会自觉复核全局。凡是机器能检查的约定,最终都应变成检查项,让层级基线"只能降不能升";人则只保留机器难以形式化的裁决职责,例如判断一处边界该归 L1 还是 L2。缺少 doc-lint 时,所有分层原则都退化为靠自觉维持,必然漂移。
总结
- Agent 不是失忆,它从来没有跨会话记忆。每个会话都是从入口走进来的新人。修入口,别修 Agent。
- 文档的第一读者是 Agent,人从读者变成裁决者;交付物单独计数;一份文档只服务一个读者。
- 入口只路由不承载内容;常驻层只放事后才发现就太晚的规则;当前事实与历史分开,状态和替换写成字段。
- 四层模型:L0 常驻(必须短)、L1 按工作类型(只读一份)、L2 按模块 / 变更(带状态与替换)、L3 证据(永不作为入口)。
- 系统是被事故逼出来的,也会自己崩:L0 变胖、边界模糊、写文档的 Agent 同样失忆。
系列:
- Agent 不是失忆,是找不到文档入口:大型项目文档分层(1)(本篇)
- 让 Agent 只读该读的:文档路由(2)
- 给每份文档唯一的位置:文档寻址(3)
- 让文档不腐烂:Agent 时代的文档生命周期(4)
相关推荐

OpenAI与Shield合作:Astra智能体如何革新IT故障修复
OpenAI与Shield合作推出Astra智能体,可自动登录电脑和遗留系统,实时修复IT故障,比传统模型快35%到40%,仅在需要人类判断时引入人工。

iPhone Duo续航实测:一天一充跑不掉,大屏是耗电元凶
iPhone Duo续航实测:重度使用一天需至少充电一次,6.5小时亮屏约剩30%电量。大屏展开频率是耗电关键,MagSafe电池背夹可救场,长期衰减同样值得关注。

Kimi K3.1 Pro开源发布:2.8万亿参数逼近Opus
Moonshot发布开源MoE模型Kimi K3.1 Pro,2.8万亿总参数、1040亿激活参数,Terminal Bench 4.0仅落后Opus 5.5不到1分,单任务成本约三分之一,API价格减半,开源权重11月4日开放。