生产级 RAG 架构总览

生产环境中的 RAG 不是简单的"文档切块 + 向量搜索 + LLM 生成"三步走,而是一个包含数据接入、预处理、索引、检索、重排、生成、后处理等多环节的完整系统。

┌─────────────────────────────────────────────────────────────────┐
│                      RAG 生产架构                                │
├────────────────┬────────────────────────────────────────────────┤
│  数据层        │  文档加载 → 清洗 → 分块 → Embedding → 向量库    │
│  检索层        │  混合检索(向量+BM25) → 重排 → 上下文组装        │
│  生成层        │  Prompt 模板 → LLM 调用 → 流式输出 → 后处理     │
│  基础设施层    │  API网关 → 负载均衡 → 缓存 → 监控 → 日志        │
└────────────────┴────────────────────────────────────────────────┘

向量数据库选型

三大主流向量库对比:

维度MilvusWeaviateQdrant
部署复杂度高(依赖etcd/MinIO)低(单二进制)
索引算法HNSW/IVF/DiskANNHNSWHNSW
混合检索需配合ES内置BM25内置稀疏向量
动态schema支持支持支持
集群方案成熟支持支持(一致性哈希)
性能(1M向量,768d)~1200 QPS~800 QPS~1500 QPS
社区活跃度
适用场景大规模(亿级)中规模+GraphQL中小规模+低延迟

选型建议:

  • 文档量 < 500万,优先 Qdrant,部署简单、性能优秀
  • 需要 GraphQL 接口和内置混合检索,选 Weaviate
  • 亿级向量、需要分片和 DiskANN,选 Milvus

Elasticsearch 混合检索方案

纯向量检索在关键词匹配场景下表现差(如产品型号、人名搜索)。混合检索结合 BM25 和向量检索,取长补短:

from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk
import numpy as np

es = Elasticsearch(["http://es:9200"])

# 创建同时支持文本和向量的索引
mapping = {
    "mappings": {
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_smart"
            },
            "content_vector": {
                "type": "dense_vector",
                "dims": 1024,
                "index": True,
                "similarity": "cosine"
            },
            "metadata": {
                "type": "object",
                "enabled": False
            }
        }
    }
}

es.indices.create(index="rag_docs", body=mapping)

def hybrid_search(query_text, query_vector, top_k=5):
    """混合检索:BM25 + 向量检索 + RRF 融合"""
    body = {
        "query": {
            "bool": {
                "should": [
                    # BM25 文本检索
                    {
                        "match": {
                            "content": {
                                "query": query_text,
                                "boost": 1.0
                            }
                        }
                    }
                ]
            }
        },
        "knn": {
            "field": "content_vector",
            "query_vector": query_vector,
            "k": top_k * 2,
            "num_candidates": top_k * 10
        },
        "size": top_k,
        "_source": ["content", "metadata"]
    }
    
    results = es.search(index="rag_docs", body=body)
    return results["hits"]["hits"]

Docker Compose 完整部署

# docker-compose.yml
version: '3.8'

services:
  # API 网关
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - rag-api
    restart: always

  # RAG API 服务
  rag-api:
    build: .
    environment:
      - QDRANT_URL=http://qdrant:6333
      - ES_URL=http://elasticsearch:9200
      - REDIS_URL=redis://redis:6379
      - LLM_API_KEY=${LLM_API_KEY}
      - EMBEDDING_MODEL=bge-m3
      - EMBEDDING_URL=http://embedding-server:8080/embed
    depends_on:
      - qdrant
      - elasticsearch
      - redis
      - embedding-server
    restart: always
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2.0"

  # 向量数据库
  qdrant:
    image: qdrant/qdrant:v1.12.0
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_data:/qdrant/storage
    restart: always

  # Elasticsearch(混合检索)
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms2g -Xmx2g"
    volumes:
      - es_data:/usr/share/elasticsearch/data
    restart: always

  # Redis(缓存 + 限流)
  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    restart: always

  # Embedding 服务
  embedding-server:
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5
    command: ["--model-id", "BAAI/bge-m3", "--port", "8080"]
    volumes:
      - embedding_models:/data
    restart: always

  # 监控
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

volumes:
  qdrant_data:
  es_data:
  redis_data:
  embedding_models:

关键监控指标

指标告警阈值说明
检索延迟 P99> 500ms向量检索慢,检查索引或分片
生成延迟 P99> 15sLLM 响应慢,考虑流式输出
检索召回率< 80%分块策略或 Embedding 质量问题
上下文利用率< 60%检索结果与问题相关性低
API 错误率> 1%检查上游服务状态
向量库内存> 85%需要扩容或优化索引
# Prometheus 指标埋点
from prometheus_client import Counter, Histogram, Gauge

rag_search_duration = Histogram(
    'rag_search_duration_seconds',
    'RAG retrieval duration',
    ['search_type']  # hybrid/vector/keyword
)

rag_generation_duration = Histogram(
    'rag_generation_duration_seconds',
    'LLM generation duration'
)

rag_context_relevance = Gauge(
    'rag_context_relevance_score',
    'Average context relevance score'
)

# 使用示例
with rag_search_duration.labels('hybrid').time():
    results = hybrid_search(query, vector)

扩容策略

垂直扩容优先:向量检索是内存密集型,优先加内存。Qdrant 在 32GB 内存下可稳定承载 500 万条 1024 维向量。

水平扩容触发条件:

  • 向量数量超过单机内存的 70%
  • QPS 持续 > 2000
  • 检索延迟 P99 > 300ms

实际扩容步骤:

  1. Qdrant 启用分片:PUT /collections/docs { "sharding_method": "auto" }
  2. ES 增加节点,设置 number_of_replicas: 1
  3. API 层无状态,直接加实例 + Nginx 负载均衡
  4. Embedding 服务可按需扩容,缓存常见文档的向量

实战避坑清单

  • 分块策略:中文文档用 RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50),按句子边界切分,不要按固定字符
  • Embedding 模型:中文场景用 bge-m3bge-large-zh-v1.5,不要用 text-embedding-ada-002(中文表现差)
  • 重排序:检索 top 20 → Cross-Encoder 重排 → 取 top 5,效果提升显著
  • 缓存:对高频相同问题做语义缓存(向量相似度 > 0.95 直接返回缓存结果)
  • 限流:按 API Key 限流,防止恶意调用拖垮 LLM 推理服务

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。