验证大语言模型输出在边界处的表现 | AI生成和翻译
今天,一个 Jekyll 构建因某篇博文 front matter 中的 YAML 解析错误而失败了。出问题的这篇博文——由我的 ww note 工具从剪贴板内容生成——其 title: 字段根本不是一个标题。那是一篇四段的中文文章,包含换行符、全角冒号和项目符号列表,全部塞进了一个 YAML 键中。
流程是这样的:剪贴板内容 → 调用 LLM 要求“一个非常短的英文标题(最多六个词)” → 将响应写入 front matter 的 title: 中。LLM 忽略了指令。代码信任了响应。YAML 解析失败了。
修复只有三行代码:
title = re.sub(r"\*", " ", raw).strip()
if len(title) >= TITLE_MAX_CHARS:
raise ValueError(f"生成的标题为 {len(title)} 个字符(必须小于 {TITLE_MAX_CHARS} 个字符):{title!r}。")
return title
这个经验教训比 LLM 更古老:不要信任不受你控制的系统的输出,尤其是在不变量改变格式的边界处。 LLM 是一个远程的、非确定性的服务。它的输出进入一个结构化格式(YAML),其中任何一个放错位置的换行符都会破坏整个文档。这个边界应该得到与用户提交的表单字段同等的审查——进行模式检查、长度限制、失败时大声报错。
这个 bug 的有趣之处不在于 LLM 行为失常。LLM 时常行为失常;那是意料之中的。有趣的是,故障在源头是 沉默的,而在完全别的地方却是 响亮的。笔记被写入磁盘时没有任何错误。文件对人眼来说看起来没问题。数天后,另一台机器上的 Jekyll 工作流将这种损坏暴露为单元测试失败,堆栈跟踪指向一个数天未动过的文件的第 15 行。
从产生错误数据到某物注意到问题之间的这段距离——正是工程成本高昂的地方。在边界处进行验证将距离缩短为零。异常在 generate_title 内部抛出,距离 LLM 调用仅三帧,且错误消息中包含有问题的字符串。你会在它发生的瞬间看到,就在你可以修复它的那台机器上。
我想记住几个推论:
提示指令不是约束。 “最多六个词”是对模型的一个提示,而不是保证。如果你需要保证,就需要在响应返回后执行强制约束的代码。这适用于长度、格式、语言和内容;这就像你不会只依靠 <input maxlength="6"> 作为服务器端对长输入的唯一防御一样。
“快乐路径测试”会撒谎。 当我最初测试 ww note 时,LLM 每次都返回合理的标题。六个词的标题。真正的标题。验证缺口是隐形的,因为 LLM 在配合。只有当模型——出于某种原因在那次特定调用中——决定忽略指令并倾泻出一篇论文时,bug 才浮出水面。验证不是针对你已经见过的情形;而是针对你还没见过的情形。
失败时大声报错胜过沉默失败,即使“大声”意味着脚本崩溃。 之前的行为是:损坏的笔记悄无声息地写入,数天后在 CI 中构建失败。新的行为是:ww note 以 ValueError 和清晰的消息退出,然后我重新运行。第二种显然更好。沉默的损坏是最糟糕的失败模式,因为当你注意到时,证据已经被备份、跨机器同步,甚至可能被复制到其他文档中。
我针对触发此问题的那篇具体的中文论文案例添加了一个测试。不是因为那个确切的输入会再次发生——几乎肯定不会——而是因为这个测试现在记录了契约:generate_title 可以失败,但不能返回一个 200 字符的段落。如果有人(包括未来的我)放松了这个检查,测试会提出异议。
更广泛的模式——我想更积极地在处理 LLM 的代码中应用——是:每个从“模型输出”进入“结构化格式”的值都要经过验证器。标题字符串有长度上限。JSON 响应有模式检查。文件名有正则表达式过滤。代码输出需要解析。每个边界的成本是几行代码;节省的是不用因为远程模型状态不佳而醒来面对一个损坏的 Jekyll 构建。