用事实提取与占位符测试约束 AI 文档
方案通过 AST 提取仓库事实,让模型只生成连接性文字和占位符,并将具名决策交给人工负责。每个章节生成两次后比较占位符集合,以识别事实依赖不稳定、需要人工明确的部分。
AI 深度解读
代码里的默认值已经改了,文档里的旧数字却还在。这类错误很难靠审查 AI 生成的文字发现,因为审查者必须先记住正确答案。作者提出了一套分工:代码能推导的事实交给生成器,事实之间的说明交给模型,涉及取舍的决定由具名的人负责。
最有意思的是其中的「占位符稳定性测试」:同一节生成两次,看模型是否引用相同的事实。它能帮助筛出边界不清的段落,但不能证明内容正确。对维护组件库、SDK 或 CLI 文档的团队来说,这套方法值得拆开看,尤其是成本最低的事实提取环节。
文档里哪些内容,根本不该让模型重写
作者把文档按章节分成三类:derived、connective 和 decided。
derived 是代码已经知道的事实,例如函数参数、配置常量。connective 是串联这些事实的解释性文字。decided 则记录团队为什么这样设计,例如为什么刻意压低重试次数、为什么放弃另一个方案。
这个划分有实际依据:抽象语法树(AST)可以读出参数,却读不出团队的取舍。下游队列不支持幂等,因此限制重试,这种因果关系需要人记录。
能从代码推导的事实,应当减少人工转述。
作者提供的 tools/docs_facts.py 只使用 Python 标准库,遍历 src 下的 .py 文件,生成 docs/_generated/facts.json。它记录公开函数的必填与可选参数、docstring 首行,以及模块级大写常量,并附上源文件和行号。
这里的「公开」采用了很朴素的判断:跳过名称以 _ 开头的函数。常量则通过 ast.literal_eval 读取,无法按这段逻辑求值的表达式不会成为事实。
作者建议提交 facts.json,让公共签名的变化出现在 diff 中,再用 tools/docs_facts.py --check 检查代码和事实文件是否同步。代码改了却没重新生成文件,检查就失败。
这比让模型重新描述一遍参数更容易核对。不过,它首先保证的是提取结果与源码同步。示例没有展示占位符如何渲染成最终页面,完整发布链路仍需补齐。
模型连续两次引用相同事实,就能接管这一节吗
作者要求先在章节文件的 front matter 里标明分类。例如,retry-budget 被设为 class: decided,同时记录 owner: "@your-handle"、决策文件 docs/decisions/0007-retry-budget.md,以及 reviewed: 2026-10-24。
分类不确定时,先交给人负责。要改为 connective,先通过稳定性测试;相同输入下连续两轮测试失败,则自动退回人工负责。
模型的写作权限也被收窄:只写说明文字和 {{fact:KEY}} 占位符,不直接填写签名、默认值或退出码。调用入口放在环境变量中,例如 export DRAFT_CMD='python tools/draft.py --temperature 0',通过标准输入读取章节材料,通过标准输出返回 Markdown。
作者还用 NUMERAL = re.compile(r"\b\d+(?:\.\d+)?\b") 检查去掉占位符后的正文,发现数字就报错。目的很直接:防止模型认为某个值太简单,顺手写进句子。
下面这段测试检查模型两次生成时选取的事实是否一致:
PLACEHOLDER = re.compile(r"\{\{fact:([^}]+)\}\}")
for section in sorted(pathlib.Path("docs").glob("**/*.seam.md")):
runs = [set(PLACEHOLDER.findall(draft(section))) for _ in range(2)]
if runs[0] != runs[1]:
print(f"UNSTABLE {section}: {sorted(runs[0] ^ runs[1])}")
for key in runs[0] - set(facts):
print(f"UNKNOWN {section}: {key} is not in facts.json")
要看的是 set(...) 这一行:它比较事实键的集合,不比较句子、顺序或引用次数。集合的对称差非空,作者就认为章节没有明确说明哪些事实与它有关;出现 facts.json 中不存在的键,则属于模型编造事实句柄,应当硬失败。
但这仍然只是筛查。两次都漏掉同一个必要事实,也会通过;两次选用不同但合理的事实,也可能被判不稳定。低温度模型同样可能稳定地写错。
代码片段还有落地缺口:它只打印结果,没有展示非零退出码;未知键只检查了第一轮,也没有实现「连续两轮失败后退回人工」的状态记录。这些都需要在接入 CI 前补上。
人工审查应该花在取舍上,而不是重复核对数字
decided 章节需要具名负责人、决策记录和复核日期。修改这类章节却不更新 reviewed,lint 就拒绝通过。
作者用 python tools/docs_lint.py --max-age 90 检查复核期限,失败输出示例包含 reviewed 2026-05-02、177 days 和 owner @former。日期能暴露长期无人复核的页面,负责人字段则让责任有明确落点。
这比一句「请团队审查」更容易执行。不过,更新日期只证明有人改了字段,不能证明他重新检查过取舍;负责人是否离职,也需要额外的信息来源。
最后,作者把三项检查合并成统一入口,让本地、pre-commit hook 和 CI 执行同一套流程:
.PHONY: docs
docs:
python tools/docs_facts.py --check
python tools/docs_lint.py --max-age 90
python tools/prose_stability.py
要注意最后一行:只有 prose_stability.py 在发现问题时返回失败状态,make docs 才能真正拦截提交或发布。
作者称,这套标准库 Python 检查在规模适中的仓库里几秒就能结束,但没有给出仓库规模、模型调用延迟或实测记录。尤其稳定性测试需要调用模型,整体耗时仍取决于章节数量和服务响应。
我的判断
我赞同把事实、说明和决定分开。相比「生成 API 文档,其余全部手写」,这个方案给模型安排了更明确的工作范围,也让人工审查更集中。
但原文没有提供对照数据,无法确认它能减少多少审查时间、漏掉多少错误,或让多少章节稳定地转为模型维护。「移除大部分参考文字的人工审查」仍是作者的经验判断。
占位符一致,只能证明两次选材一致,不能证明解释成立。
比起传统模板生成,占位符允许模型组织更灵活的说明,也增加了语义错误的空间。如果一个段落只有固定事实和固定句式,模板可能更省事。对于设计原因复杂的章节,直接维护决策记录也更稳妥。
数字禁令同样只是粗筛。正则无法覆盖中文数字、拼写数字和所有嵌在标识符里的值,更拦不住模型编造没有数字的承诺。它适合补漏,不适合作为完整事实校验。
事实提取器也只是示例:它读取顶层函数和部分常量,没有完整覆盖类方法、仅限关键字参数等 Python 接口形式。作者明确承认,AST 读取语法,无法判断参数命名或 docstring 是否准确。
另外,原文披露了 MonkeyCode 产品推广合作,并推荐其免费模型和免费服务器。作者没有提供不同模型或运行平台的对照结果。这套流程是否值得采用,应当与推荐平台分开判断;免费资源的可用性也不能成为发布前提。
能拿来做什么
- 如果你维护组件库或 Node SDK,可以先借用「提交事实文件并检查同步」的做法,用适合 JavaScript、TypeScript 的解析工具提取公开接口。先明确覆盖范围,再让文档引用这些事实。
- 给 AI 编程助手起草接口说明时,可以要求它使用已有事实键,并校验每个键确实存在。对于完整性要求高的章节,还要明确必需引用哪些事实,避免两次都漏掉却通过测试。
- 把重试策略、兼容性取舍等章节交给具名负责人,记录决策依据和复核日期。只有团队愿意持续维护这些记录,日期检查才有价值。
- 先把事实同步检查接进 CI,再试验稳定性测试。接入前补齐失败退出码、两轮未知键检查和分类变更规则,观察它是否真的减少审查负担。