事件驱动的 Agent 运行时
事件驱动的 Agent 运行时
学习目标
本章要解决什么问题:AutoGen Core API 如何实现 Agent 之间的解耦通信。你将学到:
SingleThreadedAgentRuntime的消息队列和路由机制- 三种订阅策略的使用场景
send_message(RPC)与publish_message(pub/sub)的选择依据RoutedAgent装饰器模型的签名
前置知识
本章涉及事件驱动 Agent 运行的通用原理,建议先阅读:
下文假设你已理解 pub/sub 架构和订阅系统的基本概念,直接聚焦 AutoGen 的具体实现。
项目实践
SingleThreadedAgentRuntime 的消息循环
AutoGen Core 的运行时基于 asyncio 实现单线程事件循环。核心数据结构是三个消息信封:
PublishMessageEnvelope — 用于 publish_message(一对多广播)SendMessageEnvelope — 用于 send_message(点对点 RPC)ResponseMessageEnvelope — 用于返回 RPC 响应消息循环的工作方式:
关键实现细节:
- 每个 Agent 有独立的消息队列
- 消息通过
Queue异步传递,而非直接调用 - 序列化注册表(
SerializationRegistry)在发送端序列化、接收端反序列化
订阅策略实战
AutoGen 提供三种订阅策略,通过装饰器或注册方法绑定到 Agent:
# 方式一:装饰器注册(推荐)@default_subscriptionclass MyAgent(RoutedAgent): ...
@type_subscription("chat")class ChatAgent(RoutedAgent): ...
# 方式二:手动注册订阅await runtime.register_factory("my-agent", MyAgent)await runtime.add_subscription(TypeSubscription("chat", "my-agent"))| 策略 | 匹配规则 | 适用场景 |
|---|---|---|
@default_subscription | 匹配默认 Topic | 简单的点对点通信 |
@type_subscription("type") | 精确匹配消息类型 | 专用消息处理器 |
TypePrefixSubscription | 匹配类型或 Topic 前缀 | 一组相关消息 |
RoutedAgent 装饰器
RoutedAgent 是 AutoGen Core 中最常用的 Agent 基类,使用三个装饰器区分消息处理类型:
class MyAgent(RoutedAgent): @message_handler async def handle_any(self, message: MyMessage, ctx: MessageContext) -> Response: # 处理事件和 RPC ...
@event async def handle_event(self, message: MyEvent, ctx: MessageContext) -> None: # 仅处理 pub/sub 事件,无返回值 ...
@rpc async def handle_rpc(self, message: MyRequest, ctx: MessageContext) -> Response: # 仅处理 RPC,必须返回值 ...装饰器的核心作用:将方法签名(消息类型 → 返回类型)注册到运行时的路由表中,使得运行时可以根据消息类型自动选择处理方法。
RPC vs Pub/Sub 的选择
# RPC:需要返回值response = await runtime.send_message( message=MyRequest(data="..."), recipient=AgentId("my-agent", "default"),)
# Pub/Sub:广播通知,无返回值await runtime.publish_message( message=MyEvent(status="done"), topic_id=DefaultTopicId(),)选择依据:
- 需要获取 Agent 处理结果 →
send_message(RPC) - 通知多个 Agent 某事发生 →
publish_message(pub/sub) - 不关心谁处理了消息 →
publish_message
问题与规避
单线程运行时的阻塞陷阱
AutoGen Core 默认使用 SingleThreadedAgentRuntime,所有 Agent 共享同一个 asyncio 事件循环。
陷阱:如果在消息处理方法中执行同步阻塞操作(如 time.sleep()、同步 HTTP 请求),会阻塞整个运行时,其他 Agent 无法处理消息。
规避:
- 所有 I/O 操作必须使用
async版本 - 必须调用同步代码时使用
asyncio.to_thread() - 对于高吞吐场景,考虑使用
autogen-ext中的 GRPC 分布式运行时
消息序列化的性能开销
每个消息在发送端序列化、接收端反序列化。对于高频消息(如每秒数千条),序列化开销可能显著。
规避:
- 使用 Protobuf 而非 JSON 序列化(通过
PROTOBUF_DATA_CONTENT_TYPE) - 减少消息体大小,只传递必要的引用而非完整数据
设计取舍
为什么用 asyncio 队列而非线程池?
优势:
- 无锁设计:单线程避免了锁竞争的复杂度
- 内存效率:不需要为每个消息创建新线程
- 调试友好:调用栈连续,易于追踪
代价:
- 无法利用多核 CPU
- 阻塞操作会冻结整个运行时
与直接函数调用的对比
| 维度 | 直接调用 | AutoGen 事件驱动 |
|---|---|---|
| 代码行数 | 少 | 多(需要注册、订阅) |
| 解耦度 | 无(硬编码依赖) | 高(通过 Topic 解耦) |
| 多播 | 不支持 | 天然支持 |
| 调试 | 简单 | 需要追踪消息流 |
AutoGen 选择事件驱动的动机是支持多 Agent 协作场景中的灵活消息路由,而非追求最小的代码量。
参考来源
- AutoGen Core API Reference — 运行时 API 文档
- 源码验证:
autogen-core/_single_threaded_agent_runtime.py(消息循环实现) - 源码验证:
autogen-core/_routed_agent.py(装饰器路由)