事件驱动的 Agent 运行时
事件驱动的 Agent 运行时
学习目标
读完本章后,你将能够:
- 理解事件驱动架构与函数调用式 Agent 的根本区别
- 设计基于发布/订阅的 Agent 间通信机制
- 实现灵活的订阅系统实现 Agent 到 Topic 的路由映射
- 区分 RPC(请求-响应)和 pub/sub(广播)两种消息模式
- 评估单线程与分布式运行时的适用场景
核心概念
运行时(Runtime)的职责
事件驱动的 Agent 运行时是整个系统的中枢,承担以下职责:
- 消息路由:将消息从发送者传递到接收者
- 生命周期管理:Agent 注册、启动、关闭
- 消息序列化:不同 Agent 间的消息格式转换
- 订阅管理:维护 Topic 到 Agent 的映射关系
两种消息模式
| 模式 | 方法 | 语义 | 返回值 | 适用场景 |
|---|---|---|---|---|
| RPC | send_message | 点对点请求-响应 | 有(Response) | 需要获取确定结果 |
| Pub/Sub | publish_message | 一对多广播 | 无(Fire-and-forget) | 事件通知、状态广播 |
RPC 模式适合需要明确返回值的场景,例如询问某个 Agent 获取处理结果。调用方会等待响应或异常。
Pub/Sub 模式适合一对多通知场景,例如一个 Agent 完成工作后通知所有订阅者。发送方不等待响应。
订阅系统
订阅系统是事件驱动架构的核心创新点。它定义了 Agent 对哪些 Topic 感兴趣,以及消息如何路由到正确的 Agent。
三种订阅策略:
| 订阅类型 | 匹配规则 | 路由策略 | 使用场景 |
|---|---|---|---|
TypeSubscription | 精确匹配消息类型 | 单 Agent | 专用处理器 |
DefaultSubscription | 匹配默认 Topic | 单 Agent | 简单一对一通信 |
TypePrefixSubscription | 匹配类型前缀 | 多 Agent | 一组相关消息的路由 |
TypePrefixSubscription(带 topic) | 匹配 Topic 前缀 | 多 Agent | 命名空间分组 |
消息处理上下文
每个消息处理调用都会附带一个 MessageContext,提供:
- sender:消息发送者的 Agent ID
- topic_id:消息发布的 Topic
- is_rpc:是否为 RPC 调用
- cancellation_token:取消令牌,支持异步取消
Agent 类型
| 类型 | 特点 | 使用场景 |
|---|---|---|
RoutedAgent | 基于装饰器的消息路由 | 通用 Agent,需灵活处理多种消息 |
ClosureAgent | 轻量级闭包 Agent | 简单的一次性处理逻辑 |
BaseAgent | 最底层抽象 | 自定义 Agent 基类 |
RoutedAgent 使用装饰器标记消息处理方法:
@message_handler:通用处理器,处理事件和 RPC@event:仅处理事件(pub/sub),无返回值@rpc:仅处理 RPC,必须返回值
问题与规避
单线程运行时的并发限制
单线程运行时(SingleThreadedAgentRuntime)使用 asyncio 事件循环,所有 Agent 处理在同一条线程中执行。
陷阱:如果某个 Agent 的处理方法中包含阻塞操作(如同步 I/O),会阻塞整个运行时。
规避策略:
- 所有耗时操作必须使用
async方法 - 使用
asyncio.to_thread()包装同步阻塞调用 - 对于高吞吐场景,考虑切换到分布式运行时
消息队列背压
当消息生产速率超过消费速率时,队列会持续增长。
规避策略:
- 监控队列深度,设置告警阈值
- 使用有界队列(
maxsize参数)配合背压策略 - 对低优先级消息使用
publish_message(无响应等待),减少队列压力
订阅冲突与消息重复消费
多个订阅可能匹配同一 Topic,导致消息被多次处理。
规避策略:
- 使用
TypeSubscription而非TypePrefixSubscription当需要精确路由时 - 在消息中添加幂等 ID,防止重复处理的副作用
设计取舍
为什么选择事件驱动而非直接调用?
优势:
- 解耦:Agent 间不需要互相引用,降低耦合度
- 可扩展:新增 Agent 只需注册订阅,无需修改现有代码
- 灵活性:同一消息可被多个 Agent 处理,支持多播
代价:
- 调试复杂度:消息流转路径不直观,需要专门的追踪工具
- 性能开销:消息序列化、路由匹配引入额外延迟
替代方案对比
| 方案 | 解耦度 | 调试难度 | 多播支持 | 性能 |
|---|---|---|---|---|
| 直接函数调用 | 低 | 低 | 不支持 | 最高 |
| 事件驱动 pub/sub | 高 | 高 | 支持 | 中等 |
| gRPC 同步调用 | 中 | 中 | 不支持 | 高 |
| 消息队列(RabbitMQ) | 高 | 高 | 支持 | 中等(网络开销) |
参考来源
- Actor Model — 事件驱动并发计算模型的理论基础
- AutoGen Core API Documentation — 运行时 API 参考
补充:LlamaIndex 的 Workflow 事件驱动状态机
来源:LlamaIndex llama-index-core v0.14.22 + llama-index-workflows PyPI 包
LlamaIndex 的 Agent 循环基于一个事件驱动状态机实现,与传统的 while True 循环有本质区别。
@step 装饰器构建事件流图
每个 Agent 的方法用 @step 装饰器标记,引擎通过静态分析函数签名自动推断该步骤接受什么事件、产生什么事件:
@stepasync def run_agent_step(self, ctx: Context, ev: AgentSetup) -> AgentOutput: return await self.take_step(ctx, ev)@step 的本质是定义事件流图的边:输入事件类型 → 处理 → 输出事件类型。引擎将所有 @step 方法组装成一个有向图,事件沿着图的边流动。
并行工具调用的等待-聚合模式
LlamaIndex 通过 ctx.collect_events() 实现并行工具调用的结果聚合:
当 Agent 需要调用多个工具时,所有工具调用并行发出,collect_events 等待全部结果到达后才进入聚合步骤。这避免了传统循环中工具串行调用的延迟累积。
Context.store 的状态管理
Context 对象提供一个 KV 存储 ctx.store,用于管理 Agent 的运行时状态:
memory:对话历史(列表)state:自定义状态字典max_iterations:最大迭代次数iteration_count:当前迭代计数器
设计优势:与简单 while 循环相比,事件驱动架构天然支持并发工具调用、状态持久化(Context 可序列化)、和可观测性(每步状态推送给订阅者)。