《RAG 五阶段详解:从原理到实践》

1 阅读8分钟

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. 要求标注引用来源(可解释性)

    请在回答末尾标注信息来源,如"来源:文档 1"
    
  2. 处理"检索不到"的情况(避免幻觉)

    如果检索内容与问题无关,请回答"抱歉,我没有找到相关信息"
    
  3. 控制回答长度(避免啰嗦)

    回答控制在 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 设计标注来源,处理未知

关键洞察

  1. 重排是最值得投入的优化——200-500ms 延迟换取 20%+ 准确率
  2. 分片策略没有标准答案——按文档类型调整
  3. 先跑通再优化——不要在选择工具上浪费太多时间

我的下一步计划

  • 测试不同重排模型的效果对比
  • 实现混合检索(向量 + 关键词)
  • 用 RAGAS 评估整体效果

互动

你在 RAG 实践中遇到过什么问题?

欢迎评论区交流,我会逐一回复!


参考资料