跳到主要内容

非确定性服务的 API 契约:随机输出下的版本管理

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的内容审核服务返回 {"severity": "MEDIUM", "confidence": 0.85}。下游计费系统将 severity 解析为枚举值 ["low", "medium", "high"]。一次模型更新后,服务偶尔开始返回首字母大写的 "Medium"。没有任何部署发生,没有 schema 变更。集成在生产环境中悄然崩溃,整整六天无人察觉——因为所有 HTTP 状态码都是 200。

这是 LLM 支撑服务 API 契约的根本问题:表面看起来像 REST API,但底层行为是概率性的。标准契约工具假设确定性。当这个假设被打破时,它是悄无声息地崩溃的。

为什么传统契约测试会失效

消费者驱动契约测试(CDCT)——即 Pact 风格的模型——在确定性系统中运作良好。消费者编写指定预期交互的测试,提供者验证能否满足这些交互。契约是一个静态工件,要么通过,要么失败。

这个模型在 LLM 支撑的服务中会因三个原因而崩溃:

输出在 token 级别是概率性的。 相同的提示在 temperature 为 0 时大多数情况下会产生相同的输出,但并非总是如此——浮点非确定性、批处理效应以及跨提供商区域的硬件差异,即便在"确定性"配置下也会引入方差。在更高的 temperature 下,输出空间是真正随机的。

行为的改变不伴随 schema 变更。 一次模型更新可以在不改变任何字段名的情况下,改变输出的语气、冗长度、可靠性或事实准确性。语义化版本是为结构性破坏性变更而设计的,它没有词汇来表达"相同的 schema,幻觉率提高了 8%"。

失败模式是沉默的。 当传统 API 违反契约时,你会收到 4xx 或 5xx。当 LLM 支撑的服务违反语义契约时,它返回 200,带着看似合理的 JSON,却在下游悄然失败。在生产环境中,检测到这类问题的中位时间是数天,而不是数秒。

一个内容审核系统在三个提示词版本中分别返回 "MEDIUM""medium""Medium",已经悄悄三次破坏了调用方。这些破坏都没有触发任何告警。

你实际会遇到的四种失败模式

理解契约在实践中的断裂点,比理论框架更有用。以下是在生产系统中反复出现的四种模式:

模型更新导致的 Schema 漂移。 当你从一个模型版本升级到另一个时,输出结构可能会发生微妙变化——增加的冗长度将原本干净的 JSON 包裹在解释性文字中,字段顺序改变,之前始终存在的可选字段开始被省略。这些都不会出现在变更日志里。

提示词漂移的连锁反应。 看似纯粹是表述上的小改动——将"用有效的 JSON 回复"换成"始终返回可解析的 JSON"——会以重要的方式改变输出分布。研究表明,提示词更新是 LLM 系统生产事故的主要驱动因素。变更在你的版本控制系统中,但它对下游消费者的影响是不可见的,直到它显现出来。

静默的质量退化。 你的 API 返回格式正确、符合 schema 的 JSON,但内容是错误的。无论实际信号如何,置信度分数都聚集在 0.9。实体提取悄悄丢弃低置信度的实体而不是标记它们。这类失败不会触发任何 schema 验证;它需要语义监控。

多智能体管道中的组合失败。 智能体 A 和智能体 B 各自满足它们的契约。当 A 的输出进入 B 的提示词时,组合行为违反了一个任何单独测试都没有覆盖的契约。对 LLM 系统中 73+ 种不同契约类型的生产研究表明,组合失败是最难发现的。

提示词驱动 API 的语义化版本管理

传统语义化版本映射到结构变更:主版本用于破坏性 schema 变更,次版本用于新增变更,修订版本用于 bug 修复。当契约是 schema 时,这很有效。

对于 LLM 支撑的服务,契约包含 schema 版本管理无法捕获的行为属性。一个新模型在某些输入上拒绝率提高 8%,对某些消费者来说是破坏性变更。一个提示词调整将输出语气从中性变为带有倾向性,对于将文字下游传递给用户的调用方来说是破坏性变更。这些都不会出现在 JSON Schema diff 中。

更实用的版本管理模型:

触发条件版本升级
破坏性结构变更(字段删除、类型变更、新增必填字段)主版本
新增可选输出字段、准确性提升、延迟降低次版本
错别字修复、格式调整、无语义影响修订版本
相同 schema,影响可靠性或语义的行为变更至少次版本;如需调用方更新行为则升主版本

关键的补充是最后一行。即便 JSON Schema 完全相同,导致拒绝率、幻觉频率或输出冗长度变化的模型升级也应该增加次版本号。调用方理应收到这个信号。

由此衍生出两条操作规则:

不可变性。 一旦版本发布,它就不会改变。v1.2.3 端点始终表现得像 v1.2.3——当你升级底层模型时,它不会悄悄改变行为。如果模型变了,版本就变了。这迫使你明确说明改变了什么,并给调用方一个稳定的目标。

版本化完整的执行上下文。 提示词版本不只是一个字符串,它是提示词 + 模型 + temperature + 检索配置的组合。当其中任何一个发生变化时,输出分布就会改变。把它们放在一起版本化,放在一起追踪。

用行为不变量替代精确匹配契约

Pact 风格的契约使用精确匹配期望:"这个端点返回这个确切的结构。"对于 LLM 服务,应转向基于不变量的契约:"这个端点始终返回满足这些属性的结构。"

这种区别很重要。即便输出是随机的,不变量也可以是确定性的:

  • severity 始终是 {"low", "medium", "high"} 中的成员——不区分大小写
  • confidence 始终是 [0, 1] 范围内的浮点数
  • 如果 confidence < 0.5,则 review_required 始终为 true
  • 响应始终能解析为有效的 JSON,没有多余文字

这些不变量可测试、可自动化,并且不会在模型生成略有不同措辞时崩溃。它们还能捕获真正重要的失败:大小写敏感性 bug、置信度值返回 "0.85"(字符串)而非 0.85(浮点数)、缺失字段。

属性测试框架(Python 中的 Hypothesis,TypeScript 中的 fast-check)自动生成多样化的测试输入,并验证不变量在所有输入上都成立。将属性测试与示例测试结合使用,能显著提高 bug 检测率——这种组合能捕获任何单一方法都无法发现的边缘情况。

对于无法表达为结构不变量的语义属性——"摘要在给定输入下是准确的"——在测试套件中使用 LLM 作为评判者进行采样,而不是在热路径上。对 5% 的生产流量进行采样,语义评估,当退化时发出告警。

在源头强制 Schema 合规性

事后验证是脆弱的:你生成输出,然后检查它是否有效,如果无效就重试。这能工作,但会增加延迟,对于复杂 schema 也不能保证最终合规。

约束解码颠覆了这一方法。与其在生成后进行验证,不如修改 token 采样过程,使得只有有效的 token 才会被生成。模型无法生成无效输出,因为无效的 token 在每一步都被屏蔽了。

OpenAI 的结构化输出(2024 年发布)为 JSON Schema 合规性实现了这一点。第一次调用由于 schema 编译而较慢,但后续调用速度快,并返回 100% 符合 schema 的输出。Instructor 等库在标准补全之上添加了基于 Pydantic 的验证和自动重试,以更广泛的模型支持换取一些可靠性。本地推理引擎(vLLM、LM Studio)提供文法约束解码,适用于任何模型。

实际含义:如果你的 API 契约包含 JSON Schema,在生成时而不是验证时强制执行它。事后验证在失败发生后才捕获。约束解码使其在结构上不可能发生。

安全演进的部署模式

如果你在没有回滚路径的情况下发布破坏性变更,以上所有方法都无济于事。部署 LLM API 变更的模型更像数据库迁移,而不是功能部署。

带行为监控的金丝雀发布。 将 5% 的流量路由到新的提示词/模型版本。不仅监控错误率和延迟,还要监控 schema 合规率、字段存在率和语义质量分数。与基线进行比较。根据行为指标(而不仅仅是 HTTP 指标)决定推进或回滚。

高风险端点的影子流量。 在任何生产流量之前,对真实请求的副本运行新版本并比较输出。从结构上和语义上进行差异对比。这能在用户看到之前捕获"相同 schema,不同行为"这类失败。

模型升级的兼容性适配器。 升级基础模型时,临时并行运行新旧模型,比较输出,并测量负向翻转率——之前处理正确但现在处理不正确的输入比例。对兼容性保留模型升级的研究表明,通过显式适配器训练,这一指标可以降低约 40%。即便没有自定义适配器,在全量发布前在影子模式下测量负向翻转率也能捕获最严重的回归。

维护并行版本。 端点的 v1v2 可以在迁移期间共存。调用方在准备好时选择迁移到 v2。这比大多数团队想要的操作开销更大,但当下游消费者无法按你的节奏吸收行为变化时,这是正确的模型。

组织层面的问题

大多数这类失败不是技术问题——它们是流程失败。拥有 LLM 服务的团队发布了模型更新。消费输出的团队三天后才发现解析开始失败。

解决方案是将提示词和模型版本视为公共 API 表面,采用与 schema 变更相同的变更管理流程:

  • 提示词变更经过代码审查,并明确标注行为影响
  • 模型升级在任何生产流量前运行影子对比
  • 破坏性变更(结构性或语义性)需要与消费者团队协调推出
  • 行为 SLO(schema 合规率、语义准确性采样)触发告警,而不仅仅是错误率和延迟

在模型更新期间经历服务中断的 67% 的 LLM 应用共有一个模式:模型变更被视为内部实现细节。它不是。当模型变化时,API 就变化了。这就是契约。

结论

LLM 支撑服务的工程挑战不在于让它们工作——而在于防止它们悄然崩溃。传统契约工具假设一种不存在的确定性。版本管理系统假设行为变更伴随着 schema 变更,但实际并非如此。

前进的路径结合了:在生成时强制执行结构(约束解码)、行为不变量测试(即便输出变化时也成立的属性)、捕获行为变更而不仅仅是 schema 变更的语义化版本管理,以及借鉴数据库迁移而非功能标志的部署模式。

在生产环境中保持稳健的服务,不是那些拥有最复杂模型的服务。而是那些以确定性代码同等严格性对待概率性输出的服务——因为它们的调用方依赖于此。

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