AI从聊天框进化到能自己干活三小时的智能体,但背后的网络传输还活在“一问一答”的旧时代。这种错位导致系统随时崩溃、任务莫名丢失、成本暴涨。解决问题的关键,在于把“即时问答”的指令模式,改写成“记录事实”的事件流模式。这涉及到事件驱动架构、消息队列、幂等性、任务溯源和偏移量提交等一套后端基建的思维转换,而不是什么更高级的提示词。
聊天框的底裤兜不住三小时的活
2023年到2025年,市面上绝大多数AI产品都长着同一副骨架。手机发个请求,服务器调个模型,结果顺着网线原路流回来。这是对话的形状。
一次提问,一次回答。几秒钟的等待,全部装进一个HTTP请求里。这个形状在过去两年挺好用,因为AI干的活确实就是聊聊天。
可这形状最近一年裂开了。不是REST接口不好,REST本身没毛病。毛病出在一个请求配一个响应,描述的是一个有明确时长的交易。而我们现在塞给模型的任务,压根没有明确时长。
智能体的循环一旦跑在请求处理器内部,任务的一生就跟网络插座绑死了。然后麻烦就来了。
超时设置到处都有,但没有一个是你自己能控制的。手机和服务器之间,隔着浏览器、内容分发网络、负载均衡器、网关入口,个个都对空闲连接有自己的耐心底线。GitHub那边更绝,只给十秒钟,超时就掐线,所以官方文档直接建议把数据先存起来,到后台慢慢处理。
失败就意味着从头再来。要是任务跑了十二步,第九步时进程崩了,前八步的结果全在消失的内存里。更糟的是,客户端重发请求时,根本不知道第一次是不是已经把邮件发出去、把退款操作完成了。安全的重试需要给任务一个身份标识,光秃秃的请求本身没有这东西。
也没地方让其他人旁听。一个请求只有一个呼叫方和一个接收方。当模型评估流程、操作审计链、成本追踪器都想盯着同一趟运行,就得在处理器内部塞一堆额外调用。处理器就这么变成了一个没人设计过的分发中心。
等待更是不可能。智能体要是需要等人工审批才能继续,就得把网络连接挂上几个小时,正经系统没人这么干。所以团队只好搬出数据库的一行记录、一个轮询循环、一个状态机。到了这一步,他们其实已经不小心造了个消息队列出来。
干活的AI早就不等人了
这不是纸上谈兵。大概十八个月里,AI任务的耗时翻了几个数量级,底层的传输通道却纹丝没动。
Anthropic的工程团队发过一份详细报告,讲他们怎么用长时智能体框架搭应用。有一次,一句“做个浏览器里能用的数字音频工作站”,AI吭哧吭哧干了三小时五十分钟,光令牌就烧掉124美元。光是那个生成代码的智能体,就一口气连续跑了两个多小时。更早一版框架更狠,六小时,两百美元。作为对比,没用框架的单智能体,二十分钟搞定,只要九美元,但出来的东西核心功能根本不能用。
对后端工程师来说,有意思的不是成本。有意思的是,那个框架是用一堆看着像基础设施的东西搭起来的,而不是靠提示词堆出来的。任务被拆成小块,智能体之间靠写在文件里的结构化工件传递上下文,这样新来的智能体就能接着干。另有一个专门的评估智能体,负责读生成器的输出,再把意见写回去让它改。这就是生产者、消费者、以及两者之间持久存在的记录。
提供方也在往同一个方向走。OpenAI的响应API里加了个后台模式,专门让你不用挂在连接上傻等。因为像Codex和Deep Research这类智能体,解决一个问题可能要花好几分钟。为了这个把网络插座一直开着,那是给自己找麻烦,不是设计选择。
python
resp = client.responses.create(
model="gpt-5.6",
input="审计这个仓库,给每个失败的测试开个拉取请求。",
background=True, # 立刻返回,状态是"排队中"
)
# 连接已经断了。活还没干完。
while resp.status in {"queued", "in_progress"}:
sleep(2)
resp = client.responses.retrieve(resp.id)
这就是一个任务编号加一个状态字段。这是最低限度的承认:任务已经活过了请求的寿命。一旦你在模型调用这一步接受了这个事实,迟早得为模型调用周边的一切接受它。
事件驱动不是什么新鲜玩意儿
得小心说这话,容易说过头。
事件驱动架构不是新东西,也不是因为AI智能体才冒出来的。微服务十年前就得出了同样的结论,原因也一样:同步调用会把组件的可用性和延迟绑在一起,系统越大越糟糕。Confluent公司写多智能体系统的文章时,直接点破了这层联系,把智能体看作会发出和收听事件的东西,而不是互相打电话的组件。
这也不是万能药。要是做的产品就是个两秒内回话的助手,每一步都经过消息中间件,除了增加延迟屁用没有。一问一答的模式,对短交互、单轮次的任务依然是对的。真正在变的,是重心的位置。AI工作中那些长时间、自主运行、异步处理的份额一直在涨,这一块需要一种把耗时、故障、观测都当作常态而非异常来处理的模型。
消息的性质变了
事件驱动的核心,在于消息的含义变了。
在一问一答的系统里,消息是一条带着期望的指令。它说的是“干这个,我就在这儿等着,完事告诉我”。呼叫方必须知道接收方是谁,必须在接收方运行时保持在线,必须把接收方的失败当成自己的事来处理。
在事件驱动的系统里,消息是一条关于已发生事实的陈述。它是过去时态的,不可更改的,不携带任何关于谁会读它的预期。智能体.工具.返回这个事件并不要求任何人去做什么。它只是记录下工具返回了,可能有好多方会据此行动,也可能谁都不会。
这一个改变就换来了其余所有好处。生产者不知道谁在消费,所以新增消费者完全不用碰生产者。事实是持久的,所以离线了的消费者回头还能读到。记录是保留的,所以崩溃了的消费者可以重新读一遍。
这套系统的零件
事件。一条事实,带着结构定义、键值、时间戳和唯一标识。在AI系统里,把事件本身做小比普通系统更关键,因为提示词和工具输出都很大,所以真正的数据通常存在对象存储里,事件里只放个引用。
生产者。任何观察到事实并把事实写下来的东西。用户的动作、定时任务、数据库的变更数据捕获流、外面第三方发来的网络钩子。
消息中间件,或者说日志。持久化、有序、可重放的中间层。Apache Kafka是常见的参照物,Pulsar和Redpanda也同类,还有亚马逊的SQS和EventBridge、谷歌的Pub/Sub、微软的Event Grid这些托管服务,各有利弊。
主题和分区。主题是一条命名过的流,分区是里面的排序和并行单元。键值相同的事件会落到同一个分区,互相之间保持顺序。所以按任务编号给智能体事件做键值,很重要。
消费者组和偏移量。一组完全相同的工人,一起分担分区的读取。加一个工人进来,负载就重新分配,生产者完全不知道。一个工人挂了,它负责的分区就分给同伴。偏移量表示消费者读到哪儿了。在工作做完之后提交偏移量,而不是在做之前,这是区分崩溃丢工作和崩溃重复工作的关键。
结构定义、交付语义和死信队列。结构定义注册表让松耦合不至于变成没耦合。大部分系统是至少交付一次,所以重复是常态,处理器必须能幂等处理。任何把重试次数耗尽的消息,会被送进死信主题,等人来翻看,而不是直接消失。
持久执行层。Temporal和Restate把同样的原理用到了函数级别而不是服务级别。它们把每个完成的步骤记成日志,崩溃后就重放日志,已经完成的步骤直接返回缓存结果。对智能体的内部循环来说,这往往比直接操作主题更合适。
麻烦事儿也不少
这套设计的大部分成本,压在了运维的人身上。
调试变难了。没哪个堆栈能横跨四个主题和三个服务。必须从一开始就把关联编号贯穿所有事件,必须做分布式追踪。等后面再加,成本高得吓人。
最终一致性会暴露到界面上。写入被确认了但还没真正生效,中间这段时间用户看到什么,这是产品问题,不只是工程问题。
保证的范围比人以为的窄。顺序保证只在分区内部,不跨主题。精确一次交付只在特定边界内有效,不延展到智能体刚调用的第三方API。所以要按至少一次来设计,让操作效果可以幂等。
消息中间件是个正经系统。把Kafka跑好,需要懂Kafka的人。小团队的话,用托管队列加PostgreSQL的任务表能撑很久。有意这么选,不算没追求。
重放不能复现模型运行。重放日志给的是同样的输入,但模型可能给出不同输出。重放是为了审计、评估和恢复,不是为了确定性复现。搞错这点,事故复盘时会得出错误结论。
存储不便宜。智能体的痕迹数据很大。点鼠标事件觉得不花钱的保留策略,放到完整的对话历史记录上就不一样了。这正是把真正数据体量从日志里移出去的好处所在。
智能体本来就是事件串
把外壳扒掉,智能体的循环就是观察、决策、行动,反复来。这三件事都是在某个时间点发生过的事实。所以不管有没有人把它写成事件序列,这循环本身就是事件序列。
把它写下来,改变了循环的构成。历史不再是一个变量里存着的列表,而是一条存在进程外部的日志。智能体的状态,就是从开头到某个偏移量的所有事件汇总之后的结果。智能体本身,就是读日志、在末尾追加下一条事实的一个函数。恢复变成了一次读取,而不是重新构建。
Anthropic那个框架里,通过文件交接的方式,暗地里就在干这个。一个智能体写的工件,被下一个智能体读,这就是一条持久、有序的记录,带着状态跨过了上下文重置。原理一样,只是用文件系统代替了消息中间件。在它们那个规模下合情合理,参与方一多就开始吃力了。
附带的好处是,为了可靠性而建的日志,刚好也是做评估需要的数据集。重放一次运行,拿到的是精确的工具调用、参数和结果,而不是事后写的摘要。要搭测试框架时,这些数据往往最难搞到手。
智能体怎么嵌进事件驱动的后端
一旦把智能体看作消费者兼生产者,它就不再是什么特殊零件了。
大家常写的那些多智能体模式,映射到这上面来非常顺畅。编排器加工人模式,就变成一个带键值的主题加一个消费者组。编排器按键值分发工作,永远不需要管理跟任何单个工人的连接,所以工人可以随便增删,编排器压根不知道。层级结构就是同样的安排递归应用,每个非叶节点的智能体编排自己的子树。黑板模式,就是多个智能体往共享工作区里写东西而不直接喊话,这对应一个主题,好几个智能体同时往里面写也同时从里面读。
人工审批,在一问一答模式下最头疼的情况,在这里变得不值一提。智能体写一条智能体.审批.需要,然后自己停掉。几小时后有人给答复,一个服务写一条智能体.审批.通过,一个工人从停掉的地方接着跑。什么都没一直挂着,什么都没丢。
python
consumer = Consumer({
"bootstrap.servers": "localhost:9092",
"group.id": "执行器-智能体群",
"auto.offset.reset": "earliest",
"enable.auto.commit": False, # 工作持久化之后才提交偏移量
})
# ...
if 已处理(任务编号, 事件["步骤编号"]): # 重放时去重
consumer.commit(msg)
continue
结果 = 执行工具(事件["工具"], 事件["参数"])
producer.produce(
"智能体.工具.返回",
key=任务编号.encode(), # 同一个键让同一个任务的事件保持顺序
value=json.dumps({...}).encode(),
)
producer.flush()
consumer.commit(msg)
这里面有两点比别的都重要。自动提交关掉了,所以偏移量只有在结果安全写入之后才往前推进。事件按任务编号做键值,所以同一个任务的所有事情互相之间保持顺序。我在智能体系统里见过的重复操作和状态乱序的bug,几乎都能追溯到这两点之一。
如果是从一个能用的同步系统开始迁移,我试过管用的路子很老套。给每次运行分配一个任务编号并把它存下来,把循环从请求处理器里挪到带队列的工人里,每一步都写成一行记录而不是一个变量,每个工具调用都按任务编号和步骤编号做成幂等的。做完这些,再去考虑数据量和消费者数量是不是值得上消息中间件,因为对很多团队来说,答案老实说是否定的。
收尾的话
这篇文章有个版本会说,Kafka因为AI迎来了第二春。我觉得那不是最有用的读法。最有用的读法是,AI这边正在撞上分布式系统那边已经琢磨了二十年的事儿,而且是从一个不寻常的方向撞上来的——它们是从聊天框起步的,而不是从事务日志起步的。
长时智能体里最难的部分,难的不是提示词。
难的是顺序、幂等性、部分故障、背压、状态交接、可观测性。
换句话说,是那些一直很难、现在正被更多人撞上的那部分活儿。
如果正在搭的智能体跑起来比一个请求该占的时间还长,下一步值得做的不是更好的提示词。是把你智能体的事件到底是什么写下来,然后看看这一件事就替你的架构决定了多少东西。
AI的活已经从秒级干到小时级,传输通道却还停在对话时代。真正难的不是写提示词,是把顺序、幂等、故障、状态交接这些老问题用新方式理顺。把那串事件写下来,架构自己会说话。