主题
第 2 章 · RAG 工作流程详解:从文档到答案的完整旅程
🎯 本章目标:详细拆解 RAG 的每一步是怎么运转的,让你能给同事画出完整流程图,并解释每一步的"为什么"。
目录
- 整体流程鸟瞰
- 离线阶段(Indexing):把书搬进图书馆
- 在线阶段(Retrieval):图书管理员找书
- 在线阶段(Generation):基于书写答案
- 完整时序图与代码骨架
- 生活类比:开一家"AI 律师事务所"
- 常见问题与思考
1. 整体流程鸟瞰
RAG 系统分两个阶段:离线建库 和 在线问答。
💡 关键认知:
- 离线阶段 = 准备工作(搬书入库),用户感受不到
- 在线阶段 = 用户使用,要求快、准、稳
2. 离线阶段 Indexing
🎯 目的:把人类可读的文档,转换成机器可检索的格式。
2.1 步骤一:文档加载(Loading)
做什么?
把各种格式的文档读进内存,统一变成纯文本。
真实场景的输入
企业知识库通常是大杂烩:
| 格式 | 例子 | 难度 |
|---|---|---|
.txt / .md | 普通文本 | ⭐ 简单 |
.pdf | 论文、手册 | ⭐⭐⭐ 中等(涉及版面、扫描件 OCR) |
.docx | Word 文档 | ⭐⭐ 中等 |
.html | 网页 | ⭐⭐ 中等(要去广告/导航) |
.xlsx / .csv | 表格 | ⭐⭐⭐ 中等(结构信息易丢失) |
.pptx | 幻灯片 | ⭐⭐⭐ 难 |
| 图片 + OCR | 扫描件、截图 | ⭐⭐⭐⭐ 难 |
| 数据库表 | MySQL/MongoDB | ⭐⭐⭐ 中等 |
常用工具
| 工具 | 特点 |
|---|---|
LangChain Document Loaders | 内置 100+ 加载器 |
unstructured | 处理脏数据强 |
LlamaParse | LlamaIndex 出品,PDF 解析强 |
pdfplumber / PyMuPDF | PDF 专用 |
BeautifulSoup | 解析 HTML |
输出
一堆 Document 对象,每个包含:
Document {
content: "这里是文档的正文文本...",
metadata: {
source: "员工手册v3.2.pdf",
page: 47,
section: "报销流程",
update_time: "2024-09-01"
}
}💡 关键经验:metadata 千万别省! 后面 Rerank、过滤、引用追溯全靠它。一开始没加,后面想加要重新入库,巨痛苦。
2.2 步骤二:文档切分(Chunking)
为什么必须切?
| 原因 | 解释 |
|---|---|
| 大模型 Context 有限 | 4K、8K、128K,再大也装不下整本书 |
| 检索精度 | 一整本书都算 "相关",等于啥都没匹配上 |
| 成本 | Token 越长,调用 LLM 越贵 |
| 召回准确性 | 切碎了才能精准定位"哪一段"和问题相关 |
切分策略概览(详见第 3 章)
| 策略 | 思路 | 适合 |
|---|---|---|
| 固定长度 | 每 500 字符切一刀 | 简单文本 |
| 递归字符切分 | 优先按段落→句子→词切 | 通用,最常用 |
| 按文档结构 | 按章节/Markdown 标题切 | 结构化文档 |
| 语义切分 | 按句子语义相似度切 | 长篇连续文本 |
| 基于 Token | 按 Token 数切(防止超限) | 严格的 Context 控制 |
一个关键超参:Chunk Overlap(重叠)
原文:「猫咪发烧 39 度不算严重 / 但需要观察是否伴随食欲不振 / 若 24 小时未恢复要送医」
无重叠切分:
Chunk 1: 「猫咪发烧 39 度不算严重」
Chunk 2: 「但需要观察是否伴随食欲不振」
Chunk 3: 「若 24 小时未恢复要送医」
有重叠切分(重叠 1 句):
Chunk 1: 「猫咪发烧 39 度不算严重 / 但需要观察是否伴随食欲不振」
Chunk 2: 「但需要观察是否伴随食欲不振 / 若 24 小时未恢复要送医」💡 作用:避免关键信息被切断在两个 Chunk 之间。 经验值:Overlap 一般取 Chunk 长度的 10%-20%。
2.3 步骤三:向量化(Embedding)
做什么?
把每个文本 Chunk 转成一个高维向量(一串浮点数)。
形象理解
Embedding 模型就是 "文本翻译机":
- 输入:人类的文字("猫咪发烧")
- 输出:机器能算距离的数字(
[0.12, -0.45, 0.78, ..., 0.33],通常 384-3072 维)
为什么需要向量?
因为计算机不懂语义,但计算机会算两个向量之间的距离。
如果设计得好,语义相近的文本,向量距离也近:
"猫咪发烧" → [0.12, -0.45, 0.78, ...]
"小猫体温升高" → [0.13, -0.43, 0.79, ...] ← 几乎一样
"今天股市大跌" → [-0.55, 0.21, -0.30, ...] ← 差很远主流 Embedding 模型对比
| 模型 | 维度 | 语言 | 价格 | 特点 |
|---|---|---|---|---|
text-embedding-3-small (OpenAI) | 1536 | 多语 | $0.02/M Token | 性价比高 |
text-embedding-3-large (OpenAI) | 3072 | 多语 | $0.13/M Token | 效果好 |
BGE-M3 (BAAI) | 1024 | 多语 | 免费开源 | 中文 SOTA,支持长文本 |
BGE-Large-zh-v1.5 | 1024 | 中文 | 免费开源 | 纯中文场景王者 |
Cohere embed-v3 | 1024 | 多语 | $0.10/M Token | 检索优化 |
Qwen-Embedding (阿里) | 1024 | 中文 | 免费/付费 | 中文优秀 |
2.4 步骤四:存入向量数据库
为什么需要专门的向量数据库?
传统数据库(MySQL)擅长精确匹配(WHERE id = 123),但不擅长 "找最相似的 K 个"。
向量数据库专门针对 "近邻搜索(ANN, Approximate Nearest Neighbor)" 优化,能在亿级向量中毫秒级找出 Top-K。
主流向量数据库对比
| 数据库 | 类型 | 特点 | 适合 |
|---|---|---|---|
| Milvus | 独立 | 工业级、分布式 | 大规模生产 |
| Qdrant | 独立 | Rust 写、高性能 | 中小规模生产 |
| Weaviate | 独立 | 内置混合检索 | 生产 |
| Chroma | 嵌入式 | 极简、本地开发 | Demo / 小项目 |
| Pinecone | SaaS | 全托管 | 不想自己运维 |
| pgvector | PostgreSQL 插件 | 复用 PG 生态 | 已有 PG 的项目 |
| Elasticsearch | 搜索引擎 | 内置向量字段 | 已用 ES 的项目 |
| Redis Stack | 缓存数据库 | 内置 vector 模块 | 轻量场景 |
存储结构示意
向量库里每条记录长这样:
{
id: "doc_001_chunk_15",
vector: [0.12, -0.45, ...],
metadata: {
text: "猫咪退烧的家庭处理方法:保持环境凉爽...",
source: "宠物手册.pdf",
page: 12,
chapter: "急救"
}
}2.5 离线阶段小结
| 阶段 | 输入 | 输出 | 关键决策 |
|---|---|---|---|
| 加载 | 文件 | 纯文本 + metadata | 用什么 Loader |
| 切分 | 纯文本 | 文本片段 | Chunk 大小、重叠、策略 |
| 嵌入 | 文本片段 | 向量 | 选哪个 Embedding 模型 |
| 入库 | 向量 + 元数据 | 数据库记录 | 选哪个向量库 |
3. 在线阶段 Retrieval
🎯 目的:用户每次提问时,毫秒级从向量库中找出最相关的 K 个片段。
3.1 步骤一:问题预处理(可选但重要)
用户原始问题可能不利于检索,先做处理。
常见处理
| 处理 | 例子 | 解决的问题 |
|---|---|---|
| 去除停用词 | "请问 猫咪 发烧 怎么办" → "猫咪 发烧 怎么办" | 减少噪声 |
| 拼写纠正 | "猫米" → "猫咪" | 用户打错字 |
| 意图识别 | "什么是 RAG" → 知识查询;"帮我订机票" → 工具调用 | 分流处理 |
| 多轮指代消解 | 上文 "我家有只布偶猫" + 当前 "它生病了" → "我家布偶猫生病了" | 多轮对话 |
| 查询改写(Query Rewrite) | "39 度" → "猫咪 39 度 是否正常 发烧" | 补全上下文 |
💡 简单系统可以省略这一步,但高质量系统几乎都会做。
3.2 步骤二:问题向量化
把用户问题用和入库时同一个 Embedding 模型转成向量。
"我家猫咪发烧 39 度怎么办?"
↓ Embedding
[0.15, -0.41, 0.80, ...]⚠️ 重要原则:入库时用什么模型,查询时必须用同一个模型。 否则两个向量空间不一致,检索结果会一塌糊涂。
3.3 步骤三:相似度检索
什么是 "相似度"?
两个向量越接近,相似度越高。常用度量:
| 度量方法 | 公式直觉 | 适用 |
|---|---|---|
| 余弦相似度(Cosine) | 看两个向量方向夹角,0 度最像 | 最常用,对长度不敏感 |
| 欧氏距离(L2) | 看两个点之间直线距离 | 适合归一化向量 |
| 点积(Dot Product) | 综合方向和长度 | 推荐系统多用 |
Top-K 是什么?
不是只返回 1 个最相关的,而是返回 K 个最相关的(K 通常取 3-10)。
为什么?
- 单个 Chunk 可能信息不全
- 多个 Chunk 互相印证,大模型回答更全面
- 防止单个 Chunk 检索不准
一个具体例子
用户问题:「我家猫咪发烧 39 度怎么办?」
问题向量:[0.15, -0.41, 0.80, ...]
向量库中所有 Chunk 和问题向量算相似度,取 Top-5:
Rank | Chunk ID | 相似度 | 内容预览
-----|-----------|--------|------------------
1 | chunk_15 | 0.92 | "猫咪退烧的家庭处理方法..."
2 | chunk_03 | 0.89 | "猫咪发烧的常见原因..."
3 | chunk_23 | 0.85 | "什么情况下需要立即送医..."
4 | chunk_87 | 0.81 | "宠物正常体温范围..."
5 | chunk_44 | 0.79 | "猫咪疫苗副反应..."算法层面(ANN 简介)
朴素做法:遍历每个 Chunk 算相似度,O(N) 太慢。
向量数据库用 ANN(近似最近邻) 算法加速:
| 算法 | 思路 |
|---|---|
| HNSW | 多层图导航,最流行 |
| IVF | 先聚类再搜索 |
| PQ | 向量压缩存储 |
| DiskANN | 海量数据 + SSD |
💡 小白只需知道:向量库会用 ANN 算法,把 O(N) 优化到接近 O(log N),亿级向量也能毫秒查询。
3.4 步骤四:Rerank 重排(可选但强烈推荐)
为什么需要 Rerank?
向量检索是粗排,速度快但精度有限。Rerank 是精排,用更强但更慢的模型再筛一遍。
生活类比
你想买相机,先在淘宝按 "相机" 关键词搜出 100 个(粗排),然后看每个的详情、评论、参数(精排),最后选出 5 个加购物车。
- 粗排:靠关键词/向量匹配,快
- 精排:仔细对比每个商品和你的需求,慢但准
主流 Rerank 模型
| 模型 | 类型 | 特点 |
|---|---|---|
| Cohere Rerank | API | 易用,效果好 |
| BGE-Reranker | 开源 | 中文友好 |
| bge-reranker-v2-m3 | 开源多语 | 当前 SOTA |
| Jina Rerank | API | 多语支持 |
3.5 在线检索阶段小结
4. 在线阶段 Generation
🎯 目的:把检索到的片段和用户问题一起喂给大模型,生成最终答案。
4.1 步骤一:组装 Prompt
把"问题"和"召回片段"按模板组装成完整 Prompt。
一个经典 Prompt 模板
你是一个专业的宠物医疗问答助手。
请严格根据以下【参考资料】回答用户问题。
【规则】
1. 答案必须来自参考资料,不要使用资料外的知识。
2. 若资料中没有相关信息,请回答"根据已有资料无法回答该问题"。
3. 在答案末尾标注引用的资料编号,例如 [1][3]。
4. 保持回答简洁专业。
【参考资料】
[1] {chunk_15_content}
[2] {chunk_03_content}
[3] {chunk_23_content}
[4] {chunk_87_content}
[5] {chunk_44_content}
【用户问题】
{user_question}
【你的回答】Prompt 设计的关键技巧
| 技巧 | 作用 |
|---|---|
| 明确角色 | "你是 XX 专家" 让回答更聚焦 |
| 明确规则 | "不要使用资料外的知识" 减少幻觉 |
| 明确兜底 | "无法回答时这样说" 避免硬编 |
| 明确引用 | "末尾标注 [1][3]" 便于追溯 |
| 结构清晰 | 用【】、---、Markdown 分隔不同部分 |
| Few-shot 示例 | 给一个 "好回答" 的例子 |
4.2 步骤二:LLM 生成
把组装好的 Prompt 调用大模型 API:
POST /v1/chat/completions
{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "你是宠物医疗助手..."},
{"role": "user", "content": "组装好的完整 Prompt"}
],
"temperature": 0.1 // 低温度,更确定
}💡 温度(Temperature)建议:
- RAG 场景一般用 0.0-0.3,让回答忠于事实,不要发挥
- 创意场景才用 0.7-1.0
4.3 步骤三:后处理
常见后处理
| 处理 | 作用 |
|---|---|
| 引用标注 | 把 [1][3] 替换成实际文档链接和页码 |
| 敏感词过滤 | 去除可能的敏感内容 |
| 格式美化 | Markdown 渲染、列表对齐 |
| 流式输出 | 边生成边返回,提升体验 |
| 日志记录 | 记录问题、召回、回答用于调优 |
4.4 生成阶段小结
5. 完整时序图与代码骨架
5.1 端到端时序图
5.2 伪代码骨架(不是真实代码,便于理解)
python
# ============== 离线阶段 ==============
def index_documents(file_paths):
for path in file_paths:
text = load_document(path)
chunks = split_text(text, chunk_size=500, overlap=50)
for chunk in chunks:
vector = embed(chunk.text)
vector_db.insert(
id=chunk.id,
vector=vector,
metadata={"text": chunk.text, "source": path}
)
# ============== 在线阶段 ==============
def answer_question(user_query):
# 1. 预处理
query = preprocess(user_query)
# 2. 向量化
query_vector = embed(query)
# 3. 粗排:向量检索
candidates = vector_db.search(query_vector, top_k=50)
# 4. 精排:Rerank
top_chunks = rerank(query, candidates, top_k=5)
# 5. 组装 Prompt
context = "\n".join([f"[{i+1}] {c.text}" for i, c in enumerate(top_chunks)])
prompt = PROMPT_TEMPLATE.format(context=context, question=query)
# 6. LLM 生成
answer = llm.generate(prompt, temperature=0.1)
# 7. 后处理
answer_with_refs = add_citations(answer, top_chunks)
return answer_with_refs6. 生活类比:开一家"AI 律师事务所"
用一个统一的生活类比,让你彻底记住整个流程。
6.1 设定
你想开一家 "AI 律师事务所",能自动回答用户的法律问题。
| RAG 组件 | 律所对应 |
|---|---|
| 原始文档 | 全国法律法规、判决案例 |
| Loader | 实习生(把书本扫描进电脑) |
| Splitter | 律师助理(把厚厚的法典拆成一个个条款卡片) |
| Embedding | 资深律师(给每张卡片打 "主题标签") |
| Vector DB | 律所的智能档案柜 |
| Retriever | 接案的接待律师(先找出可能相关的卡片) |
| Reranker | 主审律师(再精挑细选最相关的) |
| LLM | 总律师(基于精选案例,给客户写答复) |
| Prompt | 答复模板 |
6.2 客户问问题的全流程
👤 客户:"我邻居装修噪音影响我休息,能告他吗?"
🏢 接待律师(Retrieval):
1. 看到关键词 "噪音"、"邻居"、"诉讼"
2. 从档案柜抽出 50 张可能相关的卡片
(包括《噪声污染防治法》、相关判例等)
⚖️ 主审律师(Rerank):
1. 仔细看完 50 张卡片
2. 排除不太相关的(如"工业噪音")
3. 留下 5 张最相关的(关于"住宅噪音"、"民事诉讼")
📝 总律师(Generation):
1. 看完 5 张卡片
2. 结合客户具体情况,组织答复:
"根据《噪声污染防治法》第 X 条和《环境噪音排放标准》,
若装修噪音超过 55 分贝,您可以..."
3. 答复末尾标注引用:[条款 X、判例 Y]
📤 答复发给客户6.3 为什么这个比喻有效?
| 类比点 | 含义 |
|---|---|
| 接待律师不读完所有卡片 | 向量检索快速粗排 |
| 主审律师精挑细选 | Rerank 精排 |
| 总律师只读最相关的 5 张 | 喂给 LLM 的 Context 是精选过的 |
| 答复必须引用法条 | RAG 要追溯来源 |
| 法典更新了重新归档 | Indexing 是定期的 |
7. 常见问题与思考
7.1 容易混淆的概念
Q1: Retrieval 和 Search 是一回事吗?
几乎是。Retrieval 偏学术用词,Search 偏工程用词。 在 RAG 语境下两者通用,"信息检索(Information Retrieval, IR)" 是这个领域的学术名称。
Q2: Embedding 模型和 LLM 是同一个吗?
不是。
- Embedding 模型 是专门用来生成向量的(如 BGE、text-embedding-3)
- LLM 是用来生成文本回答的(如 GPT-4、Claude) 两者完全独立,可以自由组合。
Q3: 向量数据库和搜索引擎有什么区别?
| 传统搜索引擎(ES) | 向量数据库 | |
|---|---|---|
| 检索方式 | 关键词匹配(BM25) | 语义匹配(向量相似度) |
| 优势 | 精确匹配术语强 | 理解语义、同义词、跨语言 |
| 例子 | "Python" 必须出现 "Python" | "蟒蛇编程" 也能匹配到 "Python" |
💡 工业级 RAG 通常 两者结合(混合检索),详见第 4 章。
7.2 思考题
如果用户问的问题在知识库里完全没有相关资料,会发生什么?应该如何处理?
参考答案
- 默认情况:向量检索会强行返回 Top-K,即使相似度都很低。大模型可能基于不相关片段瞎答(甚至幻觉)。
- 正确做法:
- 设置 相似度阈值(例如 < 0.6 视为无相关结果)
- Prompt 中加 "若资料不相关请明确回答不知道"
- 高级方案:用 CRAG(纠正性 RAG) 检测召回质量,自动 fallback 到 Web 搜索
检索到 Top-5 后,为什么不直接把 5 个片段拼成一个长字符串喂给 LLM?
参考答案
实际就是这么做的,但要注意:
- 加分隔符(
[1][2]或---),让 LLM 区分不同片段 - 加引用编号,便于回答中追溯
- 按相关性排序(最相关的放最前 / 最后,受 "Lost in the Middle" 影响)
- 如果片段太长,要截断或二次切分
- 加分隔符(
如果同一个问题,每次 LLM 给出的答案都不一样,是 Bug 吗?
参考答案
不一定是 Bug,可能原因:
- Temperature 不为 0:LLM 本来就有随机性
- 检索结果不稳定:Embedding 浮点计算有微小差异,可能导致 Top-K 不一样
- LLM 服务端版本变化:大厂会悄悄更新模型
修复:
temperature=0或seed=固定值- 检索阶段做 结果缓存(同样问题命中缓存直接返回)
- 答案稳定性测试纳入回归
📌 本章小结
| 你应该已经知道 | ✅ |
|---|---|
| RAG 离线和在线两个阶段分别做什么 | □ |
| Indexing 的四步(加载、切分、嵌入、入库) | □ |
| 为什么必须切分,以及 Chunk Overlap 的作用 | □ |
| Embedding 是什么、为什么需要 | □ |
| 向量数据库与传统数据库的区别 | □ |
| 检索阶段的 Top-K 和 Rerank 的区别 | □ |
| 一个标准的 RAG Prompt 模板包含哪些要素 | □ |
| 能画出 RAG 完整时序图 | □ |
🚀 下一步
你已经理解了 RAG 的整个流程框架,下一章深入两个最关键的细节技术:怎么切分 和 怎么向量化。