AI/ML Engineering

RAG 生产实践:从原型到规模化部署的完整指南

为什么你的 RAG 应用在生产环境表现不佳?深入解析检索增强生成的工程实践,涵盖分块策略、重排序、向量数据库选型、缓存机制和评估体系。

Ioodu · · Updated: Mar 15, 2026 · 25 min read
#RAG #LLM #Vector Database #Embeddings #AI Engineering #Production

那个失败的 RAG 项目

2024 年初,我们团队接到了一个知识库问答项目。要求很简单:让用户能用自然语言查询公司内部的技术文档。

第一周:我们用 LangChain + OpenAI + Pinecone 搭了一个原型。效果惊艳——演示时 CEO 连连称赞。

第一个月:上线生产环境。用户开始抱怨:

  • “回答经常跑题”
  • “同样的提问,每次答案不一样”
  • “有些明显存在的文档搜不到”
  • “响应太慢,等 10 秒才出结果”

第三个月:项目被边缘化,使用率降到不足 5%。

这不是技术选型的问题。我们的错误在于:把原型当生产

RAG 看似简单——分块、嵌入、检索、生成——但魔鬼在细节。从能用到好用,中间隔着一整套工程实践。

这篇文章总结我过去两年在 3 个 RAG 项目中踩过的坑,以及最终沉淀下来的生产级方案。

RAG 架构全景

基础架构

┌─────────────────────────────────────────────────────────────────┐
│                        RAG Pipeline                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │  文档摄取  │───▶│  文本分块  │───▶│ Embedding│───▶│ 向量存储  │  │
│  │ Ingestion│    │ Chunking │    │  Model   │    │  VectorDB│  │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘  │
│         ▲                                              │        │
│         │                                              │        │
│    增量更新                                          相似度检索  │
│         │                                              │        │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │  答案生成  │◀───│   LLM    │◀───│  重排序   │◀───│  检索器   │  │
│  │ Generate │    │  (GPT-4) │    │ Reranker │    │ Retriever│  │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

生产架构的复杂性

生产环境需要考虑:

  1. 数据流水线:增量更新、版本控制、错误处理
  2. 查询优化:缓存、路由、并发控制
  3. 质量控制:评估体系、反馈循环、A/B 测试
  4. 运维保障:监控、告警、成本控制

第一阶段:文档摄取与处理

分块策略:不是越小越好

错误认知:“分块越小,检索越精准”

实际情况

  • 块太小 → 丢失上下文,语义不完整
  • 块太大 → 噪声过多,影响生成质量

我们的演进路径

// 第 1 版:固定字符数(效果不好)
const chunks = text.match(/.{1,500}/g) || [];

// 第 2 版:按段落分割(稍好,但不均匀)
const chunks = text.split('\n\n');

// 第 3 版:语义分块(当前方案)
interface SemanticChunk {
  content: string;
  startIndex: number;
  endIndex: number;
  metadata: {
    headings: string[];      // 所属章节标题
    codeBlocks: string[];    // 包含的代码块
    tables: string[];        // 包含的表格
  };
}

// 实现:递归字符文本分割 + 语义边界检测
function semanticChunking(
  document: Document,
  options: {
    chunkSize: number;       // 目标大小:512 tokens
    chunkOverlap: number;    // 重叠:50 tokens
    separators: string[];    // 分隔符优先级
  }
): SemanticChunk[] {
  // 1. 识别文档结构(标题层级)
  const structure = parseDocumentStructure(document);

  // 2. 按结构单元初步分割
  const units = splitByStructure(structure, options);

  // 3. 处理边界,确保语义完整
  return mergeSmallChunks(units, options.chunkSize);
}

关键经验

文档类型推荐策略Chunk 大小
技术文档按章节 + 代码块边界512-1024 tokens
法律合同按条款语义边界256-512 tokens
产品说明按功能模块384-768 tokens
论文文献按段落 + 引用边界512 tokens

元数据工程

不要只存储文本。丰富的元数据能显著提升检索质量:

interface DocumentChunk {
  id: string;
  content: string;
  embedding: number[];

  // 基础元数据
  metadata: {
    source: string;           // 来源文档
    page?: number;           // 页码
    createdAt: Date;         // 创建时间
    updatedAt: Date;         // 更新时间
  };

  // 结构化元数据(用于过滤和路由)
  tags: {
    docType: 'api' | 'guide' | 'faq' | 'tutorial';
    category: string[];       // 分类层级
    keywords: string[];       // 提取的关键词
    codeLanguage?: string;    // 代码语言
  };

  // 语义元数据(用于重排序)
  semantics: {
    summary: string;          // 内容摘要
    questions: string[];      // 该 chunk 能回答的问题
    entities: string[];       // 关键实体
  };
}

元数据提取自动化

// 使用 LLM 自动提取元数据
async function enrichMetadata(chunk: string): Promise<Partial<DocumentChunk>> {
  const prompt = `
分析以下文档片段,提取结构化信息:

${chunk}

请返回 JSON 格式:
{
  "summary": "50字以内的摘要",
  "questions": ["该片段能回答的3个问题"],
  "keywords": ["5个关键词"],
  "docType": "api/guide/faq/tutorial之一"
}
`;

  const response = await llm.complete(prompt, { json: true });
  return JSON.parse(response);
}

第二阶段:Embedding 与向量存储

Embedding 模型选型

不要默认使用 OpenAI text-embedding-ada-002

我们的对比测试(中文技术文档场景):

模型维度检索准确率@5延迟成本
OpenAI text-embedding-3-small153678%120ms$0.02/1M
OpenAI text-embedding-3-large307282%180ms$0.13/1M
BGE-M3 (开源)102485%80ms免费
bge-large-zh-v1.5102488%60ms免费

结论:中文场景下,BGE 系列表现更好且成本为零。

// 统一的 Embedding 接口
interface EmbeddingProvider {
  embed(texts: string[]): Promise<number[][]>;
  dimension: number;
}

// 本地部署的 BGE 实现
class BGEEmbeddingProvider implements EmbeddingProvider {
  private model: OnnxInferenceSession;
  dimension = 1024;

  async embed(texts: string[]): Promise<number[][]> {
    // 使用 ONNX Runtime 本地推理
    const inputs = await this.tokenize(texts);
    const outputs = await this.model.run(inputs);
    return this.normalize(outputs.last_hidden_state);
  }
}

向量数据库选型

数据库最佳场景优势劣势
Pinecone快速启动,无运维托管服务,易用成本高,Vendor lock-in
Milvus/Zilliz大规模生产功能丰富,性能强运维复杂
Qdrant中小型项目开源,易部署,性能好社区相对小
pgvector已有 Postgres无需新数据库性能一般
Chroma原型开发简单易用不适合生产

我们的选择:Qdrant

# docker-compose.yml
version: '3.8'
services:
  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_storage:/qdrant/storage
    environment:
      QDRANT__SERVICE__GRPC_PORT: 6334

  # 使用 gRPC 接口性能更好
// Qdrant 客户端封装
import { QdrantClient } from '@qdrant/js-client-rest';

class VectorStore {
  private client: QdrantClient;

  constructor() {
    this.client = new QdrantClient({
      host: process.env.QDRANT_HOST,
      apiKey: process.env.QDRANT_API_KEY,
    });
  }

  async search(
    collection: string,
    vector: number[],
    options: {
      topK?: number;
      filter?: Record<string, any>;
      withPayload?: boolean;
    }
  ): Promise<SearchResult[]> {
    const results = await this.client.search(collection, {
      vector,
      limit: options.topK || 10,
      filter: options.filter,
      with_payload: options.withPayload ?? true,
      // 关键:使用 HNSW 索引参数优化
      params: {
        hnsw_ef: 128,  // 搜索时探索因子
        exact: false,
      },
    });

    return results.map(r => ({
      id: r.id as string,
      score: r.score,
      content: r.payload?.content as string,
      metadata: r.payload?.metadata,
    }));
  }
}

索引优化

HNSW 参数调优

// 创建集合时配置 HNSW 参数
await client.createCollection(collectionName, {
  vectors: {
    size: 1024,           // Embedding 维度
    distance: 'Cosine',   // 距离度量
    hnsw_config: {
      m: 16,              // 每个节点的连接数(越大越精准,越慢)
      ef_construct: 100,  // 构建时的探索因子
    },
  },
  optimizers_config: {
    indexing_threshold: 10000,  // 达到此数量开始索引
  },
});

混合搜索(稠密 + 稀疏)

// 结合向量相似度和关键词匹配
async function hybridSearch(query: string): Promise<SearchResult[]> {
  // 1. 稠密检索(语义相似)
  const denseResults = await vectorSearch(query, { topK: 20 });

  // 2. 稀疏检索(关键词匹配,使用 BM25)
  const sparseResults = await keywordSearch(query, { topK: 20 });

  // 3. RRF (Reciprocal Rank Fusion) 融合
  return reciprocalRankFusion([denseResults, sparseResults], {
    k: 60,  // RRF 常数
  });
}

第三阶段:检索策略

查询重写

用户的原始查询往往不是最优的检索输入。

// 查询扩展
async function rewriteQuery(originalQuery: string): Promise<string[]> {
  const prompt = `
将用户问题改写为更适合向量检索的形式。
生成 3 个不同角度的改写版本。

用户问题:${originalQuery}

要求:
1. 使用更正式、更具体的术语
2. 补充隐含的上下文
3. 不同版本侧重不同方面

返回 JSON 数组格式。
`;

  const variations = await llm.complete(prompt, { json: true });
  return [originalQuery, ...variations];
}

// HyDE (Hypothetical Document Embeddings)
async functionhydeQuery(query: string): Promise<number[]> {
  // 1. 让 LLM 生成假设的理想回答
  const hypotheticalAnswer = await llm.complete(`
    请回答以下问题,提供详细的技术解释:
    ${query}
  `);

  // 2. 对假设回答做 embedding,而不是原始查询
  return embeddingModel.embed(hypotheticalAnswer);
}

多路召回

单一检索策略往往不够:

async function multiRetrieverSearch(query: string): Promise<Chunk[]> {
  const [semanticResults, keywordResults, graphResults] = await Promise.all([
    // 路 1:语义检索
    semanticSearch(query, { topK: 10 }),

    // 路 2:关键词检索
    keywordSearch(query, { topK: 10 }),

    // 路 3:知识图谱检索(如有)
    graphSearch(query, { topK: 5 }),
  ]);

  // RRF 融合去重
  return fuseResults([semanticResults, keywordResults, graphResults]);
}

重排序(Reranking)

向量相似度 ≠ 回答相关性。必须使用重排序模型。

// 使用 cross-encoder 重排序
async function rerank(
  query: string,
  candidates: Chunk[],
  options: { topK: number }
): Promise<Chunk[]> {
  // 准备输入对
  const pairs = candidates.map(c => ({
    query,
    document: c.content,
    chunk: c,
  }));

  // 调用重排序模型(如 bge-reranker-large)
  const scores = await reranker.score(pairs);

  // 按重排序分数排序
  const ranked = pairs
    .map((p, i) => ({ ...p.chunk, rerankScore: scores[i] }))
    .sort((a, b) => b.rerankScore - a.rerankScore);

  return ranked.slice(0, options.topK);
}

重排序的价值:在我们的测试中,加入重排序后回答准确率提升了 23%。

第四阶段:生成优化

上下文压缩

检索到的 chunks 可能包含冗余信息,需要压缩:

async function compressContext(
  query: string,
  chunks: Chunk[],
  maxTokens: number
): Promise<string> {
  const prompt = `
以下是与问题相关的文档片段。请提取与问题直接相关的信息,去除冗余内容。

问题:${query}

文档片段:
${chunks.map((c, i) => `[${i + 1}] ${c.content}`).join('\n\n')}

要求:
1. 保留关键事实、数据、步骤
2. 去除与问题无关的背景信息
3. 保持原始信息的准确性
4. 输出压缩后的内容
`;

  return llm.complete(prompt);
}

Prompt 工程

const RAG_PROMPT = `你是一个专业的技术助手。基于提供的参考资料回答问题。

## 参考资料
{context}

## 用户问题
{question}

## 回答要求
1. 优先使用参考资料中的信息
2. 如果参考资料不足,明确说明
3. 引用来源,格式:[1], [2]
4. 保持简洁专业
5. 不确定时回答"根据现有资料,我无法确定..."

## 你的回答`;

引用溯源

interface CitedResponse {
  answer: string;
  citations: Array<{
    index: number;
    source: string;
    quote: string;
  }>;
}

// 生成带引用的回答
async function generateWithCitations(
  query: string,
  chunks: Chunk[]
): Promise<CitedResponse> {
  const context = chunks
    .map((c, i) => `[${i + 1}] ${c.content}\n(Source: ${c.metadata.source})`)
    .join('\n\n');

  const response = await llm.complete(RAG_PROMPT.replace('{context}', context).replace('{question}', query));

  // 解析引用
  const citations = extractCitations(response, chunks);

  return { answer: response, citations };
}

第五阶段:评估与监控

离线评估体系

// 评估指标
interface RAGMetrics {
  // 检索质量
  retrieval: {
    recall@k: number;       // 相关文档被检索到的比例
    precision@k: number;    // 检索结果中相关的比例
    mrr: number;            // 平均倒数排名
  };

  // 生成质量
  generation: {
    faithfulness: number;   // 忠实度(是否编造)
    answerRelevance: number; // 回答相关性
    contextPrecision: number; // 上下文使用精准度
  };

  // 端到端
  endToEnd: {
    correctness: number;    // 人工评估正确率
    userSatisfaction: number; // 用户满意度
  };
}

// 使用 RAGAS 框架评估
async function evaluateRAG(
  testSet: TestCase[],
  ragPipeline: RAGPipeline
): Promise<RAGMetrics> {
  const results = await Promise.all(
    testSet.map(async (test) => {
      const response = await ragPipeline.query(test.question);

      return {
        faithfulness: calculateFaithfulness(response, test.groundTruth),
        answerRelevance: calculateRelevance(response.answer, test.question),
        contextPrecision: calculateContextPrecision(
          response.retrievedChunks,
          test.relevantChunks
        ),
      };
    })
  );

  return aggregateMetrics(results);
}

在线监控

// 关键指标监控
interface RAGTelemetry {
  // 延迟
  latency: {
    embedding: number;
    retrieval: number;
    generation: number;
    total: number;
  };

  // 检索
  retrieval: {
    chunksCount: number;
    avgScore: number;
    cacheHitRate: number;
  };

  // 生成
  generation: {
    inputTokens: number;
    outputTokens: number;
    cost: number;
  };

  // 质量信号
  quality: {
    userRating?: number;    // 用户反馈
    hasCitation: boolean;   // 是否包含引用
    confidence: number;     // 模型置信度
  };
}

// 发送指标到监控
async function recordTelemetry(
  queryId: string,
  telemetry: RAGTelemetry
) {
  await metrics.histogram('rag.latency.total', telemetry.latency.total);
  await metrics.gauge('rag.retrieval.chunks', telemetry.retrieval.chunksCount);
  await metrics.counter('rag.query.total', 1);

  // 异常告警
  if (telemetry.latency.total > 5000) {
    await alert.send('RAG latency exceeded 5s', { queryId });
  }
}

第六阶段:高级优化

查询缓存

class RAGCache {
  private semanticCache: SemanticCache;  // 基于 embedding 相似度
  private exactCache: LRUCache<string, CachedResult>;

  async get(query: string): Promise<CachedResult | null> {
    // 1. 精确匹配
    const exact = this.exactCache.get(query);
    if (exact) return exact;

    // 2. 语义相似匹配
    const similar = await this.semanticCache.findSimilar(query, threshold: 0.95);
    if (similar) return similar.result;

    return null;
  }
}

增量索引

// 监听文档变更
async function incrementalUpdate(documentId: string) {
  const changes = await detectChanges(documentId);

  for (const change of changes) {
    if (change.type === 'added') {
      await indexNewChunks(change.chunks);
    } else if (change.type === 'removed') {
      await deleteChunks(change.chunkIds);
    } else if (change.type === 'modified') {
      await updateChunks(change.chunks);
    }
  }
}

多租户隔离

// 租户级别的数据隔离
async function tenantSearch(
  tenantId: string,
  query: string
): Promise<SearchResult[]> {
  return vectorStore.search('documents', embedding, {
    filter: {
      must: [
        { key: 'tenantId', match: { value: tenantId } },
        { key: 'status', match: { value: 'active' } },
      ],
    },
  });
}

部署架构示例

# docker-compose.prod.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - QDRANT_HOST=qdrant
      - REDIS_HOST=redis
      - EMBEDDING_MODEL=/models/bge-large-zh
    deploy:
      replicas: 3

  worker:
    build: .
    command: celery worker
    environment:
      - QDRANT_HOST=qdrant
    deploy:
      replicas: 2

  qdrant:
    image: qdrant/qdrant:latest
    volumes:
      - qdrant_data:/qdrant/storage
    deploy:
      resources:
        limits:
          memory: 8G

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  # 本地 embedding 服务
  embedding:
    image: huggingface/transformers-pytorch
    volumes:
      - ./models:/models
    command: python server.py --model /models/bge-large-zh
    deploy:
      resources:
        limits:
          memory: 4G

成本优化

优化策略节省效果实施难度
本地 Embedding80% API 成本
查询缓存30-50% Token 成本
上下文压缩20-30% Token 成本
模型路由(小模型过滤)40% LLM 成本
批量处理20% 总体成本

总结

生产级 RAG 不是简单的 “向量数据库 + LLM”。它是一个系统工程:

  1. 数据处理:分块策略、元数据工程决定检索上限
  2. 检索优化:多路召回 + 重排序是必需项
  3. 生成控制:Prompt 工程、引用溯源建立信任
  4. 评估闭环:持续监控、A/B 测试驱动优化
  5. 运维保障:缓存、限流、成本控制确保可持续

RAG 的核心矛盾是:召回率 vs 精准度、成本 vs 质量、延迟 vs 复杂度。没有银弹,只有根据业务场景的权衡。


参考资源


你的 RAG 项目遇到了哪些坑?欢迎在评论区分享。

本文总结自我过去两年在多个 RAG 项目中的实战经验,包括一个 10万+ 文档的企业知识库系统。

---

评论