文章从占位供应商混入真实输出的事故出发,讨论如何测试“模型审查模型”的验证层。重点是按失败类别扩展检查范围,覆盖类别不匹配、企业失效等能通过结构校验的语义错误。
上一课讨论的是:如何验证模型交付给你的内容。事情的起因是这样的:有一组 prompt 连续几周都能返回真实、符合筛选条件的供应商,但到了 staging 环境,返回的却是一些占位垃圾内容——字面意义上的 Vendor A、Vendor B、Vendor C。于是,我构建了一个验证层,而其中的最后一道门,是让一个模型检查另一个模型。
随后,FromZeroToShip 在评论区提出了三个问题,而这三个问题关注的都不是模型本身,而是这道门。这才是更难审视的部分,而且我之前并没有把所有内容都写下来。下面是完整版本。
比这个问题所预设的要多,而且并不是因为我特别擅长凭空设想各种故障。
占位符输出改变了我处理失败的方式。我不再只修复眼前这个具体问题,而是会追问:它属于哪一类问题?而这个类别远比“模型输出了示例数据”要宽泛得多。它指的是:某个建议看起来没问题,实际上却无法使用。基于这次故障,我把两种自己从未真正遇到过的情况也加入了清单:
这两种情况都与占位文本无关,而且都能轻松通过 schema 检查,看起来就像完全真实的答案。它们还促使我修改了生成建议的 prompt,而不仅仅是修改验证这道门。如果我只修复自己实际遇到的故障,这两个问题就依然会潜伏在系统里。
所以,这份清单并不完全是对历史问题的回顾。它的增长方式,是从你遇到的某个具体故障出发,将其归纳为所属的整个问题类别;此后,它也会继续根据运行中的系统实际抛给我的问题不断扩展,而不是只依赖我当初能够想到哪些风险。
它万无一失吗?并不是。剩下的才是真正值得担心的情况:结果读起来很真实,能够通过 schema 检查,也满足我给出的所有条件,但它依然是错的。你无法从系统内部验证某个猜测是否属实。你能做的,只是降低猜错所带来的成本。这意味着:在出错代价高昂的阶段引入 human in the loop;把置信度展示出来,让答案可以被核查;同时为用户提供一种明确的方式,让他们能够提出异议并重新生成结果。
如果从来没有人亲眼看见某项检查拒绝过任何内容,那它就还没有得到证明。通常,你会通过破坏它所保护的对象来验证它:观察检查是否会因为预期的原因变红,然后再把对象恢复正常。
但这种方法在这里行不通。模型的输出本质上是一种猜测。你无法预测它什么时候会出错,也无法要求它随时按需出错。因此,你构建这道门想要防范的故障,恰好也是你无法主动制造出来的故障。
所以,我们不能直接诱发这种故障,而需要模拟它。我们可以要求 mock model 返回某一类特定的错误答案,而且已经有测试专门做这件事:故意生成失败结果,然后断言这道门会因为预期的原因拒绝它。这仍然是同一个 TDD 循环,只不过由 mock 提供真实模型无法按需交付的故障。运行时出现的真实错误响应也不会被丢弃。它们会作为测试用例重新加入测试集,因此,测试所覆盖的内容会随着实际发生的问题持续增长。
这些 mock 还能一举两得,因为同一套配置也可以驱动界面:当调用响应缓慢、返回格式错误,或者完全没有返回时,用户会看到什么。
不算。上一课本应明确说明这一点,而不是把两种选择描述成同等的方案。
验证门执行的是一个不同的任务,使用的也是不同的 prompt,因此即使采用同一个模型,它依然能发现很多问题:占位符、填充内容、不符合既定条件的条目,以及第一次调用没有遵循的指令。但它无法发现双方共有的盲点。如果模型相信了某个错误信息,那么当你要求它检查自己的答案时,它很可能还会再次相信这个错误。因此,当你担心的故障源于判断失误,而不是是否遵循要求时,就应该让验证门改用完全不同的 provider 和 model。
而且,长时间“未发现任何问题”并不能证明一切都很干净。到了这个阶段,你 QA 的其实是 model API,而不是自己的系统。这又回到了第二个问题:要区分一道正常工作的验证门和一道永远放行的验证门,唯一的办法就是故意让它失败。
三个问题,背后其实指向同一件事。我构建了一个用于检查模型的验证层,却没有用同样严格的标准去审视这个验证层本身。
验证门也是代码,所以要像测试代码一样测试它。故意让它失败,确认它确实因为你构建它时所针对的原因而失败;同时,不要把长时间没有发现问题误读为任何形式的证明。
剩下的部分,是无法通过测试彻底消除的。你必须假设数据可能是错的,并围绕这一前提进行设计。至于具体该如何应对,则完全取决于出错会给你带来多大的代价。
感谢 @fromzerotoship 对这些问题的追问。如果你也在 AI workflow 之上构建产品,并遇到了类似的问题,我很乐意与你交流经验。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。