跳到主要内容

你的评测框架是单用户运行的,但你的智能体并非如此。

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 Agent 通过了 92% 的评估测试集。你发布了它。在上线一小时的真实流量中,发生了一些从未在任何追踪(trace)中出现过的事情:Agent 在频率限制(rate-limit)重试风暴中停滞不前,一个客户在工具响应中看到了另一个客户的草稿邮件,你的模型供应商连接池处于 100% 的占用率,而 CPU 却处于闲置状态。这些失败没有一个源自模型。它们存在于你测试的方式与生产环境运行方式之间的鸿沟中。

这个鸿沟表现为同一种形式。你的评估工具(eval harness)在一个固定数据集上一次循环一个 Agent。而你的生产环境则在共享基础设施上同时运行许多 Agent。顺序评估隐藏了每一个前提条件为“两个事物接触同一个资源”的 Bug。在你将对抗性并发(adversarial concurrency)构建到评估工具本身之前,这些 Bug 只会以紧急运维(on-call)报警的形式出现。

顺序评估工具无法发现的 Bug

标准的 for case in dataset: run_agent(case) 循环在结构上无法捕捉到四个类别的错误。每一个类别都有一个看似正常的测试环境伪装,以及一个丑陋的生产环境真面目。

单租户工具频率限制耗尽。 单个 Agent 运行所使用的额度远低于任何合理的每分钟配额。但来自同一租户的 9 个 Agent 在同一时间窗口启动则不然。一份关于多 Agent 系统的报告显示,9 个并发 Agent 在 22 分钟内耗尽了 GitHub 每小时 5,000 次的配额,同步重试在单次事件中产生了 60 多个连锁的 429 错误。评估通过率是 100% —— 每个 Agent 都能独立完成一个 PR。集成方式才是 Bug 所在。

草稿板和记忆键冲突。 框架严重依赖字符串键值的共享存储:agent:{run_id}memory:{user_id}:notescache:{hash(query)}。具有确定性 ID 的单用户测试不会发生冲突。但在具有并发运行、缺少租户前缀或相似输入导致哈希冲突的生产环境中,会产生静默覆盖,即 Agent B 读取了 Agent A 在三毫秒前写入的内容。这与共享计数器上的经典竞态条件(race condition)形式相同,只不过损坏的状态是一个 JSON 对象,而下一个 Agent 会将其视为权威上下文。

语义缓存跨租户泄露。 语义缓存用精确匹配换取嵌入向量相似度。租户感知的键可以防止泄露;而忽略租户的键会将缓存变成一个侧信道。最近的研究表明,针对共享语义缓存的对抗性查询,对属于其他用户的条目达到了 90.6% 的命中率——这意味着一个巧妙设计的提问会得到为别人计算出来的答案。顺序单租户评估从未引入第二个租户,因此测试永远无法发现攻击者(或有 Bug 的哈希函数)会看到的内容。

供应商连接池饥饿。 流式补全(Streaming completions)在生成的整个持续时间内都会占用连接。一个 30 秒的响应意味着 30 秒的连接池占用,而大部分时间都花在等待 Token 的空闲状态。在“每个任务一个线程”的模型中,少数运行时间较长的 Agent 就可以占满连接池中的每一个连接,而 CPU 占用率仅为 5%。单用户基准测试显示系统很快且连接池健康。生产环境图表则显示连接池已满,新请求排在那些半分钟内都不会结束的流之后。

请注意这种模式。每个类别的失败方式看起来都像是基础设施问题,而不是智力问题。每个类别都至少需要两个同时运行的 Agent 才能复现。每一个在你的黄金数据集(golden dataset)面前都是不可见的。

为什么“在测试环境运行正常”这句话是错误的

当一名工程师说测试环境(staging)通过时,他们通常指的是两件事之一:离线评估套件的分数高于阈值,或者在测试环境中进行的离线冒烟测试返回了正确答案。这两者都是单角色实验。测试环境通常一次只有一名工程师在操作,通常使用全新的数据库和预热的缓存。即使是部署在压力测试后的测试环境,通常也只是回放均匀分布的合成流量——这是实际工作负载的“乖巧版”。

真实的工作负载是突发性的、多租户的,并且即使在没有攻击者参与的情况下,分布也是具有对抗性的。单个客户的批处理作业会产生相关的调用。Cron 定时任务会导致来自每个账户的同步流量。重试策略会将一个缓慢的上游请求转化为放大的负载。你的评估工具中不存在这些模式,因为你从未同时运行两个案例。

Anthropic 自身关于 Agent 评估的工程文章指出,试验之间的共享状态既可能掩盖失败,也可能虚增性能——他们观察到 Claude 通过检查之前试验留下的 git 历史记录,在某些任务上获得了不公平的优势。那是“友好”的版本。而“敌对”的版本是两个生产环境中的 Agent 在同一个 git 工作副本上发生竞态,并将其损坏。

这种视角的转变虽然令人不快,但却很准确:顺序评估工具并不是在测量你的 Agent。它是在最宽容的调度假设下测量你的 Agent。而生产环境会收回这种宽容。

设计多用户评估框架(Eval Harness)

解决方法并不复杂。这关乎一种原则:在评估阶段,构建出与生产环境完全相同的条件。四个设计动作可以将单用户测试框架升级为多用户框架。

并行运行用例,而不仅仅是批处理。 批处理意味着提交许多用例进行顺序处理。并行则意味着启动 N 个代理,在相同的墙钟时间内共享资源。实现方式是在现有的用例运行器基础上增加线程池或异步分发(async fan-out)——但有一个关键补充:资源(工具、缓存、内存存储、模型客户端池)在各次运行之间必须是相同的实例,而不是为每个用例单独准备的固定装置。核心在于“竞争”。每个用例之间的隔离会使测试失去意义。

显式注入突发模式(Burst Patterns)。 均匀的并行化(以恒定速率运行 50 个代理)可以捕获一些 Bug,但不是全部。最病态的情况是“协同启动”:50 个代理在同一个 100 毫秒的时间窗口内启动。同步重试、缓存雪崩和连接池耗尽需要这种初始冲击才能显现。在框架中将协同启动设为一等场景。同时也要包含阶梯式增长(staggered ramps)和缓慢流出(slow drains)——它们的失败模式各不相同。

混合租户与对抗性探测。 运行同一个租户的 50 个副本的多用户框架会遗漏跨租户 Bug。在运行中分配明确的租户 ID,然后添加探测用例,其任务是询问:“我能看到另一个租户的数据吗?”探测器是一个经过精心构造的查询,旨在最大限度地提高与另一个租户敏感查询的语义相似度,并在第一个租户填充缓存后运行。如果探测器返回了另一个租户的响应,那么你就发现了一个 Bug。这种在评估中进行的红队测试,正是跨会话泄漏检测所真正需要的。

根据竞争指标评分,而不仅仅是任务成功率。 单用户框架报告任务准确率。多用户框架需要在负载下报告相同的准确率,外加竞争信号:工具 429 速率、缓存命中但租户错误率、草稿板(scratchpad)写入冲突、p99 连接池等待时间。如果一次回归测试中准确率保持在 92%,但工具的 429 速率翻了三倍,那么它应该无法通过审核。准确率数字本身掩盖了性能倒退。

值得借鉴的对抗性并发模式

你不需要从头开始发明对抗场景。分布式系统工程师使用了二十年的基于属性的测试和混沌工程工具包可以直接应用。以下是值得移植到你的框架中的模式清单:

  • 突发协同启动。 N 个相同的代理在 100 毫秒窗口内启动,共享一个工具配额。断言:没有代理失败;尾部延迟保持在有界范围内;429 重试风暴不会放大。
  • 交错的工具调度。 两个代理的计划要求以相反的顺序使用相同的工具。断言:当工具串行化时不会发生死锁;两个代理都能完成。
  • 租户探测对。 租户 A 用敏感输出填充缓存。租户 B 发出类似的查询。断言:B 永远不会逐字看到 A 的缓存输出。使用变异的措辞运行数千次此类测试。
  • 长流 + 突发短任务。 一个代理持有一个 30 秒的流式生成;20 个短生成代理尝试启动。断言:短任务代理不会因为长任务占满连接池而饿死。
  • 写入中途崩溃。 杀死一个持有草稿板锁的代理进程。断言:后续代理看到一致的状态,而不是写了一半的二进制对象,且锁的 TTL 会正常释放。
  • 乱序消息。 使用队列时,以非发出顺序向代理交付工具结果。断言:代理会拒绝陈旧的结果,而不是将其视为最新结果。

每一项都需要一到两天的开发工作。每一项都能堵住一类生产事故,而无论单用户准确率如何提高,都无法防止这些事故的发生。

你的 CI 流水线会发生什么变化

多用户框架的运行成本比顺序执行更高,额外的成本体现在两个方面:突发测试期间消耗的真实供应商配额,以及测试套件更长的墙钟时间。这两者都值得投入,但需要明确预算。

在每个 PR 上运行顺序准确率套件。在合并网关或每日构建计划中运行竞争套件,而不是在每次推送时运行,这样可以将成本控制在有限范围内。将竞争指标视为与准确率分离的 SLI——你需要一个仪表盘,将准确率、突发下的 429 速率、跨租户泄漏率和连接池等待时间显示为四条独立的趋势线。其中任何一项的倒退都是发布阻断因素。

竞争套件还需要自己的预发布(staging)环境,理想情况下应该镜像生产环境的连接限制、缓存拓扑和工具配额。一个只有 10 个连接的测试池永远无法重现需要 50 个连接才会出现的资源饥饿 Bug。如果你真实的连接上限是 100,你的框架就需要能够使接近该值的压力饱和——这通常意味着框架运行在你的开发笔记本电脑之外。

隐藏的收益

在评估中运行对抗性并发还有一个二阶收益。它迫使团队跨越边界进行沟通,否则这些问题会被搁置。谁拥有缓存上的租户密钥?工具返回 429 时的重试策略是什么?崩溃下草稿板一致性的 SLA 是什么?顺序评估让每个人都能推迟回答这些问题。并发评估则要求必须回答,因为失败模式在测试时是具体且可重现的,而不是在生产环境中模糊且罕见的。

将你的评估框架视为生产环境的模型。如果模型缺失了并发性,那么评估就是在测量别的东西——一个干净的、理想化的、单用户版本的系统,而这种系统在你的笔记本电脑之外并不存在。构建与工作负载相匹配的框架。你发现的 Bug 正是你原本会发布到生产环境中的 Bug。

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