🏠 首页 攻略 RAG 进阶实战:从基础到生产级应用的完整指南

RAG 进阶实战:从基础到生产级应用的完整指南

RAG 不仅仅是把文档分块扔进向量数据库。本文深入讲解生产级 RAG 系统的关键技术:检索增强策略、重排序优化、索引构建和性能调优。

RAG 进阶实战:从基础到生产级应用的完整指南

你搭过 RAG 系统吗?

简单版本:把文档切块,存入向量数据库,用 embedding 模型做相似度检索,然后把检索结果和用户的 prompt 一起喂给大模型。

听起来很简单,对吧?但当你真正把它用到生产环境,问题就来了:

  • 检索结果相关性差,答非所问
  • 响应速度慢,用户体验糟糕
  • 长文档处理效果差,关键信息遗漏
  • 不知道哪里出了问题,调试困难

今天这篇,帮你从"能跑"进阶到"好用"。


一、检索质量提升:不只是余弦相似度

问题:余弦相似度的局限

传统的向量检索使用余弦相似度,计算的是向量之间的夹角。但这有个问题:

余弦相似度只关注方向,不关注绝对距离。两个向量即使夹角很小,内容可能并不相关。

方案一:混合检索

结合多种检索方式:

from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OpenAIEmbeddings
from langchain.retrievers import BM25Retriever

# 向量检索
vector_retriever = Chroma(
    embedding_function=OpenAIEmbeddings(),
    collection_name="docs"
).as_retriever(search_kwargs={"k": 5})

# 关键词检索(BM25)
bm25_retriever = BM25Retriever.from_documents(documents)

# 混合检索
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.7, 0.3]
)

混合检索结合了语义理解和关键词匹配的优势。对于技术文档、代码搜索等场景,效果明显更好。

方案二:元数据过滤

在检索时加入元数据过滤,可以大幅提高准确性:

# 检索时指定元数据过滤
retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={
        "k": 5,
        "filter": {
            "source": "user_manual",
            "language": "zh",
            "date_range": {"$gte": "2025-01-01"}
        }
    }
)

比如用户问"如何重置密码",你可以先过滤出"用户手册"类型的文档,排除掉"开发文档"。

方案三:重排序(Rerank)

检索返回的结果需要进一步排序。可以用重排序模型来提升准确性:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# 加载重排序模型
cross_encoder = HuggingFaceCrossEncoder(
    model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"
)

# 创建压缩检索器
compressor = CrossEncoderReranker(encoder=cross_encoder, top_n=3)
compression_retriever = ContextualCompressionRetriever(
    base_retriever=vector_retriever,
    document_compressors=[compressor]
)

重排序模型会重新评估检索结果的排序,把最相关的文档排在前面。


二、文档处理优化:切块的艺术

问题:切块方式影响检索效果

切块太粗,检索结果不够精确。切块太细,上下文信息丢失。

方案一:按语义切块

不是简单地按字符数切分,而是按语义边界切分:

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,      # 每个块的大小
    chunk_overlap=100,   # 块之间的重叠
    separators=["\n\n", "\n", "。", " ", ""],  # 按这些分隔符切分
    length_function=len
)

RecursiveCharacterTextSplitter 会尝试按你指定的分隔符依次切分,优先按段落切,其次按句子切。

方案二:父子索引(Parent-Child Indexing)

这是生产环境常用的技巧:

  1. 把文档切分成小块(子文档)
  2. 检索时返回小块
  3. 但实际传给大模型的是小块的父文档(更大的上下文)
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 小粒度切分用于检索
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400,
    chunk_overlap=50
)

# 大粒度切分用于返回上下文
parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1200,
    chunk_overlap=200
)

# 先用子文档切分检索,再映射回父文档

这样既能精确检索,又能提供足够的上下文。

方案三:元数据增强

给每个文本块添加元数据,帮助检索器理解内容:

from langchain.docstore.document import Document

doc = Document(
    page_content="如何重置密码...",
    metadata={
        "section": "用户帮助",
        "topic": "账户安全",
        "priority": "high",
        "author": "support_team"
    }
)

检索时可以用这些元数据做加权。


三、索引构建最佳实践

批量处理大文档

处理大文档时,不要一次性加载:

from langchain_community.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 分批次加载和处理
loader = DirectoryLoader(
    "./documents",
    glob="**/*.md",
    show_progress=True
)

all_documents = loader.load()

# 分批处理
batch_size = 100
for i in range(0, len(all_documents), batch_size):
    batch = all_documents[i:i + batch_size]
    chunks = splitter.split_documents(batch)
    # 分批添加到向量数据库
    vector_store.add_documents(chunks)
    print(f"Processed {i + len(batch)}/{len(all_documents)} documents")

增量更新

不要每次都重新索引全部文档:

import hashlib
from datetime import datetime

def compute_doc_hash(content):
    return hashlib.md5(content.encode()).hexdigest()

# 检查文档是否变更
if doc_hash not in existing_hashes:
    # 只更新变更的文档
    update_document(doc)

索引监控

定期检查索引质量:

# 检查向量数据库状态
stats = vector_store.get_stats()
print(f"总文档数: {stats['total_documents']}")
print(f"总向量数: {stats['total_vectors']}")

# 检查最近检索的准确率(需要人工标注)

四、性能优化

检索加速

  1. 使用本地向量数据库(如 Chroma、Qdrant)而非在线服务
  2. 启用 HNSW 索引(近似最近邻搜索)
  3. 缓存检索结果
from functools import lru_cache
import hashlib

@lru_cache(maxsize=1000)
def cached_retrieve(query: str, top_k: int = 5):
    # 查询缓存
    query_hash = hashlib.md5(query.encode()).hexdigest()
    return vector_store.similarity_search_with_score(query, k=top_k)

流式输出

不要等所有检索结果都返回再生成回答:

from langchain.callbacks import AsyncIteratorCallbackHandler

async def stream_response(query):
    callback = AsyncIteratorCallbackHandler()
    
    # 异步检索和生成
    task = asyncio.create_task(
        chain.ainvoke({"question": query}, config={"callbacks": [callback]})
    )
    
    # 流式输出
    async for token in callback.aiter():
        yield token

批量 Embedding

# 批量生成 embedding,减少 API 调用次数
embeddings = embedding_model.embed_documents(
    [doc.page_content for doc in documents]
)

五、常见问题排查

问题一:检索结果不相关

排查步骤:

  1. 检查 embedding 模型是否适合你的领域
  2. 检查切块大小是否合理
  3. 检查是否有足够的训练数据来微调模型

解决方案:

# 尝试不同的切块策略
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", " ", ""]
)

# 尝试不同的 embedding 模型
from langchain.embeddings import HuggingFaceEmbeddings
embedding = HuggingFaceEmbeddings(
    model_name="thenlper/gte-large"
)

问题二:回答太长或不简洁

解决方案:

  1. 调整 prompt,要求简洁回答
  2. 限制检索的文档数量
  3. 使用摘要策略
prompt = """请根据以下上下文,简洁回答用户问题。
回答不超过3句话。

上下文:
{context}

用户问题:{question}

回答:"""

问题三:系统响应慢

排查步骤:

  1. 检查向量数据库查询性能
  2. 检查 embedding 模型推理时间
  3. 检查 LLM API 响应时间

解决方案:

  1. 使用更快的 embedding 模型(如 text-embedding-3-small)
  2. 启用向量数据库的缓存
  3. 并行检索多个索引源

六、完整生产级 RAG 架构

用户请求
    ↓
API Gateway(限流、认证)
    ↓
Query Preprocessor(查询预处理)
    ├─ 查询改写
    ├─ 实体识别
    └─ 意图分类
    ↓
Retrieval Layer(检索层)
    ├─ 向量检索
    ├─ 关键词检索
    └─ 元数据过滤
    ↓
Reranker(重排序)
    ↓
Context Builder(上下文构建)
    ├─ 父子索引映射
    └─ 冗余去重
    ↓
LLM Generator(生成层)
    ├─ Prompt 组装
    └─ 流式生成
    ↓
Output Validator(输出验证)
    ├─ 内容过滤
    └─ 格式校验
    ↓
响应返回

总结

从基础 RAG 到生产级应用,关键在于:

  1. 检索质量:混合检索 + 重排序
  2. 文档处理:语义切块 + 父子索引
  3. 索引管理:增量更新 + 监控
  4. 性能优化:缓存 + 批量处理 + 流式输出

你的 RAG 系统现在卡在哪个环节?是哪个问题困扰你最多?

AI Token 计算器 可以帮你估算 RAG 系统的 token 消耗,优化成本。