AI学习-RAG 实战指南:从入门到工业级落地
RAG 实战指南:从入门到工业级落地
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:检索精度不够
当文档变长或问题更复杂时,简单的检索可能失效。
原因分析:
-
切片策略不当:如果把整篇文档作为一个 chunk,信息密度太低;如果切得太碎,又可能切断语义。
-
模型能力限制:基础的 text2vec 模型对中文语义的理解有限。
-
单一检索方式的局限:纯向量检索对精确关键词不敏感,纯 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.5N−n+0.5+1
其中 N 是文档总数,n 是包含词 t 的文档数。
如果"报销"只出现在 1 个文档中,IDF 值会很高;如果"的"出现在所有文档中,IDF 值就很低。这让 BM25 能自动识别关键词的重要性。
向量检索的核心:语义空间
Embedding 模型把文本映射到高维向量空间,语义相近的文本在空间中距离更近。这个能力来自预训练阶段的两个任务:
-
Masked Language Model (MLM):随机遮挡词,让模型预测。例如"公司年假[MASK]策…",模型必须理解"年假"和"政策"的关联才能猜对。
-
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∣∣A⋅B
为什么不直接用欧氏距离?
考虑两个向量:
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 系统的核心在于三个环节的配合:
- 切片策略:决定信息粒度
- 向量模型:决定语义理解能力
- 检索策略:决定召回率和精度
建议的学习路径:
- 先跑通最小示例
- 遇到实际问题(精度、速度)
- 理解背后的原理
- 逐步替换更强组件
- 建立监控和评估体系
参考资料
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)