级联问题:为什么 Agent 副作用在大规模运行时会呈爆炸式增长
一个团队交付了一个文档处理智能体(agent)。它在开发环境中表现完美:读取文件、提取数据、将结果写入数据库,并发送确认 webhook。他们运行了 50 个测试用例,全部通过。
部署两周后,在 100 个并发智能体实例运行时,数据库中出现了 40,000 条重复记录,三个下游服务收到了数千个虚假的 webhook,一个共享配置文件被两个同时运行的智能体各覆盖了一半。
智能体本身没有出错。系统崩溃是因为没有任何一个独立的智能体测试曾被要求与其他智能体共同处于同一个运行环境中。
这就是级联问题。它既不是模型故障,也不是提示词故障。这是一种系统性故障,单元测试从结构上就无法捕捉到它,因为单元测试在设计上就是隔离执行的。导致生产事故的行为——竞态条件、重试放大、共享状态损坏——只有在多个智能体实例同时与同一个现实世界的资源交互时才会出现。
理解级联问题需要像分布式系统工程师思考服务那样去思考智能体:不要把它们看作正确的程序,而要看作是一个共享、竞争环境中的参与者。
隔离如何掩盖了问题
单元测试给了你清晰的答案,但你问的问题在生产环境中并不存在。测试中的问题是:“给定此输入,该智能体是否能产生正确的输出?”而生产环境中的问题是:“当 100 个该智能体的副本同时针对同一个数据库、文件系统和外部 API 运行时,会发生什么?”
这是两个不同的问题。它们之间的鸿沟就是级联问题产生的地方。
考虑一个处理任务队列的简单智能体:读取下一个未处理的项,处理它,并将其标记为已完成。在隔离状态下,这是正确的。但在有 10 个并发智能体的情况下,它们都会在其中任何一个标记完成之前读取同一个“下一个未处理项”,导致同一个任务被处理了 10 次。这并不是模型错误——智能体完全按照指令行事。这是一个经典的“检查时间到使用时间”(time-of-check to time-of-use, TOCTOU)竞态条件,与分布式数据库工程师几十年来一直在处理的问题一模一样。
同样的结构出现在智能体运行的各个地方:
- 文件写入:两个更新同一个配置文件的智能体将相互覆盖彼此的更改。最后写入者获胜。两个智能体都成功完成,但结果是损坏的。
- 重试放大:工具层的一个失败会触发工具内部的重试、智能体 SDK 中的重试以及智能体自身重试循环中的重试。一个简单的网络超时会演变成 27 次 API 调用。
- 状态累积:如果不使用原子操作,一个向共享日志文件追加内容或更 新共享计数器的智能体在并发情况下会产生错误的结果,即使单次追加操作本身是正确的。
ZenML 对 1,200 多个生产部署的分析发现,生产失败最常见的来源并不是模型质量,而是这类基础设施和集成失败。模型表现正确,但系统没有。
三种故障模式详解
重试放大
大多数智能体架构在多个独立层级都设有重试逻辑:HTTP 客户端重试网络错误,工具包装器重试失败的工具调用,而智能体循环则重试失败的步骤。在正常情况下,这些层级是不可见的。但在发生故障时,它们会产生复合效应。
计算方法很简单:如果三个层级中的每一层在失败时都重试三次,那么一个上游错误就会产生 27 次下游调用。如果这 27 次调用是向支付 API 写入数据或向外部服务发送消息,其后果是显而易见的。
修复方法需要跨层级协调重试语义。带有抖动的指数退避(exponential backoff with jitter)可以防止同步的重试风暴。幂等键(idempotency keys)可以防止即使重试成功也导致的重复执行。熔断器(circuit breakers)则能在重试放大升级之前将其制止——在连续 N 次失败后,完全停止尝试该操作,而不是持续冲击一个已经降级的依赖项。
一个金融应用团队报告了一个熔断器配置:它在连续三次失败时跳闸,进入 30 秒的开启状态,然后在恢复之前在半开状态下测试恢复情况。核心见 解是:阈值的设置必须足够激进以防止级联反应,但又不能过于激进导致瞬时故障触发不必要的开启状态。
并发变动
当多个智能体实例读取共享状态、对其进行修改并写回时,除非读取和写入是原子的,否则结果将取决于时间顺序。这不是一个新颖的问题——它是促使数据库事务、分布式锁和比较并交换(compare-and-swap)操作出现的相同问题。智能体也无法幸免。
具体的故障模式取决于资源类型:
文件:两个智能体读取一个 JSON 配置文件,添加一个条目并写回,将静默地丢失其中一个智能体的条目。第二次写入会在没有报错的情况下覆盖第一次写入。
数据库:除非数据库在约束层面(而不仅仅是在应用层面)强制执行唯一性,否则执行“先检查后行动”(如果记录不存在,则插入)的智能体在并发情况下会创建重复记录。
外部 API:如果在检查之后、修改之前有另一个智能体修改了资源状态,那么在修改前检查资源状态的智能体看到的将是过时的状态。乐观锁(optimistic locking)模式可以处理这种情况:在你的写入请求中包含你读取的版本号,并让服务器拒绝与新版本冲突的写入。
通用原则是:不要将智能体设计成基于它们并不排他拥有的状态来做决定。要么获取排他所有权(锁),要么使用在写入中包含前提条件的原子操作(比较并交换),或者将操作设计为无论并发修改如何都是安全的(幂等性)。
