Skip to content

第 2 章 · RAG 工作流程详解:从文档到答案的完整旅程

🎯 本章目标:详细拆解 RAG 的每一步是怎么运转的,让你能给同事画出完整流程图,并解释每一步的"为什么"。


目录

  1. 整体流程鸟瞰
  2. 离线阶段(Indexing):把书搬进图书馆
  3. 在线阶段(Retrieval):图书管理员找书
  4. 在线阶段(Generation):基于书写答案
  5. 完整时序图与代码骨架
  6. 生活类比:开一家"AI 律师事务所"
  7. 常见问题与思考

1. 整体流程鸟瞰

RAG 系统分两个阶段:离线建库在线问答

💡 关键认知

  • 离线阶段 = 准备工作(搬书入库),用户感受不到
  • 在线阶段 = 用户使用,要求快、准、稳

2. 离线阶段 Indexing

🎯 目的:把人类可读的文档,转换成机器可检索的格式。

2.1 步骤一:文档加载(Loading)

做什么?

把各种格式的文档读进内存,统一变成纯文本。

真实场景的输入

企业知识库通常是大杂烩:

格式例子难度
.txt / .md普通文本⭐ 简单
.pdf论文、手册⭐⭐⭐ 中等(涉及版面、扫描件 OCR)
.docxWord 文档⭐⭐ 中等
.html网页⭐⭐ 中等(要去广告/导航)
.xlsx / .csv表格⭐⭐⭐ 中等(结构信息易丢失)
.pptx幻灯片⭐⭐⭐ 难
图片 + OCR扫描件、截图⭐⭐⭐⭐ 难
数据库表MySQL/MongoDB⭐⭐⭐ 中等

常用工具

工具特点
LangChain Document Loaders内置 100+ 加载器
unstructured处理脏数据强
LlamaParseLlamaIndex 出品,PDF 解析强
pdfplumber / PyMuPDFPDF 专用
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.51024中文免费开源纯中文场景王者
Cohere embed-v31024多语$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 / 小项目
PineconeSaaS全托管不想自己运维
pgvectorPostgreSQL 插件复用 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 RerankAPI易用,效果好
BGE-Reranker开源中文友好
bge-reranker-v2-m3开源多语当前 SOTA
Jina RerankAPI多语支持

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_refs

6. 生活类比:开一家"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 思考题

  1. 如果用户问的问题在知识库里完全没有相关资料,会发生什么?应该如何处理?

    参考答案
    • 默认情况:向量检索会强行返回 Top-K,即使相似度都很低。大模型可能基于不相关片段瞎答(甚至幻觉)。
    • 正确做法:
      1. 设置 相似度阈值(例如 < 0.6 视为无相关结果)
      2. Prompt 中加 "若资料不相关请明确回答不知道"
      3. 高级方案:用 CRAG(纠正性 RAG) 检测召回质量,自动 fallback 到 Web 搜索
  2. 检索到 Top-5 后,为什么不直接把 5 个片段拼成一个长字符串喂给 LLM?

    参考答案

    实际就是这么做的,但要注意:

    • 加分隔符([1] [2]---),让 LLM 区分不同片段
    • 加引用编号,便于回答中追溯
    • 按相关性排序(最相关的放最前 / 最后,受 "Lost in the Middle" 影响)
    • 如果片段太长,要截断或二次切分
  3. 如果同一个问题,每次 LLM 给出的答案都不一样,是 Bug 吗?

    参考答案

    不一定是 Bug,可能原因:

    • Temperature 不为 0:LLM 本来就有随机性
    • 检索结果不稳定:Embedding 浮点计算有微小差异,可能导致 Top-K 不一样
    • LLM 服务端版本变化:大厂会悄悄更新模型

    修复:

    • temperature=0seed=固定值
    • 检索阶段做 结果缓存(同样问题命中缓存直接返回)
    • 答案稳定性测试纳入回归

📌 本章小结

你应该已经知道
RAG 离线和在线两个阶段分别做什么
Indexing 的四步(加载、切分、嵌入、入库)
为什么必须切分,以及 Chunk Overlap 的作用
Embedding 是什么、为什么需要
向量数据库与传统数据库的区别
检索阶段的 Top-K 和 Rerank 的区别
一个标准的 RAG Prompt 模板包含哪些要素
能画出 RAG 完整时序图

🚀 下一步

你已经理解了 RAG 的整个流程框架,下一章深入两个最关键的细节技术:怎么切分怎么向量化

👉 第 3 章 · 文档切分与向量化