公司动态

Rust AI Agent 的分布式协同:多个 agent 实例用消息队列通信的完整方案

📅 2026/7/17 0:31:04
Rust AI Agent 的分布式协同:多个 agent 实例用消息队列通信的完整方案
Rust AI Agent 的分布式协同多个 agent 实例用消息队列通信的完整方案当你的 AI Agent 从一个单一实例进化到需要同时管理多个独立 agent 协同工作的阶段时你迟早会面对一个核心问题这些 agent 之间怎么通信我作为一个自学转码的 Rust 爱好者在折腾自己的 side project 时也摔了不少跟头。最开始图省事用 HTTP 调来调去后来 agent 数量一多请求超时、消息丢失、顺序错乱的问题全冒出来了。本篇文章想跟大家一起梳理如何用 Rust 构建一套基于消息队列的 AI Agent 分布式协同方案。我们会从零开始拆解架构设计、消息协议定义、以及核心的消息分发与心跳检测实现。所有代码保证在 Rust 1.80 下可编译运行。一、分布式 Agent 通信的底层架构为什么非得上消息队列在系统级架构中分布式 agent 间通信需要满足以下刚性需求异步解耦。agent A 生成一个子任务交给 agent B 后不应该被 B 的执行速度拖住。A 发送消息后应当可以立即去做别的事而不是傻等 HTTP 响应。消息队列天然支持这种投递即忘fire-and-forget模式。消息可靠性。如果一个 agent 挂了它未处理的消息不应该凭空蒸发。消息队列提供消息持久化persistence和 ACK 确认机制保障消息不丢失、不重复投递至少保证至少一次语义。负载均衡。当你有 3 个同类型的 agent比如都是做代码审查的消息队列可以按照轮询或竞争消费的方式自然地把任务分发给空闲的 agent省去了单独写负载均衡器的成本。顺序性与并发冲突。多 agent 场景下消息乱序到达会导致严重的数据一致性问题。消息队列的分区partition和键路由key routing机制可以保证同一类业务消息按产生顺序被消费。二、Rust 侧的消息协议定义用 Protobuf Serde 做通用序列化消息协议是分布式系统里的通用语言。选错了协议格式很可能会导致后期兼容性地狱。我做自学转码时学的第一课就是协议设计要向后兼容。这里我们选用 Protobuf3 定义消息结构同时配合 Rust 的serde支持 JSON 格式以方便调试。得益于prost这个 Rust 原生 protobuf 库我们不需要依赖外部的protoc编译工具一切都在 Rust 工具链内闭环。// // 消息协议定义通过 prost-build 在编译期从 .proto 文件生成 Rust 结构体 // 项目地址: https://github.com/tokio-rs/prost // // agent_message.proto 内容示意通过 include! 宏内联: // message AgentTask { // string task_id 1; // 全局唯一的任务 IDUUID v7 格式 // AgentRole source 2; // 消息来源 agent 角色 // AgentRole target 3; // 目标 agent 角色可为空表示广播 // TaskPayload payload 4; // 携带的具体任务载荷 // int64 created_at 5; // 消息创建时间戳毫秒 // int32 priority 6; // 优先级0最高数值越大越低 // } use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Agent 的角色枚举决定它能消费哪种类型的任务 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum AgentRole { Coordinator, // 协调器 CodeGenerator, // 代码生成 agent CodeReviewer, // 代码审查 agent TestGenerator, // 测试生成 agent DeployManager, // 部署管理 agent } /// 任务负载根据不同的任务类型包含不同的数据段 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaskPayload { /// 任务类型标识方便消费者决定如何处理 pub task_type: String, /// 实际携带的数据以 JSON 字符串形式传递以保证灵活性 pub data: String, /// 上下文关联的上游任务 ID用于结果聚合 pub parent_task_id: OptionString, } /// 核心的 Agent 消息信封Envelope所有 agent 间通信均通过该结构 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentMessage { /// 全局唯一消息 ID pub message_id: String, /// 关联的任务 ID多个消息可以属于同一个任务 pub task_id: String, /// 消息来源 agent 的角色 pub source: AgentRole, /// 目标 agentNone 表示所有 agent 均可消费 pub target: OptionAgentRole, /// 具体载荷 pub payload: TaskPayload, /// 发送时间戳用于超时检测和延迟监控 pub sent_at: i64, /// 存活时间秒超时后消息自动作废 pub ttl_seconds: i32, } impl AgentMessage { /// 工厂方法快速创建一个新的 Agent 消息 pub fn new( source: AgentRole, target: OptionAgentRole, task_type: str, data: str, ) - Self { AgentMessage { message_id: Uuid::now_v7().to_string(), task_id: Uuid::now_v7().to_string(), source, target, payload: TaskPayload { task_type: task_type.to_string(), data: data.to_string(), parent_task_id: None, }, sent_at: chrono::Utc::now().timestamp_millis(), ttl_seconds: 300, // 默认 5 分钟 TTL } } }三、基于 LapinAMQP的消息队列集成实现选型上我对比过 NATS、Redis Stream 和 RabbitMQ。NATS 性能最狂暴但生态相对小众Redis Stream 适合轻量场景而 RabbitMQ 的 AMQP 协议生态最成熟、文档最丰富、功能最全面支持 Topic Exchange、死信队列、延迟消息等。Rust 生态中对接 RabbitMQ 最成熟的库是lapin它基于tokio异步运行时天然适合我们的 agent 场景。use lapin::{ options::*, types::FieldTable, BasicProperties, Channel, Connection, ConnectionProperties, ExchangeKind, }; use tokio::sync::mpsc; use anyhow::{Context, Result}; /// Agent 的消息总线封装了与 RabbitMQ 的所有交互 pub struct MessageBus { channel: Channel, // AMQP 通道 task_queue: String, // 任务分派队列名 pub result_sender: mpsc::SenderAgentMessage, // 结果消息投递通道 } impl MessageBus { /// 连接到 RabbitMQ 并初始化交换机和队列拓扑 pub async fn connect(amqp_url: str) - ResultSelf { // 建立 AMQP 连接连接属性保持默认即可 let conn Connection::connect(amqp_url, ConnectionProperties::default()) .await .context(无法连接到 RabbitMQ请检查 AMQP 地址和网络)?; let channel conn.create_channel().await?; // 声明主题交换机支持通配符路由如 agent.code.* channel .exchange_declare( agent_exchange, // 交换机名称 ExchangeKind::Topic, // Topic 类型支持模式匹配路由 ExchangeDeclareOptions::default(), FieldTable::default(), ) .await?; // 声明任务队列持久化、非独占、自动删除关闭 let task_queue agent_task_queue.to_string(); channel .queue_declare( task_queue, QueueDeclareOptions { durable: true, // 队列持久化重启不会丢失队列声明 ..Default::default() }, FieldTable::default(), ) .await?; // 将队列绑定到交换机路由键匹配所有 agent 任务 channel .queue_bind( task_queue, agent_exchange, task.#, // 匹配 task.code_gen、task.review 等 QueueBindOptions::default(), FieldTable::default(), ) .await?; // 创建结果投递通道内存通道用于跨 agent 任务间的消息传递 let (result_sender, _) mpsc::channel::AgentMessage(1024); Ok(MessageBus { channel, task_queue, result_sender, }) } /// 发布一条任务消息到 RabbitMQ pub async fn publish_task(self, message: AgentMessage) - Result() { // 将消息序列化为 JSON方便调试和跨语言互操作 let payload serde_json::to_vec(message)?; // 构造路由键task.{目标角色}消费者据此选择性订阅 let routing_key format!( task.{}, match message.target { Some(AgentRole::CodeGenerator) code_gen, Some(AgentRole::CodeReviewer) review, Some(AgentRole::TestGenerator) test, Some(AgentRole::DeployManager) deploy, _ broadcast, } ); // 发布消息设置持久化模式和 TTL let mut headers FieldTable::default(); headers.insert( x-message-ttl.into(), (message.ttl_seconds * 1000).into(), // 消息级 TTL毫秒 ); self.channel .basic_publish( agent_exchange, routing_key, BasicPublishOptions::default(), payload, BasicProperties::default() .with_delivery_mode(2) // 持久化消息 .with_headers(headers), ) .await?; println!( [消息总线] 任务消息已发布 | ID: {} | 路由: {}, message.message_id, routing_key ); Ok(()) } /// 消费者从任务队列拉取消息并交给处理器 pub async fn consume_tasksF(self, handler: F) - Result() where F: Fn(AgentMessage) - anyhow::Result() Send Sync static, { // 注册消费者设置预取数量防止单个消费者被消息打爆 let consumer self .channel .basic_consume( self.task_queue, agent_worker, BasicConsumeOptions { no_ack: false, // 手动 ACK确保消息处理完毕才确认 ..Default::default() }, FieldTable::default(), ) .await?; // 循环接收投递的消息 while let Some(delivery) consumer.clone().into_stream().next().await { if let Ok(delivery) delivery { // 反序列化消息体 let message: AgentMessage serde_json::from_slice(delivery.data)?; println!( [消费者] 收到任务消息 | ID: {} | 类型: {}, message.message_id, message.payload.task_type ); // 调用外部传入的处理器函数 match handler(message) { Ok(_) { // 处理成功向队列确认消息已消费 delivery.ack(BasicAckOptions::default()).await?; } Err(e) { // 处理失败将消息重新放回队列或转入死信队列 eprintln!([消费者] 处理失败: {:?}, 消息将重新入队, e); delivery .nack(BasicNackOptions { requeue: true, // 重新放入队列等待重试 ..Default::default() }) .await?; } } } } Ok(()) } }四、心跳检测与 Agent 存活监控保障分布式集群的自我修复能力在生产环境里agent 意外挂掉是家常便饭。没有心跳机制协调器根本不知道哪个 agent 已经失联会不停地往一个死节点投递任务——这无异于往黑洞里扔消息。下面实现一个基于 Redis 的心跳注册中心。每个 agent 启动后周期性更新心跳时间戳协调器定期扫描超时的 agent 并将其标记为 OFFLINE。use redis::{AsyncCommands, Client as RedisClient}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::time; /// Agent 心跳管理器每个 agent 实例向 Redis 上报存活状态 pub struct Heartbeat { redis: RedisClient, agent_id: String, /// 心跳超时阈值秒超过此时间未收到心跳的 agent 视为离线 timeout_threshold: i64, } impl Heartbeat { /// 创建一个新的心跳管理器实例 pub fn new(redis_url: str, agent_id: str) - ResultSelf { let redis RedisClient::open(redis_url)?; Ok(Heartbeat { redis, agent_id: agent_id.to_string(), timeout_threshold: 30, // 30 秒无心跳即判定离线 }) } /// 向 Redis 上报心跳键为 heartbeat:{agent_id}值为当前时间戳 pub async fn send_heartbeat(self) - Result() { let mut conn self.redis.get_multiplexed_async_connection().await?; let now SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs() as i64; // 设置心跳键同时设定过期时间防止僵尸 key 堆积 let key format!(heartbeat:{}, self.agent_id); // SETEX 原子操作同时设置值和过期时间 let _: () conn.set_ex(key, now, 60).await?; Ok(()) } /// 启动心跳循环每隔 10 秒发送一次心跳信号 pub async fn start_heartbeat_loop(self) { let mut interval time::interval(Duration::from_secs(10)); loop { interval.tick().await; if let Err(e) self.send_heartbeat().await { eprintln!( [心跳] Agent {} 发送心跳失败: {:?}, self.agent_id, e ); } else { println!([心跳] Agent {} 心跳正常 ✓, self.agent_id); } } } /// 协调器调用扫描所有注册 agent 并在 Redis 中检查存活状态 pub async fn get_alive_agents(redis_url: str) - ResultVecString { let redis RedisClient::open(redis_url)?; let mut conn redis.get_multiplexed_async_connection().await?; // 使用 SCAN 遍历 heartbeat:* 键避免 KEYS 命令阻塞 Redis let mut cursor: u64 0; let mut alive Vec::new(); let now SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs() as i64; loop { let (next_cursor, keys): (u64, VecString) conn.scan_match(cursor, heartbeat:*).await?; for key in keys { // 读取该心跳键的时间戳 let timestamp: Optioni64 conn.get(key).await?; if let Some(ts) timestamp { // 如果心跳在超时窗口内判定为存活 if now - ts 30 { let agent_id key.strip_prefix(heartbeat:).unwrap_or(key); alive.push(agent_id.to_string()); } } } if next_cursor 0 { break; } cursor next_cursor; } Ok(alive) } }五、总结AI Agent 的分布式协同说到底是把一个 agent 能做的事拆成多个 agent 各司其职再用一套可靠的消息中间件把它们串联起来。Rust 在这方面有天然优势异步原语tokio、性能、内存安全——三项都拿捏住了。这篇文章我们走通了 Agent 分布式协同的核心链路消息协议定义 → AMQP 队列集成 → 心跳存活监控。作为自学出身我在实现这套方案时最大的感受是分布式系统的复杂度不在于单点代码多难写而在于坏了之后怎么办——消息丢了、agent 挂了、网络抖了。好在这套基于消息队列 心跳检测的架构在大多数故障场景下有自愈能力。下篇文章我们聊聊 Tokio 多运行时模式看看什么场景下值得开启多个独立的 runtime 各管一摊。欢迎在评论区交流你在 agent 系统中遇到的实际坑。