给 AI 文档示例加一道可执行校验
提出将文档代码块纳入 CI 执行,或明确标注为示意代码并指定负责人。通过无依赖的 Python 执行器和检查门禁,减少 AI 生成示例未经运行就宣称输出结果的问题。
AI 深度解读
AI 写文档最容易埋下的坑,是给出一段看起来合理、却从未运行过的命令,再配上煞有介事的输出。作者提出了一条很具体的规矩:每个代码块都要经过 CI 执行,或者明确标成示意并指定负责人;模型可以提议代码,输出断言由人负责。
这套做法的价值,在于把文档里的可执行承诺变成可检查的事实。不过,原文附带的 Python 执行器存在截断,隔离方式也有前后矛盾。思路可以借鉴,代码不能直接当成完整工具使用。
为什么要单独检查代码块
文档中的解释文字和可执行命令,出错后的代价不同。一个概念解释不清,读者可能多读两遍;一条错误命令,则可能让读者反复排查环境,以为问题出在自己身上。
作者因此把围栏代码块作为检查单位。模型可以负责教程里的引导、过渡、变更说明,但命令实际打印什么,需要运行结果支撑。尤其当 CLI、SDK、配置格式发生变化时,模型生成的示例可能仍沿用旧知识。
这里的「人负责输出」,不必理解成人工敲出每个字符。它强调的是责任边界:模型给出的预期结果,不能未经核验就成为文档承诺。
能运行成功和输出符合文档,是两件需要分别检查的事。
作者还给出了一个评审层面的理由:让评审者手动验证六条命令,很可能只认真检查两条,其余快速扫过。这是作者对评审习惯的判断,原文没有给出统计数据。但它指出了一个实际问题:如果机器已经验证了能自动检查的部分,人就可以把时间留给解释是否准确、示例是否合适等问题。
三种标签,把示例的责任写进文档
作者要求把标签直接写在代码围栏的 info string 中,也就是开头三个反引号后面、通常放语言名的位置。这样看 diff 时就能看到检查约定,不必去找另一份配置。
三种标签各管一件事:
runnable表示代码需要执行。output表示这是需要比对的预期输出。illustrative owner=@handle表示代码仅作示意,并指定负责人。
未加标签的代码块被视为规则违规,而非排版问题。示意代码缺少 owner,同样违规。
这对需要云凭据、付费账户、硬件或 GUI 的示例尤其有用。无法自动运行时,明确承认验证边界,比放一个没有依据的通过标记更诚实。不过,标签只是声明,读者能否理解它,还取决于站点如何呈现;原文没有提供对应的页面展示方案。
可选参数只有两个:运行较慢时使用 timeout=120,输出存在不稳定内容时使用 match=contains。执行器的默认超时为 30 秒。作者刻意限制参数数量,避免评审者为了理解检查规则,再承担一套配置成本。
输出比较是这套方案最值得细看的部分。以下代码用于消除少量格式差异,并在精确匹配和子串匹配之间切换:
def normalize(text: str) -> str:
return "\n".join(ln.rstrip() for ln in text.strip().splitlines() if ln.strip())
def matches(expected: Block, actual: str) -> bool:
want, got = normalize(expected.body), normalize(actual)
return want in got if expected.meta.get("match") == "contains" else want == got
要看最后一行:只有预期输出块带有 match=contains,才采用子串匹配,否则要求规范化后的文本相等。
这个区别会影响检查强度。日志里多了耗时信息,可能适合子串匹配;如果稳定片段只剩一个过于宽泛的单词,检查通过也很难说明示例正确。另外,代码里的 text.strip() 还会去掉全文首尾空白,因此它比原文所说的「只处理行尾空白和空行」略宽。
执行器怎么接入 CI,原文又缺了什么
作者选择用 Python 标准库实现工具,不引入第三方依赖。示例支持 python、bash、sh,通过 subprocess.run 执行脚本,收集标准输出和标准错误,并将两者拼接后用于比较。
这里的「无依赖」只适用于执行器。文档示例如果依赖其他 CLI 或 SDK,CI 仍需要准备这些环境,原文没有展开安装过程。
代码会在两个可执行块之间寻找第一个 output 块作为预期结果。一个容易忽略的细节是:找不到预期输出时,它仍然可以执行成功,只是不进行输出比较。因此,给代码贴上 runnable,并不能自动证明文档里描述的行为已经得到验证。
作者建议通过 Git 获取纳入版本管理的 Markdown,再交给执行器:
python tools/docex.py $(git ls-files 'docs/**/*.md' 'README.md') --report docex-report.json
要注意 git ls-files:它决定了输入文件范围,覆盖 docs/**/*.md 和 README.md,报告写入 docex-report.json。这条命令也隐含要求文件路径不包含会被 Shell 分词的空格,不能直接当成通用文件遍历方案。
原文说退出码 2 用来表示标签违规,并区分 nonzero-exit、timeout、unsupported-language、output-mismatch 等状态。这种分类有助于排查,但状态只能提供线索:命令非零退出未必是依赖变化,输出变化也未必只是文字过时。
更关键的是,提供的源码在失败记录处截断,围栏解析的字符串也出现了断裂。报告写入、退出码返回等部分没有完整展示,无法从现有代码确认这些约定已经实现。
临时目录的粒度同样需要澄清。作者前面说每个示例独立运行,实际代码却是每个文件创建一个临时目录,文件内的示例按顺序共用它。这样支持教程逐步生成文件,也允许前一个示例留下的文件影响后一个。它能清掉上一次运行的残留,不能阻止同一文件中的示例共享状态。
我的判断
我赞成把「代码能否执行」「输出是否一致」「谁负责无法执行的示例」拆开管理。相比反复在提示词里要求模型认真核对,这种检查能提供明确的失败位置,评审者也更容易讨论事实。
但原文没有给出采用前后的错误率、评审耗时或维护成本。这是一套工程经验和实现草稿,不能据此声称它已经显著减少了文档缺陷。作者关于模型每次添加示例都会漏标签的说法,也没有实验支撑。
与在测试目录里维护示例相比,直接运行 Markdown 的优势是减少文档和测试各存一份代码造成的漂移;代价是要处理 Markdown 解析、运行环境、输出波动和示例之间的依赖。对于已有文档测试工具的项目,优先接入现有工具,通常比维护这份解析器更省事。
安全前提也不能省略。tempfile.TemporaryDirectory() 提供工作目录,不提供权限隔离。执行 Pull Request 中的代码,需要考虑不可信贡献者和凭据暴露问题;作者明确要求不可信 fork 使用可丢弃沙箱。
原文还披露文章属于 MonkeyCode 的产品推广,提到其免费模型访问和免费服务器选项,但没有给出额度、配置或隔离能力。它们可以作为写作和执行的资源选择,不能充当验证能力或安全边界的证据。
能拿来做什么
- 为前端工具或 Node 项目的 README 增加执行检查时,先挑安装、构建、配置读取等能在 CI 重现的示例,并显式准备 Node 和包管理器环境。原文执行器不支持 JavaScript,不能直接把
js围栏标成runnable就期待它执行。 - 让 AI 补充命令示例时,同时要求它声明示例类别;预期输出应由维护者根据实际运行结果确认。涉及云服务或人工界面的步骤,则标注
illustrative owner=@handle,留下明确的维护责任。 - 对确定性输出采用精确比较,对确有波动的输出再使用
match=contains。评审时检查匹配片段是否足以证明目标行为,避免只匹配一句泛化的成功提示。 - 如果要实现这套工具,先补齐解析、报告和退出码逻辑,并确定文件内是否允许共享状态。报告至少保留
file、line、status、expected、actual,让文档失败能够直接定位和复核。