语义差异:当行级对比毫无意义时,如何评审 Prompt 变更
一位队友提交了一个拉取请求(PR)。Diff 只有三个单词。一行变红了 —— Do not add information not present in the source.(不要添加原文中不存在的信息)—— 另一行变绿了 —— Make your best guess if the source is incomplete.(如果原文不完整,请做出最佳猜测)。改动很小,意图也合理,代码审查只用了 11 秒。你批准了它。一周后,你的客服机器人开始自信地编造根本不存在的退款政策,而你正在翻阅日志,试图查出幻觉率是从什么时候开始翻倍的。
Git Diff 完美地完成了它的工作。它准确地向你展示了哪些字符发生了变化。但它无法展示唯一重要的事情:这些字符背后的行为已经从“不确定时拒绝”变成了“不确定时编造”。对于代码,文本差异是行为差异的忠实代理 —— 将 < 改为 <=,审查者可以推断出后果。而对于提示词(Prompts),文本差异和行为差异几乎没有任何关系。
这就是在审查流程中将提示词视为代码的核心问题。我们将它们放入 Git,开启 PR,请求批准 —— 然后使用 为表面变化与行为变化之间存在可预测映射的媒介而设计的工具来审查它们。提示词打破了这一假设。审查者的工作不是阅读变化的文字,而是观察变化的行为。而行为存在于行差异(Line Diff)无法触及的地方。
为什么行差异会撒谎
自然语言的两个属性使得提示词 Diff 不可靠,且它们的影响方向截然相反。
首先是效应的非线性。语言模型经常打破“输出变化的幅度应与输入变化的幅度成正比”的预期。整段文字的改写可能让输出基本保持不变,而增加一个逗号、一个大写字母或调整子句顺序就可能改变整个回复。审查者本能地认为大的 Diff 是危险的,小的 Diff 是安全的。但在提示词中,Diff 的大小与影响范围几乎没有相关性。上述三个单词的改动比一个仅优化措辞的 50 行重构更危险。
其次是媒介的模糊性。代码只有一个具有明确语义的解释器;而提示词是由一个概率引擎解释的,其理解取决于你无法完全看到的上下文。“简洁一点”和“包含你的推理过程”在孤立状态下都非常清晰,但审查者无法通过查看合并后的提示词来计算模型将如何在这两者之间进行权衡。冲突只会在行为中显现,而且只在碰巧触发它的输入上显现。
除此之外,还存在非确定性。即使提示词固定,在非零温度下,相同的输入在多次运行中也可能产生不同的输出。因此,当审查者费心手动测试一项更改时 —— 修改前运行一次,修改后运行一次,目测两个输出 —— 他们实际上是在从两个 不同的分布中采样两个点,并假装它们之间的差异就是修改的效果。有时确实如此。但通常那只是噪声。单次手动抽查,虽然是大多数团队实际在做的事情,却是最不可靠的工具,而且还是感觉上最具有说服力的工具。
语义 Diff 到底在比较什么
如果文本 Diff 向你展示了提示词的变化,那么语义 Diff 向你展示的就是输出的变化。它不是版本控制系统的一个功能,而是你需要构建的东西。这个流程很直接,值得明确说明,因为大多数团队都跳过了它:
- 获取一组固定的代表性输入 —— 这应该是一组预留的真实案例,而不是你在编写提示词时精心挑选的例子。
- 针对所有输入运行旧提示词,并记录输出。
- 针对相同的输入运行新提示词,并记录输出。
- 比较这两组输出,找出行为发生变化的情况。
这就是语义 Diff:不是 old_prompt → new_prompt,而是在你信任的语料库上的 old_outputs → new_outputs。关于提示词更改的所有有趣信息在这里都是可见的,而在行差异中是不可见的。退款政策的回归本应立即显示为一簇案例,在这些案例中,模型从“我没有该信息”变为了编造的答案。
微妙之处在于,你不能逐字符对比自由文本输出 —— 非确定性保证了即使行为完全一致,它们在表面上也会有所不同。你需要一种基于含义而非字节的比较方式。这就是评分(Scoring)发挥作用的地方,也是为什么构建语义 Diff 比添加 Git 钩子(Git Hook)工作量更大的原因。
在不自欺欺人的情况下对差异进行评分
有三种实用的方法可以将两组输出转化为判断,按成本升序排列,按团队实际需要昂贵方案的频率降序排列。
断言(Assertions)和不变性(Invariants) 是成本最低且捕获问题最多的方法。输出是否仍能解析为有效的 JSON?必填字段是否存在?是否在 Token 预算内?是否避开了禁用词汇?这些是确定性的、快速的,它们能在人工介入之前就捕获经典的“更改了输出格式并破坏了下游解析器”的回归问题。从这里开始。很大一部分提示词事故其实是契约破坏,Schema 检查可以免费发现这些问题。
LLM 作为评委(LLM-as-judge)评分 处理断言无法表达的情况 —— 摘要是否忠于原文、语气是否恰当、拒绝是否正确。在这里,可靠的模式是成对比较(Pairwise Comparison),而不是绝对评分。与其要求评委模型给新输出打 7/10 分,不如将旧输出和新输出并排展示给它,问它哪一个更好地满足了评分标准。相对判断通常比绝对判断更稳定,因为“这两个中哪一个更好”是一个比“这一个的理想分数是多少”更容易回答的问题。运行两种排序以消除位置偏见,并且永远不要信任一个你尚未针对少量人工标注集进行过评估的评委 —— 一个未经验证的评委只是你流水线中第二个未经审查的提示词。
对标记案例进行人工审查 是最后一步,而前两个廉价层级的意义在于使其具有可行性。你不希望审查者阅读一百对输出。你希望断言和评委将那十个行为真正发生变化的案例交给他们,这样他们稀缺的注意力就能落在语义 Diff 最明显的地方。
让审核门槛落到实处
如果这些工作只是存在于某人偶尔想起来才运行一次的 notebook 中,那将毫无意义。行为差异(behavioral diff)必须成为文本差异所在 Pull Request (PR) 的一部分,否则审核者将继续凭感觉批准那些只有三个词的改动。
具体而言,这意味着每当 Prompt 文件发生变更时,评估套件都会在 CI 中运行,就像代码变更会触发测试一样。结果——包括断言的通过/失败情况、与当前生产环境 Prompt 的两两对比胜率(pairwise win rate),以及行为发生变化的特定输入列表——都会在人工审核之前发布到 PR 上。审核者的屏幕上会堆叠显示两个差异:一个是反映作者意图的行差异(line diff),另一个是反映模型实际行为的语义差异(semantic diff)。当两两对比胜率低于当前版本时,合并将被阻断,就像单元测试失败会阻断合并一样。
这也重新定义了 Prompt PR 描述应该包含的内容。一个优秀的描述会说明行为意图(“减少对模糊但安全请求的过度拒绝”),指出风险(“可能会增加对真正无法回答的问题的幻觉”),并指明用于测试预期改进和潜在退化的评估案例。作者被迫说明他们预期改变的行为,而语义差异要么证实这一预期,要么揭示意料之外的偏差。
