一.什么是MQ
1.概念
MQ(Message Queue,消息队列) 是异步消息中间件 , 它的本质就是一个中间件 ,用来在两段代码之间异步传递消息。
它的核心 就是生产者将消息发给队列排队,消费者从队列里面去拿订单,进行处理。
- 生产者:发消息丢到队列,不用等对方立刻处理,发完你去忙别的(异步解耦)。
- 队列:消息的缓冲区,消息存放在这里。
- 消费者:从队列拉 / 推消息,执行业务。
比如我们在蜜雪冰城点奶茶,你是生产者,你下单后,你就等着,大屏幕上有你的排队信息,那个就是队列。蜜雪冰城的工作人员是消费者,他会按照队列里面的订单帮我们调制饮料。
这个过程就非常像MQ,我们下单后,不用催促店员赶紧做,也不用担心店员不做,我们知道,只要店员叫号,过去拿奶茶就好。
2.核心能力
MQ的核心能力是:解耦、削峰、异步,可靠投递(可靠性) 。 
- 解耦 ------ 就是我们只管下单,不管最后到底是哪位服务员在给我生产奶茶。
- 削峰------就是奶茶高峰时段,大家只管点奶茶,点好后你自己等着,工作人员按照自己的工作节奏和点餐顺序一个一个生产。
- 异步------就是我们点完餐后不用一直站在推台边盯着催促,你可以去玩手机干其他事情。
- 可靠投递------就是你点的奶茶订单不会无故消失。
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 核心组件
-
Exchange 交换机:接收生产者消息,根据路由规则分发到队列
-
Queue 队列:存放消息,绑定在 Exchange 上
-
Binding 绑定:Exchange ↔ Queue 的关联关系,带 routing key
-
Virtual Host(vhost) :虚拟隔离环境,类似数据库,多业务隔离
-
ACK 机制:消费者处理完消息,手动 ack 告诉 rabbitmq;没 ack 消息会重新入队,防止丢消息
他的核心模型是:
js
生产者(Producer) → Exchange(交换机) → Binding(绑定规则) → Queue(队列) → 消费者(Consumer)
- Producer:发消息的一方
- Exchange :不直接存消息,只负责按路由规则把消息分发给队列(像路由器)
- Queue:真正存消息的地方,消息消费后会被删除
- Binding:交换机和队列之间的绑定关系(含 routing key)
- Consumer:从队列取消息处理的一方
4.交换机的四种类型:
diff
- direct:精确匹配 routing key
- fanout: 广播,忽略 routing key,绑定的队列全发
- topic:模糊匹配(最常用)
- headers:header 匹配,很少用
- 生产者 publish:填交换机名称、routingKey,消息交给交换机,由服务端根据交换机类型做路由分发
- 交换机类型(direct/topic/fanout/headers):在
assertExchange()指定(两边任意一方执行都行,一般消费者写) - 消费者 bindQueue :队列绑定交换机,设置绑定
key。服务端根据交换机类型+ routingKey + bindingKey判断消息投递到哪个队列。
4.1举例topic的使用方式
csharp
// 消费者/启动阶段:声明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
csharp
channel.publish(
EXCHANGE_NAME,
'agent.rag.run', // ✅这里就是 routingKey
Buffer.from(JSON.stringify(payload)),
{ persistent: true, messageId: taskId }
);
消费者 使用routingKey
csharp
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上执行命令
js
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个作用:
- 如果本地没有
rabbitmq:3‑management镜像 → 自动从 docker hub 拉取下载镜像 - 下载完成后直接创建容器、后台
-d启动 RabbitMQ。
4.2 验证是否启动成功
- 浏览器打开
http://localhost:15672,输入账号密码登录,能进界面 = 服务正常。 - node 代码连接字符串:
amqp://admin:123456@localhost:5672
4.3在代码里面连接RabbitMQ
连接RabbitMQ,Node 官方标准库:**amqplib**
在 Agent 里面,执行 LLM 长任务、文档解析、多工具调用,不能阻塞 HTTP 请求等,都要用到 RabbitMQ ;接口收到请求,投递任务到 MQ,Agent Worker 独立进程消费 MQ 任务。
js
npm install amqplib
生产者代码如下:
javascript
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);
作为生产者,我们只需要将任务投递到队列里面就好了。
- worker 启动:创建
agent_topic_ex(topic 类型交换机),创建rag_queue队列,执行bindQueue绑定,绑定键agent.rag.#。 - 生产者发送消息,
routingKey=agent.rag.run。 - RabbitMQ(Broker)根据topic 模糊匹配规则 :
agent.rag.run匹配绑定键agent.rag.#,消息路由投递到rag_queue队列。 - worker 消费
rag_queue队列,拿到消息执行 Agent。
消费者代码如下:
javascript
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);
});
消费者的任务就是从消息队列里面拿任务,然后运行就好了。
channel.assertExchange(EXCHANGE_NAME, 'topic', {durable:true}):在这里指定交换机类型 topic,幂等,存在就跳过。channel.bindQueue(队列名, 交换机名, bindingKey):建立队列与交换机的绑定关系,设置绑定键。- 绑定键
agent.rag.#:#代表匹配后面任意字符。
bash
# 1. 启动worker(单独进程,常驻后台)
node worker.js
# 2. 新开终端,运行生产者提交任务
node producer.js