RAG 实战指南:从入门到工业级落地

5分钟跑通第一个系统,边做边理解原理,逐步走向生产可用


目录

  1. 5分钟快速上手
  2. 实战中的典型问题与优化
  3. 核心原理拆解
  4. 性能优化策略
  5. 工业级实现

1. 5分钟快速上手

1.1 安装依赖

pip install langchain langchain-community faiss-cpu sentence-transformers openai

这几个包构成了 RAG 系统的核心:

  • langchain:编排框架
  • faiss-cpu:向量搜索引擎(Facebook 开源)
  • sentence-transformers:Embedding 模型
  • openai:LLM 客户端(兼容 Ollama)

1.2 最小可用代码

创建 quick_start.py

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

documents = [
    "公司年假政策:入职第一年享有5天年假,第二年起每年增加1天。",
    "报销流程:员工需在费用发生后30天内提交报销申请。",
    "远程办公规定:每周最多可申请3天远程办公。",
]

embeddings = HuggingFaceEmbeddings(model_name="shibing624/text2vec-base-chinese")
vectorstore = Chroma.from_texts(documents, embeddings)

bm25_retriever = BM25Retriever.from_texts(documents)
bm25_retriever.k = 3

vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6]
)

question = "我入职两年了,有几天年假?"
docs = retriever.invoke(question)

print("检索结果:")
for doc in docs:
    print(f"- {doc.page_content}")

运行后输出:

检索结果:
- 公司年假政策:入职第一年享有5天年假,第二年起每年增加1天。

到这里,你已经完成了一个完整的检索流程。但有几个设计选择值得思考:

为什么同时使用 BM25 和向量检索?

如果只用向量检索,问"报销需要多少天内提交?"时,模型可能忽略"30天"这个具体数字。BM25 通过关键词匹配能精准捕捉这类信息。

反过来,如果只用 BM25,问"年假怎么算?"时,由于文档中没有"怎么算"这个词,就匹配不到。向量检索能通过语义理解召回相关文档。

两者结合,互补优势。weights=[0.4, 0.6] 表示 BM25 占 40%,向量占 60%,这个比例可以根据实际效果调整。


1.3 接入 LLM 生成答案

from openai import OpenAI

client = OpenAI(api_key="ollama", base_url="http://localhost:11434/v1")

context = "\n".join([doc.page_content for doc in docs])

prompt = f"""根据以下公司政策,简洁准确地回答问题:

政策内容:
{context}

问题:{question}

回答:"""

response = client.chat.completions.create(
    model="deepseek-r1:7b",
    messages=[{"role": "user", "content": prompt}]
)

print("\nAI回答:", response.choices[0].message.content)

输出:

AI回答:你入职两年,应该有6天年假(第一年5天 + 第二年增加1天)。

这里的关键是 Prompt 的设计。如果不提供政策上下文,LLM 只能基于训练数据回答,可能会编造信息。把检索到的文档作为上下文注入,能让回答有据可依。


2. 实战中的典型问题与优化

问题1:检索精度不够

当文档变长或问题更复杂时,简单的检索可能失效。

原因分析:

  1. 切片策略不当:如果把整篇文档作为一个 chunk,信息密度太低;如果切得太碎,又可能切断语义。

  2. 模型能力限制:基础的 text2vec 模型对中文语义的理解有限。

  3. 单一检索方式的局限:纯向量检索对精确关键词不敏感,纯 BM25 无法处理语义。

优化方案:递归字符分割

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ",", " ", ""]
)

chunks = splitter.split_text(long_document)

这个分割器的策略是:优先按段落(\n\n)分割,如果块还太大,再按换行(\n),然后按句号、逗号,最后才按字符硬切。这样能在保持语义完整的同时控制 chunk 大小。

chunk_overlap=50 的作用是保留上下文。假设一个句子被切成两半,重叠部分能确保两个 chunk 都包含完整语义。


问题2:大规模文档检索慢

Chroma 默认使用暴力搜索,文档量达到 10 万+ 时,查询可能超过 500ms。

解决方案:FAISS + HNSW 索引

from langchain_community.vectorstores import FAISS

vectorstore = FAISS.from_documents(chunks, embeddings)
vectorstore.save_local("faiss_index")

# 后续直接加载
vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)

FAISS 使用 HNSW(Hierarchical Navigable Small World)索引,这是一种多层图结构。搜索时从顶层开始快速定位大致区域,逐层向下缩小范围,最后在底层做精确匹配。时间复杂度从 O(N) 降到 O(log N)。

性能对比(10万向量):

方法 查询耗时
Chroma(暴力搜索) ~500ms
FAISS HNSW ~5ms

另外,save_local 会把索引持久化到磁盘,下次启动直接加载,避免重复 Embedding。


问题3:内存占用高

768 维 float32 向量在大规模场景下非常吃内存。100 万向量约需 3GB。

解决方案:向量量化

import numpy as np

# FP16 量化:内存减半,精度损失 <1%
vectors_fp16 = np.array(vectors, dtype=np.float16)

# INT8 量化:内存降至 1/4,精度损失 <5%
from faiss import IndexIVFPQ
index = IndexIVFPQ(quantizer, dimension, nlist, m, 8)

量化的本质是降低数值精度。对于语义相似度任务,0.1mm 的误差通常不影响结果,但能大幅减少内存占用。


3. 核心原理拆解

3.1 混合检索的设计逻辑

前面提到了 BM25 和向量检索的结合,现在深入看它们的工作原理。

BM25 的核心:IDF(逆文档频率)

BM25 给每个词计算一个权重,罕见词的权重更高。公式中的 IDF 部分:

I D F ( t ) = log ⁡ N − n + 0.5 n + 0.5 + 1 IDF(t) = \log\frac{N - n + 0.5}{n + 0.5} + 1 IDF(t)=logn+0.5Nn+0.5+1

其中 N 是文档总数,n 是包含词 t 的文档数。

如果"报销"只出现在 1 个文档中,IDF 值会很高;如果"的"出现在所有文档中,IDF 值就很低。这让 BM25 能自动识别关键词的重要性。

向量检索的核心:语义空间

Embedding 模型把文本映射到高维向量空间,语义相近的文本在空间中距离更近。这个能力来自预训练阶段的两个任务:

  1. Masked Language Model (MLM):随机遮挡词,让模型预测。例如"公司年假[MASK]策…",模型必须理解"年假"和"政策"的关联才能猜对。

  2. Next Sentence Prediction:判断两个句子是否连贯。这教会模型理解话题一致性和逻辑关系。

经过亿级文本训练后,模型学会了把语义映射到几何空间中。

为什么融合有效?

BM25 擅长精确匹配(关键词、数字、专有名词),向量检索擅长语义理解(同义词、paraphrase)。两者结合能覆盖更多场景。

权重的选择(0.4 vs 0.6)取决于你的数据特点:

  • 专业术语多、数字密集 → 提高 BM25 权重
  • 语义多样、表达灵活 → 提高向量权重

可以通过 A/B 测试找到最优比例。


3.2 向量化全流程

以"公司年假政策规定员工入职第二年享有6天年假"为例,看看文本如何变成向量。

Step 1: Tokenization

[CLS] 公 司 年 假 政 策 规 定 员 工 入 职 第 二 年 享 有 6 天 年 假 [SEP]

[CLS][SEP] 是特殊标记,标识句子的开始和结束。BERT 类模型使用 WordPiece 算法分词,常用词保持完整,罕见词拆分成子词。

Step 2: Embedding Lookup

每个 token 通过查表得到初始向量:

"公" → [0.12, -0.45, 0.78, ..., 0.33]  (768维)
"司" → [-0.08, 0.34, -0.56, ..., 0.21]
...

这些向量最初是随机初始化的,但经过训练后,语义相近的词向量也会相近。

Step 3: Transformer Encoder

这是最关键的部分。模型堆叠了 12 层 Transformer,每层的核心是 Self-Attention(自注意力机制)。

Self-Attention 让每个词"关注"句子中的其他词,计算相关性权重。例如:

"年假" → {"政策": 0.92, "享有": 0.85, "天数": 0.78, ...}
"政策" → {"年假": 0.90, "规定": 0.75, ...}

通过这种方式,模型能捕捉词与词之间的关系。多层堆叠后,浅层学习语法结构,中层学习短语组合,深层学习语义逻辑。

Multi-Head 的意思是多个"注意力头"并行工作,每个头关注不同的方面(语法、语义、情感等),最后合并结果。

Step 4: Pooling

把所有 token 的向量聚合成一个句子向量。常用方法:

  • Mean Pooling:求平均
  • CLS Token:取 [CLS] 位置的向量
  • 加权池化:根据注意力权重加权

最终得到一个 768 维的稠密向量。


3.3 余弦相似度的选择

检索时,我们计算查询向量与所有文档向量的余弦相似度

cos ⁡ ( θ ) = A ⋅ B ∣ ∣ A ∣ ∣ × ∣ ∣ B ∣ ∣ \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{||\mathbf{A}|| \times ||\mathbf{B}||} cos(θ)=∣∣A∣∣×∣∣B∣∣AB

为什么不直接用欧氏距离?

考虑两个向量:

vec1 = [1, 2, 3]        # 短句
vec2 = [10, 20, 30]     # 长句(语义相同,长度放大10倍)

欧氏距离会很大(因为长度不同),但余弦相似度 = 1.0(方向完全一致)。

文本向量的长度受句子长短影响,但我们关心的是语义方向。余弦相似度通过除以模长,消除了长度的影响,只保留方向信息。

取值范围 [-1, 1],文本相似度通常在 0.6-0.95 之间。


3.4 模型选型参考

模型 维度 C-MTEB 特点
BAAI/bge-large-zh-v1.5 1024 ~65.7+ 中文最强,推荐首选
BAAI/bge-m3 1024 优秀 多语言支持好
text2vec-base-chinese 768 中等 轻量级,快速上手
gte-Qwen2-7B-instruct 3584 顶级 极致性能,资源消耗大

MTEB(Massive Text Embedding Benchmark)是权威的评测榜单,分数越高代表语义理解能力越强。

维度不是越高越好。768-1024 是性价比最高的区间,再高会显著增加计算成本和内存占用。


4. 性能优化策略

4.1 批量 Embedding

# 串行:10万文档需要 1.4 小时
for chunk in chunks:
    vector = embeddings.embed_query(chunk)

# 批量:只需 5 分钟
texts = [chunk.page_content for chunk in chunks]
vectors = embeddings.embed_documents(texts)

批量处理能充分利用 GPU/CPU 的并行能力,提速 15-20 倍。


4.2 查询缓存

from functools import lru_cache

@lru_cache(maxsize=1000)
def cached_search(question: str):
    return retriever.invoke(question)

FAQ 场景中很多问题会重复出现,缓存能显著降低延迟。


4.3 重排序(Re-ranking)

向量检索召回的 Top 10 中可能混入不相关文档。可以用 Cross-Encoder 精排:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker

compressor = CrossEncoderReranker(model_name="BAAI/bge-reranker-base", top_n=5)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=ensemble_retriever
)

Bi-Encoder(向量模型)快速但粗糙,Cross-Encoder 慢但精确。先用 Bi-Encoder 召回 Top 50,再用 Cross-Encoder 精排 Top 5,精度可提升 10-20%。


4.4 性能监控

import time

class PerformanceMonitor:
    def __init__(self):
        self.timings = {}
    
    def timer(self, name):
        def decorator(func):
            def wrapper(*args, **kwargs):
                start = time.time()
                result = func(*args, **kwargs)
                self.timings[name] = time.time() - start
                print(f"[{name}] {self.timings[name]:.4f}s")
                return result
            return wrapper
        return decorator
    
    def report(self):
        for op, duration in self.timings.items():
            print(f"{op}: {duration:.4f}s")

监控能帮助定位瓶颈,指导优化方向。


5. 工业级实现

5.1 模块化架构

config.py          # 配置管理
embedder.py        # Embedding 封装
vector_store.py    # FAISS 管理 + 持久化
retriever.py       # 混合检索 + 重排序
rag_system.py      # 主接口

5.2 生产配置建议

组件 推荐方案
Embedding BAAI/bge-large-zh-v1.5
向量库 FAISS HNSW
Chunk 400-600 字 + 50-100 重叠
检索权重 BM25 0.3-0.5 + 向量 0.5-0.7
重排序 BAAI/bge-reranker-base

5.3 进阶方向

  • 元数据过滤:按类别、日期筛选
  • 查询改写:LLM 生成多个查询版本
  • 多路召回:BM25 + 向量 + 关键词
  • 分布式部署:Milvus / Qdrant 支持亿级向量

总结

RAG 系统的核心在于三个环节的配合:

  1. 切片策略:决定信息粒度
  2. 向量模型:决定语义理解能力
  3. 检索策略:决定召回率和精度

建议的学习路径:

  1. 先跑通最小示例
  2. 遇到实际问题(精度、速度)
  3. 理解背后的原理
  4. 逐步替换更强组件
  5. 建立监控和评估体系

参考资料


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐