跳到主要内容

当你的模型具有随机性时,快照测试在撒谎

· 阅读需 12 分钟
Tian Pan
Software Engineer

当你团队中的初级工程师第一次输入 --update-snapshots 并推送到 main 分支时,你的测试套件就不再是测试套件了,它变成了一份记录稿。虽然 Diff 依然显示为红绿颜色,CI 徽章依然会变为通过,但信号已经悄然反转:测试套件不再告诉你代码是否正确,而是告诉你是否有人费心看过输出。对于确定性的代码,这种风险尚在可接受范围内,因为大多数 Diff 确实是符合预期的。但当网络调用的另一端是一个随机模型时,同样的流程会让每一个 PR 变成一场硬币投掷,让每一位评审者变成一个橡皮图章。

快照测试曾是确定性世界里的一个美妙构想。你记录下上周二 render(<Button />) 的生成结果,断言本周二它会生成相同的字符串。从定义上讲,任何 Diff 都是值得人工核查的行为变更。这种模式在 Jest、Vitest、Pytest、整个 React 生态系统以及一代又一代的 UI 快照扩展中得以幸存,是因为底层契约依然成立:相同的输入加上相同的代码等于相同的输出。但这个契约对 LLM 调用并不奏效。相同的输入、相同的代码加上相同的提示词(Prompt),却会产生不同的字符串——而且这种差异并非 Bug,而是产品按设计正常运行的结果。

团队在围绕模型调用编写快照测试时会发现,文件确实会被“锁定”——大约能维持一周。接着,上游供应商对模型发布了一个微小的 0.1 补丁,你的提示词开始产生略有不同的措辞,然后十七个测试在周二早上集体变红。一半的团队成员在没看 Diff 的情况下就重新生成了快照,因为 Diff 中并没有什么“错误”,只是“不同”而已。另一半人则花了一个下午滚动查看满屏的段落重排,寻找那一个真正重要的回归缺陷,结果一无所获,于是也加入了“重新生成”阵营。到了第三个月,快照审查已经变成了肌肉记忆:看到报错,运行更新标志,然后推送。测试套件现在成了一个只写的数据结构。

为什么维护的“跑步机”无法就地修复

当这种情况开始发生时,直觉是提高容差。也许快照只在出现大的 Diff 时才报错。也许我们在比较之前先去除空格。也许我们对标准化后的版本进行哈希。每一步都让我们离“输出是正确的”越来越远,而趋向于“输出与上周的形状大致相同”,这是一个完全不同且弱得多的主张。

这种“跑步机”式维护无法被修补的原因在于,快照假设“结果值”即是“规范”。一旦你接受了结果值会随着模型补丁、提示词修订、甚至单次请求中的重试尝试而漂移,你就不再是在测试“值”本身——你是在测试它的某种有损投影,而这种有损投影是你迫于截止日期压力而凭空捏造出来的。“测试通过”的语义变成了“我在下午 4 点写的一个任意的标准化函数没有标记出差异”,而这句话不应该出现在发布流水线中。

看清这一点的最清晰方式是问:绿色的快照测试对用户可见的行为有什么保证?对于确定性代码,答案是“函数生成了与我批准的完全一致的字节”。对于模型调用,诚实的答案是“函数生成的字节,根据我嵌入在比较器中的某种指标,与我曾在某个时间点批准的字节足够接近”。如果可以选择,工程师们绝不会在编写部署门禁时,使用包含“某种指标”和“某个时间点”这类措辞的语句。

一种能应对随机性的测试分类法

取代快照并非一个决定,而是四个,每一个决定都针对测试套件的不同层级。我在交付 LLM 功能的团队中看到的行之有效的模式,是根据系统断言的维度将套件划分为不同的频带,并为每个频带使用不同的判别器(Oracle)。

语义等价优于字段级匹配。 当输出是散文或带有散文字段的结构化响应时,你真正想要的断言是“这个回答的意思与参考答案相同”,而不是“这个回答与参考答案字节级相等”。基于 Embedding 的余弦相似度(根据字段的严格程度,阈值通常调至 0.85–0.95)可以捕获真实的漂移,同时忽略措辞的改变。对于结构化输出,你使用严格相等来检查具有稳定表象的字段(如价格、类别、是/否),而对散文字段使用语义等价。混合模式需要混合断言;用同一个比较器处理整个数据块,就是你回到维护“跑步机”上的原因。

分布测试优于单样本断言。 调用模型一次并对结果进行断言,是在向一个概率系统问一个“是/否”的问题,这意味着它的误报率取决于 Temperature 和你那天早上的运气。针对固定输入运行测试十次或二十次,并对分布进行断言:包含关键事实的响应比例、数值字段的方差、模型拒绝服务的频率。这在 Token 上更昂贵,但这是让失败信号对应于真实回归而非随机抽样异常的唯一方法。

不变性测试优于数值测试。 对于大多数产品功能,那些“必须”为真的东西不是具体的数值,而是属性。输出必须是有效的 JSON。如果用户名出现在输入中,摘要必须提及它。分类器绝不能返回允许集之外的类别。当目标语言是日语时,翻译中不能包含英语单词。这些是经典意义上的基于属性的测试(Property-based testing),它们具有巨大的优势:当模型发生变化时,你不需要更新它们——属性就是规范,模型要么满足它,要么不满足。最近关于 LLM 生成代码的基于属性测试的研究进一步形式化了这一点,在输入输出对不稳定时,将高级不变性作为判别器,同样的转变也适用于 LLM 生成的内容。

针对冻结基准样本组且带有容差预算的回归测试。 这是快照测试的精神继任者,也是唯一仍然属于“这是我们批准的内容”产物的地方。你精选 50 到几百个具有代表性的输入(即“黄金样本组”),在候选版本上运行它们,使用该频带所需的任何评分器为每个输出评分,并检查总体分数是否在生产基准的容差范围内(例如 2%)。单个示例的输出允许发生变化,但总体指标不允许。这为你提供了一个针对随机系统的确定性门禁,这正是你真正需要的权衡。

让确定性套件保持诚实的 Fixture 模式

并非 LLM 应用中的每个测试都需要与模型对话。模型周围的大部分代码都是普通的确定性软件——架构验证器 (schema validators)、重试处理器、工具调度器、响应解析器、提示词模板、缓存层——它们应当享有构建现代测试套件的那种廉价、快速、字节级对等 (byte-equal) 的断言。错误在于让随机性 (stochastic) 关注点泄露到确定性套件中,每当单元测试为了设置其 fixture 而调用真实模型时,这种情况就会发生。

能够扩展的 fixture 模式是为所有不需要随机性的内容固定模型输出。将少量规范响应(每个场景一个)记录到 fixture 文件中,并让确定性测试路径从该文件而不是模型中读取。记录本身是一项独立的、手动的、批量的活动,当提示词或架构发生变化时在真实模型上运行,并生成包含记录的模型版本、提示词和采样参数的 fixture。确定性套件使用你一直使用的字节级对等工具对记录进行断言。相比之下,随机性套件与实时模型对话,并明确承认其断言是概率性的。

两个套件,两种契约,两种失败模式。确定性套件告诉你模型周围的代码是否仍然有效。随机性套件告诉你模型对你的产品是否仍然有效。将它们混为一谈——通过在单元测试中加入一两个实时模型调用,“因为它们足够快”——会导致没有人信任的脆弱 CI 和没有人阅读的快照文件。

“去检查 Diff” 并不是一项代码评审活动

隐藏在这一切背后的深层组织观点是,LLM 行为验证不是代码评审 (code review) 关注的问题。代码评审评估对源文件的更改是否正确、安全且与代码库保持一致。它假设评审者可以阅读 diff 并推断其后果。当 diff 是“模型生成了四段略有不同的散文”时,评审者在不运行评估 (evaluation) 的情况下无法推断其后果,而通过将模型输出填充到 PR 评论中并要求人类点击 LGTM (Looks Good To Me) 来假装可以推断,纯粹是一场演戏。

diff 真正要求的活动是 eval:候选行为与基准行为的结构化对比,由适合该任务的 oracle(评测机)打分,并展示在仪表盘或报告中,而不是单元测试的 diff 中。eval 的输出是“得分从 0.82 降至 0.79,这里是十个最严重的退化,这里是五个最大的改进”,而 才是人类可以评审的产物。快照 diff 的输出是“一万个字符发生了变化,请滚动查看”。其中一个产生了决策;另一个产生了疲劳。

大多数交付 AI 功能的团队需要发生的文化转变是将模型输出快照完全从代码评审界面中移除,取而代之的是 eval 结果。PR 模板不再问“你重新生成快照了吗?”,而是开始问“eval 通过了吗?如果分数变动了,是哪些示例驱动的?”CI 将 eval 作为一项作业运行,将差异作为评论发布,并基于容差预算 (tolerance budget) 而不是 diff 批准来限制合并。这比 --update-snapshots 需要更多的基础设施,但这是产生信号而非噪音的基础设施。

下周一该做什么

如果你已经在生产环境中运行了一个带有快照套件的 LLM 功能,最廉价且最有用的第一步是:统计过去 90 天内有多少快照在没有实质性 PR 评论说明原因的情况下被重新生成了。如果这个数字不小,那么该套件已经变成了一个橡皮图章,问题不在于是否要替换它,而在于替换得有多快。将套件转换为基准队列 (baseline cohort),使用适合字段形状的任何 oracle 对队列进行评分,并基于容差进行限制——任何方案都比字符串对比要好。

如果你正在开始一个新的 LLM 功能,请在编写第二次提示词迭代之前先编写 eval。诱惑是先交付核心路径,稍后再进行检测,但“稍后”是指当提示词对你尚未构建的 eval 产生了五十个微妙的依赖时,任何重构都会变成一场盲飞。一个小型、简陋的 eval——甚至是使用嵌入相似度 (embedding similarity) 评分的十个示例——也比一个精美的快照套件有用得多,并且它能与你在此基础上构建的一切产生协同效应。

这条主线与 2026 年大多数 AI 工程实践中的主线相同:在确定性软件中有效的抽象无法泛化,试图强行适配的团队会付出脆弱性、疲劳和交付退化的代价。快照曾是一个伟大的模式。但在这里,它不是正确的模式。任由它们悄悄退化为一份文字记录,同时让测试套件变成一个勾选框,这种缓慢的失败不会出现在任何仪表盘上,直到一个面向客户的退化事故发生,才会有人问为什么测试全是绿色的。

随机性系统需要统计学断言。测试套件越早反映这一点,绿色徽标就越早重新获得意义。

References:Let's stay in touch and Follow me for more thoughts and updates