跳到主要内容

智能体测试的模拟环境:构建代价为零的沙箱

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的 Agent 在预发布环境中通过了每一项测试。然后它进入了生产环境,发送了 4,000 封电子邮件,向一名客户收取了两次费用,并删除了一条本不该触碰的记录。预发布测试没有错 —— 它们只是测试了错误的东西。预发布环境让 Agent 看起来很安全,因为所有它可能破坏的东西都以错误的方式被伪造了:Mock 得足以不崩溃,但也真实得足以让你误以为测试是有意义的。

这就是“模拟保真度陷阱”。这与普通的软件测试失败不同。对于确定性函数,镜像生产环境 Schema 和 API 的预发布环境通常就足够了。对于 Agent 来说,行为产生于推理、工具输出以及跨多步轨迹的累积状态之间的相互作用。如果在这些维度中的任何一个上与生产环境存在偏差,预发布环境都会产生对其实际行为过度自信的 Agent。

解决方案不是更好的预发布环境。而是重新思考模拟的目的是什么 —— 并构建 Agent 可以对虚假基础设施采取真实行动的环境,而不是对真实基础设施采取虚假行动。

为什么预发布环境会误导 Agent

预发布环境在传统软件中失败的方式是众所周知的:Schema 漂移、陈旧数据、性能不足的硬件。对于 Agent,有一个尚未引起足够重视的深层问题:行为保真度的偏差速度比结构保真度更快。

当 Agent 在预发布环境中调用数据库时,Schema 与生产环境匹配。但数据不匹配 —— 预发布表要么是空的,要么是用演示数据填充的,或者是以改变 Agent 遇到的值分布的方式进行了匿名化处理。一个学习“检索并总结客户笔记”的 Agent,在这些笔记是三个词长和三个段落长时的行为是不同的。结构契约(API 形状)保持一致,但推理轨迹会根据数据特征发生偏移。

这体现在一种具体的失败模式中:在预发布环境中通过所有测试的 Agent 失败,并不是因为它们调用了错误的 API,而是因为某个工具返回了意料之外的数据形状,导致推理进入了从未测试过的分支。测试套件报告针对所有 47 个定义测试用例的 100% 通过率。这 47 个案例中,没有一个包含带有脏话的客户笔记、附件数量为零,或者系统启动之前的时间戳。而生产环境中这三者兼具。

还有一个更隐蔽的问题:与其他服务共享基础设施的预发布环境会产生跨测试干扰。Agent 写入共享的预发布数据库,导致跨测试运行的状态污染。独立运行能通过的测试在顺序运行时会失败,因为之前的运行留下了意料之外的记录。Agent 活动的痕迹在测试调用之间持久存在,这使得调试几乎变得不可能 —— 你不是在一个已知的环境中测试 Agent,而是在测试它与每一个测试运行所留下的累积沉淀。

保真度权衡:全模拟 vs 快速 CI

针对预发布环境问题,通常有一种反应:构建更高保真度的模拟环境,使其更接近生产环境。使用类似生产环境的数据分布填充数据库。使用生产环境的 Schema。镜像服务版本。自动保持模拟环境最新。

这种方法有一个根本性的成本:模拟环境越接近生产环境,重置成本就越高,运行速度就越慢。一个镜像生产数据分布的高保真环境需要与生产 ETL 流水线几乎同样复杂的填充逻辑。它需要能够撤销每个副作用的清理脚本。它需要版本锁定以防止测试结果随依赖项漂移而发生静默变化。

在某种程度上,全保真模拟的运行速度会比向一小部分生产环境进行的灰度发布 (canary deployment) 还要慢。如果你的 Agent 测试在全保真模拟中需要运行 45 分钟,而你每天要发布多次,那么你就制造了一个瓶颈,团队会绕过它 —— 他们会跳过测试而不是等待。

更好的思路是思考模拟环境到底是做什么用的。它不是为了证明 Agent 在生产环境中能正常工作,而是为了在回归到达生产环境之前捕获它们,并且速度快到开发者愿意自发地运行这些检查。这一目标最好由具备以下特性的环境来实现:

  • 快速拨备:在两秒钟内启动的容器或微型虚拟机 (microVMs)
  • 快速重置:默认即用即弃,在测试运行之间销毁并重建,而不是清理
  • 确定性:在每次测试开始时,相同的输入总是产生相同的可观察状态
  • 隔离性:测试运行之间不共享状态,即使是偶然的

全保真度与这四个特性都存在冲突。通过所有这些约束的模拟环境必然是对生产环境的简化。这是一个特性,而不是 Bug —— 它迫使你明确你实际测试的是哪些属性。

最高投资回报率的方法:仅模拟不可逆的操作

如果你无论如何都无法实现完全的保真度,那么正确的策略就是将模拟精力集中在最重要的地方。在测试风险方面,并非所有工具都是平等的。

考虑一个典型的 Agent 工具包:搜索知识库、总结文档、起草邮件、发送邮件、更新客户记录、安排会议、处理付款。前两个是只读操作,失败的代价很低——如果它们返回了垃圾内容,Agent 会产生错误的输出,你发现了并修复它。最后四个都有不可逆或难以逆转的副作用。发出的邮件无法撤回。退款是一个手动过程。损坏的客户记录需要数据恢复。

这种不对称性非常明显:只读工具在生产环境中失败数百次,代价也仅仅是令人恼火。而写入工具在生产环境中失败一次,其代价可能就是一次客户支持事件、一场计费纠纷,或者是传播到下游系统的数据完整性问题。

这意味着模拟的 ROI 计算是倾斜的:在模拟具有不可逆副作用的工具上投入重金,而对只读工具接受较低程度的模拟。具体来说:

  • 只读工具(搜索、检索、摘要):使用返回合理形状数据的轻量级 Mock;测试 Agent 在面对各种数据形状时是否推理正确
  • 可逆的写入工具(草稿创建、本地状态更新):使用在运行之间重置的简单内存存储;测试写入格式是否正确
  • 不可逆的写入工具(发送邮件、支付处理、触发真实世界动作的外部 API 调用):使用精确镜像真实 API 契约的高保真 Fake,捕获每一次调用以进行断言,并且永远不真正执行副作用

最后一类是大多数模拟投资获得回报的地方。一个捕获每个 sendEmail() 调用并将其存储以供断言的 Fake 邮件发送器,构建成本低,运行速度快,并且能捕获一大类 Bug:错误的收件人、重复发送、格式内容的错误。一个验证卡片 Token 格式并在不向任何人收费的情况下返回成功的 Fake 支付处理器,将在这些错误到达真实的支付渠道之前,捕获每一个格式和时序错误。

构建 Fake 基础设施的两种模式

构建 Agent 沙箱有两种架构方法,它们在权衡取舍上有所不同。

工具隔离 (Tool isolation) 在真实基础设施上运行 Agent,但通过沙箱代理路由特定的危险工具。Agent 调用 emailClient.send(),在生产环境中它会访问真实的邮件 API;在测试中,它会访问一个捕获但不发送的沙箱化邮件服务。优点是代码改动极小——你只需要更换依赖项,而不是整个环境。缺点是 Agent 仍然可以访问所有未明确沙箱化的内容,这意味着你需要严格规定哪些工具需要接受沙箱化处理。一个你忘记沙箱化的工具可能会在测试运行期间产生真实的副作用。

Agent 隔离 (Agent isolation) 将整个 Agent 运行在一个没有凭证、没有网络访问权限、也没有通往真实基础设施路径的沙箱环境中,除非通过受控代理(即“控制平面”)。Agent 仅接收三个环境变量:一个会话 Token、一个控制平面 URL 和一个会话 ID。它不会意外触及生产环境,因为从沙箱内部根本无法访问生产环境。每一个工具调用都通过代理路由,代理决定是针对真实基础设施执行、路由到 Fake,还是捕获调用以进行断言。

Agent 隔离的设置工作量更大,但能提供更强的保证。对于执行高风险操作的 Agent 来说,这是合适的模型——即任何“Agent 在测试期间意外调用了真实 API”是你无法承受的可能故障模式。对于大多数危险工具集较小且易于枚举的应用,工具隔离就足够了。

对于 CI 流水线,使用临时微型虚拟机(microVM,启动时间小于一秒,测试完成后销毁)的 Agent 隔离已成为标准模式。沙箱的配置是确定性的,运行之间不共享状态,并且可以在不同的测试场景中并行运行而互不干扰。

测试轨迹,而不仅仅是结果

预发布环境思维还有一点错误:它将测试成功视为二进制。Agent 要么完成了任务,要么没完成。对于 Agent 来说,轨迹与结果同样重要。

一个 Agent 如果通过调用错误的工具序列、从自找的错误中恢复,并最终得到了正确的答案,即使它产生了正确的输出,其轨迹也是脆弱的。这种恢复路径在稍微不同的初始状态下可能就无法奏效。同一个任务运行一千次,可能成功率为 90%,而剩下的 10% 会以仅凭输出难以诊断的方式失败。

模拟环境应该捕获并暴露完整的工具调用追踪 (Trace) —— 按顺序排列的每个调用的工具及其输入和输出。测试应该对轨迹进行断言,而不只是最终状态:此 Agent 应该在调用写入工具之前调用搜索工具;此 Agent 应该在更新客户记录之前读取它;此 Agent 不应该在验证收件人地址有效之前尝试发送邮件。

轨迹断言是高保真 Fake 优于仅返回预设响应 (Canned Responses) 的 Mock 的主要原因。Fake 可以强制执行顺序:如果收件人未预先验证,Fake 邮件程序就会拒绝发送调用,这将揭示一类 Mock(无论调用顺序如何都返回成功)永远无法发现的 Bug。

综合实践

大多数团队的实践路线图如下:

  1. 识别你的 Agent 工具箱中每一个具有不可逆或难以逆转副作用的工具。这个清单通常比你预期的要短 —— 通常只有 3–6 个工具。
  2. 为这些工具构建高保真 Fake。Fake 应该验证输入、强制执行调用顺序约束、捕获所有调用以进行断言,并返回真实的成功/失败响应,而不执行实际操作。
  3. 在 CI 中使用临时且隔离的环境运行 Agent。容器是可以接受的;microVMs 则更好。在运行之间销毁并重建环境。
  4. 断言轨迹,而不仅仅是结果。你的测试应该验证 Agent 以正确的顺序做了正确的事情,而不仅仅是最终状态看起来正确。
  5. 使用生产环境金丝雀部署作为你的高保真验证层,而不是 Staging 环境。带有真实流量和完整可观测性的 1% 金丝雀部署,能以比全面上线更低的风险,捕捉到任何模拟都无法发现的生产差异问题。

那些成功做到这一点的团队,并不是拥有最复杂 Staging 环境的团队。而是那些不再试图让 Staging 看起来像生产环境,并开始构建诚实面对其本质的模拟环境的团队:这些环境是快速、隔离的竞技场,用于在故障触达用户之前捕捉特定类别的错误。

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