让文档不再腐烂:Agent 时代的文档生命周期机制

用三条原则、doc-lint 检查器与强制力阶梯,让 Agent 时代的文档写完不腐烂。
文章记录了一次企业支付审批系统文档清理的完整方案,针对 Agent 成为代码主要作者后文档加速腐烂的现实。它先归纳腐烂三种形态——工作做完没回写、约定依赖自觉、旧事实不退场,根因是文档的写入、检查与退场从未被设计。随后给出三条原则:回写是变更的一部分、机器执行约定人类只做裁决、入口层零容忍深层用只降不升的基线。落地工具是拥有 36 个检查码和 62 个单元测试的 doc-lint,靠路由表一行与发布封版两个强制点生效,而非钩子或 CI。规则沿「叙述→L0→L1→代码审查→辅助函数」的强制力阶梯攀爬。清理实测把违规从 304 降到 0,并诚实地列出机制自身的四种断裂。
让文档不再腐烂:Agent 时代的文档生命周期机制
当 Agent 成为代码的主要作者,文档腐烂的速度反而更快了。规则明明写在规范里,违反规则的文档就躺在规范旁边。这不是假设,而是一次企业支付审批系统文档清理中记录下的真实状态。
原文作者在系列第四篇中给出了完整答案:从原则、机制到断裂点,一套让文档在时间维度上存活的工程方案。前三篇分别解决「读哪份文档」「去哪找」「信哪个副本」,这一篇解决最后一环——写完之后,怎么让它不腐烂。

文档腐烂的三种形态
一次文档健康检查暴露出的问题有一个共同点:规则早已写下,违反规则的文档就放在规则旁边。文档模型规范定义了 8 种状态,计划索引里实际出现了 9 种拼写,completed、done 和中文自由书写混在一起。规范说索引「只保留六列」,实际有 8 列,另有 4 行列数不对,无人察觉。
把症状归类,文档腐烂有三种形态。工作做完了但没有回写:实现记录缺失,下一个 Agent 只能读到计划。约定依赖自觉:词汇表、列数、ID 格式只写在规范里,提交时没有任何检查。旧事实不肯退场:日期横幅、过期的概览、个人记忆里的旧结论一直挂着。
根因是一致的:文档什么时候写、写在哪里、谁来检查、什么时候退场,这些只被「期望」过,从未被设计过。原文作者点破了一个关键事实——没有机器强制,注意到不等于修复。最便宜的做法是留一句话备注,一旦这句话写下,问题就成了「已知情况」,永远不会被修复。
三条设计原则
原则一:回写是变更的一部分。 「完成」的定义包含把文档写回去——实现记录、索引中的状态、案例、规范、手册。该写而没写的,这次变更就不算完成。实际怎么做的、在哪里偏离了计划,只存在于执行那次工作的会话里,会话结束,信息就消失了。
原则二:机器执行约定,人类只做裁决。 写规范和违反规范的可能是同一个 Agent 的两次会话。凡是可解析的约定——词汇表、ID、元数据字段、表头——都交给检查器。人类只出现在机器无法判断的地方:定义对不对、这个例外批不批、这个问题修不修。
原则三:入口层零容忍,深层有只降不升的基线。 每个会话都从入口进入,入口的一个错误会被每个会话逐字执行,所以必须为 0。深层文档是庞大的存量,要求一次性清零等于停工清扫。实际做法是把今天的数字记为基线,新问题不得推高基线,修一个,基线降一个。
回写循环:让「完成」有定义
原则一落地为一张「变更类型 → 完成前必须回写什么 → 谁检查」的表。其中「谁检查」这一列不能为空。回写规则早在清理之前就已存在——开发后补填计划的「实际执行记录」,同时更新路线图——但执行完全靠自觉,结果是一批已完成的工作包没有实现记录。
降低门槛,回写才真的会发生。要求小型缺陷也写四件套,结局通常是只有一个需求文件,回写被完全跳过。轻量形态只需要一份需求文档,但当状态进入「已实现」,文档必须增加一节「实现记录」。
交付物在循环内、路线外。用户手册、变更日志、交接说明面向业务用户和运维,Agent 维护它们,但不从中学习工作方式。路由表只记录「哪个变更之后必须更新它」。原文提到,用户手册迟了八天,就是那一行缺失的代价。
从事故到检查:规则的强制力阶梯
每次事故之后,一条规则会走一条流水线:事故 → 成文规则 → 检查。问题是它经常走到一半就停了。作者把规则的强制力排成一个由弱到强的阶梯:
- 叙述:把事故写成故事,只有恰好读到的会话受影响。
- L0 一句话规则:写进入口文件,附一句原因和一个链接,每个会话都会读到,但遵不遵守看 Agent 自觉。
- L1 细节:讲清怎么做、什么算违规,规则从「已知」变成「可执行、可判定」。
- 代码审查检查:违规在合并前被审查者对照清单拦下,审查者常常是另一个 Agent。
- 共享辅助函数或自动检查:错误的写法根本写不出来,或者一运行就变红,不再依赖任何人记得。
爬得越高,越不依赖「被读到和被记住」。文中给了两个对照案例。下载端点的魔数头检查爬到了第五级:测试从只检查文件名后缀,改为通过共享辅助函数校验文件开头的魔数(标识格式的固定字节),正确做法成了最便宜的做法。而「取迁移编号前检查所有并行分支和 worktree」这条规则至今仍是手工规则,停在了半路。
流水线对每条规则只问一个问题:它还能不能再爬一级? 停在低阶的规则不一定错,有些判断机器做不了,但必须明确这条规则依赖自觉。
强制力阶梯与控制论中的「防呆」思路同源:第五级的「共享辅助函数或自动检查」对应日语中的「ポカヨケ(poka-yoke,防呆)」,即让错误做法在物理或结构上不可能发生,而不是依赖操作者记住规则。前四级本质仍是「提醒 + 人/Agent 判断」的不同强度版本。之所以把它画成阶梯而非二元分类,是为了给每条规则一个可追问的方向:能从 L0 爬到代码审查,就不要停在 L0;能从审查爬到自动检查,就不要停在审查。
「只降不升」基线的来源:这一手法在静态分析与技术债管理中被称为「ratchet(棘轮)」——类似 Rust 的 clippy 基线或 SonarQube 的 quality gate。核心是不要求存量一次性清零,而把当前违规数冻结为不可增长的上限,每次修复让上限下移一格,长期形成单向收敛。它把「大清理」拆成「每次变更不恶化」,与 Agent 的短会话生命周期天然匹配。
doc-lint:文档的测试套件
doc-lint 是这次清理的核心产物,一个检查文档的脚本,拥有 36 个检查码和 62 个自己的单元测试。检查范围覆盖几个组别:
ID 与注册:稳定 ID 唯一,工作包目录名与 ID 机械对应,每个工作包都在索引中注册,否则对从索引进入的 Agent 而言它不存在。
词汇表与元数据:状态和类型只能取词汇表中的值,元数据块必须完整,Replaced by 是必填键。这里有个细节——字段可以填 none,但不能省略,否则「读到被取代文档就先跳转」的规则会原地打转。工具只检查键是否存在,不检查被取代文档是否真的填了新 ID,那一半仍依赖审查。
索引形态:表头固定六列,每行列数一致,标题和「下一步」有长度限制,日期横幅被禁止。
链接:入口层一个断链就是错误,全仓断链不得超过基线(对应原则三)。
输出是一行一个问题,带检查码、文件和原因,Agent 读完即可修复。工具还顺手做了两件「让正确做法最便宜」的事:
python3 scripts/doc-lint.py
python3 scripts/doc-lint.py next-id --type <type>
python3 scripts/doc-lint.py gen-views
依次是:跑全部检查;返回下一个空闲 ID,取号不再依赖 Agent 自己数目录;从案例主表重新生成派生视图。视图不得手改,与主表不一致即为错误。
稳定态规则与新项目配置
检查器代码里没有一个日期常量,没有一个历史路径。既有项目的特殊情况集中在一个接入配置里:豁免清单、冻结目录、全仓断链基线、规则生效日期和工作包根目录。新项目没有这个文件:没有冻结区,没有生效日期,基线为零。「项目历史不得进入检查器代码」这条边界是在审查中守住的。
强制点:不是钩子,也不是 CI
项目明确决定既不挂 git hook 也不设 CI 门禁,只有两个强制点:入口文件路由表中的一行,以及发布封版检查清单中的一步。为什么这仍算机器强制?路由表是 Agent 一定会读的地方,「改了文档就跑 doc-lint」对每个编辑文档的会话都是常驻指令;发布封版是每个版本必经的关口;而钩子拦住 Agent 时,最便宜的出路往往是绕过而不是修文档,把检查放进 Agent 自己的工作流,输出会直接告诉它哪一行错了。
代价也要说清楚:没有钩子,doc-lint 是否运行取决于 Agent 读到并遵守那一行,两次封版之间违规可能先进仓库,最晚在下一次封版被拦下。
doc-lint 与通用 linter 的区别:传统 linter(如 ESLint、Pylint)针对代码语法与风格,而 doc-lint 检查的是文档作为「协作契约」的结构属性——稳定 ID、词汇表取值、表头列数、元数据键的完整性、断链数量。它不判断一段叙述是否准确,只判断机器可解析的约定是否被遵守。这类工具通常以规则码(check code)为单位组织,每个码对应一条可复现的判定条件,输出格式刻意做成「一行一个问题」,便于 Agent 直接定位修复。
为什么脚本要自带单元测试:检查器本身也是代码,也会出错——文中链接检查不跳过代码块导致 58 个误报就是实例。62 个单元测试的作用不是验证被检查的文档,而是给检查器建立可信度基线:当规则调整或新增检查码时,测试告诉维护者哪些既有判定被无意破坏。没有测试的检查器,一次「改进」就可能让整个仓库的错误数暴涨,进而被 Agent 用豁免清单绕过。
记忆分层与新鲜度
个人记忆只保留协作偏好,项目级教训按层级进入 L0 或 L1。迁移前先验证再搬,搬完再查一次事实。清理中迁移的 21 条记忆里有 4 条部分过期,只搬了仍然成立的部分。更有意思的是逐字搬迁会把错误一起搬走——旧入口文件里「dev = 用户概念中的生产/真实数据」这句话本身就是错的,逐字搬进新写的开发环境文档后,由审查发现、用户纠正,仓库和个人记忆才一起修正。
当前事实类文档(项目概览、路线图、各类规范)最怕的是「曾经对过、现在错了」。规则是头部必须携带 Last reviewed: YYYY-MM-DD,缺失即错误;审查日期早于最近一次发布意味着上次确认还是在上一版之前。普通检查只告警,封版模式下项目概览或路线图过期直接判错。路线图顶部最多保留 5 条横幅,更早的移入归档,计划索引一条都不允许——横幅记录「最近发生了什么」,封顶才能让它滚动起来。
Agent 记忆的两种介质:个人记忆通常指跨会话持久化的用户偏好或摘要(由平台在会话间注入),项目级记忆则是仓库内可被任何会话读取的文件(如 AGENTS.md、规范、路线图)。两者腐烂方式不同:个人记忆容易保留过期结论,项目文件容易「曾经对过、现在错了」。文中「Last reviewed: YYYY-MM-DD」是给后者加的时效戳,思路与文档工程里的「内容保鲜日期」(content freshness date)一致——到了日期就要么确认,要么改写,否则视为失效。
逐字迁移为何会搬运错误:旧入口文件里那句「dev = 用户概念中的生产/真实数据」是一个语义错误,但它写在权威位置,被当作既有事实。逐字搬迁不会区分「原文措辞」与「原文断言是否成立」,于是错误随文字一起进入新文档,还获得了新文档的权威性。正确做法是先对断言做一次独立核验(本例中由审查 + 用户纠正完成),再决定保留、修正还是丢弃。
人类在哪里裁决
原则二的后半句在清理中有完整记录:批准计划(计划中的代码已在仓库副本上预演过)、划定边界(把项目历史映射从检查器代码移进一次性迁移脚本和项目配置)、纠正定义、驳回一个假缺陷(保存端点的注释比实现更窄,用户指出详情页在点「批准」前会调用它,判断被撤回,文档改为「注释比实现窄,代码为准」)、以及决定不做(不挂钩子不设 CI 是项目级决策)。
它们共同点:机器能发现不一致,但判断不了哪一边是对的。
清理实测:304 到 0
33 个提交,同一把尺子测量前后状态。doc-lint 违规从 304 个 ERROR 降到 0;入口文件 AGENTS.md 从 457 行降到 242 行;全仓断链前后都是 521(入口层均为 0)——这 521 条全部进入只降不升的基线。
链接检查还给了个反向案例:用清理前的旧检查器测清理后的树得到 638,比清理前还多,这是误报——旧检查器把代码块里的示例链接当成断链,而清理后的文档示例更多。清理本身也暴露了问题:迁移脚本是代码,必须在真实数据上先跑一遍(预演前报 308 个 ERROR,预演后为 0);一次性迁移脚本不可重跑,这一点必须写进文档,否则下一个 Agent 会因为文档写着而重跑它。
机制自身的四种断裂
检查器本身会错。 链接检查最初不跳过代码块,光是清理计划自身的示例链接就产生 58 个误报,既有文档再产生 12 个。规则也会过期——词汇表、列数、长度限制都由当下的实践设定,实践变了规则没变,Agent 会扭曲文档去满足过时的规则。对策是把检查规则以表格形式列在文档模型规范里,与检查码一一对应,规则逼得文档变形时,把规则变更交给人类。
强制过重会逼 Agent 绕路。 无法满足的规则会被 Agent 用最便宜的方式满足:必填节里写模板话、字段里填形式上合法的值、把文件加进豁免清单。所以强制必须配例外路径,且例外要「显式、集中在一处、可见」。
「只降不升」目前只写在了纸上。 断链有数字基线,超了就是错误;豁免清单没有。模板豁免在清理中从 315 精简到 314,合并主干分支时又因既有文档增加 4 条变成 318。这次增加有理由,但即便没理由,也不会有检查报错。原则三的后半句目前依赖有人读 diff,这是机制中最软的一环。
禁用词清单会泄露自己。 这在写作本系列的仓库里真实发生过:写计划时,整份脱敏词表和真实配置名被当作测试夹具提交了。处理方式是词表只存在于不提交的目录、测试夹具运行时拼接、检查器增加全仓扫描(含提交信息扫描)、重建仓库历史。教训是:不能出现的词清单本身就是敏感信息,检查器的配置也是文档,也有自己的生命周期。
检查器也需要自己的生命周期:文中提到的四种断裂其实指向同一个盲区——规则、豁免清单、禁用词表、检查器代码本身也是「文档」,也会腐烂,而它们通常没有对应的检查器。所以「规则的规则」必须显式存在:检查规则以表格形式与检查码一一对应、豁免清单要有集中式的可见审计、禁用词表不能随仓库提交、检查器代码里不得出现日期常量或历史路径(历史进入一次性迁移脚本与项目配置)。这条边界正是通过代码审查守住的,也再次印证原则二——机器执行可判定的约定,人类裁决判断不出对错的部分。
核心结论
文档在三个地方腐烂:工作做完没有回写、约定依赖自觉、旧事实不肯退场。写下的规则拦不住它,因为写文档的 Agent 同样健忘。解法是三条原则加一套机制:回写是变更的一部分,机器执行约定、人类只做裁决,入口零容忍、深层只降不升。每条规则都坐在强制力阶梯上,要问的是「它还能不能再爬一级」。 \ n在这套体系里,人类不再是读者和维护者,而是裁决者。
相关推荐

uGAP:统一100+非洲地理数据集的AI平台
uGAP是开普敦大学团队开发的非洲地理空间AI平台,统一120余个非洲数据集并提供Python SDK,支持数据发现、处理与模型训练一站式工作流,已用于气旋洪水制图等实战场景。

Cursor能控制你的电脑了:AI代理可驱动桌面应用测试
Cursor 新增桌面控制能力,AI 代理可驱动 iOS 模拟器等桌面应用测试自己构建的功能。本文分析这一更新对开发工作流的影响、技术实现可能路径及可靠性考量。

Vibecoding自研推理引擎:理想丰满,现实骨感
Reddit开发者分享Vibecoding自研推理引擎的真实经历:用WebGL2和GLSL在浏览器跑GGUF Q4模型,却陷入tokenizer调试地狱。本文分析浏览器端LLM推理的工程难点,以及为何直接clone llama.cpp是更理性的选择。