# 19 - RAG 检索增强生成

---

**本章课程目标：**

- 把 [第 2 章 RAG-搭建企业私有/个人知识库](2-RAG-搭建企业私有&个人知识库.md) 里建立的 RAG 与知识库直觉，继续推进到 LangChain 代码实现层。
- 掌握 LangChain 中构建 RAG 最常见的几类组件：**文档加载器、文本分割器、嵌入模型、向量数据库、检索器、提示词模板与聊天模型**。
- 跑通并理解本章全部案例：**多种文档加载、文本切分、Redis 向量检索、完整 RAG 智能运维助手**，把 [第 18 章 向量数据库与 Embedding 实战](18-向量数据库与Embedding实战.md) 的内容真正串成一个可落地的问答系统。

**学习建议：** RAG 最好分成两段看：离线把文档处理成可检索知识，在线根据用户问题召回并组装上下文。读本章时可以用两种颜色标出来：哪些代码属于“建库”，哪些代码属于“问答”。文档加载、切分、Embedding、向量库、Prompt 这些组件的位置清楚了，换任何框架都能读懂。

**官方文档与资源**：详见 [工具导航与参考资料索引 - RAG与向量检索](工具导航与参考资料索引.md#RAG与向量检索)。

---

## 1、RAG 简介

### 1.1 定义

> 如果你已经读过 [第 2 章](2-RAG-搭建企业私有&个人知识库.md)，那么这里可以不再从“知识库产品怎么用”重新讲起，而是直接把 RAG 放回代码语境里理解。

**RAG（Retrieval-Augmented Generation，检索增强生成）**，本质上是一种“**先检索，再生成**”的应用架构。用户提问后，系统不会立刻只靠大模型自身记忆回答，而是先从外部知识库中检索相关材料，再把这些材料与问题一起交给大模型生成答案。

到了工程实现层，本章更关心的是下面这些问题：

- 外部知识在代码里以什么对象表示
- 文档为什么要先加载、再切块、再向量化
- 向量库和检索器分别负责哪一步
- 检索结果最终怎么进入 Prompt 或消息上下文

所以可以把本章看作：**把第 2 章里的知识库问答体验，翻译成 LangChain 里的数据结构与组件流水线。**

### 1.2 RAG 的作用

这一点在 [第 2 章](2-RAG-搭建企业私有&个人知识库.md) 里已经从产品角度讲过：

RAG 主要是在解决 **知识冻结、私有知识缺失、最新信息不可用、回答缺少依据** 这类问题。

本章不再重复展开，而是把重点前移到一个更适合开发者的问题上：**既然已经知道要用 RAG，那么代码里到底是哪些环节决定了回答质量？**

从工程视角看，影响效果最明显的通常不是“有没有接上向量库”这么简单，而是：

- 文档是否被正确加载
- 文本是否被合理切块
- Embedding 是否稳定
- 检索是否召回了真正相关的片段
- Prompt 是否把上下文用对了

和其他方案的关系，仍然可以用下面这张表快速回顾：

| 方案           | 本质                   | 优势                     | 局限                                |
| -------------- | ---------------------- | ------------------------ | ----------------------------------- |
| **直接问模型** | 不接外部知识，直接生成 | 上手最快                 | 容易不知道私有 / 最新知识，幻觉较多 |
| **RAG**        | 先检索资料，再生成     | 更新快、改动小、可追溯   | 依赖文档质量、分块策略、检索效果    |
| **微调**       | 调整模型参数           | 能改变回答风格、任务习惯 | 成本高、更新慢，不适合高频改知识    |
| **RAG + 微调** | 外部知识 + 参数适配    | 兼顾知识与表达           | 成本和系统复杂度更高                |

同时也要知道，**RAG 不是没有代价的万能方案**。在真实项目里，它通常会带来几个现实权衡：

- **响应时延更高**：每次问答前都要多做一次检索，有时还会经过过滤、重排等步骤。
- **Token 消耗更高**：检索结果会进入 Prompt，召回内容越多，送给模型的上下文越长。
- **效果依赖链路质量**：文档质量、切块策略、Embedding 质量、检索效果、Prompt 约束方式，都会影响最终回答。

### 1.3 RAG 的标准流程

> 在 [第 2 章](2-RAG-搭建企业私有&个人知识库.md) 里，我们已经从产品与平台角度看过两阶段流程；这里换到 LangChain 代码视角，再把它重述一遍。

LangChain 官方文档通常也把 RAG 拆成两大阶段：**索引（Indexing）**、**检索与生成（Retrieval and Generation）**。

这不是为了重复，而是为了把“平台里的按钮和配置项”翻译成“代码里的组件与数据流”。

![RAG 完整数据流程：离线完成文档加载、切分、向量化和入库，在线完成问题检索、上下文组装与答案生成](images/19/19-1-3-1.png)

#### 1.3.1 索引阶段：先把知识库准备好

索引阶段面对的是“**原始文档**”，例如 Word、PDF、Markdown、TXT、CSV、JSON 等文件。它的目标不是回答问题，而是把这些文档处理成“**未来方便检索**”的形态。

> 你在第 2 章里看到的“上传文件、分段、建知识库”，放到代码里，基本就是这一阶段。

这一阶段通常包括：

1. **加载（Load）**：把原始文件读成 LangChain 的 `Document` 对象。
2. **分割（Split）**：把长文档切成较小的片段，便于后续向量化和检索。
3. **向量化（Embed）**：把每个文档片段转成向量。
4. **存储（Store）**：把“片段内容 + 向量 + 元数据”写入向量数据库。

![索引阶段细节示意](images/19/19-1-3-2.png)

这里有两个很容易忽略的点：

- **索引通常是离线做的**。  
  比如每天定时重建一次知识库，或在文档更新后增量写入；它不一定跟用户问答发生在同一时刻。

- **索引不只是“存文本”**。  
  它真正要存的是“**文本片段 + 向量表示 + metadata**”，这样后面才可能做相似检索、来源展示、条件过滤。

> **什么是 metadata？**  
> `metadata` 是和文档片段绑定的附加信息，例如文件路径 `source`、页码 `page`、标题、作者、日期、分类等。  
> 在 RAG 里，真正参与向量化的是正文 `page_content`；`metadata` 更多用于**来源展示、过滤条件、结果解释**。

#### 1.3.2 检索与生成阶段：每次提问时动态查资料

当用户真正发起问题时，系统进入第二阶段。这对应的就是第 2 章里“基于知识库提问 / 生成回答”的那部分能力，只不过这里我们要看清楚它在代码里究竟分成了哪几步：

1. 用户输入问题。
2. 把问题也向量化。
3. 去向量库里找最相似的文档片段。
4. 把这些片段作为 `context` 放进 Prompt。
5. 再把 `context + question` 一起发给大模型生成答案。

![检索阶段：查询向量化、相似检索、上下文组装与生成](images/19/19-1-3-3.png)

这一阶段的核心不是“再去建库”，而是“**拿已经建好的索引来查资料**”。也就是说：

- **索引阶段**：提前备好资料库。
- **检索阶段**：每次提问时现场查资料。

#### 1.3.3 管道式 RAG 与 Agent 式 RAG

本教程这一章，重点是最经典、最容易上手的 **管道式 RAG**。它与 LangChain 官方文档中的 **2-Step RAG** 是同一类思路：**先检索、再生成**，流程由代码固定，而非由模型临时决策。

它的特点很明确：每次用户提问，系统都会先检索一次；是否检索不由模型临时决定，而是由你的代码流程提前写死；整体结构通常也是“检索器 + 提示词模板 + 聊天模型”的固定流水线。

这类方案适用于：企业知识库问答、文档问答、规范 / 手册 / FAQ 检索增强。

如果要让“模型自己决定要不要检索、什么时候检索、检索几次”，那就更接近 **Agent 式 RAG**（对应官方文档中的 **Agentic RAG**）。更完整的智能体编排会在后续 [第 21 章 Agent 智能体](21-Agent智能体.md) 等章节展开；先把本章这种**两阶段、单次检索、单次生成**的主线学扎实更重要。

#### 1.3.4 一个更贴近生产环境的增强版流程

如果只是入门，先把握前面的“两阶段”即可；但从真实项目角度看，很多 RAG 系统会在“检索与生成”之间再补几步，让结果更稳。第 2 章里你已经在 Dify 里看到了 Top K、Rerank、混合检索这些参数，这里可以把它们和代码链路对上：

1. **先召回候选片段**：检索器不一定只有向量检索，也可能是关键词、混合检索。
2. **再做过滤或重排**：按 `metadata` 过滤范围，或用 reranker 对候选片段重新打分。
3. **最后再进 Prompt**：把更少但更相关的片段送给模型，而不是把一大堆噪声上下文都塞进去。
4. **生成后保留来源**：真实项目里常常会返回文件名、页码、片段来源，便于核验。
5. **持续做评测与观测**：离线看召回命中率、答案质量，线上看检索链路和生成链路谁在掉链子。

所以，**RAG 不等于“向量库 + 大模型”这么简单**。更完整的理解应该是：**数据处理、检索策略、上下文组织、答案生成、来源追踪与效果评测**共同组成了一条工程链路。

---

## 2、RAG 文本处理核心知识

### 2.1 LangChain 组件与标准流程

RAG 并不是某一个单独类就能完成的功能，它更像一条由多个组件拼起来的流水线。LangChain 的价值，正是在于它把这些组件都统一成了较一致的接口，方便我们按步骤搭起来。

本章最常见的组件分工如下：

| 组件           | 作用                                              | 常见类 / 说明                                                    |
| -------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
| **Document**   | LangChain 中统一的文档对象                        | 由 `page_content` + `metadata` 组成                              |
| **文档加载器** | 从 TXT、PDF、Word、Markdown、JSON、CSV 等读入文档 | `TextLoader`、`PyPDFLoader`、`UnstructuredWordDocumentLoader` 等 |
| **文本分割器** | 把长文档切成较小片段                              | `RecursiveCharacterTextSplitter` 最常见                          |
| **嵌入模型**   | 把文本片段转成向量                                | 本项目常见 `DashScopeEmbeddings`                                 |
| **向量数据库** | 存储向量并支持相似检索                            | Redis / RedisStack、Chroma、FAISS 等                             |
| **检索器**     | 用户提问时从向量库召回相关片段                    | 常见由 `vector_store.as_retriever()` 得到                        |
| **提示词模板** | 把检索结果和用户问题组织成 Prompt                 | `PromptTemplate`、`ChatPromptTemplate`                           |
| **聊天模型**   | 基于上下文生成最终答案                            | `ChatOpenAI`、`init_chat_model()` 等                             |

用面试里的说法，可以这样概括整个流程：

1. 用**文档加载器**把原始文件转成 `Document`。
2. 用**文本分割器**把长文档切成多个片段。
3. 用**嵌入模型**把片段转成向量，并写入**向量数据库**。
4. 用户提问时，把问题拿去做向量检索，得到相关片段。
5. 把片段作为上下文，和用户问题一起填进 Prompt。
6. 调用大模型，得到最终答案。

![RAG 从检索到生成的完整数据流](images/19/19-2-1-1.svg)

#### 2.1.1 from_documents 与 add_texts：两种常见的入库方式

你在本仓库里会同时看到两种写法：`from_documents(...)`、`add_texts(...)`

它们都能把内容写入向量库，但适合的输入形态和工程语义不一样。

| 方法                 | 更适合什么场景                      | 你手里通常有什么数据       | 常见理解                   |
| -------------------- | ----------------------------------- | -------------------------- | -------------------------- |
| **`from_documents`** | 已经完成“加载 + 分割”之后的一步入库 | `Document` 列表            | 更像“把文档片段整批建库”   |
| **`add_texts`**      | 已经有向量库实例，需要持续追加内容  | 字符串列表 + 可选 metadata | 更像“往现有索引里追加文本” |

可以把它们看作两种数据入口，并不冲突，只取决于**你手里现在是 `Document` 列表还是纯文本列表**：

- **文档流驱动**：`Loader -> Splitter -> List[Document] -> from_documents(...)`（典型 RAG 建索引）
- **纯文本流驱动**：`texts + metadata -> add_texts(...)`（接口落库、增量追加、脱离 Loader 的实验）

#### 2.1.2 结合本章案例理解

本仓库里正好有两个很典型的案例，可以把这两个方法的区别讲得很清楚。

**第一类：`from_documents`，对应完整 RAG 链路**

在综合案例 EmbeddingRagLLM.py 里，流程是：

1. 用 `Docx2txtLoader` 加载 `alibaba-java.docx`
2. 用 `CharacterTextSplitter` 切分文档
3. 得到一批切好的 `Document`
4. 调用 `Redis.from_documents(...)` 直接写入向量库

这种写法非常贴近“真正的 RAG 业务流程”，因为：

- 你的知识原本就在文档里
- 文档先经过加载和切分
- 入库时保留了 `Document` 结构
- 后续可以自然衔接 `as_retriever()`

也就是说，`from_documents(...)` 更像是“**把已经准备好的知识片段整批建成索引**”。

**第二类：`add_texts`，对应单独演示向量库存取**

在 `RedisVectorStore.py` 里，案例没有先走 Loader 和 Splitter，而是直接给出一组字符串 `texts` 与对应的 `metadata`，再按两步完成写入：

1. 先创建 `RedisVectorStore`；
2. 再调用 `add_texts(texts, metadata)` 写入。

这种写法更像是在演示：

- 向量库本身如何使用
- 纯文本如何批量入库
- 已有索引如何继续追加内容

所以它更适合拿来理解“**向量存储层怎么工作**”，也更适合做一些小规模实验、增量写入、脱离文档加载器的独立入库逻辑。

#### 2.1.3 再往后一步：检索案例和它们是什么关系

`RedisVectorStore_SimilaritySearch.py` 虽然和上面两个文件放在一起，但它所在的其实是 **RAG 的检索阶段**，不是索引阶段。

它和前面两种写入方式的关系是：

1. 先通过 `from_documents(...)` 或 `add_texts(...)` 把数据写进向量库
2. 再通过 `similarity_search_with_score(...)` 去查库

所以这三个案例可以连起来理解成：

- `RedisVectorStore.py`：怎么把文本写进去
- `RedisVectorStore_SimilaritySearch.py`：怎么把相关内容查出来
- `EmbeddingRagLLM.py`：怎么把“查出来的内容”再喂给大模型完成回答

这样看，三者就不是零散的小例子，而是 RAG 完整链路的三个片段。

#### 2.1.4 入门时怎么选

如果你是刚开始学，建议按下面的顺序理解：

1. **先把 `add_texts(...)` 看懂**  
   因为它更直接，容易理解“文本 -> 向量 -> 向量库”这件事。

2. **再看 `from_documents(...)`**  
   因为它更贴近真实 RAG 项目，能把“文档加载、文本切分、向量化入库”串起来。

3. **最后把它们放回完整案例里理解**  
   你会发现：RAG 的重点从来不只是“把内容存进去”，而是“存进去以后，如何在提问时检索出来，并和 Prompt、LLM 结合起来”。

【案例源码】`案例与源码-2-LangChain框架/10-rag/RedisVectorStore.py`（写入文本并存入 Redis）

[RedisVectorStore.py](案例与源码-2-LangChain框架/10-rag/RedisVectorStore.py ":include :type=code")

【案例源码】`案例与源码-2-LangChain框架/10-rag/RedisVectorStore_SimilaritySearch.py`（相似性检索）

[RedisVectorStore_SimilaritySearch.py](案例与源码-2-LangChain框架/10-rag/RedisVectorStore_SimilaritySearch.py ":include :type=code")

### 2.2 文档加载器（Document Loaders）

RAG 的第一步，往往不是“接模型”，而是“**把你的知识读进来**”。文档加载器的职责，就是把不同来源的数据统一转换成 LangChain 的 `Document` 格式。

LangChain 官方对文档加载器的定位很明确：它们为不同数据源提供了统一读取接口，最终都转成 `Document`。因此，无论你面对的是本地文件、企业文档平台、网页、数据库还是第三方系统，后续都能用统一方式进入切分、向量化和检索流程。

先记住两个统一接口就够了：

- `load()`：一次性加载全部文档
- `lazy_load()`：按需流式加载，适合大文件或大批量数据

#### 2.2.1 统一成 Document 的意义

`Document` 是 LangChain 在 RAG 里的基础数据结构，它通常有两个核心字段：

- `page_content`：正文内容
- `metadata`：来源、页码、文件名、分类等附加信息

有些实现或版本里，你还会看到可选的 `id` 字段，用来标识文档或文档片段。这样设计的好处是，后面的分割器、向量库、检索器都不需要关心“这段内容最初来自 PDF 还是 CSV”，它们只需要面向统一的 `Document` 处理即可。

这也是为什么你会经常看到这样的链路：

**原始文件 -> Loader -> List[Document] -> TextSplitter -> VectorStore**

![文档加载器继承关系示意](images/19/19-2-2-1.jpeg)

#### 2.2.2 如何选择加载器

选择加载器时，不要一开始就背几十个类名，先抓住两个原则：

1. **先按文件类型选**
   - TXT 用 `TextLoader`
   - PDF 用 `PyPDFLoader`
   - Word 用 `UnstructuredWordDocumentLoader` 或 `Docx2txtLoader`
   - Markdown 用 `UnstructuredMarkdownLoader`
   - JSON 用 `JSONLoader`
   - CSV 用 `CSVLoader`

2. **再按解析精度和成本选**
   - 想快速跑通案例，优先选依赖少、接口直接的加载器
   - 想保留更丰富版面结构、标题层级、表格信息，再考虑更强的解析器

比如本章综合案例 `EmbeddingRagLLM.py` 用的是 `Docx2txtLoader`，因为它足够直接、适合快速把 `alibaba-java.docx` 读入并跑通端到端 RAG；

而单独的 Word 加载案例 `RagLoadDocDemo.py` 则使用了 `UnstructuredWordDocumentLoader`，更适合说明“不同文件格式可以统一进入 RAG 流程”。

如果以后你在真实项目里遇到更复杂的 PDF、扫描件、表格型文档，通常还会引入 OCR、版面分析工具、专门的 PDF 解析服务等更强的方案。这部分已经超出本章案例范围，但你需要先建立一个判断：**RAG 效果好不好，很多时候不是模型先出问题，而是文档解析质量先决定了一半。**

#### 2.2.3 文档加载器案例

下面这些案例都在 `10-rag/docloads/` 目录下，建议你按文件格式逐个跑一遍。学习重点不是死记类名，而是观察：**无论加载什么文件，输出都会进入同一种 `Document` 结构。**

- **TXT（纯文本）**

【案例源码】`案例与源码-2-LangChain框架/10-rag/docloads/RagLoadTxtDemo.py`

[RagLoadTxtDemo.py](案例与源码-2-LangChain框架/10-rag/docloads/RagLoadTxtDemo.py ":include :type=code")

- **PDF**

【案例源码】`案例与源码-2-LangChain框架/10-rag/docloads/RagLoadPdfDemo.py`

[RagLoadPdfDemo.py](案例与源码-2-LangChain框架/10-rag/docloads/RagLoadPdfDemo.py ":include :type=code")

- **Word（.docx）**

【案例源码】`案例与源码-2-LangChain框架/10-rag/docloads/RagLoadDocDemo.py`

[RagLoadDocDemo.py](案例与源码-2-LangChain框架/10-rag/docloads/RagLoadDocDemo.py ":include :type=code")

- **Markdown**

【案例源码】`案例与源码-2-LangChain框架/10-rag/docloads/RagLoadMarkdownDemo.py`

[RagLoadMarkdownDemo.py](案例与源码-2-LangChain框架/10-rag/docloads/RagLoadMarkdownDemo.py ":include :type=code")

- **JSON**

【案例源码】`案例与源码-2-LangChain框架/10-rag/docloads/RagLoadJsonDemo.py`

[RagLoadJsonDemo.py](案例与源码-2-LangChain框架/10-rag/docloads/RagLoadJsonDemo.py ":include :type=code")

- **CSV**

【案例源码】`案例与源码-2-LangChain框架/10-rag/docloads/RagLoadCSVDemo.py`

[RagLoadCSVDemo.py](案例与源码-2-LangChain框架/10-rag/docloads/RagLoadCSVDemo.py ":include :type=code")

### 2.3 文本分割器（Text Splitters）

文档加载之后，通常还不能直接拿去建 RAG。原因很简单：**原始文档经常太长**。

这会带来两个现实问题：

- **检索效果差**：整篇文档太大，向量表达会过于粗糙，难以精确定位到真正相关的小段内容。
- **生成成本高**：就算检索回来整篇文档，也很可能塞不进模型上下文，或者把大量无关内容一并送给模型。

所以，RAG 中几乎都会有“**切块（chunking）**”这一步。

LangChain 官方也明确建议：面对通用文本时，`RecursiveCharacterTextSplitter` 往往是最推荐的入门分割器。它会尽量优先保留较大的语义单位，例如段落、句子；如果某一段还太长，再继续往更小层级切。

#### 2.3.1 为什么要切块

切块的本质，就是把大文档拆成多个更容易参与检索的小段。这样做之后，系统更容易只召回真正相关的片段，也更容易控制每次送给模型的上下文长度；无关内容变少后，干扰会下降，后面做来源标注和精确引用也会更方便。

在真实项目里，切块策略会直接影响 RAG 的最终效果。很多时候不是模型不行，而是：

- 块切得太大，相关信息被淹没
- 块切得太小，语义被切碎
- 没有重叠，导致一句话被拦腰截断

所以，**分割策略本身就是 RAG 质量的重要一环。**

还有一个很关键的现实点：即使某些大模型已经支持长上下文，也不意味着“把整篇文档直接塞进去”就是好方案。因为上下文越长，越容易混入无关信息，也越容易让真正关键的内容被稀释。RAG 里的“切块 + 检索”，本质上是在帮模型先做一轮信息筛选。

#### 2.3.2 常见分割器与适用场景

| 分割器                             | 作用                                                                 |
| ---------------------------------- | -------------------------------------------------------------------- |
| **RecursiveCharacterTextSplitter** | 通用首选，优先保持较大语义单位，必要时递归切得更细                   |
| **CharacterTextSplitter**          | 按指定分隔符切，简单直接                                             |
| **MarkdownHeaderTextSplitter**     | 按 Markdown 标题层级切分                                             |
| **HTMLHeaderTextSplitter**         | 按 HTML 标题结构切分                                                 |
| **TokenTextSplitter**              | 按 token 数控制块大小，更贴近模型上下文限制                          |
| **语义切分（Semantic Chunking）**  | 按语义变化切分，尽量让相关内容保留在同一块中，但成本更高、实现更复杂 |
| **代码类分割器**                   | 按函数、类、逻辑块切分代码文本                                       |

本章案例主线，还是以 **`RecursiveCharacterTextSplitter`** 为主，因为它最适合帮助你建立“分块”这件事的直觉。

这里补充一个真实项目里的判断：切分策略不是越高级越好，而是要看“值不值得”。像语义切分这种方案，理论上更有机会保留完整语义，但它往往需要额外的向量计算或更复杂的实现。对于初学者和大多数入门项目来说，先把 `RecursiveCharacterTextSplitter` 用好，通常比一开始就追求复杂切分更重要。

#### 2.3.3 重点参数理解

`RecursiveCharacterTextSplitter` 最常用的是下面几个参数：

| 参数              | 含义           | 实践理解                                      |
| ----------------- | -------------- | --------------------------------------------- |
| `chunk_size`      | 单块最大长度   | 块太大不利于检索，块太小又容易语义碎片化      |
| `chunk_overlap`   | 相邻块重叠长度 | 防止句子、语义被截断，常见取块大小的 10%～20% |
| `length_function` | 长度计算方式   | 默认常用 `len`，即按字符数；也可按 token 数   |
| `separators`      | 优先切分分隔符 | 决定先按段落、换行、句号还是更细粒度去切      |

对初学者来说，先把下面这条经验记住就够用了：

- **通用文档问答**：优先用 `RecursiveCharacterTextSplitter`
- **先调 `chunk_size` 和 `chunk_overlap`**
- **跑通后再逐步优化切分规则**

真实项目里，不存在一个“永远最优”的块大小。它和文档类型、语言、问答粒度、模型上下文长度都有关系。学习时先跑通主链路，再基于效果调参，是更现实的路线。

![chunk_size 每个块之间有一部分重叠](images/19/19-2-3-1.png)

#### 2.3.4 文本分割器案例

- **分割纯文本**

【案例源码】`案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveTextSplitter.py`

[RecursiveTextSplitter.py](案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveTextSplitter.py ":include :type=code")

- **分割纯文本（V2：验证重叠与完整性）**

【案例源码】`案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveTextSplitterV2.py`

[RecursiveTextSplitterV2.py](案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveTextSplitterV2.py ":include :type=code")

- **分割 Document 对象（先加载再分割）**

【案例源码】`案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveDocumentSplitter.py`

[RecursiveDocumentSplitter.py](案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveDocumentSplitter.py ":include :type=code")

这三个案例分别对应三种入口：

- `split_text()`：字符串 -> 字符串列表
- `create_documents()`：字符串列表 -> `Document` 列表
- `split_documents()`：`Document` 列表 -> 更小的 `Document` 列表

其中在真实 RAG 项目里，最常见的往往是最后一种，也就是：

**loader.load() -> split_documents() -> embeddings -> vector store**

### 2.4 进阶方向速览

入门管道跑通之后，真实项目里的优化通常会沿着下面几条线展开：

- **混合检索（Hybrid）**：关键词检索（如 BM25）与向量检索结合，缓解“专有名词、编号、错误码”等仅靠向量不够准的问题。
- **重排序（Rerank）**：先向量召回较多候选片段，再用交叉编码器等模型对「问题—片段」重新打分，提高最终送入 Prompt 的质量。
- **查询改写**：多查询扩展、HyDE 等，用额外一步改善问句与文档的匹配（常与 Agent 或固定预处理脚本结合）。
- **评测与观测**：准备一批「问题—期望引用片段或标准答」做回归；线上可用 [LangSmith](https://docs.langchain.com/langsmith/observability-llm-tutorial) 等工具追踪检索与生成全链路，定位是检索差还是 Prompt / 模型问题。

![RRF 重排序示意：融合多路检索结果的排名，缓解单一路径召回不稳的问题](images/19/19-2-4-1.png)

![高级 RAG 五维增强体系：从查询、索引、检索器、生成器和管道五个方向优化效果](images/19/19-2-4-2.png)

进一步看，生产级 RAG 的优化通常可以从五个方向入手：

| 方向   | 常见做法                            | 主要解决什么                     |
| ------ | ----------------------------------- | -------------------------------- |
| 查询侧 | 查询改写、多查询、HyDE              | 用户问法和文档说法对不上         |
| 索引侧 | 合理切块、Overlap、元数据、解析质量 | 入库内容太碎、太脏或缺少过滤字段 |
| 检索侧 | 向量检索、BM25、混合检索、RRF       | 单一路径召回不稳定               |
| 排序侧 | Rerank、阈值过滤、Top K 调整        | 候选片段多但质量参差不齐         |
| 生成侧 | 引用来源、上下文压缩、答案约束      | 回答不可信或上下文太长           |

学习时先把 Redis 向量检索链路跑通；等项目规模变大，再考虑 Milvus 这类专业向量库里的 HNSW、BM25、Analyzer、标量过滤和混合检索能力。

这些主题与 [第 21 章 Agent 智能体](21-Agent智能体.md)、后续 LangGraph 相关章节可以形成连续深入路线。

---

## 3、RAG 综合案例：智能运维助手

### 3.1 需求说明

本章综合案例的目标，不是做一个抽象的“百科问答”，而是做一个更贴近真实业务的场景：**智能运维助手**。

假设我们手里有一份错误码说明文档，例如 `alibaba-java.docx`，里面记录了各种错误码及含义。现在希望用户输入错误码后，系统能够：

- 理解用户的问题
- 去知识库里找到对应的错误码说明
- 基于检索到的内容生成可读答案

这类需求在真实项目里非常常见，因为：

- 企业内部的错误码通常是**私有知识**
- 文档会持续更新
- 不能指望通用大模型天然知道所有内部编码含义

所以它适合用 RAG 来做。

本案例的技术主线是：

- **文档来源**：`alibaba-java.docx`
- **嵌入模型**：阿里百炼向量模型
- **向量库**：Redis / RedisStack
- **大模型**：通义 / DeepSeek 等聊天模型
- **框架**：LangChain

### 3.2 Before：未使用 RAG 的局限

如果不做 RAG，只是直接问模型：

> `00000 和 A0001 分别是什么意思？`

模型可能出现几种情况：

- 根本不知道这些编码属于哪个系统
- 按通用语义胡乱猜测
- 给出似是而非、但无法核验的回答

这不是模型“笨”，而是因为这类知识通常不在它训练时稳定可得的公共语料里。也正因为如此，企业项目里很少把“私有知识问答”完全交给裸模型处理。

所以，真正的问题不是“模型会不会回答”，而是：**它回答时有没有看到你自己的业务文档。**

### 3.3 After：使用 RAG 的完整流程

【案例源码】`案例与源码-2-LangChain框架/10-rag/EmbeddingRagLLM.py`

[EmbeddingRagLLM.py](案例与源码-2-LangChain框架/10-rag/EmbeddingRagLLM.py ":include :type=code")

这个综合案例，基本把本章前面讲过的内容都串起来了。它的实际流程是：

1. 用 **`Docx2txtLoader`** 加载 `alibaba-java.docx`
2. 用 **`CharacterTextSplitter`** 把文档切成块
3. 用 **`DashScopeEmbeddings`** 把片段向量化
4. 用 **Redis 向量库** 存储这些片段
5. 用 **`as_retriever()`** 生成检索器
6. 用 **`PromptTemplate`** 组织 `context + question`
7. 用 **聊天模型** 基于检索结果生成最终答案

其中最值得注意的一行是：

```python
rag_chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm
```

上面这一行用的是 [第 15 章 LCEL 与链式调用](15-LCEL与链式调用.md) 里的 **LCEL 管道**写法：把字典、`RunnablePassthrough`、Prompt 与 LLM 用 `|` 串成可执行链。它几乎就是“管道式 RAG”的缩影：

- `retriever` 根据用户问题去查知识库，产出 `context`
- `RunnablePassthrough()` 把原始问题继续往后传
- `prompt` 把 `context + question` 组装成提示词
- `llm` 读取提示词并生成答案

#### 3.3.1 贴近真实项目的原因

这个案例虽然规模不大，但已经具备了真实 RAG 项目的几个关键特征：

- **知识来自业务文档，而不是写死在 Prompt 里**
- **知识先入库，再在问答时动态检索**
- **回答依赖上下文，不再完全依赖模型记忆**
- **同一个问题，可以对比“有知识库”和“无知识库”的差异**

这正是很多企业项目的第一阶段形态：先做一个“能用、可验证、可扩展”的 RAG 原型，再逐步优化：

- 切块策略
- 检索条数 `k`
- Prompt 约束方式
- 来源展示
- 召回重排
- 多轮对话结合记忆

#### 3.3.2 本案例还有哪些值得你注意

1. **综合案例用的是 `CharacterTextSplitter`**  
   这能帮助你快速跑通流程；但在通用文本场景里，实际项目中更常见的首选仍然是 `RecursiveCharacterTextSplitter`。

2. **Prompt 里明确写了“如果文本中没有相关信息，请直接说明”**  
   这是 RAG 里常见的约束写法。因为检索不是百分百准确，Prompt 应该引导模型“基于上下文回答”，而不是脱离上下文自行发挥。

3. **脚本做了“有 RAG / 无 RAG”的对比演示**  
   这一点适合教学，也适合项目早期验证价值。只有做对比，你才更容易判断：问题究竟出在模型本身，还是出在检索链路。

4. **Redis 只是这个案例里的向量存储后端**  
   RAG 的本质不是绑定 Redis，而是“检索增强生成”这条流程。以后你换成 Chroma、FAISS、Milvus、PgVector，整体思路并不会变。

---

**章节思考题：**

1. 一个完整 RAG 系统里，哪些步骤属于离线建库，哪些属于在线问答？

   **参考思路：** 离线建库包括文档加载、清洗、切分、Embedding、入向量库；在线问答包括问题向量化、召回、重排、上下文组装、模型生成和答案返回。两段分清，排障会简单很多。

2. 文本切分为什么不是越细越好，也不是越大越好？

   **参考思路：** 太细会丢上下文，太大又会引入噪声并浪费 token。好的切分要兼顾语义完整、召回精度和上下文成本，必要时还要保留标题、层级和来源信息。

3. 如果 RAG 答案出现幻觉，你会如何判断是检索问题还是生成问题？

   **参考思路：** 先看召回片段是否包含答案依据。如果没召回到，查文档、切分、Embedding 和检索参数；如果召回到了但模型乱答，查 Prompt、引用约束、上下文排序和输出要求。

4. 为什么 RAG 需要保留来源和 metadata？

   **参考思路：** 来源能让答案可追溯，metadata 能支持过滤、排序、权限控制和排障。企业场景里，只答对还不够，还要知道依据来自哪里、是否有权限使用。

**本章小结：**

- **RAG 的本质**：不是训练模型，而是先检索外部知识，再让模型基于知识生成答案。
- **RAG 的两阶段**：索引阶段负责“准备知识库”，检索阶段负责“每次提问时动态查资料”（与官方文档中的 **2-Step RAG** 一致）。
- **本章的新增重点**：相比 [第 18 章](18-向量数据库与Embedding实战.md)，这一章补上了“文档从哪来、如何切块、如何把检索结果喂给模型”。
- **本章案例主线**：从多格式文档加载，到文本切分，再到 Redis 检索和智能运维助手，构成完整的入门版 RAG 系统；但真正上线时，效果还取决于文档质量、切块策略、召回、重排、Prompt 约束和评估闭环是否做扎实。
- 学完本章后，你应当能够：用“**索引阶段**”和“**检索与生成阶段**”两段式描述 RAG 的标准流程；知道文档加载器、文本分割器、Embedding、向量库、检索器、Prompt、聊天模型在 RAG 中各自负责哪一步；区分 **管道式 RAG** 和 **Agent 式 RAG** 的差别，并理解 RAG 质量由“文档、检索、生成、评估”共同决定。

**建议下一步：** 先把本章的文档加载、文本切分、综合案例至少各跑通一个，再回头对照 [第 18 章 向量数据库与 Embedding 实战](18-向量数据库与Embedding实战.md)，把“向量化 → 入库 → 检索”这条底层链路重新连一遍；如果你准备继续学习“什么时候固定写死检索流程，什么时候让系统自己决定是否检索”，就顺势进入 [第 21 章 Agent 智能体](21-Agent智能体.md)。
