ai agent --- 异步处理之 Rabbit MQ

0 阅读6分钟

一.什么是MQ

1.概念

MQ(Message Queue,消息队列) 是异步消息中间件, 它的本质就是一个中间件,用来在两段代码之间异步传递消息。

它的核心 就是生产者将消息发给队列排队,消费者从队列里面去拿订单,进行处理。

  1. 生产者:发消息丢到队列,不用等对方立刻处理,发完你去忙别的(异步解耦)。
  2. 队列:消息的缓冲区,消息存放在这里。
  3. 消费者:从队列拉 / 推消息,执行业务。

比如我们在蜜雪冰城点奶茶,你是生产者,你下单后,你就等着,大屏幕上有你的排队信息,那个就是队列。蜜雪冰城的工作人员是消费者,他会按照队列里面的订单帮我们调制饮料。

这个过程就非常像MQ,我们下单后,不用催促店员赶紧做,也不用担心店员不做,我们知道,只要店员叫号,过去拿奶茶就好。

2.核心能力

MQ的核心能力是:解耦、削峰、异步,可靠投递(可靠性)。 image.png

  • 解耦 —— 就是我们只管下单,不管最后到底是哪位服务员在给我生产奶茶。
  • 削峰——就是奶茶高峰时段,大家只管点奶茶,点好后你自己等着,工作人员按照自己的工作节奏和点餐顺序一个一个生产。
  • 异步——就是我们点完餐后不用一直站在推台边盯着催促,你可以去玩手机干其他事情。
  • 可靠投递——就是你点的奶茶订单不会无故消失。

3.常见产品

常见 MQ 产品:RabbitMQ、Kafka、RocketMQ、ActiveMQ、Pulsar、Redis Streams。 选型一句话:

  • 业务任务、复杂路由选 RabbitMQ;
  • 日志、海量数据流选 Kafka;
  • RocketMQ 擅长事务、延迟消息;
  • Redis Streams 适合轻量小流量场景。

二.RabbitMQ 是什么

1.概念

RabbitMQ 是基于 AMQP 协议实现的消息队列,用 Erlang 开发。它是一个独立的服务,我们在docker里面

2.特点

特点:

✅ 协议标准、可靠性强(支持持久化、ACK 确认)

✅ 灵活路由(Exchange 交换机是核心,Kafka 没有这个设计)

✅ 支持多种模式:简单队列、工作队列、发布订阅、路由、主题 topic

❌ 高吞吐不如 Kafka,适合业务消息(订单、任务),不适合海量日志流

3.RabbitMQ 核心组件

  1. Exchange 交换机:接收生产者消息,根据路由规则分发到队列

  2. Queue 队列:存放消息,绑定在 Exchange 上

  3. Binding 绑定:Exchange ↔ Queue 的关联关系,带 routing key

  4. Virtual Host(vhost) :虚拟隔离环境,类似数据库,多业务隔离

  5. ACK 机制:消费者处理完消息,手动 ack 告诉 rabbitmq;没 ack 消息会重新入队,防止丢消息

他的核心模型是:

生产者(Producer) → Exchange(交换机) → Binding(绑定规则) → Queue(队列) → 消费者(Consumer)
  • Producer:发消息的一方
  • Exchange:不直接存消息,只负责按路由规则把消息分发给队列(像路由器)
  • Queue:真正存消息的地方,消息消费后会被删除
  • Binding:交换机和队列之间的绑定关系(含 routing key)
  • Consumer:从队列取消息处理的一方

4.交换机的四种类型:

-   direct:精确匹配 routing key
-   fanout: 广播,忽略 routing key,绑定的队列全发
-   topic:模糊匹配(最常用)
-   headers:header 匹配,很少用

  1. 生产者 publish:填交换机名称、routingKey,消息交给交换机,由服务端根据交换机类型做路由分发
  2. 交换机类型(direct/topic/fanout/headers):在 assertExchange() 指定(两边任意一方执行都行,一般消费者写)
  3. 消费者 bindQueue:队列绑定交换机,设置绑定 key。服务端根据交换机类型 + routingKey + bindingKey 判断消息投递到哪个队列。

4.1举例topic的使用方式

// 消费者/启动阶段:声明topic交换机(这里定类型!!)
await channel.assertExchange('agent_topic_ex', 'topic', {durable:true})
await channel.bindQueue('rag_queue', 'agent_topic_ex', 'agent.rag.#')

// 生产者:只负责往交换机发消息,不需要知道是topic类型
channel.publish('agent_topic_ex', 'agent.rag.run', body)

topic的匹配规则

* 是匹配任意一个段 # 是匹配0 到任意个段

在消费者里面,确定topic类型以后,就要确定他的匹配规则:agent.rag.#,他的意思是找前缀是agent.rag.的routingKey。

那什么是 routingKey?就是生产者里面publish里面的第二个参数agent.rag.run

生产者使用routingKey

channel.publish(
  EXCHANGE_NAME,
  'agent.rag.run',   // ✅这里就是 routingKey
  Buffer.from(JSON.stringify(payload)),
  { persistent: true, messageId: taskId }
);

消费者使用routingKey

await channel.bindQueue(QUEUE_NAME, EXCHANGE_NAME, 'agent.rag.#');
//                                                    ↑ bindingKey

topic 匹配过程:

  • 消息 routingKey:agent.rag.run
  • 队列 bindingKey:agent.rag.# 👉 #匹配后面的run,匹配成功,消息路由进rag_queue。

4.如何使用它

4.1.安装

在docker上执行命令

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 rabbitmq:3-management

上面这个命令有2个作用:

  1. 如果本地没有 rabbitmq:3‑management 镜像 → 自动从 docker hub 拉取下载镜像
  2. 下载完成后直接创建容器、后台 -d 启动 RabbitMQ。

4.2 验证是否启动成功

  1. 浏览器打开 http://localhost:15672,输入账号密码登录,能进界面 = 服务正常。
  2. node 代码连接字符串:amqp://admin:123456@localhost:5672

4.3在代码里面连接RabbitMQ

连接RabbitMQ,Node 官方标准库:**amqplib**

在 Agent 里面,执行 LLM 长任务、文档解析、多工具调用,不能阻塞 HTTP 请求等,都要用到 RabbitMQ;接口收到请求,投递任务到 MQ,Agent Worker 独立进程消费 MQ 任务。

npm install amqplib

生产者代码如下:

const amqp = require('amqplib');
const crypto = require('crypto');

const RABBIT_URL = 'amqp://localhost:5672';
const EXCHANGE_NAME = 'agent_topic_ex';

async function publishAgentTask(taskData) {
  const connection = await amqp.connect(RABBIT_URL);
  const channel = await connection.createChannel();

  // 幂等:不存在就创建topic交换机,已存在仅校验
  await channel.assertExchange(EXCHANGE_NAME, 'topic', { durable: true });

  const taskId = crypto.randomUUID();
  const payload = {
    taskId,
    prompt: taskData.prompt,
    agentType: 'rag_agent',
    params: taskData.params,
    createAt: Date.now()
  };

  channel.publish(
    EXCHANGE_NAME,
    'agent.rag.run', // routingKey
    Buffer.from(JSON.stringify(payload)),
    { persistent: true, messageId: taskId }
  );

  console.log(`任务已投递 taskId: ${taskId}`);

  await channel.close();
  await connection.close();
  return taskId;
}

// 测试发送
publishAgentTask({
  prompt: "分析这份文档",
  params: { fileId: "file_123" }
}).catch(console.error);

作为生产者,我们只需要将任务投递到队列里面就好了。

  1. worker 启动:创建agent_topic_ex(topic 类型交换机),创建rag_queue队列,执行bindQueue绑定,绑定键agent.rag.#。
  2. 生产者发送消息,routingKey=agent.rag.run。
  3. RabbitMQ(Broker)根据topic 模糊匹配规则:agent.rag.run匹配绑定键agent.rag.#,消息路由投递到rag_queue队列。
  4. worker 消费rag_queue队列,拿到消息执行 Agent。

消费者代码如下:

const amqp = require('amqplib');

async function runAgent(payload) {
  console.log(`Agent开始执行任务:${payload.taskId}`);
  // 模拟LLM耗时
  await new Promise(resolve => setTimeout(resolve, 3000));
  return { answer: `Agent结果:${payload.prompt}` };
}

async function startAgentWorker() {
  const connection = await amqp.connect('amqp://localhost:5672');
  const channel = await connection.createChannel();

  const EXCHANGE_NAME = 'agent_topic_ex';
  const QUEUE_NAME = 'rag_queue';

  // 定义topic交换机
  await channel.assertExchange(EXCHANGE_NAME, 'topic', { durable: true });

  // 声明仲裁队列
  await channel.assertQueue(QUEUE_NAME, {
    durable: true,
    arguments: { 'x-queue-type': 'quorum' }
  });

  // 队列绑定到topic交换机,bindingKey:agent.rag.#
  await channel.bindQueue(QUEUE_NAME, EXCHANGE_NAME, 'agent.rag.#');

  channel.prefetch(1);
  console.log('Agent Worker已启动,等待任务...');

  channel.consume(QUEUE_NAME, async (msg) => {
    if (!msg) return;
    try {
      const task = JSON.parse(msg.content.toString());
      const result = await runAgent(task);
      console.log('Agent任务完成:', result);
      channel.ack(msg); // 成功确认
    } catch (err) {
      console.error('Agent执行失败', err);
      channel.nack(msg, false, false);
    }
  });
}

startAgentWorker().catch(err => {
  console.error('worker启动失败', err);
});

消费者的任务就是从消息队列里面拿任务,然后运行就好了。

  1. channel.assertExchange(EXCHANGE_NAME, 'topic', {durable:true}):在这里指定交换机类型 topic,幂等,存在就跳过。
  2. channel.bindQueue(队列名, 交换机名, bindingKey):建立队列与交换机的绑定关系,设置绑定键。
  3. 绑定键 agent.rag.#:#代表匹配后面任意字符。
# 1. 启动worker(单独进程,常驻后台)
node worker.js

# 2. 新开终端,运行生产者提交任务
node producer.js