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;        // 全局唯一的任务 ID,UUID 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: Option<String>,
}

/// 核心的 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,
    /// 目标 agent(None 表示所有 agent 均可消费)
    pub target: Option<AgentRole>,
    /// 具体载荷
    pub payload: TaskPayload,
    /// 发送时间戳,用于超时检测和延迟监控
    pub sent_at: i64,
    /// 存活时间(秒),超时后消息自动作废
    pub ttl_seconds: i32,
}

impl AgentMessage {
    /// 工厂方法:快速创建一个新的 Agent 消息
    pub fn new(
        source: AgentRole,
        target: Option<AgentRole>,
        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
        }
    }
}

三、基于 Lapin(AMQP)的消息队列集成实现

选型上,我对比过 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::Sender<AgentMessage>, // 结果消息投递通道
}

impl MessageBus {
    /// 连接到 RabbitMQ 并初始化交换机和队列拓扑
    pub async fn connect(amqp_url: &str) -> Result<Self> {
        // 建立 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_tasks<F>(&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) -> Result<Self> {
        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) -> Result<Vec<String>> {
        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, Vec<String>) =
                conn.scan_match(cursor, "heartbeat:*").await?;

            for key in keys {
                // 读取该心跳键的时间戳
                let timestamp: Option<i64> = 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 系统中遇到的实际坑。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐