解释temperature>0时LLM输出的随机性源自sampler而非模型本身,一次早期分叉会通过自回归放大成完全不同后半句;建议测试时设temperature=0消除采样噪音。
一次因改写而失败的快照测试,检测到的并不是回归问题。它检测到的是五种不同的变异来源,而你只能控制其中之一——弄清楚到底是哪一种,才能避免团队在一个 sprint 里徒劳无功。
语言模型返回的是下一个 token 的概率分布。由其他东西来完成采样选择,而这个选择器就是 sampler,受 temperature、top_p 和 top_k 调控。只要 temperature 大于 0,选择过程就是一次从分布中的采样,因此两个完全相同的请求一旦有一个 token 出现差异就会分叉——而由于生成是自回归的,一个不同的 token 会改变它之后所有 token 的上下文。一个早期分叉就能产生截然不同的后半段内容。这也正是为什么 diff 看起来是灾难性的,而语义上的变化却微乎其微。
这同样解释了为什么快照 diff 的大小几乎不携带任何关于底层变化规模的信息。句子二中的单个单词替换与一个完全错误的答案,产生的 diff 形状是相似的。
将 temperature 设为 0 请求的是贪婪解码:始终选择得分最高的 token。这消除了 sampler 作为变异来源,值得为测试做此设置。但这并不会让端点变得确定性,原因藏在 API 之下。
浮点数加法不满足结合律。前向传播中的归约操作在硬件上切分求和的顺序,取决于请求落入哪个批次。两次运行将你的请求与不同邻居分到一组,可能产生在最后几位有所不同的 logits。当得分最高的两 token 几乎持平,这就足以翻转贪婪选择——而一个翻转的 token 会改变下游所有内容。
平局必须被打破。贪婪解码假设存在唯一的最大值。当两个 token 得分完全相同时,结果取决于你看不到的实现细节。
Seeds 是尽力而为,且文档说明了这一点。OpenAI 将其 seed 参数文档描述为一次尽力而为的确定性尝试,并返回一个 system_fingerprint 来标识后端配置,恰恰是为了让你能够识别底层发生了什么变化。将 fingerprint 写入文档就是承认后端会变;把它当作保证来对待。
剩下的两个来源与解码完全无关。像裸模型名这样的别名会解析到提供商当前指向的任何版本,而这个指针会随着你的部署不变而移动——静默的模型更新正是造成"没人发布任何东西但测试套件早上却红了"的原因。使用带日期或版本号的模型 id 进行固定可以消除大部分此类问题,代价是需要主动进行迁移。
而且请求本身可能并非你所想的那样。从模板组装而来的 system prompt、用无序 key 序列化的 tool schema、依赖于带平局的最近邻搜索排序的检索文档集、注入到上下文中的时间戳:这些都会让你的 prompt 在源代码完全相同的情况下与昨天不同。在责怪模型之前,先记录你发送的确切字节并对它们做 diff。
这个诊断成本低廉,但几乎没人去跑它。取失败用例,用当前配置调用五次,不做任何修改。如果五次输出彼此之间的差异,与它们和快照之间的差异方式相同,那快照测试的就是方差,而 bug 在测试本身。如果五次输出彼此一致但与快照不一致,那就是有什么东西变了——你的 prompt、你的上下文组装、或者端点——下一步是将序列化请求与快照拍摄时的请求进行 diff。
这个区分值得做成一个 fixture 来自动化:给每个 golden 文件同时记录精确的请求 payload。它把"模型是否退化了"这个争论转换成两行 diff。
这里不是在反对测试模型输出。反对的是对模型输出中信息量最低的那个属性做断言:它的精确字节。几乎其他所有属性都足够稳定,可以做断言。响应能按 schema 解析。调用的 tool 是 lookup_order 且只调用了一次。答案包含上下文中的那个 order id,且不包含其他 order id。finish_reason 是 stop 而不是 length。没有邮件地址残留在输出中。用问题的改写版本得到相同的分类结果。
这些都是属性和不变式,它们失败的原因值得你了解。模糊快照匹配覆盖了仍然需要保留制品的中间地带,而 LLM 输出的不变式覆盖了根本不需要 golden 文件的断言。
趁机制还记忆犹新,再养成一个习惯。快照失败时,第一个问题不是"这可以接受吗"而是"这是五种中的哪一种",而五种来源留下的指纹各有不同。Sampler 方差会移动文本但保持结构完整。上下文组装 bug 通常也会移动结构,因为模型回答的是一个微妙不同的问题。端点变化往往同时移动套件中的每个用例而不是某一个,且通常会同时改变服务的模型 id 或 fingerprint。真正的 prompt 回归会移动一个连贯的子集——即受到你编辑的子句影响的那部分用例——而保留其余。如果在读取任何单个 diff 之前先看失败数量及其分布,能比直接读 diff 更快地回答这个问题。
五种机制中有两种涉及究竟是哪个端点提供了服务。如果你的套件运行在多个提供商上,对返回的解析后模型标识符而非你请求的模型标识符做断言是有价值的——一个报告服务模型和所选路由的网关(如 Multigrid 所做),将"测试隔夜变红"转换成响应中的一个字段,你可以将其快照化。