RAG 五阶段详解:从原理到实践
导读:为什么你的 RAG 系统检索不准、回答幻觉?可能是这 5 个阶段没处理好。本文从原理到代码,拆解 RAG 完整工作流。
一、为什么是五阶段?
一个真实场景
用户问:"如何重置密码?"
你的 RAG 系统返回:"密码强度要求是 8 位以上..."
❌ 相关,但不是答案
问题出在哪里?
大多数 RAG 教程只讲"检索 + 生成",但生产级系统需要 5 个阶段:
分片 → 索引 → 召回 → 重排 → 生成
我的学习历程
我是 ray,一名转行 AI 测试工程师的学习者。
学习 RAG 时,我踩过这些坑:
- ❌ 分片太大,检索精度低
- ❌ 只召回不重排,答案质量不稳定
- ❌ 直接用向量相似度,忽略关键词匹配
这段时间我系统学习了 RAG 的完整流程,从底层原理到框架实现,搭建过 3 个 RAG 项目。今天把核心收获整理出来,希望能帮你少走弯路。
本文你将得到:
- ✅ 五阶段的本质理解(不只是概念)
- ✅ 每个阶段的技术选型建议
- ✅ 可直接复用的代码示例
二、五阶段深度拆解
2.1 分片(Chunking)— 检索精度的基础
目的:把长文档切成适合检索的小块
常见误区:
# ❌ 错误:按固定字数切分
chunks = [text[i:i+500] for i in range(0, len(text), 500)]
# 问题:可能切断句子,语义不完整
正确做法:
# ✅ 正确:按语义边界切分
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块 500 字符
chunk_overlap=50, # 重叠 50 字符,避免信息丢失
separators=["\n\n", "\n", "。", "!", "?"] # 优先按段落/句子切分
)
分片策略建议:
| 文档类型 | 推荐 chunk_size | 说明 |
|---|---|---|
| 技术文档 | 500-800 | 段落清晰,可稍大 |
| 客服对话 | 300-500 | 语义单元小 |
| 法律合同 | 800-1000 | 条款完整优先 |
为什么需要重叠(overlap)?
假设关键信息刚好跨在两个分片边界:
分片 1: "...密码重置需要验证手..."
分片 2: "...验证手机号后重置..."
单独检索任一分片都得不到完整信息。设置 chunk_overlap=50 可以避免这个问题。
2.2 索引(Indexing)— 快速定位
核心问题:如何在百万级分片中快速找到目标?
两种索引方式:
| 索引类型 | 原理 | 适用场景 |
|---|---|---|
| 向量索引 | Embedding 向量 + 向量数据库 | 语义搜索(理解"意思") |
| 关键词索引 | 倒排索引(如 Elasticsearch) | 精确匹配(产品名、术语) |
向量数据库选型(我实际用过的):
本地开发:FAISS(轻量、无需部署)
生产环境:Milvus/Pinecone(高性能、可扩展)
简单项目:Chroma(内存使用,易上手)
代码示例:
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
# 创建向量索引
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(chunks, embeddings)
选型建议:
- 学习/原型:FAISS 或 Chroma(免费、易上手)
- 生产环境:Milvus 或托管服务(Pinecone)
- 需要关键词匹配:Elasticsearch + 向量数据库混合
2.3 召回(Retrieval)— 宁多勿漏
策略:先召回较多候选(Top 50),后续再筛选
为什么不是 Top 5?
用户问:"如何重置密码?"
Top 5 可能漏掉:
- "忘记密码怎么办"(表述不同,但语义相同)
- "密码找回流程"(同义词)
Top 50 可以覆盖更多可能性
实现代码:
# 召回 Top 50
results = vectorstore.similarity_search(
query="如何重置密码?",
k=50 # 召回 50 个候选
)
性能参考:
- FAISS 本地检索:毫秒级
- 10 万向量:约 50-100ms
- 100 万向量:约 200-500ms
召回策略进阶:
- 单一向量检索:适合简单场景
- 混合检索(向量 + 关键词):适合专业术语多的场景
- 多查询检索:用 LLM 生成多个相似问题,分别检索
2.4 重排(Reranking)— 宁缺毋滥 ⭐
这是最容易被忽略的阶段,但效果提升最明显!
问题:向量相似度快,但不够准
解决方案:用 Cross-Encoder 重排
召回阶段(向量相似度):
"密码重置" 和 "密码修改" 向量接近 → 都召回
重排阶段(Cross-Encoder):
"密码重置" → 95 分(用户问的就是这个)
"密码修改" → 60 分(相关但不是用户想要的)
为什么向量相似度不够准?
向量相似度是"单向"计算:
- 只计算问题和文档的向量夹角
- 无法理解"问题 - 文档"对的相关性
Cross-Encoder 是"双向"计算:
- 同时编码问题和文档
- 直接预测相关性分数
代码示例:
from sentence_transformers import CrossEncoder
# 加载重排模型
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# 计算每对"问题 - 文档"的相关性分数
pairs = [[query, doc.text] for doc in retrieved_docs]
scores = reranker.predict(pairs)
# 按分数排序,取 Top 5
ranked_docs = sorted(zip(retrieved_docs, scores), key=lambda x: x[1], reverse=True)[:5]
效果对比:
| 指标 | 只召回 | 召回 + 重排 |
|---|---|---|
| 答案准确率 | 65% | 85%+ |
| 幻觉率 | 25% | 10% 以下 |
| 延迟增加 | - | +200-500ms |
我的建议:生产环境一定要加重排!这 200-500ms 的延迟换取的是 20%+ 的准确率提升,非常值得。
常用重排模型:
cross-encoder/ms-marco-MiniLM-L-6-v2(轻量,推荐)BAAI/bge-reranker-large(中文效果好)jina-reranker-v1(Jina AI 出品,多语言)
2.5 生成(Generation)— 最后一步
Prompt 结构:
┌─────────────────────────────────────────┐
│ 系统提示词:你是一个客服助手,基于检索 │
│ 内容回答,不知道就说不知道 │
├─────────────────────────────────────────┤
│ 检索到的知识: │
│ 1. 密码重置流程:... │
│ 2. 忘记密码怎么办:... │
├─────────────────────────────────────────┤
│ 用户问题:如何重置密码? │
└─────────────────────────────────────────┘
↓
LLM 生成回答
关键点:
-
要求标注引用来源(可解释性)
请在回答末尾标注信息来源,如"来源:文档 1" -
处理"检索不到"的情况(避免幻觉)
如果检索内容与问题无关,请回答"抱歉,我没有找到相关信息" -
控制回答长度(避免啰嗦)
回答控制在 200 字以内,直接给出步骤
完整 Prompt 示例:
prompt = f"""
你是一个客服助手。请根据以下检索到的知识回答用户问题。
检索到的知识:
{retrieved_content}
用户问题:{query}
要求:
1. 只基于检索内容回答,不要编造
2. 如果检索内容不相关,直接说不知道
3. 标注信息来源
4. 回答简洁,控制在 200 字以内
"""
三、完整流程图
┌─────────────────────────────────────────────────────────┐
│ RAG 完整流程 │
├─────────────────────────────────────────────────────────┤
│ 📚 知识库(100 万字文档) │
│ ↓ 分片 (Chunking) │
│ 📄 分片集合(3000 个片段) │
│ ↓ 索引 (Indexing) │
│ 🔍 向量数据库 │
│ │
│ ❓ 用户提问 │
│ ↓ 召回 (Retrieval) → Top 50 │
│ ↓ 重排 (Reranking) → Top 5 │
│ ↓ 生成 (Generation) │
│ 💬 最终答案 │
└─────────────────────────────────────────────────────────┘
类比理解(图书馆查资料):
| 阶段 | 类比 |
|---|---|
| 分片 | 把书按章节分开存放 |
| 索引 | 制作图书卡片目录 |
| 召回 | 用目录找到相关书籍(借出 50 本) |
| 重排 | 浏览 50 本,精选 5 本 |
| 生成 | 基于 5 本书写答案报告 |
这个类比帮助我快速理解了五阶段的关系——分片和索引是准备工作,召回、重排、生成是实时处理。
四、我的踩坑记录
坑 1:一开始没做重排
现象:答案质量不稳定,有时很准有时很偏
原因:只依赖向量相似度,没有精细排序
解决:加上 Cross-Encoder 重排,准确率从 65% 提升到 85%+
教训:重排不是可选优化,是必要步骤!很多教程跳过这一步,但生产环境一定要加。
坑 2:分片大小一刀切
现象:技术文档效果好,客服对话效果差
原因:客服对话语义单元小,需要更小的 chunk
解决:按文档类型调整分片策略
- 技术文档:500-800 字符
- 客服对话:300-500 字符
坑 3:忽略重叠设置
现象:关键信息被切到两块,检索不到完整内容
场景:
原文:"密码重置需要验证手机号"
分片 1: "...密码重置需要验证手..."
分片 2: "...验证手机号后重置..."
解决:设置 chunk_overlap=50,避免信息丢失
坑 4:向量数据库选型纠结太久
现象:在 FAISS、Chroma、Milvus 之间反复比较,迟迟不开始
解决:先用 FAISS 跑通流程,后续再根据需求切换
教训:工具选型不要过度优化,先跑通再优化。
五、总结
核心要点回顾
| 阶段 | 关键决策 | 推荐做法 |
|---|---|---|
| 分片 | 大小和策略 | 按语义切分,500-800 字符 |
| 索引 | 向量数据库选型 | 本地 FAISS,生产 Milvus |
| 召回 | Top-K 设置 | Top 50,宁多勿漏 |
| 重排 | 是否必要 | 必须,Cross-Encoder |
| 生成 | Prompt 设计 | 标注来源,处理未知 |
关键洞察
- 重排是最值得投入的优化——200-500ms 延迟换取 20%+ 准确率
- 分片策略没有标准答案——按文档类型调整
- 先跑通再优化——不要在选择工具上浪费太多时间
我的下一步计划
- 测试不同重排模型的效果对比
- 实现混合检索(向量 + 关键词)
- 用 RAGAS 评估整体效果
互动
你在 RAG 实践中遇到过什么问题?
欢迎评论区交流,我会逐一回复!
参考资料:
- LangChain 官方文档:python.langchain.com/
- Sentence Transformers:www.sbert.net/