跳转到主要内容

AI 的依赖注入:在不损失测试保真度的情况下模拟模型调用

阅读需 1 分钟Tian PanTian Pan

我调查过的最残酷的 Bug 报告来自一个团队,他们的 CI 在六周内一直显示为绿色(通过)。每一次提示词更改都通过了完整的测试套件。每一次工具调用都有一个模拟(mock)。每一次集成测试都断言了大模型在预发布环境中返回的精确字符串。然而,每一个测试都在撒谎。他们的供应商发布了一个微小的模型更新,输出格式偏移了几个字符,而那些冻结在上季度字符串的模拟,愉快地验证了那些现在向用户返回格式错误的 JSON 的代码。

这就是我想谈论的失效模式。在代码结构层面,AI 应用的依赖注入很容易做对(你的提示词运行器接受一个客户端接口,你在测试中传入一个伪造对象,搞定)。但在“保真度”层面,也就是真正重要的属性上,很难做对:通过的测试能否预测生产环境不会崩溃?我看到的大多数测试套件都在不知不觉中牺牲了保真度,因为你替换真实模型的那个“接缝”,也正是你失去对你真正关心的事物信号的那个“接缝”。

修复方法不是“更仔细地模拟”。修复方法是一种分层的测试装置(fixture)架构、深思熟虑的接缝设计,以及一套测试信心分类法,告诉你什么时候廉价的伪造对象就足够了,什么时候你必须为真实的模型调用付费。这三者共同构成了一个测试套件,它在每次提交时仍然只需几秒钟即可运行,但不再对生产行为撒谎。

没人谈论的保真度鸿沟

模拟一个 LLM 调用非常容易,但也具有欺骗性的误导。你代码里某处有一个 chat.completions.create,你用一个返回 {"choices": [{"message": {"content": "..."}}]}MagicMock 替换它,测试就通过了。问题在于,你的模拟只是特定模型在特定时间点输出的一个快照。你的代码与模型之间的实际契约比那个快照丰富得多:分词行为、工具调用参数形状、拒绝模式、结构化输出中的空格怪癖、温度(temperature)与提示词的交互方式,以及那些并不显而易见的情况——模型突然决定你的系统提示词意味着一些与你预期不同的东西。

当团队针对模拟的字符串编写断言时,他们并不是在针对模型测试应用程序。他们是在针对他们对模型的记忆测试应用程序。而这种记忆是会腐烂的。供应商发布静默更新。模拟测试继续通过。一份来自跟踪此类行为的团队的报告指出,Bug 通过 LLM 生成的测试进入了生产环境,因为这些测试的断言与当前的实现挂钩,而不是契约——它们关注精确的返回值和模拟的内部逻辑,而不是行为属性。测试套件在重构后显示为绿色,而生产环境中实际运行的路径却是损坏的。

辨别方法很简单:如果你的 LLM 模拟返回的是一个手写的、在系统中任何真实模型输出中都不匹配的字符串,那么你写的测试只有在你更改测试本身时才会失败。它与现实脱节。

分层测试装置架构

摆脱困境的方法是像数据库测试对待数据装置(fixtures)那样对待 LLM 测试装置:将其视为一种深思熟虑的分级体系,每一层都有不同的成本/保真度权衡。在实践中行之有效的三层架构:

第一层 —— 存根伪造(Stub fakes)。一种类似 FakeListLLM 的对象,按索引返回预设的响应。LangChain 提供了一个,每个严肃的 LLM 框架也都有类似的实现。它们的存在是为了让你测试应用程序的基础结构:控制流、错误处理、重试逻辑、工具调用周边的状态机。它们不是在测试模型,而是在测试模型周围的一切。请大量使用它们。它们的运行时间以毫秒计。多写这类测试。

第二层 —— 录制磁带(Recorded cassettes)。使用 pytest-recording (VCR.py) 或等效工具向供应商录制一次真实的 HTTP 流量,然后在后续的每次测试运行中回放该录音。第一次运行是真实的且缓慢的;之后的每次运行都是确定且快速的。至关重要的是,录音磁带包含了供应商响应的实际形状——请求头、流式增量、工具调用的 JSON 模式、拒绝模式的细微编码。当供应商更改该形状时,你可以重新录制并对比磁带以查看变化。这就是你如何在不为每次测试付费的情况下,将行为锁定在特定模型版本上的方法。像 vcr-langchainbaml_vcr 这样的项目专门为此模式而生。

第三层 —— 实时调用(Live calls)。一小组经过精心挑选的测试,它们确实会调用供应商,使用真实的 API 密钥,且不在每次提交时运行。通常是每晚运行。在合并到主分支(main)之前运行更好。这些是你针对模型漂移的“金丝雀”:它们回答了“无论我们的冻结模拟和录制磁带怎么说,模型是否仍然按照我们的预期行事?”这一问题。你希望这一层规模小且成本高,而不是规模大且被平均化,因为它的任务是捕捉模型本身的回归。

错误的做法是将这些层级混为一谈。用真实调用运行所有测试既昂贵、缓慢且具有不确定性。用存根伪造运行所有测试虽然廉价但会撒谎。这种架构之所以有效,是因为每一层都在测试不同的东西。

接缝设计:让提示词和工具可注入,而不污染产品代码

分层架构的前提是你实际上可以在测试时替换模型,这是你的生产代码必须支持的一种设计决策。这就是许多 LLM 代码库陷入糟糕均衡的地方:产品代码中长出了 if TESTING: 分支,或者“模型客户端”变成了一个带有 use_mock 标志的上帝对象,或者每个调用点都直接导入 SDK,导致无处进行拦截。

会员专享

余下内容仅对会员开放。

会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。

  • 完整文章,包含未公开存档的部分
  • 可落地的工作框架,附带权衡与决策依据
  • 新文章抢先看,先于公开发布

随时取消 · 一次订阅,畅读全部

参考资料

保持联系,关注我获取更多内容

阅读需 12 分钟

合规审查员作为评测编写者:为什么法律团队应该为你编写测试用例

合规审查员能发现工程评测系统性遗漏的 LLM 失败模式。将他们从文档审查环节移至回归测试套件中 —— 法律签署将转变为对每次提交时运行的固定测试用例的确认。

insider
ai-engineering
阅读需 8 分钟

你在廉价模型上测试,却在昂贵模型上部署

在廉价模型上运行 CI 和评估,而生产环境却使用尖端模型,这在最关键的地方破坏了开发与生产环境的一致性。本文将探讨为什么层级差距会使你的评估门控失效,以及缩小这一差距的预算计算方法。

insider
llm
阅读需 9 分钟

MCP 工具弃用:为什么模型仍然调用旧名称

重命名 MCP 工具不仅仅是 API 弃用 —— 这是一种模型分布的转变。本文将探讨为什么旧名称会持续出现,以及如何在不触发运维告警的情况下逐步淘汰它。

insider
mcp
阅读需 8 分钟

渐进式上下文替换:在长 AI 对话中保持质量的方法

硬截断和朴素摘要在长 AI 对话中都会导致质量下降。滚动替换模式——对近期轮次保持原始状态,同时对较旧内容进行增量压缩——是在会话超过四十轮时保持质量的有效方法。

insider
ai-engineering
阅读需 9 分钟

沉默的回归:如何在不失去用户信任的情况下传达 AI 行为变化

当你悄悄更新模型或提示词时,高级用户会感受到真实的回归——即便整体指标有所改善。本文介绍如何检测行为漂移,并在不破坏用户信任的前提下传达 AI 变更。

insider
ai-engineering