跳到主要内容

测试不可测之物:LLM 驱动 API 的集成契约

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的测试套件通过了。CI 是绿色的。你发布了新的 prompt。三天后,一个用户反馈你的 API 正在返回带有尾随逗号的 JSON——而你的下游解析器已经静默丢弃数据长达 72 小时。你从没为此写过测试,因为 LLM 在开发环境中"总是"返回合法的 JSON。

这就是毁掉 LLM 驱动产品的失败模式:不是灾难性的模型崩溃,而是确定性测试套件在结构上无法捕获的安静、间歇性的降级。根本原因不是懒惰——而是当你的系统产生非确定性的自然语言时,"期望 == 实际"的整个范式就失效了。

修复这个问题需要重新思考你在测试什么,以及对于 LLM 驱动的 API 而言"通过"究竟意味着什么。那些弄明白这一点的工程师并没有编写更聪明的相等性断言——他们编写的是根本上不同类型的测试。

为什么 Temperature=0 救不了你

标准建议是将温度设为 0 以获得可重复的输出。这个建议在一些微妙但重要的方面是错误的。

2025 年的一项关于 LLM 非确定性的研究发现,在相同设置下运行之间的精度差异高达 15%,最佳和最差结果之间的性能差距达到 70%。罪魁祸首是 GPU 计算中的浮点非结合性:不同的硬件核心以不同的顺序执行操作,产生数值上不同(尽管语义上相似)的中间状态。当负载均衡将你的请求路由到不同的服务器时,输出就会改变。

OpenAI 的 seed 参数明确只保证"大致确定"的输出——而不是完全相同的输出——因为提供商保留了在调用之间更新基础设施的权利。如果你的测试套件今天运行一个 prompt 并记录精确输出,那么下周在没有你这边任何变化的情况下,该快照可能已经过时。

更深层的问题是,即使输出是真正相同的,精确匹配测试仍然是错误的方法。考虑一个摘要端点:"报告显示 Q3 收入下降"和"根据报告,Q3 收入有所下滑"是同一个正确答案,但字符串相等性会将其中一个标记为失败。自然语言没有规范形式。

不认识到这一点的工程师会写出同时过于脆弱(在有效的改述上失败)和过于宽松(在带有细微事实错误的输出上通过)的测试。你需要一个不同的测试模型。

反转原则:测试绝不能发生什么

对 LLM 系统最持久的测试不是关于输出必须说什么的断言——而是关于输出绝不能做什么的断言。这种反转是使 LLM 测试可处理的核心洞见。

一些具体例子使这一点变得清晰:

  • 客户支持机器人绝不能泄露内部系统提示内容
  • 结构化数据提取端点绝不能返回无效的 JSON
  • 摘要服务绝不能引入源文档中不存在的事实声明
  • 代码生成工具绝不能包含明显的安全漏洞(SQL 注入、硬编码凭据)
  • 分类端点绝不能返回定义枚举之外的标签

这些不变量易于指定、易于断言,不需要你预见每一个可能的有效输出。它们编码了实际导致生产事故的失败模式,而不是学术上的正确性问题。

实现方式因不变量类型而异。格式不变量是确定性的:解析 JSON、根据你的模式验证、检查枚举成员资格。语义不变量需要启发式方法(已知秘密模式的正则表达式、幻觉实体的 NER)或次级 LLM 评估。安全不变量通常两者都需要:轻量级分类器用于延迟敏感路径,加上针对精心策划的对抗性数据集的定期深度评估。

当你将这些作为 CI 门(而不仅仅是监控警报)连接时,你会在发布之前而不是影响用户数小时后捕获回归。

接口层的契约

对于任何消费或生成 LLM 内容的 API,你需要两层规范:结构契约和行为契约。大多数团队只构建第一层。

结构契约是你的 OpenAPI 规范或 JSON 模式。它定义了哪些字段存在、它们携带什么类型、什么枚举值有效。对于同步 LLM API,这一层应该在响应离开你的服务之前强制执行——不是通过希望模型遵循你的提示,而是通过解析和验证输出,如果不符合则明确重试或失败。如果你的 LLM 在你要求 JSON 时返回散文,那是你在代码中处理的可恢复错误,不是你在 CI 中捕获的测试失败。

行为契约更难。它定义了对任何有效响应都必须成立的属性:答案必须基于提供的上下文、语气必须匹配定义的角色、长度必须在 UX 可接受的范围内。这些属性无法被静态验证——它们需要评估。

实际模式是将行为契约编码为你的 CI 评估器在推广新提示或模型版本之前检查的评分标准。PromptFoo 和 DeepEval 等工具允许你分别以 YAML 配置或 Python 断言的形式表达这些,然后针对固定的代表性输入数据集运行它们。构建失败不是因为输出与快照不匹配,而是因为它在你明确定义的属性上得分低于阈值。

这种方法让你能够以与有回归测试套件的代码变更相同的信心来发布模型更新和提示更改——但没有精确匹配断言的脆弱性。

基于属性的测试实践

基于属性的测试(PBT)是为确定性系统开发的——给我一个输入生成器,我会找到违反你声明属性的输入。对于 LLM,这个想法适用但执行方式显著改变。

要测试的有用属性分为几类:

结构属性最容易:响应是有效的 JSON、必填字段存在、类型匹配模式。这些是确定性检查,应该在你的集成测试套件中对每个请求运行。

一致性属性测试响应在语义等价变换下是否稳定。如果你在不改变含义的情况下重新表述问题,答案应该一致。如果你重新排序列表中的项目,分类结果不应该翻转。这些是变形测试——你不知道正确的输出,但你知道两个输出必须满足什么关系。

单调性属性测试方向行为:向摘要请求添加更多上下文应该产生更长或至少不更短的摘要。提供更具体的系统提示应该减少响应方差,而不是增加它。这些让你验证在调整提示时模型行为向正确方向移动。

安全属性是不可妥协的不变量:无 PII 泄漏、无不安全内容、无幻觉引用。这些全面评估起来代价高昂(通常需要 LLM 评判者),所以实际方法是在 CI 中对代表性样本运行它们,并在定期离线评估中进行更详尽的测试。

将基于属性的测试与 GPT-4 合成的学术研究发现,可以为 API 文档中可推导的约 21% 的属性生成有效测试,平均 2.4 个样本就能正确合成。关键发现:将 PBT 与基于示例的测试相结合,将边缘案例 bug 检测率从 68.75% 提高到 81.25%。这两种方法是互补的,而不是竞争的。

语义快照而非字符串快照

传统快照测试对 LLM 输出来说是个陷阱。你在第一天捕获一个响应,将其提交到仓库,现在每一个有效的改述都是一个测试失败。一周内你就开始忽略快照失败,因为它们总是误报。

语义快照测试捕获的是含义,而不是文本。实现取决于你需要验证什么:

对于事实正确性,使用句子编码器嵌入期望响应和实际响应,并计算余弦相似度。设置阈值——通常为 0.85–0.92,取决于你需要多少改述容忍度。这可以捕获有意义的降级(模型现在回答了一个不同的问题),而不会在风格变化上失败。

对于结构化内容——摘要、项目符号提取、分类理由——使用带有定义评分标准的 LLM 评判者。评判者得到输入、参考响应、实际响应和一组要评估的标准。它返回一个分数和解释。这比嵌入相似度更昂贵,但能捕获更细微的质量回归。

对于智能体轨迹——工具调用序列、推理链、决策路径——记录执行轨迹,而不仅仅是最终输出。一个通过荒谬的工具调用序列得出正确答案的智能体是一个潜在的失败,等待在更复杂的情况下复合。对轨迹进行结构测试让你可以断言"智能体总是在写入工具之前调用验证工具"等属性,独立于最终输出所说的内容。

关键转变是将评估视为一等工程关注点,而不是最后的 QA 步骤。你的测试基础设施需要回答:"这个响应是否满足我们的属性?"而不是"这个响应是否与我们写下的内容匹配?"

在不破产的情况下将其整合到 CI

实际障碍是成本。在每次 CI 运行的每个测试案例上运行 LLM 评判者的成本高得足以让团队在时间压力下禁用这些检查,并且永远不再启用它们。

解决方案是分层评估策略:

第 1 层(每次运行,毫秒级):确定性检查——格式验证、模式一致性、枚举成员资格、延迟阈值。这些成本低廉,能捕获最具影响力的失败。将它们作为硬性门。

第 2 层(每次 PR,秒到低分钟级):针对精心策划的 50–200 个代表性输入的黄金集进行语义评估。使用嵌入相似度进行结构正确性检查,使用轻量级评判模型进行行为属性检查。这在发布前捕获提示回归。

第 3 层(每夜或发布时,分钟到小时级):针对完整测试数据集进行详尽评估、对抗性探测、安全测试。使用你最好的评判模型。为接近质量阈值的案例生成人工审查队列。

生产流量应该反馈到你的黄金集中。你的 API 在生产中收到的最难的输入——那些导致重试、纠正或放弃的输入——是最有价值的测试案例。自动将这些样本路由到你的评估管道中。几周内,你的测试套件就会根据你实际的失败模式进行校准,而不是你在开发过程中想象的失败模式。

关于 LLM 作为评判者的一个重要警告:强迫评判者仅输出数字分数会降低其可靠性。提示评判者在评分前解释其推理,可以将与人类判断的一致性从大约 75% 提高到 85%。这个解释是便宜的额外 token,可以显著提高信号质量——为它付费是值得的。

实践中的转变

在这方面做得好的团队做出了一个关键的组织转变:他们停止将 LLM 质量视为部署关注点,开始将其视为合并关注点。

当模型更新、提示更改或检索配置变更落入 PR 时,它会触发与任何代码变更相同的分层评估管道。行为契约上的回归会导致构建失败。做出变更的工程师在代码发布之前就能看到质量差异——哪些输入降级了,降级了多少,以及原因。

这使质量回归在正确的时机变得可见:当有人正在积极进行变更并能立即调查原因时。另一种选择——通过生产监控捕获回归——意味着你总是在对用户影响做出反应,而不是防止它。

基础设施投资是真实的。构建黄金数据集需要时间。校准评判提示和分数阈值需要迭代。但跳过这项工作的团队并没有避免成本——他们以事故、信任侵蚀的形式被动地付出代价,以及那种在有人注意到 JSON 解析器静默丢弃记录之前运行了 72 小时的静默数据损坏。

你的测试套件是绿色的。问题是"绿色"是否意味着什么。


实际起点:挑选一个如果被违反会导致最严重生产事故的不变量。写一个断言它的测试。在每次 PR 上运行它。这比一千个字符串匹配快照测试构成更持久的基础。

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