我叫老王,在一家中型电商公司做后端架构。上个月我们上线了一套基于 RAG(检索增强生成)的智能客服系统,本以为能解放客服团队 80% 的人工答疑压力,结果首周就被并发问题狠狠教训了一顿——大促预热期间,用户的 AI 问答请求像潮水一样涌来,后端服务直接被打成筛子,API 超时、数据库连接池耗尽、内存溢出轮番上演。

痛定思痛,我决定给系统引入异步任务队列。经过两周的调研和落地,最终选择了 Redis + BullMQ 的方案,配合 HolySheep AI 的高性价比模型接口,整个系统现在能稳定扛住每秒 500+ 的并发查询,平均响应延迟从之前的 8 秒降到了 1.2 秒。

这篇文章我会把整个方案从设计思路到代码实现完整分享出来,包含踩过的坑和排查经验,希望能帮到有类似需求的开发者。

为什么 RAG 系统需要异步任务队列

先说说我们之前踩的坑。最初的架构很简单:用户发起查询 → 后端同步调用 AI API → 返回结果。听起来没毛病,但有三个致命问题:

引入异步任务队列后,请求的处理流程变成了:用户发起查询 → 后端立即入队 → 立即返回任务 ID → 前端轮询 / WebSocket 等待结果 → 队列消费 worker 从 HolyShehe AI API 获取结果 → 回调通知前端。整个流程解耦后,系统吞吐量提升了近 10 倍。

技术选型:为什么是 Redis + BullMQ

调研阶段我对比了三种方案:

最终选型 Redis 7.2 + BullMQ 2.x,配合 TypeScript + Node.js 18 的 runtime,完美契合我们已有的技术栈。

实战代码:从任务创建到消费处理

1. 项目初始化与依赖安装

mkdir rag-async-queue
cd rag-async-queue
npm init -y
npm install bullmq ioredis openai dotenv uuid
npm install -D typescript @types/node ts-node nodemon

我建议使用 HolySheep AI 作为底层模型调用层,原因有三个:第一,国内直连延迟低于 50ms,比调用 OpenAI 官方 API 快 3-5 倍;第二,¥1=$1 的汇率政策让我们这种中小团队能节省 85% 以上的成本;第三,注册就送免费额度,可以先用起来再决定是否付费。

2. 配置管理

import { config } from 'dotenv';
import { Redis } from 'ioredis';

config();

// HolySheep AI 配置
export const HOLYSHEEP_CONFIG = {
  baseURL: 'https://api.holysheep.ai/v1',
  apiKey: process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY',
  model: 'deepseek-v3.2', // DeepSeek V3.2 目前价格最低 $0.42/MTok
  maxTokens: 2048,
  temperature: 0.7,
};

// Redis 连接配置
export const redisConnection = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  maxRetriesPerRequest: null, // BullMQ 要求设为 null
  enableReadyCheck: false,
});

console.log(✅ Redis 连接成功,延迟: ${redisConnection.status});

这里有个坑必须提醒大家:maxRetriesPerRequest 必须设为 null,否则 BullMQ 会报 Connection is closed 错误。我在踩坑日志里记录过这个问题的排查过程,排查了整整 2 个小时。

3. 定义任务队列

import { Queue, Worker, Job } from 'bullmq';
import { HOLYSHEEP_CONFIG } from './config';

// 1. 创建 RAG 查询队列
export const ragQueryQueue = new Queue('rag-query', {
  connection: redisConnection,
  defaultJobOptions: {
    attempts: 3,           // 失败最多重试3次
    backoff: {
      type: 'exponential', // 指数退避
      delay: 2000,         // 初始退避 2s
    },
    removeOnComplete: {
      age: 3600,           // 完成 1 小时后自动删除
      count: 1000,         // 最多保留 1000 条
    },
    removeOnFail: {
      age: 86400,          // 失败 24 小时后删除
    },
  },
});

// 2. 任务类型定义
export interface RAGQueryJob {
  userId: string;
  query: string;
  sessionId: string;
  context: string[];      // 从向量数据库检索到的上下文片段
  priority?: number;     // 优先级,1-10,数字越大优先级越高
}

// 3. 添加任务的辅助函数
export async function enqueueRAGQuery(data: RAGQueryJob): Promise {
  const jobId = rag-${data.userId}-${Date.now()};
  
  await ragQueryQueue.add('process-query', data, {
    jobId,
    priority: data.priority || 5, // 默认优先级 5
  });
  
  console.log(📥 任务入队成功,JobID: ${jobId});
  return jobId;
}

任务队列的配置有几个关键点:指数退避重试很重要,AI API 偶发超时很常见,立即重试往往会继续失败,指数退避能有效规避;removeOnComplete 和 removeOnFail的自动清理机制防止 Redis 内存膨胀。

4. Worker 消费者:核心处理逻辑

import OpenAI from 'openai';

// 初始化 HolySheep AI 客户端
const holySheepClient = new OpenAI({
  baseURL: HOLYSHEEP_CONFIG.baseURL,
  apiKey: HOLYSHEEP_CONFIG.apiKey,
  timeout: 30000,         // 30 秒超时
  maxRetries: 2,
});

// 创建 RAG 查询 Worker
export const ragQueryWorker = new Worker(
  'rag-query',
  async (job: Job) => {
    const { userId, query, sessionId, context } = job.data as RAGQueryJob;
    
    console.log(🔄 开始处理任务 ${job.id},用户: ${userId});
    
    // 更新任务进度
    await job.updateProgress(10);
    
    // 构造 RAG 提示词
    const systemPrompt = `你是一个专业的电商客服。请根据以下上下文信息回答用户问题。
如果上下文中没有相关信息,请礼貌地告知用户你暂时无法回答,并建议联系人工客服。

上下文信息:
${context.map((c, i) => [${i + 1}] ${c}).join('\n')}`;

    await job.updateProgress(30);
    
    // 调用 HolySheep AI API(这里以 DeepSeek V3.2 为例)
    const startTime = Date.now();
    
    try {
      const completion = await holySheepClient.chat.completions.create({
        model: HOLYSHEEP_CONFIG.model,
        messages: [
          { role: 'system', content: systemPrompt },
          { role: 'user', content: query },
        ],
        max_tokens: HOLYSHEEP_CONFIG.maxTokens,
        temperature: HOLYSHEEP_CONFIG.temperature,
      });
      
      const latency = Date.now() - startTime;
      console.log(✅ HolySheep AI 响应成功,延迟: ${latency}ms);
      
      await job.updateProgress(100);
      
      return {
        success: true,
        jobId: job.id,
        sessionId,
        answer: completion.choices[0].message.content,
        usage: completion.usage,
        latency,
      };
    } catch (error: any) {
      console.error(❌ HolySheep AI 调用失败: ${error.message});
      throw error; // 抛出错误触发重试
    }
  },
  {
    connection: redisConnection,
    concurrency: 10,           // 同时处理 10 个任务
    limiter: {
      max: 50,                 // 每秒最多处理 50 个任务
      duration: 1000,
    },
  }
);

// Worker 事件监听
ragQueryWorker.on('completed', (job, result) => {
  console.log(✅ 任务 ${job.id} 完成,耗时: ${job.finishedOn! - job.timestamp}ms);
  // 这里可以触发 WebSocket 通知前端
  notifyClient(result);
});

ragQueryWorker.on('failed', (job, err) => {
  console.error(❌ 任务 ${job?.id} 失败: ${err.message});
  // 告警通知
  sendAlert(job?.id, err.message);
});

ragQueryWorker.on('progress', (job, progress) => {
  console.log(📊 任务 ${job.id} 进度: ${progress}%);
});

// 模拟通知客户端(实际应接入 WebSocket)
function notifyClient(result: any) {
  console.log(📨 通知客户端: ${JSON.stringify(result).slice(0, 100)}...);
}

// 告警通知(实际应接入钉钉/飞书/Slack)
function sendAlert(jobId: string | undefined, error: string) {
  console.error(🚨 [告警] 任务 ${jobId} 失败: ${error});
}

这里有几点实战经验:concurrency 设置为 10是个平衡值,太高会导致 API 限流,太低又浪费并发能力;limiter 限制每秒 50 个任务是配合 HolySheep API 的速率限制来的,避免触发 429 错误;updateProgress方法能让前端实时看到任务执行状态。

5. HTTP 接口层

import express, { Request, Response } from 'express';
import { enqueueRAGQuery } from './queue';
import { getJob } from 'bullmq';

const app = express();
app.use(express.json());

// POST /api/rag/query - 提交 RAG 查询
app.post('/api/rag/query', async (req: Request, res: Response) => {
  try {
    const { userId, query, sessionId, context, priority } = req.body;
    
    if (!userId || !query || !sessionId || !context) {
      return res.status(400).json({ error: '缺少必要参数' });
    }
    
    // 入队并返回任务 ID
    const jobId = await enqueueRAGQuery({ userId, query, sessionId, context, priority });
    
    res.json({
      success: true,
      jobId,
      message: '查询已提交,请使用 jobId 轮询结果',
    });
  } catch (error: any) {
    console.error(❌ 入队失败: ${error.message});
    res.status(500).json({ error: '服务器内部错误' });
  }
});

// GET /api/rag/query/:jobId - 查询任务状态和结果
app.get('/api/rag/query/:jobId', async (req: Request, res: Response) => {
  try {
    const { jobId } = req.params;
    const job = await getJob(jobId, ragQueryQueue);
    
    if (!job) {
      return res.status(404).json({ error: '任务不存在' });
    }
    
    const state = await job.getState();
    const progress = job.progress;
    
    res.json({
      jobId,
      state,
      progress,
      result: job.returnvalue || null,
      failedReason: job.failedReason || null,
    });
  } catch (error: any) {
    console.error(❌ 查询失败: ${error.message});
    res.status(500).json({ error: '服务器内部错误' });
  }
});

const PORT = parseInt(process.env.PORT || '3000');
app.listen(PORT, () => {
  console.log(🚀 RAG Async API 服务启动,端口: ${PORT});
});

性能优化与成本控制

上线后我做了几个关键优化:

使用 HolySheep AI 后,我们对比了成本:以每月 1000 万 token 的处理量计算,官方 OpenAI API 需要约 $80,而 HolyShehe 的汇率政策让我们实际支出只有 ¥80 左右,节省了 85% 的费用。

常见报错排查

这套方案落地过程中我踩过不少坑,总结了 3 个最常见的错误及其解决方案:

错误 1:Connection is closed

// ❌ 错误写法
const connection = new Redis({
  host: 'localhost',
  port: 6379,
});

// ✅ 正确写法:必须设置 maxRetriesPerRequest: null
const connection = new Redis({
  host: 'localhost',
  port: 6379,
  maxRetriesPerRequest: null, // BullMQ 强制要求
  enableReadyCheck: false,
});

原因:BullMQ 基于 ioredis 构建,对 Redis 连接有特殊要求。如果不设置 maxRetriesPerRequest: null,BullMQ 会在某些操作时触发 Redis 的重试机制,导致连接状态异常。

错误 2:Job already exists with id

// ❌ 错误写法:并发请求可能生成相同 jobId
const jobId = rag-${userId}-${Date.now()};
await queue.add('process', data, { jobId });

// ✅ 正确写法:使用 UUID 确保唯一性
import { v4 as uuidv4 } from 'uuid';

const jobId = rag-${userId}-${uuidv4()};
await queue.add('process', data, { jobId });

// ✅ 或者让 BullMQ 自动生成唯一 ID(不传 jobId 参数)
await queue.add('process', data); // BullMQ 自动生成唯一 ID

原因:如果使用时间戳作为 jobId,在高并发场景下,两个请求可能在同一毫秒内到达,导致 ID 冲突。推荐使用 UUID 或直接让 BullMQ 自动生成。

错误 3:429 Too Many Requests

// ❌ 错误写法:Worker 并发设置过高
new Worker('queue', processor, {
  connection,
  concurrency: 50, // 太高,容易触发 API 限流
});

// ✅ 正确写法:配合 limiter 和合理的 concurrency
new Worker('queue', processor, {
  connection,
  concurrency: 10,  // 降低并发
  limiter: {
    max: 30,         // 每秒最多 30 个任务
    duration: 1000,
  },
});

// 或者使用 BullMQ 的 Rate Limiter
import { QueueEvents } from 'bullmq';

const queueEvents = new QueueEvents('queue', { connection });
queueEvents.on('rate-limited', ({ queueId }) => {
  console.log(⚠️ 队列 ${queueId} 被限流);
});

原因:HolySheep AI API 有速率限制,如果请求频率超过限制会返回 429 错误。需要在 Worker 端配置 limiter 来控制请求速率,避免触发限流。

监控与生产部署

上线前我接入了 Prometheus + Grafana 监控,关键指标包括:队列深度、消费延迟、任务成功率、API 响应时间。AlertManager 配置了当队列深度超过 1000 或失败率超过 5% 时触发钉钉告警。

# docker-compose.yml 配置示例
version: '3.8'
services:
  redis:
    image: redis:7.2-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
  
  rag-api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - REDIS_HOST=redis
      - HOLYSHEEP_API_KEY=${HOLYSHEEP_API_KEY}
    depends_on:
      - redis
  
  rag-worker:
    build: .
    command: node dist/worker.js
    environment:
      - REDIS_HOST=redis
      - HOLYSHEEP_API_KEY=${HOLYSHEEP_API_KEY}
    depends_on:
      - redis
    deploy:
      replicas: 3  # 启动 3 个 worker 实例

volumes:
  redis_data:

多 worker 实例部署时,BullMQ 会自动实现负载均衡,每个任务只会被一个 worker 处理,不用担心重复消费的问题。

总结

这套方案的核心思路是解耦:用户请求快速入队返回,后台 worker 异步处理,完美解决了同步调用带来的各种问题。使用 Redis + BullMQ 的组合,运维成本低,扩展性强,配合 HolySheep AI 的高性价比和低延迟优势,整套系统的性价比非常可观。

如果你也在做类似的 AI 应用,强烈建议从一开始就把异步队列考虑进去,别等上线后被并发问题追着打。HolySheep AI 的注册送额度活动还在进行中,可以先免费试用再决定是否付费。

有问题欢迎在评论区留言,我会尽量回复。

👉 免费注册 HolyShehe AI,获取首月赠额度