用 Markdown 写入边界约束 AI 改文档
文章将 Markdown 内容划为三类区块,通过检查器检测结构漂移,再由拼接步骤限制模型仅修改获准的文字。这样可以阻止 AI 改写人工维护的警告等关键内容,并降低审查范围。
AI 深度解读
让模型润色整页文档,最危险的结果可能是一段更顺口、却改变了上线决策的说明。作者提出的办法是:先在 Markdown 中划定归属,再让程序只替换获准修改的段落。人工写下的弃用警告,不能靠一句「请勿修改」来保护。
这套设计值得看,因为它把部分审查工作变成了结构检查。不过,原文提供的是参考实现,没有效果对照数据,而且代码与它宣称的保护范围之间,还有几处需要补齐。
哪些文字可以润色,哪些文字必须锁住
作者把文档拆成三类块,每类对应不同的修改权限:
human保存人工判断,例如弃用通知、迁移要求、禁止用于生产环境的警告。prose保存解释性文字,允许模型重写。facts保存脚本生成的事实,例如路由表,作为模型的只读上下文。
划分依据不是标题或段落长短,而是这句话会不会改变读者今晚部署什么。会影响实际操作的结论,应归入 human。
下面的示例把弃用决定、介绍文字和接口事实分开,解决的是同页内容权限混在一起的问题:
<!-- owner:human id=deprecation-charge-v1 ticket=OPS-441 -->
> **Deprecated.** `POST /v1/charge` stops accepting new API keys on 2026-12-01.
> New integrations must call `POST /v2/charges`; migration order is below.
<!-- owner:human -->
<!-- owner:prose id=intro -->
This page covers both charge endpoints and the sequencing required to migrate a live integration.
<!-- owner:prose -->
<!-- owner:facts id=route-table src=scripts/gen_routes.py -->
| Method | Path | State |
| --- | --- | --- |
| POST | /v2/charges | current |
| POST | /v1/charge | deprecated |
<!-- owner:facts -->
要留意第一行:id 用于稳定定位,ticket=OPS-441 则提供人工决定的追溯线索。这里的日期和接口是示例内容,不应当作某个真实服务的公告。
格式约定包括:标记独占一行、禁止嵌套、文件内 id 唯一,且 facts 必须声明生成脚本 src。结束标记只重复类别。
模型的修改权限应由替换程序执行,提示词只能表达意图。
检查格式,还要检查谁的文字变了
第一道检查是 linter。原文的 tools/docfence.py 扫描 Markdown,检查缺少 id、重复 id、facts 缺少 src=、闭合类别错误和块未结束等问题。运行命令是 python tools/docfence.py docs/,可以与 Markdown 链接检查放在同一个 CI job,不需要模型服务凭据。
但格式正确,不代表人工段落没被改。
作者因此增加第二道检查:通过 git show HEAD:<path> 读取上次提交,以 id 对齐提交版本和工作区中的块,再比较原有 human 正文。正文被改或整块消失,都会被报告;新页面没有历史版本,代码直接跳过。
作者还要求,主动修改人工结论时,先用独立 PR 移除保护标记,让「解除保护」成为可见的审查动作。
这里有两个落地问题。其一,如果 CI 在提交后的干净工作区运行,HEAD 与工作区往往已经相同,检查不到 PR 已提交的改动。其二,参考代码也会拦住直接移除标记的操作,却没给出批准解除保护的具体机制。团队采用前,需要确定比较基准和例外流程。
模型只交段落,程序负责拼回页面
生成阶段只发送 prose 和只读 facts,要求模型按 <<<block id=...>>> 与 <<<end>>> 返回内容。接着检查返回的块是否在授权集合内,再替换对应位置。
这段代码展示了授权检查和替换入口:
OUT = re.compile(r'<<<block id=([A-Za-z0-9._-]+)>>>(.*?)<<<end>>>', re.S)
def splice(page, model_out, granted):
got = {m.group(1): m.group(2).strip() for m in OUT.finditer(model_out)}
rogue = set(got) - set(granted)
if rogue:
raise SystemExit('model wrote outside its grant: %s' % sorted(rogue))
if '<!--' in model_out:
raise SystemExit('model output contains a fence marker')
for bid in got:
page = replace_prose(page, bid, got[bid])
return page
关键是 rogue = set(got) - set(granted):模型返回未授权的块,程序直接失败。后续 replace_prose 还会确认目标类别确实为 prose,否则拒绝替换。
作者要求先测试恶意输出,再测试正常输出。给出的 pytest 用例让模型原样返回包含人工块的整页,预期触发 SystemExit。测试样本放进 tests/fixtures/,每次修改提示词、模型或分隔符都重新执行。
原文还提到 MonkeyCode 的免费模型访问和免费服务器,并披露文章属于产品推广。作者把拼接服务描述为几百毫秒的字符串处理,但没有提供测量条件;免费额度及条款也需要另行确认。这套文档契约并不依赖该产品成立。
我的判断
我赞同把高风险结论从自动润色范围中隔离。对于解释文字与人工警告混排的 Markdown,这比单纯加强提示词更有可检查性,也比要求审查者逐行记住哪些文字不能动更容易执行。
但原文没有给出误改率、审查耗时或与其他方案的对照,这是工程设计建议,不能当作收益已经得到验证。
参考代码也不能直接承担完整保证。linter 遇到同类别的嵌套起始标记,可能把它误认成结束标记,没有严格执行禁止嵌套。拼接函数通过正则提取结果,未完整校验输出:块外文字会被忽略,重复 id 会被字典覆盖,也没有检查获准的块是否全部返回。因此,「拒绝一切授权范围外输出」比实际实现更强。
边界保护也不等于内容正确。模型仍能在 prose 中编造解释,审查者必须阅读每一处解释文字的改动;facts 声明了来源,也不代表脚本结果已经过真实性校验。
适用范围要收窄:纯手写文档收益有限,完全由 schema 生成的页面通常沿用生成流程即可。CMS 或数据库中的文档,需要在存储层实现相应权限。长生命周期分支、缺少 rebase 纪律,以及频繁重组页面,也会增加维护成本。
能拿来做什么
- 先选一页已经包含弃用通知或生产限制的 Markdown,只标记最昂贵的人工警告,再划分可润色段落,观察审查是否更清楚。
- 按原文用
mkdir -p tools tests/fixtures docs建立目录,运行python tools/docfence.py docs/;空目录预期输出total: 0 blocks。标记页面后再次检查,再用git add tools docs && git diff --cached --stat查看提交范围。 - 如果用 Node 实现同类工具,保留稳定
id、授权集合和限定替换范围,并补测嵌套标记、重复输出、块外文字和保护块被删除等情况。 - 接入日常 AI 文档润色前,先确定 PR 比较基准、人工解除保护流程,以及服务写权限只覆盖文档目录的限制;更换模型后继续运行恶意输出测试。