多Agent系统直接同步调用会引发级联故障、紧耦合、扇出风暴和无法回放审计的问题。这篇来自亚马逊技术专家的文章解释了为什么应该用事件日志取代点对点调用,让Agent通过消息代理异步通信,并给出了具体的代码示例和架构取舍。
同步调用把Agent系统变成了分布式单体
AI Agent之间直接打电话,这事儿听着就离谱。一个订单处理流程跑起来,规划Agent先给数据增强Agent打电话,增强Agent再给定价Agent打电话,定价Agent又给风控Agent打电话,每一个都得等上一个说完才动。演示的时候跑得飞快,漂亮得不行。
结果有一天下午,增强Agent拿到了一条格式坏掉的产品记录,然后它就卡在了一个重试循环里,对一个老出问题的下游API死磕。它没崩溃,也没返回任何东西,就那么把连接拽着不放,一直"思考"。规划Agent在等它,规划Agent后面的订单队列在等规划Agent,几分钟之内整条流水线就瘫了。仪表盘上一个错误都没有,吞吐量就那么安安静静地掉到了零。
真正让人头疼的是后来的事,有人问那个最直接的问题:Agent卡住的时候到底在干啥?答案是啥也没有。Agent之间聊了啥,没有任何日志。那场对话发生在内存里,在那一堆阻塞的HTTP调用堆栈里,等杀掉进程恢复服务的时候,唯一能看的现场痕迹就只剩下一行堆栈信息。
这不是AI的问题,这是2014年微服务的老毛病,只不过现在披了件大语言模型的外套。
直接调用同时踩了五个坑
当Agent A同步调用Agent B然后干等着,就等于同时做错了五件事,每一件都能让你半夜爬起来修系统。
紧耦合这事最要命。
A得知道B的地址、B的请求长啥样、B的响应长啥样,还得知道B这个家伙存在。加个合规Agent也想看每一笔订单,那就得去改规划Agent的代码,让它把消息再广播一份。谁调用了谁形成的那个调用图就是你的系统架构图,全都焊死在代码里了。级联故障更直接,B慢了A就慢,B挂了A就挂。五个Agent串成一串,你的可用性是五个可用性的乘积,尾延迟是五个尾延迟的和。
大语言模型Agent本来反应就慢,还不稳定,一会儿调个工具,一会儿琢磨个推理步骤,尾延迟本来就胖,五个叠在一起,那画面想想就美。扇出风暴一来,一个进来的事件要激活四个Agent,同步调用树就这么长出来了,每个连接都拽着不放,等子节点完事。并发数你看不见,也控制不住。背压是完全不存在的,A生产得比B消费快?在同步网络里,A只能阻塞,或者疯狂重试,要么超时之后把活儿弄丢了。
最要命的是没法回放也没法审计。Agent之间的互动跟一阵风似的,说没就没了。出了问题,没有决策记录,没法重新跑一遍当时的顺序,审计人员问为什么这个订单被标记了,你拿不出任何证据。整个系统里最有趣的数据,Agent们互相聊了啥,调用一返回就扔了。这些坑你听着都耳熟,十年前我们就是因为这些破事才不搞阻塞RPC的,Agent没有资格推翻这条教训。
在中间放一个日志就全解决了
解决方案老套得很,但管用:Agent之间不直接调用。它们往一个消息代理(broker)上发事件,别的Agent订阅自己感兴趣的事件就行。Agent A不调用增强Agent,它发一条order.enrichment.requested事件,然后该干嘛干嘛。增强Agent等自己准备好了,按自己能承受的速度,消费那个事件,干完活,再发一条order.enrichment.completed。谁关心增强完的订单谁就订阅,定价Agent、风控Agent,还有你下个季度新加的审计Agent。A压根不知道它们的存在。
Agent之间的契约从"知道B的API、地址和死活"简化成了一件事:事件结构(event schema)。这就是全部耦合的地方了。搞一个带版本号的类型化结构,你就把一堆乱七八糟的点对点调用变成了一条平整的、可观察的数据流。下面是一个意图事件的样子,以及生产端和消费端怎么处理它。有类型、有版本,最关键的是,它一落到日志里就是持久化的:
# 类型化的Agent"意图"事件——所有耦合都在这儿了
event = {
"event": "order.enrichment.requested", # 主题/类型
"version": "1.0", # 结构版本,安全演进
"id": "evt-7f3a", # 幂等性键
"ts": "2026-06-29T14:02:11Z",
"trace_id": "ord-9182", # 把整场对话串起来
"payload": {"order_id": "9182", "sku": "A-204", "qty": 3},
}
# 生产者:规划Agent谁都不调用——发布完就走
broker.publish("order.enrichment.requested", event)
# 消费者:增强Agent订阅,按自己的节奏干活,发结果
@broker.subscribe("order.enrichment.requested")
def on_request(evt):
enriched = enrich(evt["payload"]) # 可能很慢;没人被它卡住
broker.publish("order.enrichment.completed",
{**evt, "event": "order.enrichment.completed",
"payload": enriched})
十二行代码的机制。规划Agent不知道谁来增强订单,也不知道那个消费者现在在线不在线。它就是带着意图把事件发布了。日志存着它。整个诀窍就这么简单。
日志给的东西调用永远给不了
持久化是第一件。事件在任何一个Agent碰它之前就已经落在硬盘上了。增强Agent可以宕机一个小时,活儿在日志里等着,不会超时蒸发掉。等它回来了,从断点接着干就行。
回放这个功能,在出第一次事故之前没人觉得它有多重要。定价Agent部署了个有问题的版本?把它的消费者偏移量(consumer offset)重置一下,用修好的代码把昨天的事件重新跑一遍。要调试一个奇怪的决策?用trace_id把当时的事件序列拉出来,再看一遍那场对话,每次结果都一样。你没记录的HTTP调用是没法回放的。日志你随时可以重放。
审计几乎是白送的。合规部门迟早要逼你建的那个东西——每个Agent做的每个决定的记录——不就是这个日志吗。它已经在那儿了。每个意图,每个结果,带着时间戳排好了序。有人问为什么订单9182被标记了,你不用靠回忆重构现场。直接读日志就行。
背压来了也不怕。A生产得比B消费快不再是故障,只是延迟——你可以盯着这个数字涨,然后扩容应对。缓冲就是这个点。
松耦合是最终效果。加Agent就是加个订阅者。删Agent就是删个订阅者。换掉Agent整个实现,上游没人会注意到,因为它们只跟主题对话。那个调用图不再是承重的代码,变成路由配置了。
这套赌注在事件驱动的B2B系统里已经赢过很多次了。消息总线不是额外开销,它是整个系统里最能扛的那部分。Agent只是在这个已经被验证过的模式上,新加了生产者和消费者而已。
多个网络跳转和一个要维护的代理值得吗
这个质疑很直接:你把直接调用换成了发布再消费,多了延迟,还多了个代理要管。都对。但你得看清楚你在优化什么。多一跳的延迟是毫秒级的。直接调用设计付出的代价是一个工作流在某个Agent循环的时候卡死,每次超时就丢活儿,在事故最需要你看清情况的时候让你当瞎子。
Agent工作流根本就不是延迟敏感型的。它们的瓶颈是大语言模型的推理和工具调用,那都是按秒算的。在一条已经花三秒做推理的路径上省掉代理那一跳,误差范围都算不上。你在防的那个数字防错了。Agent系统真正的代价从来不是毫秒。是脆弱和盲目——不知道发生了什么,一倒全倒。一个能回放的事件,比你省下的一毫秒值钱多了。
诚实的取舍还是得摆出来。你现在得跑一个代理了(Kafka、NATS、SQS、Redis Streams,看你选哪个级别的)。你得按最终一致性和幂等消费者来设计,这就是为什么结构里要有那个id字段。请求/响应模式——"我现在就要这个答案才能继续"——得建模成一个关联请求事件加一个响应事件,第一次做是觉得麻烦了点,但到你第一次回放它的时候就知道值了。对于那些真正同步、低延迟、两方之间的交互,直接调用仍然没问题。成网的那个调用图才是问题,不是某一次调用的存在本身。
核心结论
同步Agent调用网就是一个分布式单体。紧耦合、级联故障、扇出风暴、没法回放。
这剧情你都看过了,别拿Agent再拍一遍。耦合面应该是一个结构,不是一个API。
有版本、有类型的事件意味着你可以加Agent、换Agent,不用动已经在跑的代码。回放和审计不是以后才补的功能。
中间放个日志,它们就自己冒出来了,而且出事故的时候你最想要的就是它们。你防错了数字。多一跳是毫秒级,推理路径是秒级。真正会坏的是耦合,不是延迟。把那些直接调用留给少数的两方同步场景。
干掉那个调用网。让代理留住那场对话。下次你忍不住想直接把Agent A接到Agent B的时候,问一个问题:凌晨三点它挂在那儿的时候,我能读到什么?如果答案是堆栈信息,那就建错了。中间放个日志,让Agent们留下点纸质痕迹。
总结一下:别让AI Agent之间直接打电话。用事件日志当中间人,你得到的不仅是能回放的审计记录,还有凌晨三点不用靠猜来修系统的踏实觉。别等被堵醒了才改。