让文档不烂——Agent 时代的文档生命周期(四)

本文是「Agent-first 大型项目文档分层」系列的最后一篇。前三篇回答了读哪份、去哪找、信哪份;本篇回答:文档写下之后怎么不烂。素材来自某企业级支付审批系统,以及在它身上做的一次文档治理。

一、规则都在,文档照样烂

第①篇结尾留下一个判断:写文档的 Agent 也会失忆。证据来自 10 月 14 日的一次文档体检,查出的问题有一个共同点:规则早就写着,违反规则的文档就躺在规则旁边。

  • 文档模型规范定义了 8 个状态,plan 索引里实际有 9 种写法,completed、done、自由中文都有。
  • 规范说索引「只强制六列」,实际是 8 列;另有 4 行列数错位,一直没人发现。
  • 第③篇开头那两个同号 bundle(F-20260909-005),后占号的那个在头部写了一行「⚠️ 同号留痕」:有人注意到了,留了言,没有修。
  • 仓库里有链接检查脚本,但它不挂在任何执行点上。入口层断链为 0,深层断链五百多条,这个数字平时没人看得见。

这些文档相当一部分出自 Agent 之手:第①篇数过,触及文档的提交里近四成带 Agent 署名,这还只是下限。写的那一刻,Agent 多半遵守了它读到的规则;但它不记得另一个会话定下的约定,也不会回头检查整个体系是否自洽。

把症状归一下,烂法有三种:干完不回写 ,实现记录缺失,下一个 Agent 只读得到计划;约定靠自觉 ,词表、列数、编号只写在规范里,没有任何东西在提交时核对;旧事实不退场,日期横幅、过期概览、个人记忆里的旧结论一直挂着。根因是同一个:文档什么时候写、写到哪、谁来查、何时退场,只被期望过,没被设计过。

「同号留痕」最能说明问题:没有机器强制时,发现不等于修复。 最省力的动作是留一句话;留了言,问题就成了「已知情况」,一直没人修。

二、三条原则

原则 1:写回是变更的一部分。(对应「无跨会话记忆」)完成的定义包含文档回写:实现记录、索引状态、用例、规范、手册,该写的没写,变更就没完成。实际怎么做的、哪里偏离了计划,只存在于做这件事的那个会话里;会话一结束,这些就没了。

原则 2:约定由机器强制,人只裁决。(对应「同时是主要作者」「能 grep / glob / 跑脚本」)写规范和违反规范的可能是同一个 Agent 的两次会话,靠自觉的约定一定漂移。只要约定可解析------词表、编号、元信息字段、表头------就交给检查工具。人只在机器判不了的地方出场:口径对不对、例外给不给、这个问题修不修。

原则 3:入口层零容忍,深层设基线且只降不升。(对应「无跨会话记忆」「按字面执行、不会意会」)每个会话都从入口进,入口里的一个错误会被每个会话按字面执行一遍,所以必须是 0。深层文档存量大,要求一次清零,等于让所有工作停下来做清扫。做法是把今天的数字记成基线(baseline):新问题不许让数字超过基线,修掉一个就下调一个。

三、机制 1:写回闭环

原则 1 落地成一张「变更类型 → 必须回写到哪 → 谁检查」的表,底稿是我们项目散在几份规范里的回写规则。按文档类型展开的完整版本见第十节,这里只列那张表里没有的两行:

变更类型 完成前必须回写 谁检查
任何登记过的变更 实现记录;元信息与索引里的状态 doc-lint
新增业务流程 移动端适配评估 路由表一行 + 代码审查

「谁检查」一列不能空。 回写规则 3 月 4 日就有了:开发完成后回填 plan 的「实际执行记录」,同时更新 roadmap。可执行全凭自觉:第①篇数过,一批已完成的 bundle 没有实现记录。

门槛降下来,回写才会发生。 改一个小缺陷也要写四件套,结果往往是只写一份需求、回写整个跳过。第③篇定义的轻量形态只要一份需求文档,但状态进入「已实现」时,文档里必须补上一节「实施记录」。

交付物在闭环里,但不进路由。 用户手册、更新日志、交接说明给业务用户和运维看,Agent 维护它们,但不从中获取工作知识;路由表只登记「什么改动之后必须更新它」。第①篇那份滞后 8 天的用户手册,就是这一行缺席的代价。

四、机制 2:从实案到检查项

每次事故之后,规则会走一条流水线:实案 → 立规 → 检查项。问题是它常常停在半路。

我把规则的强制力排成一个阶梯,从弱到强:

  1. 叙事:把事故写成一段故事。只有碰巧读到的会话受影响。
  2. L0 一句话规则:写进入口文件,带一句理由和链接。每个会话都读到,遵守与否靠 Agent 自己。
  3. L1 细则:写清怎么做、什么算违反。规则从「知道」变成「可执行、可判定」。
  4. 代码审查检查项:违反在合并前被审查方对照清单拦下,审查方常常是另一个 Agent。入口文件里的「Code review 检查项」就在这一级。
  5. 共享 helper / 自动校验:错误的写法写不出来,或者一跑就红,不再依赖任何人记得。

越往上,越不依赖「被读到并被记住」。两个例子,一个爬到第五级,一个停在半路。

爬到第五级的:下载接口的 magic header。 下载接口的测试原先只验文件名后缀,后端返回 JSON 格式的 500 错误也判绿。之后立了规:下载必须验证文件开头的 magic header(标识文件格式的固定字节),并做成共享 helper,新测试调用它就自动验证,正确的写法成了最省事的写法。helper 不是唯一出路:手写校验也允许,只要比对了 magic header 字节,这一部分仍由审查清单兜底。

停在半路的:迁移取号。 第③篇开头那次迁移撞号之后立的规「取号前先查所有并行分支和 worktree」,至今仍是一条人工规则。同类问题的另一半却爬到了第五级:plan bundle 的 f005 撞号,治理时改为取号只用 doc-lint 的取号命令,编号重复由检查直接报错。

流水线对每条规则只问一句:还能往上爬一级吗? 停在低级的规则不一定错,有些判断机器做不了;但要清楚它靠的是自觉。我建议在 L1 细则里注明每条规则当前在哪一级。

五、机制 3:doc-lint,文档的测试

doc-lint 是这次治理的核心产出:一个检查文档的脚本,即第①篇说的「文档的测试」。它有 36 种检查码,自身带 62 个单元测试。查的东西分几类:

  • 编号与登记:Stable ID 唯一;bundle 目录名与编号机械对应;每个 bundle 都在索引里登记,否则对从索引进门的 Agent 等于不存在。
  • 词表与元信息 :状态、类型只能取词表里的值;bundle 元信息块必须齐全,Replaced by 是必需键。第②篇留下的问题落在这里:这个字段可以填 none,但不能不写,「读到已替代先跳转」的规则才不会空转。工具只查键在不在,不查已替代的文档是否真填了新编号,这一半仍靠审查(本系列随附的 skill 工具已补上:superseded 的 Replaced by 必须是索引里已登记的编号)。元信息与索引的状态必须一致,元信息是唯一事实源。
  • 索引形态:表头固定六列,每行列数一致;标题和「下一步」有长度上限;禁止日期横幅。
  • 回写:已实现的 bundle 必须有实现记录,轻量形态则是「实施记录」一节(机制 1)。
  • 用例:主表表头、Case ID 格式;用例叙述不许混在主表之前;按层级、按模块两份派生视图必须与脚本生成结果一致。
  • 链接:入口层有一条断链就报错;全库断链不得超过基线(原则 3)。
  • 其他:项目层规范必须声明继承哪份通用规范;当前事实文档必须带复核日期(机制 5);记录类目录的新文件名要能被 glob 匹配;文档要符合对应模板的必备章节(模板是第③篇的内容)。

输出一行一个问题,写明检查码、文件和原因,Agent 读了就能修。它还负责两件「让正确做法最省事」的事:

bash 复制代码
python3 scripts/doc-lint.py
python3 scripts/doc-lint.py next-id --type <type>
python3 scripts/doc-lint.py gen-views

依次是:全部检查;返回下一个可用编号,取号不再靠 Agent 自己数目录;从用例主表重新生成派生视图,视图不许手改,与主表不一致就报错。

稳态规则与接入配置

检查代码里没有一个日期常量,也没有一条历史路径。存量项目的特殊情况集中在一份接入配置里:

  • 豁免清单:不合模板、不合命名的存量文档,bundle 里白名单外的存量文件,旧格式 Case ID;
  • 冻结目录 3 个,旧格式文档只读;
  • 全库断链基线 521;
  • 规则生效日期、bundle 根目录。

新项目没有这个文件:没有冻结区,没有生效日期,基线为零。这是第③篇「稳态规则 vs 存量接入」在工具上的落点:项目历史一行都不能进检查代码。这条边界是在评审里守住的,见第八节。

强制点:不靠 hook,也不靠 CI

第①篇说过,「同时是主要作者」推出机器强制。通用形态是 git hook(提交前自动运行的脚本,如 pre-commit 钩子)或 CI(合并前自动跑检查的流水线)卡点;我们项目已明确不做这两样,强制点只有两个。

一是路由表一行。 入口文件「操作 → 必读」表新增一行(节选,已脱敏;原表就是这样两列,这正是第②篇批评的写法):

markdown 复制代码
| 新建 / 修改 docs/ 下任何文档 | 文档模型规范;新建文档先复制对应模板,禁止仿写上一份同类文档;提交前跑 doc-lint 必须 0 ERROR;新变更取号只用 next-id;改用例主表后 gen-views |

二是封板门禁。 封板 checklist 加一步:封板模式的 doc-lint 0 ERROR,不通过不往下走。

为什么这样也算机器强制?我的理由有三条。

第一,路由表是 Agent 唯一确定会读到的地方,「改了 docs 就跑 doc-lint」对每个改文档的会话都是常驻的;判定由脚本做,Agent 只负责运行它。

第二,封板是每一版必经的点,checklist 由 Agent 执行、由人验收。平时漏跑的违规最晚在这里被拦下,人看到的是一行「0 ERROR」。

第三,Agent 被钩子拦住时,最省事的出路往往是绕过去,而不是修文档。检查放进 Agent 自己的工作流,输出直接告诉它哪一行错了,修比绕更省事。

代价也要说清楚。没有 hook,跑没跑 doc-lint 靠 Agent 读到那一行并照做;两次封板之间,违规可以先进仓库,最晚在封板时拦下。如果项目允许 CI,我会把同一条命令挂上去:检查逻辑不变,只是多一个执行点。

六、机制 4:记忆分层

第②篇给过判断标准:换一个 Agent、换一台机器,这条还该生效吗?该生效就进仓库。治理时把它执行了一遍,暴露出两个新问题。

迁移之前,先核实是否仍成立。 个人记忆里有一条「OCR(图片文字识别)没有服务器,刻意不接入」。迁移时核对代码与路线图,发现正式环境早已接入 GPU OCR。这条记忆早已过时,却一直作为「事实」随会话加载。它没有迁入,直接删除;迁移流程加了一步「先核实是否仍成立」。按这一步,迁入仓库的 21 条记忆里有 4 条部分过时,只迁入仍成立的部分。个人记忆没有复核日期、没有检查工具,过时了很难被发现。

原样搬移,会把错误一起搬走。 原入口文件写着「dev = 用户口径的『线上 / 真实数据』」,实际 dev、test、prod 三套环境各有所指,「线上」并不指 dev。这句话从入口文件进了个人记忆,治理时又被原样搬进新建的开发环境文档。逐字搬移保证了没丢规则,也保证了没丢错误。评审发现、用户纠正后,仓库与个人记忆两处同时改正。

所以完整规则是:个人记忆只放协作偏好;项目级教训按层级进 L0 或 L1;迁移时先核实再搬移,搬移前后各核一遍事实。

七、机制 5:新鲜度

当前事实类文档------项目概览、roadmap、各类规范------Agent 读到就信。它们最怕的是曾经对、现在不对了:第①篇提过,项目概览停在 9 月 3 日,之后发了好几版都没改。

复核日期。 当前事实文档头部必须带 Last reviewed: YYYY-MM-DD,缺了就报错。复核日期早于最近一次发版,说明上一次有人确认它还对,是在上个版本之前。日常检查对此只给提示,不阻断工作,但每次都看得见;治理合入后有 10 条这样的提示。到了封板模式(--release),项目概览与 roadmap 过期直接报错。

横幅限量。 第①篇说过,roadmap 顶部的日期横幅一条压一条,成了第二份更新日志。现在的规则是:roadmap 顶部最多保留 5 块横幅,更早的移入归档;plan 索引里一条都不许有。完整记录归实现记录与 L3 执行报告;release 两件是交付物,不收横幅。横幅说的是「最近发生了什么」,限量就是让它滚动起来。

八、人在哪里裁决

原则 2 的后半句「人只裁决」,在这次治理里有记录可查:

  • 批准 plan。 治理本身是一个文档类变更(编号 D-20261014-001),按项目规则先写 plan、经批准后实施。计划里的代码先在仓库副本上演练过,人批的是一份在真实数据上跑过的计划(第九节)。
  • 划边界。 任务评审发现,计划给定的 doc-lint 代码把本项目的旧状态映射(如 completed → verified)写进了词表,继承关系也写死了本项目路径。用户裁决后,前者移入一次性迁移脚本,后者移入项目配置。第五节「项目历史不进检查代码」,就是这次裁决守住的。
  • 纠正口径。 第六节「线上 = dev」那句,评审发现、用户纠正。
  • 否决假缺陷。 评审发现某保存接口的注释写「在途仅当前办理人」,实现却放行了创建人,记为「疑似缺陷待裁决」。用户指出:详情页点「通过」之前会先调用这个保存接口,不能收紧。补查调用链与 8 月 20 日的修复记录后撤回判断,文档改为「注释比实现窄,以代码为准」。注释不是事实源,调用链才是。
  • 拍板不做。 不加 hook、不做 CI 卡点,是项目层面的决定;doc-lint 的两个强制点据此设计。

共同点是:机器能发现不一致,判断不了哪一边对。

九、治理实录

治理相关提交 33 个。下表「治理前」是治理分支的起点,「治理后」是治理合入后的主开发分支,由同一份脚本逐项量出,只选和生命周期相关的几行:

指标 治理前 治理后
doc-lint(治理后的工具)违规数 304 个 ERROR 0 个 ERROR
入口文件 AGENTS.md 457 行 242 行(9 条 Code review 检查项仍在入口文件里)
全库断链(同一把新检查器) 521(入口层 0) 521(入口层 0)

编号、状态、元信息、索引与用例视图这些寻址相关的行,见第③篇第十一节。

同一把尺子。 304 是用治理后的 doc-lint 去量治理前的树得到的,前几位构成见第③篇。工具不同,数字就没有可比性;同一把尺子,任何人都能对着治理前的提交重跑。

断链是反过来的例子。用治理前的旧检查器量治理后的树,得到 638,比治理前还多。那是误报:旧检查器把代码块里的示例链接当成了断链,而治理后的文档里示例更多。新检查器量出来前后都是 521:深层断链数量不变,全部进了基线,只降不升。

治理过程本身也暴露了问题,挑两件和本篇论点直接相关的。

计划先在真实数据上演练。 把计划里的代码抽出来在仓库副本上跑,审批前发现 5 处缺陷:截断「下一步」文字时切进 Markdown 链接,产生了新断链;索引导语不以 > 20 开头,被横幅检查漏掉;补登索引时标题重复了 ID 前缀;一个测试漏传接入配置;还有一批旧格式 Case ID 不合新规则。演练前当时版本的工具报 308 个 ERROR,演练后 0。迁移脚本和 lint 一样是代码,要先在真数据上跑。

一次性迁移脚本不可重跑。 计划规定合并主开发分支时「取其索引并重跑迁移脚本」。终审在临时 worktree 里模拟,发现脚本会因列数错位的那几行中止;即使绕过,也会丢掉治理变更自己的索引行,并把状态改回去。最后改为手工吸收。迁移脚本只为某一个起点而写,这一点要写进文档,否则下一个 Agent 会按字面去重跑。

十、每类文档:何时写回、谁来查、人做什么

文档类型 写回触发 写到哪 谁强制 人的角色
需求 / 设计 开工前;范围或方案变化时 该变更的 bundle doc-lint(登记、元信息) 批准 plan
实现记录 状态进入「已实现」 bundle 的实现记录;轻量形态为需求里的「实施记录」一节 doc-lint 读偏差摘要,确认偏离可接受
用例索引 新增 / 修改用例 用例主表;视图由脚本生成 doc-lint 验收时确认覆盖缺口可接受
开发 / 测试规范 实案立规;踩到可复用的坑 L1 规范,高风险的加 L0 一行 代码审查 拍板新规则与口径
DB 迁移 改表结构或跨环境数据 迁移脚本与迁移索引 审查检查项;取号靠人工规则 部署前确认执行清单
部署 runbook(操作手册) 环境或流程变化 对应分册 代码审查 批准上生产
事故报告 事故之后 L3;教训另走立规流水线 人 确认根因,决定立规到哪一级
项目概览 / roadmap 发版、范围变化 当前事实文档,横幅限量 doc-lint(复核日期、横幅) 拍板口径冲突
用户手册 用户可见的功能改动 手册对应章节 代码审查 抽查后确认交付物
更新日志 / 交接说明 发版 交付物目录,按模板 封板 checklist + doc-lint 封板验收、确认交付物
个人记忆 协作偏好变化 本机记忆 无,只放偏好 纠正口径时确认两处同改

越是格式化的东西越早交给机器;内容对不对,仍要靠审查和人。「人的角色」一列没有一格是「维护」。

十一、这套机制会怎么坏

坏法一:检查工具自己出错

检查工具也是代码,也会错。链接检查器原先不跳过代码块,治理计划自己的示例链接被误报 58 条,存量文档误报 12 条;前文旧检查器量出的 638,也是同一个缺陷。模板检查按正文字样判断适用范围而漏检的一例,见第③篇。这类问题要拿自己的文档去跑检查(dogfooding,用自己的产品检查自己)才暴露得出来。

规则也会过期。词表、列数、长度上限都按今天的实践定;实践变了规则没变,Agent 就会为满足过时的规则去扭曲文档。对策:检查规则在文档模型规范里列成一张表,与检查码一一对应;规则逼得文档变形时,交给人裁决改规则,规范、代码、测试一起改,而不是让 Agent 绕过去。

坏法二:过度强制,逼 Agent 绕路

满足不了的规则,会被 Agent 用最便宜的方式满足:往必备章节里填空话,往字段里填一个形式正确的值,或者把文件加进豁免。所以强制必须配例外通道:存量问题进接入配置的豁免清单;模板里非必需的章节标「可选」;横幅限量而不是清零。治理中的旧格式 Case ID 就走了这条路:进豁免而不改名。例外通道只有一条要求:显式、集中、看得见,每条豁免都是同一个文件里的一行,改动出现在 diff 里。

坏法三:「只降不升」只是写着

规范写着基线文件只允许下调,工具只兑现了一半。全库断链有数字基线,超过就报错;豁免清单却没有。模板豁免在治理中先是 315 份,精简后 314 份,合并主开发分支时又加了 4 份,成了 318 份:那 4 份是规则生效前就已建档、随合并进来的存量文档,提交说明写明了原因。这次增加有理由,可即使没有理由,也不会有任何检查报错;清单里已经失效的条目,工具同样不报。原则 3 的后半句,今天只靠审查 diff。补法不难,比如把豁免条数与上一版的 tag(发版时打在提交上的版本标记)对比,只许少不许多;在补上之前,它是这套机制里最软的一环。

坏法四:禁止出现的词,清单自己泄露

这件事发生在写本系列的仓库里:写计划时,整份脱敏词表和真实配置名被当作测试样例写进了入库文件,终审发现。处理是:词表只存在不入库的目录;测试样例运行时拼接;检查脚本新增全仓扫描,提交说明也一起扫;重建仓库历史,旧历史只留本机备份。教训是:「禁止出现的词」的清单,本身就是敏感信息。 检查工具的配置也是文档,同样有生命周期。

十二、小结

  1. 文档烂在三处:干完不回写、约定靠自觉、旧事实不退场。规则写着拦不住,写文档的 Agent 也会失忆。
  2. 三条原则:写回是变更的一部分(无跨会话记忆);约定由机器强制,人只裁决(同时是主要作者、能跑脚本);入口层零容忍,深层设基线且只降不升(每个会话都从入口进、按字面执行)。
  3. 每条规则都在强制力阶梯上,从叙事到自动校验;要问的是「还能往上爬一级吗」。
  4. doc-lint 是文档的测试。不靠 hook 和 CI,强制点是路由表一行加封板门禁;代价是违规可以先进仓库,由封板兜底。
  5. 个人记忆只放协作偏好,迁移前先核实;当前事实带复核日期,横幅限量滚动。
  6. 机制自己也会坏:工具会误报,规则会过期,豁免可能悄悄增加,连脱敏清单都会泄露。检查工具也要被检查。

系列收尾

四篇连起来是一句话:在 Agent 写大部分代码的项目里,文档是给 Agent 用的路由系统。第①篇给出四层模型;第②篇修路;第③篇定位置;本篇让这一切不随时间烂掉。人在其中不再是读者和维护者,而是裁决者。

四篇的机制会提炼成一个 skill,分新项目初始化与存量项目接入两种模式,单独发布。

系列导航

  1. Agent 不是失忆,是找不到入口------大型项目文档分层设计(一)
  2. 让 Agent 只读该读的------文档路由设计(二)
  3. 让每份文档都有唯一的位置------文档寻址设计(三)
  4. 让文档不烂------Agent 时代的文档生命周期(四)(本篇)

配套仓库(含 agent-first-docs skill):github.com/IllegalCree...

相关推荐
qinglongcn1 小时前
Agent 可靠性的数学——为什么编程是第一个跑通的赛道
agent·ai编程
IllegalCreed1 小时前
Agent 不是失忆,是找不到入口——大型项目文档分层设计(一)
ai编程
IllegalCreed1 小时前
让每份文档都有唯一的位置——文档寻址设计(三)
ai编程
blackappleD2 小时前
Claude Max 每月送 100 美元 API 额度:哪些能用、哪些不能用
ai编程
柯南46682 小时前
【AI工程师精讲】10:微调方案选型:全参微调、LoRA、QLoRA 到底怎么选
人工智能·ai编程
changshuaihua0012 小时前
LangChain · LangGraph · LangSmith 全景解析:LLM应用开发完整生态
人工智能·大模型·ai编程
六维空间2 小时前
让 AI 必须覆盖国内厂商:我把「选题偏好」写成了硬规则
ai编程
长安米粒贵2 小时前
Agent 接了工具之后,最危险的不只是幻觉:聊聊提示注入的 5 个边界
ai编程
用户842558130872 小时前
点下去为什么没反应:二次确认面板、hash 路由与一扇走错的门
ai编程