RAG 从入门到生产:构建高性能检索增强生成系统的完全指南

AI 3 次阅读
RAG 从入门到生产:构建高性能检索增强生成系统的完全指南

分类: 1 | 标签: LLM, RAG, 检索增强生成, 向量数据库, LangChain

从文档分块到向量检索,从提示工程到高级 RAG 模式,本文带你全面掌握检索增强生成(RAG)的架构设计、调优技巧与生产部署实践。涵盖 LangChain、LlamaIndex、Milvus 等主流工具,提供 10+ 可运行代码示例。

目录

  1. 为什么你需要 RAG?
  2. RAG 核心架构:从零拆解
  3. 文档分块(Chunking):决定检索质量的第一关
  4. 嵌入模型(Embedding)选型指南
  5. 向量数据库实战:Milvus vs Qdrant vs Chroma
  6. 检索策略:从基础到高级
  7. 提示工程与生成优化
  8. 高级 RAG 模式
  9. 评估体系:如何量化 RAG 质量
  10. 生产部署清单
  11. 总结与展望

一、为什么你需要 RAG?

想象你是一位图书管理员 💼,面前站着一位读者问你:「2024 年诺贝尔物理学奖得主的研究对 AI 有什么影响?」你没有百科全书般的记忆,但你有一个庞大的图书馆。你的策略是:先去书架找到相关书籍,阅读关键段落,然后组织答案

这就是 RAG(Retrieval-Augmented Generation,检索增强生成)的核心思想。

1.1 LLM 的三大痛点

痛点 说明 RAG 如何解决
知识截止日期 训练数据有时间窗口,无法回答最新问题 实时检索外部知识库
幻觉(Hallucination) 模型可能编造不存在的事实 基于检索到的真实文档生成回答
领域知识缺失 通用模型不懂企业内部文档 将私有知识库作为检索源

1.2 为什么 2025 年 RAG 如此重要?

随着大模型上下文窗口从 4K 扩展到 128K 甚至 1M,你可能会问:既然一次能塞进整本书,还需要 RAG 吗?答案是需要,原因有三:

  1. 成本问题:把整本 500 页手册塞进上下文,每次查询消耗 300K+ tokens,API 费用飙升。RAG 只需检索 2-3 个相关片段(约 2K tokens),成本降低 99%
  2. 注意力稀释:研究表明,LLM 在长上下文的「中间部分」注意力会显著下降(Lost in the Middle 现象)。RAG 把相关信息放在提示的开头和结尾,利用首尾优势
  3. 知识更新延迟:上下文窗口大 ≠ 知识新。RAG 可以实时接入最新文档、数据库变更流和 API 返回数据

1.3 RAG 相比微调的优势

微调(Fine-tuning)需要标注数据、GPU 算力、重新部署,而 RAG 只需要构建知识库即可「热插拔」新知识。两者的对比如下:

维度 RAG 微调
知识更新速度 实时(更新文档即可) 需要重新训练
透明度 可溯源(返回引用文档) 黑盒
成本 低(只需向量数据库 + API) 高(GPU + 标注数据)
可控性 高(修改文档即可调整行为) 低(需要重新训练和测试)
适用场景 知识密集型 QA、企业知识库 风格迁移、特定领域术语

💡 核心洞察:RAG 不是微调的替代品,而是互补方案。最佳实践通常是「微调 + RAG」的混合架构——用微调让模型「学会领域语言」,用 RAG 让它「获取最新知识」。


配图

二、RAG 核心架构:从零拆解

一个标准 RAG 系统由两大阶段构成:索引阶段(Offline)查询阶段(Online)

┌─────────────────────────────────────────────────┐
│                  索引阶段(Offline)              │
│                                                 │
│  文档 → 分块 → 嵌入 → 向量数据库 ← 知识更新      │
│                                                 │
├─────────────────────────────────────────────────┤
│                  查询阶段(Online)               │
│                                                 │
│  用户问题 → 嵌入 → 检索 → 重排序 → 生成 → 回答   │
│                                                 │
└─────────────────────────────────────────────────┘

2.1 索引阶段的五个步骤

  1. 文档加载(Document Loading):从 PDF、网页、数据库等源加载文档
  2. 文档解析(Parsing):提取文本内容,去除格式噪声
  3. 文本分块(Chunking):将长文档切分成适合检索的片段
  4. 向量嵌入(Embedding):将文本块转换为高维向量
  5. 索引存储(Indexing):将向量存入向量数据库

2.2 查询阶段的五个步骤

  1. 问题嵌入(Query Embedding):将用户问题转为向量
  2. 相似度检索(Similarity Search):在向量空间中查找最相似的文档块
  3. 重排序(Reranking):使用更精准的模型对候选块重新排序
  4. 上下文构建(Context Assembly):将检索结果拼接为提示上下文
  5. 生成回答(Generation):LLM 基于上下文生成最终回答

2.3 最简单的 RAG 实现

让我们用 LangChain 快速搭建一个「Hello World」级别的 RAG:

# requirements: langchain, langchain-openai, chromadb, pypdf
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA

# 1. 加载文档
loader = PyPDFLoader("company_handbook.pdf")
documents = loader.load()

# 2. 分块
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ".", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"📦 共产生 {len(chunks)} 个文本块")

# 3. 向量化 + 存储
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

# 4. 创建 RAG 链
llm = ChatOpenAI(model="gpt-4o", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 4})
)

# 5. 提问
response = qa_chain.invoke("公司的年假政策是什么?")
print(f"🤖 回答: {response['result']}")

这个 30 行代码的 RAG 系统已经可以回答基于 PDF 文档的问题。但要达到生产级别,我们还需要深入每个环节的优化技巧。


三、文档分块(Chunking):决定检索质量的第一关

分块策略直接影响检索的召回率和精确率。分块太大 → 噪声多、相关性低;分块太小 → 语义不完整、上下文断裂。

3.1 主流分块策略对比

策略 原理 优点 缺点 适用场景
固定长度 按字符数切割 简单、可预测 可能截断句子 结构化文本
递归分割 按分隔符优先级递归切分 保持段落完整性 chunk_overlap 需调参 通用场景 ⭐
语义分割 用模型判断语义边界 语义完整 成本高、速度慢 高质量要求
文档结构 按 Markdown 标题/表格分割 保持文档层次 依赖文档格式 技术文档

3.2 递归分块的精细控制

from langchain_text_splitters import RecursiveCharacterTextSplitter

# 中文友好的递归分块
splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,           # 约 800 字符
    chunk_overlap=100,         # 保留 100 字符上下文
    length_function=len,
    separators=[
        "\n## ", "\n### ",   # Markdown 标题
        "\n\n", "\n",         # 段落
        "。", "!", "?",     # 中文标点
        ". ", "! ", "? ",    # 英文标点
        " ", ""               # 最后兜底
    ]
)

# 实验:不同 chunk_size 对检索的影响
for size in [300, 500, 800, 1200]:
    splitter.chunk_size = size
    chunks = splitter.split_documents(documents)
    avg_len = sum(len(c.page_content) for c in chunks) / len(chunks)
    print(f"chunk_size={size:>4} -> {len(chunks):>3} 块, 平均长度 {avg_len:.0f} 字符")

3.3 高级技巧:Parent Document Retriever

一个经典矛盾:检索时需要小块保证精度,生成时需要大块保证上下文。

解决方案:Parent Document Retriever 先检索小块,生成时返回对应的「父文档」。

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

# 子文档(用于检索,精度高)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# 父文档(用于生成,上下文全)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)

store = InMemoryStore()
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
    search_kwargs={"k": 5}  # 检索 5 个子块,返回对应的父块
)

四、嵌入模型(Embedding)选型指南

嵌入模型将文本映射为高维向量,语义相近的文本在向量空间中距离更近。选对嵌入模型对 RAG 效果至关重要。

4.1 主流嵌入模型对比

模型 维度 最大 Token MTEB 评分 中文支持 API 价格
OpenAI text-embedding-3-small 1536 8191 62.3 支持 $0.02/1M tokens
OpenAI text-embedding-3-large 3072 8191 64.6 支持 Matryoshka 降维 $0.13/1M tokens
BGE-M3 (BAAI) 1024 8192 64.2 中文最佳 免费(本地部署)
Cohere embed-v3 1024 512 62.1 支持 $0.10/1M tokens
Jina embeddings v3 1024 8192 61.5 支持 有限免费
text2vec-large-chinese 1024 512 - 支持 免费(本地部署)

🔬 实测建议:中文场景首选 BGE-M3(BAAI 出品,MTEB 中文榜首),成本敏感用 text-embedding-3-small。

4.2 嵌入模型的本地部署

# 使用 BGE-M3 本地部署(支持中英双语)
# pip install sentence-transformers

from sentence_transformers import SentenceTransformer

# 加载 BGE-M3 模型(约 2.2GB,首次自动下载)
model = SentenceTransformer("BAAI/bge-m3")

# 批量编码文档
documents = [
    "RAG 是检索增强生成的缩写",
    "向量数据库用于存储和检索高维向量",
    "LangChain 是一个 LLM 应用开发框架"
]

# 生成嵌入向量
embeddings = model.encode(
    documents,
    normalize_embeddings=True,   # 归一化便于余弦相似度
    show_progress_bar=True
)

print(f"嵌入维度: {embeddings.shape[1]}")
print(f"向量范围: [{embeddings.min():.4f}, {embeddings.max():.4f}]")

# 查询示例
query = "什么是 RAG?"
query_embedding = model.encode(query, normalize_embeddings=True)

# 计算相似度
from numpy import dot
similarities = [dot(query_embedding, doc_emb) for doc_emb in embeddings]
for doc, sim in zip(documents, similarities):
    print(f"  {sim:.4f} | {doc}")

4.3 嵌入模型性能基准测试

import time
import numpy as np

def benchmark_embedding(model, texts, batch_size=32):
    """测试嵌入模型的吞吐量"""
    start = time.time()
    embeddings = model.encode(texts, batch_size=batch_size,
                              normalize_embeddings=True)
    elapsed = time.time() - start
    
    tokens_estimate = sum(len(t) for t in texts)  # 近似估计,中文约1字≈1.5-2.5 tokens
    return {
        "总文本数": len(texts),
        "估计 Token 数": tokens_estimate,
        "耗时(秒)": f"{elapsed:.2f}",
        "吞吐量(tokens/s)": f"{tokens_estimate/elapsed:.0f}",
        "向量维度": embeddings.shape[1]
    }

# 模拟 1000 个文档块
test_texts = ["这是一段测试文本,用于评估嵌入模型的性能。"] * 1000
result = benchmark_embedding(model, test_texts)
for k, v in result.items():
    print(f"  {k}: {v}")

五、向量数据库实战:Milvus vs Qdrant vs Chroma

向量数据库是 RAG 系统的「记忆引擎」。选型需权衡性能、运维成本和生态集成。

5.1 三大向量数据库全面对比

维度 Milvus Qdrant Chroma
定位 分布式、云原生 高性能、嵌入式友好 开发者工具
部署方式 Docker/K8s/Zilliz Cloud Docker/嵌入式 Python 库 / Client-Server
索引算法 IVF/HNSW/DISKANN HNSW HNSW
过滤能力 标量过滤 + 表达式 强大的 Payload 过滤 元数据过滤
水平扩展 原生支持 集群模式(企业版) 单机
Python SDK pymilvus qdrant-client chromadb
适用规模 百万级到十亿级 万级到千万级 千级到十万级
学习曲线 较陡 中等

5.2 Milvus 实战:从 Docker 到代码

# 一键启动 Milvus Standalone
wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml
docker compose up -d
# pip install pymilvus "pymilvus[model]"
from pymilvus import MilvusClient, DataType

# 连接
client = MilvusClient(uri="http://localhost:19530")

# 创建带 Schema 的 Collection
schema = client.create_schema(auto_id=False, enable_dynamic_field=True)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("text", DataType.VARCHAR, max_length=65535)
schema.add_field("vector", DataType.FLOAT_VECTOR, dim=1024)
schema.add_field("source", DataType.VARCHAR, max_length=256)

# 创建索引
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="vector",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 200}
)

client.create_collection(
    "rag_knowledge",
    schema=schema,
    index_params=index_params,
    consistency_level="Strong"
)

# 插入数据
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3")

docs = [
    {"id": i, "text": f"文档片段 {i}:这是第 {i} 个知识条目...",
     "source": "internal_wiki.md"}
    for i in range(1000)
]
embeddings = model.encode([d["text"] for d in docs], normalize_embeddings=True)

data = [
    {**doc, "vector": emb.tolist()}
    for doc, emb in zip(docs, embeddings)
]
client.insert("rag_knowledge", data)

# 检索
query = "什么是向量数据库?"
query_vec = model.encode(query, normalize_embeddings=True).tolist()

results = client.search(
    "rag_knowledge",
    data=[query_vec],
    limit=5,
    output_fields=["text", "source"],
    search_params={"metric_type": "COSINE", "params": {"ef": 64}}
)

for i, hits in enumerate(results):
    for j, hit in enumerate(hits):
        text_preview = hit['entity']['text'][:60]
        print(f"  [{j+1}] 距离={hit['distance']:.4f} | {text_preview}...")

5.3 Qdrant 的 Payload 过滤技巧

Qdrant 最大的特色是强大的元数据过滤能力,适合需要「混合检索 + 条件筛选」的场景。

from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue

client = QdrantClient(host="localhost", port=6333)

# 检索 + 过滤:只搜索「技术文档」来源、创建于 2025 年的内容
results = client.search(
    collection_name="rag_docs",
    query_vector=query_vec,
    limit=5,
    query_filter=Filter(
        must=[
            FieldCondition(key="source", match=MatchValue(value="technical_doc")),
            FieldCondition(key="year", match=MatchValue(value=2025))
        ]
    )
)

配图

六、检索策略:从基础到高级

基础的向量检索无法应对复杂查询。让我们逐步升级检索策略。

结合向量检索(语义相似度)和关键词检索(BM25 精确匹配),取长补短。

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# 创建两个检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
bm25_retriever = BM25Retriever.from_documents(
    documents, k=10, preprocess_func=lambda x: x.lower()
)

# 集成检索器(RRF 融合算法)
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.7, 0.3],  # 向量 70% + BM25 30%
    c=60  # RRF 常量
)

results = ensemble_retriever.invoke("公司的隐私政策")

6.2 重排序(Reranking)

初检索返回的 Top-K 是用轻量模型做的粗略筛选,用更精准的 Cross-Encoder 重排序可以大幅提升精度。

# pip install sentence-transformers
from sentence_transformers import CrossEncoder

# 加载 Cross-Encoder 重排序模型
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)

# 第一步:粗检索(向量相似度)
candidates = vectorstore.similarity_search(query, k=20)

# 第二步:精排序(Cross-Encoder)
pairs = [[query, doc.page_content] for doc in candidates]
scores = reranker.predict(pairs)

# 按分数排序,取 Top 5
scored_docs = sorted(
    zip(candidates, scores),
    key=lambda x: x[1],
    reverse=True
)[:5]

for i, (doc, score) in enumerate(scored_docs):
    preview = doc.page_content[:80]
    print(f"  [{i+1}] rerank_score={score:.4f} | {preview}...")

实测数据:在内部测试集上,加入重排序后 Top-5 命中率从 72% 提升至 91%。

6.3 查询变换(Query Transformation)

用户提问往往简短或口语化。通过查询变换可以提升检索质量:

HyDE (Hypothetical Document Embeddings):先让 LLM 生成一个假设性回答,用这个回答去做检索。假设回答比原始问题包含更多语义信息,能更准确地匹配知识库。

from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

hyde_template = """请根据以下问题写一段假设性回答(200字以内),即使你不确定也要尝试。
问题: {question}
假设性回答:"""

hyde_prompt = ChatPromptTemplate.from_template(hyde_template)
hyde_chain = hyde_prompt | llm | StrOutputParser()

# 用生成的假设回答进行检索
hypothetical_answer = hyde_chain.invoke({"question": query})
retrieved_docs = vectorstore.similarity_search(hypothetical_answer, k=5)

Multi-Query 策略:从多个角度改写同一问题,每个角度的检索结果合并去重。

# 生成多个查询变体
multi_query_prompt = """从不同角度生成3个关于以下问题的变体,帮助覆盖更广的搜索结果。
原始问题: {question}
变体(每行一个):"""

variants_response = llm.invoke(
    multi_query_prompt.format(question=query)
)
variants = [v.strip("- ") for v in variants_response.content.strip().split("\n") if v.strip()]

# 对每个变体分别检索并合并
all_docs = []
for variant in variants:
    docs = vectorstore.similarity_search(variant, k=3)
    all_docs.extend(docs)

# 去重(按 page_content)
seen = set()
unique_docs = []
for doc in all_docs:
    if doc.page_content not in seen:
        seen.add(doc.page_content)
        unique_docs.append(doc)

七、提示工程与生成优化

检索只是前奏,提示工程决定最终的答案质量。

7.1 RAG 提示模板设计

from langchain.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 生产级 RAG 提示模板
rag_template = """你是一个专业的技术助手。请**仅根据以下提供的上下文信息**回答问题。
如果上下文不足以回答,请诚实地说「根据现有资料无法确定」,不要编造信息。

## 规则
1. 回答必须基于上下文,每条引用标注来源
2. 使用 markdown 格式化输出,重要内容加粗
3. 如果涉及代码,使用代码块并注明语言
4. 回答末尾列出「参考来源」

## 上下文
{context}

## 问题
{question}

## 回答
"""

prompt = ChatPromptTemplate.from_template(rag_template)

def format_docs(docs):
    return "\n\n---\n\n".join(
        f"[来源 {i+1}] {doc.page_content}"
        for i, doc in enumerate(docs)
    )

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

response = rag_chain.invoke("如何配置 RAG 的 chunk_size?")
print(response)

7.2 上下文窗口管理

当检索结果总长度超过模型的上下文窗口时,需要策略性地压缩:

# 使用 LangChain 的上下文压缩
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# LLM 提取器:只保留与问题相关的部分
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vectorstore.as_retriever()
)

compressed_docs = compression_retriever.invoke(query)
for doc in compressed_docs:
    print(f"压缩后长度: {len(doc.page_content)} 字符")

# 使用 token 计数器确保不超出上下文
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")

context_text = format_docs(compressed_docs)
token_count = len(enc.encode(context_text))
max_context_tokens = 120000  # GPT-4o 128K 的 93%(留余量给 system + response)

if token_count > max_context_tokens:
    print(f"警告: 上下文过大 ({token_count} tokens),需要进一步压缩")

7.3 引用溯源(Citation)

生产环境的 RAG 必须可溯源:

# 为每个检索文档分配唯一 ID
docs_with_ids = []
for i, doc in enumerate(retrieved_docs):
    doc.metadata["citation_id"] = f"src_{i+1}"
    docs_with_ids.append(doc)

# 提示中要求引用
citation_prompt = """
回答中请用 [src_N] 格式标注每条引用。
在回答末尾列出所有引用来源。

上下文: {context}
问题: {question}
"""

# 回答示例效果:
# 「RAG 系统的 chunk_size 通常设置在 300-1000 之间 [src_1][src_3]。
#  过小会导致语义不完整 [src_2]。」

八、高级 RAG 模式

基础 RAG 是「检索 → 生成」的单向流程,高级模式引入了反思、路由和迭代机制。

8.1 Self-RAG:自我反思的 RAG

Self-RAG 让模型在生成过程中自我评估是否需要检索,以及检索到的内容是否相关。

核心流程分为三步:

  1. 决策阶段:判断问题是否需要检索外部知识
  2. 检索 + 生成阶段:如需检索,执行检索并生成回答
  3. 反思阶段:评估生成的回答是否与检索内容一致
# Self-RAG 的简化实现
from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

class RetrievalDecision(BaseModel):
    need_retrieval: bool = Field(description="是否需要检索外部知识")
    reason: str = Field(description="决策理由")

parser = PydanticOutputParser(RetrievalDecision)

# 第一步:判断是否需要检索
decision_prompt = """判断以下问题是否需要检索外部知识。
如果问题涉及实时信息、专业知识或具体事实 -> 需要检索。
如果是通用问候、简单计算或常识性问题 -> 不需要检索。

问题: {question}
{format_instructions}"""

# 第二步:根据决策执行检索或直接回答
# 第三步:生成后评估答案是否与检索到的内容一致(自检)

8.2 Agentic RAG:多步推理检索

复杂问题需要分解为子问题,逐步检索、逐步推理:

# 使用 LangGraph 构建 Agentic RAG
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator

class AgentState(TypedDict):
    question: str
    sub_questions: list[str]
    retrieved_docs: Annotated[list, operator.add]
    answer: str
    iteration: int

def decompose_question(state):
    """将复杂问题分解为子问题"""
    prompt = f"""将以下问题分解为2-4个独立的子问题,每个子问题可以单独检索回答。
问题: {state['question']}
子问题(一行一个):"""
    sub_qs = llm.invoke(prompt).content.strip().split("\n")
    return {"sub_questions": [q.strip("- ") for q in sub_qs if q.strip()]}

def retrieve_for_sub_question(state):
    """为每个子问题检索相关内容"""
    idx = state.get("iteration", 0)
    if idx < len(state["sub_questions"]):
        sub_q = state["sub_questions"][idx]
        docs = vectorstore.similarity_search(sub_q, k=3)
        return {"retrieved_docs": docs, "iteration": idx + 1}
    return {"iteration": idx + 1}

# 构建 Graph
workflow = StateGraph(AgentState)
workflow.add_node("decompose", decompose_question)
workflow.add_node("retrieve", retrieve_for_sub_question)
# ...(连接的边和回答生成逻辑)

8.3 Graph RAG:知识图谱增强检索

微软提出的 Graph RAG 结合了知识图谱的结构化关系与向量检索的语义匹配能力,特别适合回答需要全局理解的问题。

传统 RAG 流程:

问题 → 向量检索 → 获取相关文本块 → 生成

Graph RAG 流程:

问题 → 向量检索(语义匹配)+ 图谱查询(关系推理)→ 融合结果 → 生成

核心思路:从知识图谱中提取实体关系,构建「社区摘要」(Community Summaries),让模型在回答全局性问题时能引用更高层次的洞察。

适用场景对比

问题类型 传统 RAG Graph RAG 示例
事实查询 ✅ 效果好 过度设计 「CEO 是谁?」
局部检索 ✅ 效果好 ✅ 也可以 「年假政策是什么?」
全局聚合 ❌ 效果差 ✅ 效果好 「研发投入的整体趋势?」
关系推理 ❌ 做不到 ✅ 效果好 「A 部门和 B 部门的协作关系?」

Graph RAG 的代价是构建成本更高(需要实体识别 + 图谱构建),建议在传统 RAG 无法满足需求时再引入。

8.4 自适应 RAG(Adaptive RAG)

根据问题复杂度动态选择检索策略:

def classify_complexity(question):
    """分类问题复杂度,选择不同策略"""
    prompt = f"""将以下问题分类为以下三种之一。
simple: 简单事实查询(如「什么是 Python?」)
medium: 需要多段信息回答(如「Python 和 Java 的主要区别」)
complex: 需要多步推理(如「公司 Q3 财报对明年研发预算的影响」)

问题: {question}
分类:"""
    
    result = llm.invoke(prompt).content.strip().lower()
    
    strategies = {
        "simple": {"k": 3, "rerank": False, "hyde": False},
        "medium": {"k": 10, "rerank": True, "hyde": False},
        "complex": {"k": 20, "rerank": True, "hyde": True, "sub_questions": True}
    }
    return strategies.get(result, strategies["medium"])

常见问题(FAQ)

Q1: chunk_size 设多大最合适?

没有「万能数字」,但可以参考以下经验:

嵌入模型 推荐 chunk_size 说明
BGE-M3(1024 维) 500-800 字符 模型 token 限制 8192,chunk 不宜过长
text-embedding-3-small 300-500 tokens 小模型对长文本的表示能力有限
text-embedding-3-large 500-1000 tokens 大模型可以处理更长的上下文

黄金法则:每个 chunk 应包含一个完整的思想单元(一个段落、一个定义、一段代码示例),而不是强行按固定长度截断。用 chunk_overlap=chunk_size/6 作为起点,根据检索效果微调。

Q2: 为什么我的 RAG 检索不到相关文档?

常见原因及排查顺序:

  1. 嵌入模型不匹配:中文数据用了英文优化的嵌入模型 → 换成 BGE-M3
  2. chunk_size 过大或过小:过大包含噪声,过小丢失语义 → 尝试 300、500、800 三组对比实验
  3. 查询格式不一致:用户口语化提问 vs 文档正式表述 → 使用 HyDE 或查询改写
  4. 向量数据库索引参数不当:HNSW 的 M 和 efConstruction 参数影响召回 → M=16, efConstruction=200 是比较稳妥的起点

Q3: 如何估算 RAG 系统的成本?

以一个中等规模的知识库(10 万文档块)为例:

环节 模型 单价 每次查询成本
查询嵌入 text-embedding-3-small $0.02/1M tokens ~$0.000002
向量检索 Milvus HNSW 免费(本地) $0
重排序 bge-reranker-v2-m3 免费(本地) $0
生成回答 GPT-4o-mini $0.15/1M tokens ~$0.00015
合计 - - ~$0.00015/次

按每天 1000 次查询计算,月度成本约 $4.50。即使用 GPT-4o 也仅约 $30/月。

Q4: RAG 和长上下文模型到底怎么选?

场景 推荐方案 理由
少量文档、频繁全文查询 长上下文模型 省去索引构建成本
海量文档、精准片段查询 RAG 成本低、延迟低
需要引用溯源 RAG 天然支持文档引用
跨文档综合分析 RAG + 长上下文 先检索再全量分析

实战建议:先上 RAG,如果发现检索经常遗漏关键信息,再用长上下文模型做「兜底」——RAG 检索到的片段 + 原始文档的前后文一起送入模型。

Q5: 多语言 RAG 怎么做?

核心挑战:用户用中文问,知识库是英文的。解决方案:

  1. 多语言嵌入模型:使用 BGE-M3(支持 100+ 语言)或 multilingual-e5-large
  2. 查询翻译:将用户查询翻译为知识库语言后再检索
  3. 双路检索:同时对原始查询和翻译后查询进行检索,结果合并
# 多语言 RAG 的双路检索策略
from deep_translator import GoogleTranslator

query_cn = "公司的年假政策是什么?"
query_en = GoogleTranslator(source='zh-CN', target='en').translate(query_cn)

# 双语检索,结果合并去重
docs_cn = vectorstore.similarity_search(query_cn, k=3)
docs_en = vectorstore.similarity_search(query_en, k=3)

all_docs = docs_cn + docs_en
# 去重逻辑...

Q6: 如何处理表格和图片等非文本内容?

对于表格,最佳实践是保留 Markdown 表格格式,而不是转为纯文本——嵌入模型对结构化文本的理解更好:

原始 PDF 表格 → 解析为 Markdown 表格 → 嵌入
| 产品 | Q1 收入 | Q2 收入 | 增长率 |
|:---|:---|:---|:---|
| 产品 A | 100 万 | 150 万 | 50% |

对于图片,使用多模态嵌入模型(如 Jina CLIP)或单独构建图片描述索引。


配图

九、评估体系:如何量化 RAG 质量

没有评估的优化是盲目的。RAG 评估需要从三个维度入手。

9.1 三大评估维度

维度 指标 含义 工具
检索质量 Hit Rate, MRR, NDCG 正确文档是否在 Top-K 中 Ragas, TruLens
生成质量 Faithfulness, Answer Relevancy 回答是否忠于上下文 Ragas, DeepEval
端到端质量 人工评分, LLM-as-Judge 整体满意度 人工评审

9.2 使用 Ragas 进行评估

# pip install ragas datasets
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

# 准备评估数据
eval_data = {
    "question": [
        "公司的年假政策是什么?",
        "如何申请报销?",
        "CEO 是谁?"
    ],
    "answer": [
        "根据员工手册第 3 章,年假为每年 15 个工作日...",
        "报销流程:1. 填写报销单 2. 部门审批...",
        "根据 2024 年组织架构,CEO 是张三..."
    ],
    "contexts": [
        ["员工享有每年 15 个工作日的带薪年假..."],
        ["报销需填写《费用报销单》并经部门经理审批..."],
        ["2024 年董事会决议,任命张三为 CEO..."]
    ],
    "ground_truth": [
        "年假为 15 个工作日",
        "填写报销单 -> 部门审批 -> 财务审核",
        "CEO 是张三"
    ]
}

dataset = Dataset.from_dict(eval_data)
results = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)

print("RAG 评估结果:")
for metric, score in results.items():
    bar = "#" * int(score * 20)
    print(f"  {metric:25s}: {score:.3f} {bar}")

9.3 RAGAS 指标详解

  • Faithfulness(忠实度):回答中有多少陈述可以在上下文中找到依据。值域 [0, 1],低于 0.7 需要检查幻觉问题
  • Context Precision(上下文精确度):Top-K 检索结果中有多少是真正相关的。值域 [0, 1],低于 0.5 需要优化检索策略
  • Answer Relevancy(回答相关性):回答与问题的相关程度。值域 [0, 1],低分意味着回答跑题或废话太多

十、生产部署清单

将 RAG 系统推向生产需要关注以下方面:

10.1 架构选型 Checklist

组件 推荐方案 备选方案
向量数据库 Milvus(高 QPS)/ Qdrant(中等规模) Chroma(原型验证)
嵌入模型 BGE-M3(中文)/ text-embedding-3-large(英文) multilingual-e5-large(混合场景)
LLM GPT-4o(精度)/ Claude 3.5 Sonnet GPT-4o-mini(低成本)/ Llama 3(合规)
缓存 Redis(查询缓存 + 向量缓存) -
API 网关 Nginx + rate limiting Kong

10.2 性能优化

RAG 系统的性能瓶颈通常不在 LLM 生成,而在检索阶段。以下是实战中验证过的优化手段。

10.2.1 批量编码减少 API 往返

# 优化技巧 1:批量编码减少 API 往返
query_embeddings = model.encode(queries, batch_size=16)

10.2.2 结果缓存策略

# 优化技巧 2:结果缓存
from functools import lru_cache
import hashlib

@lru_cache(maxsize=1000)
def cached_retrieve(query_hash: str, k: int):
    """缓存检索结果,相同查询直接返回"""
    return vectorstore.similarity_search(query, k=k)

缓存的三个层级:

层级 缓存内容 过期策略 命中率提升
L1: 查询缓存 query -> 检索结果 TTL 1小时 30-40%(热点查询)
L2: 嵌入缓存 text -> embedding vector 永久(内容不变) 20-30%(重复文档)
L3: 生成缓存 query+context -> answer TTL 24小时 10-15%(高频问答)

10.2.3 向量索引调优

HNSW 是最常用的向量索引算法,两个关键参数直接影响性能:

参数 含义 推荐值 调大效果 调小效果
M 每个节点的最大连接数 16-32 召回提升、构建变慢 内存减少、召回下降
efConstruction 构建时的搜索宽度 200-500 索引质量高、构建慢 构建快、召回下降
ef(查询时) 搜索时的候选集大小 64-256 召回提升、延迟增加 延迟降低、召回下降

⚠️ 生产环境建议:M=16, efConstruction=200 是成本与效果的最佳平衡点。ef(查询时)设为 64 用于一般场景,精度要求高时提升到 128。

10.2.4 异步 RAG 流水线

# 优化技巧 3:异步处理
import asyncio

async def async_rag_pipeline(query: str):
    # 并行执行检索和查询变换
    retrieval_task = asyncio.create_task(async_retrieve(query))
    rewrite_task = asyncio.create_task(async_rewrite_query(query))
    
    docs, rewritten_query = await asyncio.gather(retrieval_task, rewrite_task)
    return generate_response(rewritten_query, docs)

10.2.5 延迟优化全貌

优化手段 延迟降低 实现难度 副作用
查询缓存 -90%(命中时) 需要缓存失效策略
嵌入批量处理 -60%(批量场景) 需要积累请求
索引参数调优 -30% 可能影响召回
模型量化(INT8) -40% 精度微降
异步流水线 -50%(端到端) 需要改造架构

10.3 监控指标

关键监控维度:

指标 阈值 告警动作
检索延迟 P99 < 500ms 扩容向量数据库
生成延迟 P99 < 3000ms 切换更快模型 / 加缓存
最高相似度 > 0.6 低于阈值检查 embedding 质量
Token 消耗 每日预算内 超预算降级为小模型
回答忠实度 > 0.8 低于阈值检查幻觉问题

10.4 安全与合规

  • 数据脱敏:检索返回的内容中过滤 PII(身份证号、手机号等)
  • 权限控制:不同用户只能检索其有权访问的文档
  • 审计日志:记录每次查询、检索结果和生成回答
  • 内容过滤:对生成的回答进行敏感词检查
  • Rate Limiting:防止恶意高频查询消耗 token 预算

十一、总结与展望

核心要点回顾

  1. 分块是基石:chunk_size 和 overlap 的选择直接影响检索质量。chunk 应包含完整的思想单元而非硬截断,建议用 Parent Document Retriever 模式同时保证检索精度和生成完整性
  2. 混合检索是标配:纯向量检索有盲区,向量 + BM25 关键词 + Cross-Encoder 重排序的三层检索架构是最佳实践,Top-5 命中率可从 72% 提升至 91%
  3. 嵌入模型选择:中文场景首选 BGE-M3(1024 维,MTEB 中文榜首,本地免费),成本敏感用 OpenAI text-embedding-3-small。务必在目标数据上做基准测试,不要盲信排行榜分数
  4. 评估不能少:用 Ragas 的 Faithfulness、Context Precision、Answer Relevancy 三个指标量化质量,建立评估基线后再优化,避免凭感觉调参
  5. 生产要监控:检索延迟 P99 < 500ms、生成延迟 P99 < 3s、最高相似度 > 0.6、回答忠实度 > 0.8——四个关键阈值必须实时监控并设置告警
  6. 成本可控:以 GPT-4o-mini 为例,单次查询成本仅约 $0.00015,每日千次查询月度不到 $5。瓶颈不在 LLM 生成,而在检索链路优化

🎯 一条最重要的建议:RAG 系统的质量取决于最弱的一环。嵌入模型、分块策略、检索算法、提示模板——任何一环出问题都会拖累最终效果。因此,先跑通全流程,建立评估基线,再有针对性地优化瓶颈环节,是所有成功 RAG 项目的共同经验。

技术趋势展望

  • Agentic RAG:通过多步推理和工具调用,让 RAG 能处理越来越复杂的开放式问题
  • Multimodal RAG:不仅检索文本,还能检索图片、表格、音视频等多模态内容
  • Streaming RAG:边检索边生成,降低首字延迟,提升用户体验
  • Federated RAG:跨多个知识库联合检索,同时保护数据隐私
  • Corrective RAG:检索后自检文档相关性,不相关则自动触发网络搜索或重新检索

初学者入门路线图

如果你刚开始学习 RAG,建议按以下路径循序渐进:

阶段 目标 推荐工具 预计时间
第 1 周:Hello RAG 用 LangChain + Chroma 跑通基本流程 LangChain, Chroma, OpenAI 3-5 天
第 2 周:深入理解 替换不同嵌入模型、向量数据库,对比效果 BGE-M3, Qdrant, Milvus 5-7 天
第 3 周:检索优化 实现混合检索、重排序、查询变换 BM25, Cross-Encoder, HyDE 5-7 天
第 4 周:评估体系 用 Ragas 建立评估流程,量化优化效果 Ragas, DeepEval 3-5 天
第 5 周:生产部署 添加缓存、监控、Rate Limiting,准备上线 Redis, Prometheus 5-7 天

推荐资源

RAG 技术栈正在快速演进。建议保持对 LangChain、LlamaIndex、Milvus 等核心框架的关注,并在实际项目中逐步引入高级模式。从简单的 Naive RAG 开始,逐步迭代优化,远比追求一步到位更务实。 🚀


本文由 MarkShareX AI 自动创作,分类:AI,方向:RAG 实战