跳到主要内容

行为契约:编写工程师真正能测试的 AI 需求

· 阅读需 11 分钟
Tian Pan
Software Engineer

大多数在 QA 阶段夭折的 AI 项目,死因并非模型不好,而是没有人在构建之前就"好"的定义达成共识。工单里的验收标准往往写着"摘要功能应生成准确、相关的摘要"——当工程师问"准确"是什么意思时,得到的答案是"看到就知道"。这不是行为需求,这是一种期许。

问题在于团队把原本用于确定性软件的需求流程照搬到了本质上具有随机性的系统上。当你为数据库查询写 assertTrue(output.equals("Paris")) 时,测试的通过与否是完全确定的。但把同样形式的断言用在 LLM 上,得到的是一个对所有有效的同义表达都失败、对所有自信的幻觉都通过的测试。单元测试在骗你,而它所依据的规格从来就不是为"生成输出分布而非单一值"的系统设计的。

这就是行为契约问题:如何为 AI 系统编写产品需求,使其既精确到可以测试,又灵活到能容纳合理的变体,同时对你愿意容忍的失败模式保持诚实?

为什么经典验收标准在 AI 上会失效

经典验收标准源自契约式设计(design by contract)——这一思想由 Bertrand Meyer 在 1980 年代的 Eiffel 语言中正式提出:每个软件组件都应规定其前置条件(运行前必须为真的条件)、后置条件(返回时保证的内容)和不变量(始终成立的条件)。这套模型对确定性代码运转优雅。一个排序函数接受一个列表,必须返回一个包含相同元素、按非递减顺序排列的列表。你可以对每次调用验证这一断言。

后置条件在 LLM 上失效,因为"组件返回时所保证的内容"不是单一值——而是可能字符串空间上的一个分布。同一问题的两个正确答案看起来不同。同一答案在六次调用中可能以六种方式表述,全都是可接受的。要求精确匹配会让所有答案失败;接受任何内容会让幻觉通过。二元后置条件是错误的抽象。

失败会级联到项目管理层面。产品经理以可交接给 QA 的语言编写需求,QA 编写映射到这些需求的测试。如果需求说"聊天机器人必须正确回答事实问题",QA 挑五个示例问题,检查答案,提交绿色构建。三周后,功能在第六个问题上失败——那个没人想到要纳入的问题。测试套件不是因为工程师粗心才出错,而是因为"正确回答事实问题"描述的是一类输入和期望的行为属性,而非"正确"的定义或失败频率的可接受上限。

四字段契约格式

AI 的行为契约用四个字段替代了经典后置条件,这四个字段共同描述了系统必须做什么、针对什么范围的输入、达到什么质量阈值,以及如何验证。

输入类:对契约所管辖的输入类别的精确描述。不是"用户问题"——那是整个产品界面。而是:"从结账流程提交的、关于产品定价的单轮问题,其中用户已通过身份验证,且问题包含产品 SKU。"输入类限定了契约的边界。未能限定输入空间的需求无法测试,因为一个对 90% 的问题有效、对 10% 失败的系统,可能完全可以接受,也可能是灾难性的崩溃——取决于是哪 10%。

期望行为:对期望输出属性的描述,而非对期望输出的描述。不是"答案应该是 $29.99",而是"响应应包含所引用 SKU 的正确价格,以美元表示,且同一响应中不包含矛盾的定价信息。"属性可以跨表面上看起来不同的输出进行验证,可以通过程序化方式检查(响应是否包含与目录匹配的价格形式的字符串?),也可以通过基于模型的评判检查(此响应是否自相矛盾?)。关键转变是从规定制品本身转向规定制品的属性。

失败预算:在测量窗口内,来自定义输入类的输入中,允许期望行为失败的比例。这是经典需求永远省略的字段,也是契约中最重要的字段。一个 2% 的时间给出错误价格的定价功能,与一个 0.1% 的时间出错的功能,是截然不同的产品。两者又都不同于一个在边缘 SKU 上 30% 失败、在前 100 畅销品上 0.01% 失败的功能。失败预算迫使团队明确表达可接受失败的分布,而不是暗示要求零失败——没有任何 AI 系统能达到这一标准——或者在未定义"低"的情况下说"低错误率就好"。

测试预言机:对如何判断特定输入的给定输出是否满足或违反期望行为属性的具体、可执行的描述。这个字段将其他三个字段从需求语言转化为代码。预言机可以是正则检查、结构化提取、与真值表的对比、模型评分标准,或包含指定标准的人工审核协议。预言机必须足够具体,使两位工程师独立运行后对同一输出得出相同判断。

契约实践

以下是文档摘要功能的行为契约示例:

  • 输入类:通过网页 UI 摘要端点提交的英文文档,长度在 500 到 5000 词之间,类别为"法律"、"技术"和"新闻"。
  • 期望行为:摘要必须覆盖源文档中提及的所有主要实体(精确率:不包含虚构实体;召回率:不遗漏提及超过两次的实体),长度必须在源文档的 10% 至 25% 之间,且不得引入源文档中不存在的事实断言。
  • 失败预算:在黄金测试集上,违反任何单一属性的请求少于 3%;违反不虚构属性的请求少于 0.5%。
  • 测试预言机:使用 spaCy NER 提取同时应用于源文档和摘要来评估实体精确率和召回率,实体与规范化列表交叉引用;通过 GPT-4 级别的评判模型标记事实新颖性,该模型被提示识别摘要中无法追溯到源文档的具体断言,并记录每条断言的判断结果供人工审核。

注意这个契约没有说什么:它没有规定模型、提示词或推理策略。行为契约与实现无关。它们规定系统必须做什么,而非如何做。这一区别对 AI 功能至关重要,因为实现——模型版本、提示词、检索配置、后处理逻辑——在不断变化。针对特定提示词编写的契约,在有人编辑系统消息中的一句话时就会过时。针对行为属性编写的契约,则能经历模型升级、提示词重写和架构变更而存续。它是你在 CI 中对任何变更进行测试的依据,无论变更了什么。

编写真正能限定问题边界的输入类

输入类字段的失败方式是可预测的,值得逐一列举。最常见的失败是过于宽泛:"关于我们产品的问题"涵盖了如此广泛的表面,以至于任何样本都无法代表它。第二种失败是过于狭窄:指定少数几个命名示例而非一个类别。十二个示例输入不是一个输入类——它是一个测试集,而且是个很小的测试集。输入类需要谓词,而非枚举:当且仅当输入满足条件 A 和 B 且不满足条件 C 时,该输入属于此类。

一个有用的启发式方法:当你能在不运行模型的情况下用常数时间编写一个接受或拒绝任何输入的函数时,输入类的定义才是正确的。如果分类需要推理,那它是行为属性,而非输入类。

定义良好的输入类也使失败预算分配变得可行。一旦你有了具有已知流量比例的输入类——这个类占生产流量的 60%,那个占 5%——你就可以编写具有差异化失败预算的契约。高流量、高风险的类获得严格的预算;低流量、低风险的边缘情况获得宽松的预算。这就是避免将每个需求都设计到最坏情况标准的方法。

在没有真值的情况下构建测试预言机

预言机字段是大多数团队停滞的地方,因为他们把"测试预言机"与"标注数据集"混为一谈。大规模构建带有人工标注的标注数据集既昂贵又缓慢。对于许多 AI 功能,尤其是开放式生成任务,在发布前以足够规模进行人工标注是不可行的。

实际的解决方案是:预言机在纯自动化到纯人工判断的光谱上存在,大多数生产级预言机是混合型的。定价机器人可以用 SQL 查询验证事实准确性——不需要人工。摘要功能可以用自动实体提取进行召回率检查,用基于模型的评判进行幻觉检测,并以 1% 的比例进行人工抽查以对照真值校准评判模型。客户服务功能可以用基于标准的模型评判来评估语气和解决质量,每季度对分层样本与人工评分进行验证。

对任何需求的预言机设计问题是:与行为属性强相关的最廉价自动化信号是什么,以及该代理在什么采样率下需要人工校准才能保持可靠?"足够强的相关性"通过衡量你的自动化预言机与人工判断在校准集上的一致率来确定。在你的特定任务上与人类 95% 一致的预言机适合作为部署阻断标准;70% 一致的预言机是噪声。

契约作为产品与工程之间的接口

四字段契约格式解决了困扰大多数 AI 项目的结构性张力:产品经理用业务语言编写需求;工程师用技术语言实现和测试;双方的翻译都不到位。写"用户应该得到快速、有帮助的答案"的 PM 并不懒惰——他们是在适合自己制品的语域中写作,而那个制品是功能规格,而非测试计划。基于该规格实现并写出"模型运行了且没有抛出错误"这样测试的工程师也不是失职——他们是在用被给予的东西工作。

行为契约迫使协商显式地发生,而非隐式地进行。失败预算字段尤其是一个伪装成技术参数的业务决策。在功能弊大于利之前,有多少比例的用户可以收到错误的价格?这是一个有数字答案的产品问题,在实现开始之前回答它,与在支持升级之后发现答案,在质量上截然不同。契约格式使延迟这一协商变得不可能,因为没有一个数字,规格就字面上是不完整的。

采用这种格式的团队报告了一个二阶收益:需求与代码一起被版本化,当模型变更时,关于行为属性是否仍然成立的讨论在部署前而非部署后发生。契约即测试计划,测试计划即契约,它们是同一份文档。

前进之路

2026 年,大多数交付 LLM 功能的工程团队对其功能的测试严格程度,不如对登录表单的测试严格。登录表单有二元通过条件、回归套件和部署门控。摘要功能有几个非正式示例和一种"通常看起来还好"的共识。行为契约并不能解决 AI 评估的所有难题——数据分布漂移、评估者一致性、对抗输入——但它弥补了需求缺口,而这个缺口使那些更难的问题甚至无法被正确地框定。

可操作的起点是用四字段格式重写一个现有的 AI 功能需求。确定输入类。将行为描述从输出规格转为属性规格。选择一个业务真正能承诺的失败率。定义你将在 CI 中运行的预言机。如果四个字段中的任何一个感觉无法填写,那种感觉本身就是信息:它意味着这个需求从一开始就从来不够精确到可以测试。

可以执行的规格才是可以辩护的规格。从这里开始。

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