跳到主要内容

提示词契约测试:防止一个团队的修改破坏另一个团队的智能体

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个平台团队修改了意图分类器的 Prompt,旨在“更好地处理复合问题”。只改动了一个句子。他们自己的评估套件(eval suite)变绿了——复合问题的准确率提升了 6 个百分点。他们在下午 3 点合并了代码。到下午 5 点,三个下游 Agent 团队开始收到告警:路由 Agent 将退款请求发送到了物流队列,摘要 Agent 在不同的边界处截断,而工单打标 Agent 开始输出一个没有任何 Schema 能识别的类别。那些下游团队中没有一个参与了评审。也没有人负责“意图 Prompt”的轮值。

这不是假设。当 Prompt 变成共享依赖却未成为共享 API 时,这就是必然发生的情况。提升一个团队指标的 Prompt 修改,可能会悄悄破坏另一个团队建立在其之上的假设。与破坏性的 API 变更不同,这里没有反序列化错误,没有 Schema 不匹配,没有 500 错误——下游只是开始做出微妙的、更糟糕的决策。

传统的 API 工程在几十年前就通过契约测试(contract tests)解决了这个问题。消费者发布它所期望的形状;提供者有义务保持该形状正常工作。Pact、消费者驱动的契约、共享 Schema——这是 HTTP 服务发布工程的正统做法。Prompt 也应该遵循同样的纪律,而大多数组织仍然像处理团队间传递的贴纸一样对待它们。

为什么 Prompt 的行为像无版本的 API

Prompt 具有 API 契约的所有属性。它有输入(插入模板的变量)、输出(模型的响应,下游代码解析的对象)、副作用(工具调用、路由决策、检索查询)以及行为不变性(语气、格式、拒绝条件)。下游消费者针对这四个方面构建心理模型。当其中任何一个发生变化时,消费者就会崩溃。

区别在于 HTTP API 会“显式报错”。在 JSON 响应中将 user_id 更改为 userId,消费者在第一次集成测试中就会抛出反序列化错误。而在 Prompt 中将“简要回答”改为“以友好的语气回答”,下游的每个测试仍然能通过——响应仍然是有效的 JSON,仍然可解析,仍然在 Token 限制内。这种破坏直到数周后才会浮现,表现为某些用户侧指标的漂移,而由于没有人回溯到那个 PR,问题很难被定位。

对生产环境中 LLM 部署的分析反复指出,Prompt 更新是导致意外行为的首要原因——排在模型版本更改和基础设施故障之前。只有当你接受 Prompt 是技术栈中测试最少、版本化最差、共享程度最高的人造产物时,这个排名才有意义。

组织层面的失效模式非常直接。团队 A 拥有 Prompt。团队 A 有一套测试团队 A 关注点的套件。团队 B 的 Agent 调用团队 A 的 Prompt 并解析输出,但团队 B 的测试通过 Mock 处理了 Prompt,或者使用了三个月前的固定数据。团队 A 发布了一个“微小的措辞调整”。测试矩阵从未交叉。生产环境变成了集成测试场。

消费者驱动的契约,转化为 Prompt 场景

Pact 的核心洞察是:由消费者——而非提供者——来定义契约。下游服务编写一个测试,声明:“当我用 X 调用你时,我期望得到匹配 Y 的响应。”提供者随后有义务在发布前针对其代码运行该测试。契约漂移会在生产者的 CI 中被捕获,而不是在消费者凌晨 3 点的故障响应中。

对于 Prompt,对应的做法是下游消费者评估锚点(downstream-consumer eval pins):由每个消费团队编写的黄金固定值(golden fixtures),存放在共享位置,并在任何上游 Prompt 更改时自动运行。负责根据意图分类进行路由的 Agent 团队编写一个固定值文件,编码他们的路由假设——“此输入必须分类为 refund,而不是 billing”、“置信度分数必须可解析为浮点数”、“响应不得超过 50 个 Token”。当平台团队编辑分类器 Prompt 时,CI 会运行每个下游锚点,如果任何一个失败,则拒绝合并。

关键在于“由……编写”。平台团队编写自己的下游测试会违背初衷——他们只会测试自己关心的东西,而这正是最初导致问题的原因。消费者驱动意味着消费者拥有。Agent 团队知道缺失置信度分数会破坏他们的桶排序;平台团队不知道,也不应该被要求知道。

一个实际的设置如下。一个共享仓库或注册表保存按名称和版本索引的 Prompt 定义。每个消费者在 consumers/ 目录下贡献一个以其服务命名的测试文件。Prompt 作者的 CI 加载每个消费者文件,针对提议的 Prompt 执行锚定的输入,并根据每个消费者的断言进行验证。失败会准确告知作者哪个下游服务将崩溃以及原因——无需告警,无需回溯排查。

实际上能捕获回归的三道关卡

全栈评估既昂贵又缓慢。你不会希望在每次 PR 时都运行一个包含 1000 行数据的评估,也绝对不想因为一个 40 分钟的测试套件而阻碍对拼写错误的修复。在实践中,借鉴自 promptfoo 和 prompt-contracts 风格工具的分层方法非常有效:

  • 结构性门控(毫秒级,无需模型调用):验证模板是否使用预期变量进行渲染,输出 schema 是否保持不变,以及所需的工具定义是否仍能编译。这些手段能在任何内容进入模型之前,捕获重命名的参数、被删除的占位符以及格式错误的 JSON schema。
  • 固件门控(秒级,有限的模型调用):针对小型、快速的模型运行消费者拥有的固定版本(pins),或重放缓存的响应。Promptproof 风格的“确定性合约检查”(包含记录好的固件且在 CI 中零实时调用)是这里最快的路径。这些能捕获 80% 的行为破坏,且这些破坏通常是显而易见的:格式漂移、字段缺失、路由倒置。
  • 语义门控(分钟级,完整评估):使用真实的生产模型运行具有代表性的黄金数据集,计算与参考输出之间的嵌入相似度,并标记超出漂移阈值的任何内容。语义差异工具能发现字符级 git diff 无法察觉的意义层面转变——例如在不改变长度的情况下改变意图的措辞改动。这些通常在合并到 main 分支或夜间运行,而不是针对每个 PR。

分层至关重要。结构性门控应在每次 push 时运行,并在几秒钟内给出反馈。固件门控应针对每次 PR 的提示词路径运行。语义门控因成本较高而应采用计划任务,而不是阻塞流程——它们的存在是为了捕获固件门控遗漏的微妙情况,而不是作为合并的门槛。

版本管理纪律:提示词的 Semver 与消费端的版本锁定

只有当消费者可以选择何时采用破坏性变更时,合约测试才有效。这需要版本管理,而提示词需要与代码相同的 Semver 纪律。

一个可行的约定:当输入 schema 发生变化(新增必填变量、重命名字段)或输出合约发生变化(新格式、不同的字段名、删除工具)时,进行**主版本(major)**升级。**次版本(minor)**升级用于向后兼容的添加——例如额外的可选指令、新的 few-shot 示例、细化的拒绝条款。**修订号(patch)**用于不太可能改变行为的措辞级编辑:拼写错误、语法、空格。

消费者锁定到一个主版本。他们会自动获得次版本和修订号更新;主版本升级则要求他们运行自己的评估并显式更新锁定版本。在迁移窗口期间(通常根据影响范围持续 30 到 90 天),提示词注册表会并行维护至少两个版本。这与优秀的 HTTP API 团队处理废弃(deprecation)的方式完全一致,原因也相同。

在实践中容易崩溃的纪律是“修订号”类别。团队总会说服自己“只是修复一下措辞”不可能影响行为——直到生产数据集的嵌入相似度分数下降了 12%。值得强制执行的经验法则是:如果更改修改了任何指令或示例,它至少是一个次版本,无论 diff 看起来多么微小。只有注释、空格和文档字符串才是安全的修订号级别编辑。

一个共享的提示词注册表——带有来源元数据、版本锁定和兼容性标签——是使这一切可操作的关键。团队通过 name@version 而不是通过复制粘贴字符串来引用提示词。注册表工具现在很常见:MLflow、promptlayer、braintrust、agenta,以及构建在 git 仓库加查询 API 上的自研系统都行。工具的选择并不如这一不变量重要:生产环境中的任何提示词都不能拥有未知的版本或未知的消费者。

陷入困境团队的迁移路径

大多数读到这篇文章的团队在生产环境中已经有了提示词,但没有合约、没有版本管理,也看不清谁在消费什么。实用的操作顺序是:

  1. 盘点消费者。 对于生产环境中使用的每个提示词,找到每一个调用点——包括嵌入在代码中、动态构建或作为配置传递的提示词。这个练习的结果通常比预期的更令人不安;一个简单的“意图分类器”往往有五六个隐式消费者。
  2. 冻结当前版本。 在任何人进行任何编辑之前,将每个消费者锁定到一个特定的提示词版本。这为构建合约层争取了时间,而不会增加新的风险。
  3. 为每个团队编写一个消费者锁定件。 每个消费团队编写一个覆盖其最高风险假设的固件文件。不需要是一个全面的套件——只需要那个“如果这个坏了我就得被 call”的单一案例。目标是覆盖的广度,而非深度。
  4. 将门控接入提示词 CI。 首先是结构性门控,因为它们是免费的。其次是固件门控。最后是语义门控,因为它们需要评估工具链和你可能尚未拥有的黄金数据集。
  5. 采用版本控制约定。 根据编辑历史追溯性地对现有提示词应用 Semver。大多数将定位在 v1.x;少数具有严重不兼容历史版本的提示词则记录多个主版本发布。

这些步骤都不需要一个完整的评估平台或专门的提示词团队。它们需要的是从“提示词是文本”到“提示词是接口”的文化转变。工程化会随之而来。

Prompt 需要像对待公共 API 一样严谨

下次有人建议对共享 Prompt 进行“快速措辞调整”时,值得问这样一个问题:你会不会在没有版本升级、弃用声明和用户可见迁移计划的情况下,对公共 REST 端点进行同等的更改?对于 HTTP API,人们的本能是谨慎。而对于 Prompt,人们的本能仍然是随意的 —— 每一个由未经审查的 Prompt 修改引起的事故都证明,这种随意的本能与其造成的巨大影响(爆炸半径)是不匹配的。

Prompt 的契约测试并不罕见。它们是 HTTP API 已经使用了十年的同一种消费者驱动模式,只是被移植到了一个新的制品上。工具链已经存在。注册表也已经存在。通常缺失的是组织层面的决策:即 Prompt —— 那些共享的、生产环境中的 Prompt —— 是受控的接口,而不是可以随意编辑的草稿空间。

只要做了这个决定,工程实现就会变得水到渠成。如果忽略它,你将不断因为不同的措辞而引发同样的故障。

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