19-RAG检索增强生成
19 - RAG 检索增强生成
本章课程目标:
- 把 第 2 章 RAG-搭建企业私有/个人知识库 里建立的 RAG 与知识库直觉,继续推进到 LangChain 代码实现层。
- 掌握 LangChain 中构建 RAG 最常见的几类组件:文档加载器、文本分割器、嵌入模型、向量数据库、检索器、提示词模板与聊天模型。
- 跑通并理解本章全部案例:多种文档加载、文本切分、Redis 向量检索、完整 RAG 智能运维助手,把 第 18 章 向量数据库与 Embedding 实战 的内容真正串成一个可落地的问答系统。
学习建议: RAG 最好分成两段看:离线把文档处理成可检索知识,在线根据用户问题召回并组装上下文。读本章时可以用两种颜色标出来:哪些代码属于“建库”,哪些代码属于“问答”。文档加载、切分、Embedding、向量库、Prompt 这些组件的位置清楚了,换任何框架都能读懂。
官方文档与资源:详见 工具导航与参考资料索引 - RAG与向量检索。
1、RAG 简介
1.1 定义
如果你已经读过 第 2 章,那么这里可以不再从“知识库产品怎么用”重新讲起,而是直接把 RAG 放回代码语境里理解。
RAG(Retrieval-Augmented Generation,检索增强生成),本质上是一种“先检索,再生成”的应用架构。用户提问后,系统不会立刻只靠大模型自身记忆回答,而是先从外部知识库中检索相关材料,再把这些材料与问题一起交给大模型生成答案。
到了工程实现层,本章更关心的是下面这些问题:
- 外部知识在代码里以什么对象表示
- 文档为什么要先加载、再切块、再向量化
- 向量库和检索器分别负责哪一步
- 检索结果最终怎么进入 Prompt 或消息上下文
所以可以把本章看作:把第 2 章里的知识库问答体验,翻译成 LangChain 里的数据结构与组件流水线。
1.2 RAG 的作用
这一点在 第 2 章 里已经从产品角度讲过:
RAG 主要是在解决 知识冻结、私有知识缺失、最新信息不可用、回答缺少依据 这类问题。
本章不再重复展开,而是把重点前移到一个更适合开发者的问题上:既然已经知道要用 RAG,那么代码里到底是哪些环节决定了回答质量?
从工程视角看,影响效果最明显的通常不是“有没有接上向量库”这么简单,而是:
- 文档是否被正确加载
- 文本是否被合理切块
- Embedding 是否稳定
- 检索是否召回了真正相关的片段
- Prompt 是否把上下文用对了
和其他方案的关系,仍然可以用下面这张表快速回顾:
| 方案 | 本质 | 优势 | 局限 |
|---|---|---|---|
| 直接问模型 | 不接外部知识,直接生成 | 上手最快 | 容易不知道私有 / 最新知识,幻觉较多 |
| RAG | 先检索资料,再生成 | 更新快、改动小、可追溯 | 依赖文档质量、分块策略、检索效果 |
| 微调 | 调整模型参数 | 能改变回答风格、任务习惯 | 成本高、更新慢,不适合高频改知识 |
| RAG + 微调 | 外部知识 + 参数适配 | 兼顾知识与表达 | 成本和系统复杂度更高 |
同时也要知道,RAG 不是没有代价的万能方案。在真实项目里,它通常会带来几个现实权衡:
- 响应时延更高:每次问答前都要多做一次检索,有时还会经过过滤、重排等步骤。
- Token 消耗更高:检索结果会进入 Prompt,召回内容越多,送给模型的上下文越长。
- 效果依赖链路质量:文档质量、切块策略、Embedding 质量、检索效果、Prompt 约束方式,都会影响最终回答。
1.3 RAG 的标准流程
在 第 2 章 里,我们已经从产品与平台角度看过两阶段流程;这里换到 LangChain 代码视角,再把它重述一遍。
LangChain 官方文档通常也把 RAG 拆成两大阶段:索引(Indexing)、检索与生成(Retrieval and Generation)。
这不是为了重复,而是为了把“平台里的按钮和配置项”翻译成“代码里的组件与数据流”。

1.3.1 索引阶段:先把知识库准备好
索引阶段面对的是“原始文档”,例如 Word、PDF、Markdown、TXT、CSV、JSON 等文件。它的目标不是回答问题,而是把这些文档处理成“未来方便检索”的形态。
你在第 2 章里看到的“上传文件、分段、建知识库”,放到代码里,基本就是这一阶段。
这一阶段通常包括:
- 加载(Load):把原始文件读成 LangChain 的
Document对象。 - 分割(Split):把长文档切成较小的片段,便于后续向量化和检索。
- 向量化(Embed):把每个文档片段转成向量。
- 存储(Store):把“片段内容 + 向量 + 元数据”写入向量数据库。

这里有两个很容易忽略的点:
索引通常是离线做的。
比如每天定时重建一次知识库,或在文档更新后增量写入;它不一定跟用户问答发生在同一时刻。索引不只是“存文本”。
它真正要存的是“文本片段 + 向量表示 + metadata”,这样后面才可能做相似检索、来源展示、条件过滤。
什么是 metadata?
metadata是和文档片段绑定的附加信息,例如文件路径source、页码page、标题、作者、日期、分类等。
在 RAG 里,真正参与向量化的是正文page_content;metadata更多用于来源展示、过滤条件、结果解释。
1.3.2 检索与生成阶段:每次提问时动态查资料
当用户真正发起问题时,系统进入第二阶段。这对应的就是第 2 章里“基于知识库提问 / 生成回答”的那部分能力,只不过这里我们要看清楚它在代码里究竟分成了哪几步:
- 用户输入问题。
- 把问题也向量化。
- 去向量库里找最相似的文档片段。
- 把这些片段作为
context放进 Prompt。 - 再把
context + question一起发给大模型生成答案。

这一阶段的核心不是“再去建库”,而是“拿已经建好的索引来查资料”。也就是说:
- 索引阶段:提前备好资料库。
- 检索阶段:每次提问时现场查资料。
1.3.3 管道式 RAG 与 Agent 式 RAG
本教程这一章,重点是最经典、最容易上手的 管道式 RAG。它与 LangChain 官方文档中的 2-Step RAG 是同一类思路:先检索、再生成,流程由代码固定,而非由模型临时决策。
它的特点很明确:每次用户提问,系统都会先检索一次;是否检索不由模型临时决定,而是由你的代码流程提前写死;整体结构通常也是“检索器 + 提示词模板 + 聊天模型”的固定流水线。
这类方案适用于:企业知识库问答、文档问答、规范 / 手册 / FAQ 检索增强。
如果要让“模型自己决定要不要检索、什么时候检索、检索几次”,那就更接近 Agent 式 RAG(对应官方文档中的 Agentic RAG)。更完整的智能体编排会在后续 第 21 章 Agent 智能体 等章节展开;先把本章这种两阶段、单次检索、单次生成的主线学扎实更重要。
1.3.4 一个更贴近生产环境的增强版流程
如果只是入门,先把握前面的“两阶段”即可;但从真实项目角度看,很多 RAG 系统会在“检索与生成”之间再补几步,让结果更稳。第 2 章里你已经在 Dify 里看到了 Top K、Rerank、混合检索这些参数,这里可以把它们和代码链路对上:
- 先召回候选片段:检索器不一定只有向量检索,也可能是关键词、混合检索。
- 再做过滤或重排:按
metadata过滤范围,或用 reranker 对候选片段重新打分。 - 最后再进 Prompt:把更少但更相关的片段送给模型,而不是把一大堆噪声上下文都塞进去。
- 生成后保留来源:真实项目里常常会返回文件名、页码、片段来源,便于核验。
- 持续做评测与观测:离线看召回命中率、答案质量,线上看检索链路和生成链路谁在掉链子。
所以,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() 等 |
用面试里的说法,可以这样概括整个流程:
- 用文档加载器把原始文件转成
Document。 - 用文本分割器把长文档切成多个片段。
- 用嵌入模型把片段转成向量,并写入向量数据库。
- 用户提问时,把问题拿去做向量检索,得到相关片段。
- 把片段作为上下文,和用户问题一起填进 Prompt。
- 调用大模型,得到最终答案。
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 里,流程是:
- 用
Docx2txtLoader加载alibaba-java.docx - 用
CharacterTextSplitter切分文档 - 得到一批切好的
Document - 调用
Redis.from_documents(...)直接写入向量库
这种写法非常贴近“真正的 RAG 业务流程”,因为:
- 你的知识原本就在文档里
- 文档先经过加载和切分
- 入库时保留了
Document结构 - 后续可以自然衔接
as_retriever()
也就是说,from_documents(...) 更像是“把已经准备好的知识片段整批建成索引”。
第二类:add_texts,对应单独演示向量库存取
在 RedisVectorStore.py 里,案例没有先走 Loader 和 Splitter,而是直接给出一组字符串 texts 与对应的 metadata,再按两步完成写入:
- 先创建
RedisVectorStore; - 再调用
add_texts(texts, metadata)写入。
这种写法更像是在演示:
- 向量库本身如何使用
- 纯文本如何批量入库
- 已有索引如何继续追加内容
所以它更适合拿来理解“向量存储层怎么工作”,也更适合做一些小规模实验、增量写入、脱离文档加载器的独立入库逻辑。
2.1.3 再往后一步:检索案例和它们是什么关系
RedisVectorStore_SimilaritySearch.py 虽然和上面两个文件放在一起,但它所在的其实是 RAG 的检索阶段,不是索引阶段。
它和前面两种写入方式的关系是:
- 先通过
from_documents(...)或add_texts(...)把数据写进向量库 - 再通过
similarity_search_with_score(...)去查库
所以这三个案例可以连起来理解成:
RedisVectorStore.py:怎么把文本写进去RedisVectorStore_SimilaritySearch.py:怎么把相关内容查出来EmbeddingRagLLM.py:怎么把“查出来的内容”再喂给大模型完成回答
这样看,三者就不是零散的小例子,而是 RAG 完整链路的三个片段。
2.1.4 入门时怎么选
如果你是刚开始学,建议按下面的顺序理解:
先把
add_texts(...)看懂
因为它更直接,容易理解“文本 -> 向量 -> 向量库”这件事。再看
from_documents(...)
因为它更贴近真实 RAG 项目,能把“文档加载、文本切分、向量化入库”串起来。最后把它们放回完整案例里理解
你会发现:RAG 的重点从来不只是“把内容存进去”,而是“存进去以后,如何在提问时检索出来,并和 Prompt、LLM 结合起来”。
【案例源码】案例与源码-2-LangChain框架/10-rag/RedisVectorStore.py(写入文本并存入 Redis)
【案例源码】案例与源码-2-LangChain框架/10-rag/RedisVectorStore_SimilaritySearch.py(相似性检索)
RedisVectorStore_SimilaritySearch.py
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

2.2.2 如何选择加载器
选择加载器时,不要一开始就背几十个类名,先抓住两个原则:
先按文件类型选
- TXT 用
TextLoader - PDF 用
PyPDFLoader - Word 用
UnstructuredWordDocumentLoader或Docx2txtLoader - Markdown 用
UnstructuredMarkdownLoader - JSON 用
JSONLoader - CSV 用
CSVLoader
- TXT 用
再按解析精度和成本选
- 想快速跑通案例,优先选依赖少、接口直接的加载器
- 想保留更丰富版面结构、标题层级、表格信息,再考虑更强的解析器
比如本章综合案例 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
【案例源码】案例与源码-2-LangChain框架/10-rag/docloads/RagLoadPdfDemo.py
- Word(.docx)
【案例源码】案例与源码-2-LangChain框架/10-rag/docloads/RagLoadDocDemo.py
- Markdown
【案例源码】案例与源码-2-LangChain框架/10-rag/docloads/RagLoadMarkdownDemo.py
- JSON
【案例源码】案例与源码-2-LangChain框架/10-rag/docloads/RagLoadJsonDemo.py
- CSV
【案例源码】案例与源码-2-LangChain框架/10-rag/docloads/RagLoadCSVDemo.py
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 - 跑通后再逐步优化切分规则
真实项目里,不存在一个“永远最优”的块大小。它和文档类型、语言、问答粒度、模型上下文长度都有关系。学习时先跑通主链路,再基于效果调参,是更现实的路线。

2.3.4 文本分割器案例
- 分割纯文本
【案例源码】案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveTextSplitter.py
- 分割纯文本(V2:验证重叠与完整性)
【案例源码】案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveTextSplitterV2.py
- 分割 Document 对象(先加载再分割)
【案例源码】案例与源码-2-LangChain框架/10-rag/textsplit/RecursiveDocumentSplitter.py
这三个案例分别对应三种入口:
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 等工具追踪检索与生成全链路,定位是检索差还是 Prompt / 模型问题。


进一步看,生产级 RAG 的优化通常可以从五个方向入手:
| 方向 | 常见做法 | 主要解决什么 |
|---|---|---|
| 查询侧 | 查询改写、多查询、HyDE | 用户问法和文档说法对不上 |
| 索引侧 | 合理切块、Overlap、元数据、解析质量 | 入库内容太碎、太脏或缺少过滤字段 |
| 检索侧 | 向量检索、BM25、混合检索、RRF | 单一路径召回不稳定 |
| 排序侧 | Rerank、阈值过滤、Top K 调整 | 候选片段多但质量参差不齐 |
| 生成侧 | 引用来源、上下文压缩、答案约束 | 回答不可信或上下文太长 |
学习时先把 Redis 向量检索链路跑通;等项目规模变大,再考虑 Milvus 这类专业向量库里的 HNSW、BM25、Analyzer、标量过滤和混合检索能力。
这些主题与 第 21 章 Agent 智能体、后续 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
这个综合案例,基本把本章前面讲过的内容都串起来了。它的实际流程是:
- 用
Docx2txtLoader加载alibaba-java.docx - 用
CharacterTextSplitter把文档切成块 - 用
DashScopeEmbeddings把片段向量化 - 用 Redis 向量库 存储这些片段
- 用
as_retriever()生成检索器 - 用
PromptTemplate组织context + question - 用 聊天模型 基于检索结果生成最终答案
其中最值得注意的一行是:
1 | rag_chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm |
上面这一行用的是 第 15 章 LCEL 与链式调用 里的 LCEL 管道写法:把字典、RunnablePassthrough、Prompt 与 LLM 用 | 串成可执行链。它几乎就是“管道式 RAG”的缩影:
retriever根据用户问题去查知识库,产出contextRunnablePassthrough()把原始问题继续往后传prompt把context + question组装成提示词llm读取提示词并生成答案
3.3.1 贴近真实项目的原因
这个案例虽然规模不大,但已经具备了真实 RAG 项目的几个关键特征:
- 知识来自业务文档,而不是写死在 Prompt 里
- 知识先入库,再在问答时动态检索
- 回答依赖上下文,不再完全依赖模型记忆
- 同一个问题,可以对比“有知识库”和“无知识库”的差异
这正是很多企业项目的第一阶段形态:先做一个“能用、可验证、可扩展”的 RAG 原型,再逐步优化:
- 切块策略
- 检索条数
k - Prompt 约束方式
- 来源展示
- 召回重排
- 多轮对话结合记忆
3.3.2 本案例还有哪些值得你注意
综合案例用的是
CharacterTextSplitter
这能帮助你快速跑通流程;但在通用文本场景里,实际项目中更常见的首选仍然是RecursiveCharacterTextSplitter。Prompt 里明确写了“如果文本中没有相关信息,请直接说明”
这是 RAG 里常见的约束写法。因为检索不是百分百准确,Prompt 应该引导模型“基于上下文回答”,而不是脱离上下文自行发挥。脚本做了“有 RAG / 无 RAG”的对比演示
这一点适合教学,也适合项目早期验证价值。只有做对比,你才更容易判断:问题究竟出在模型本身,还是出在检索链路。Redis 只是这个案例里的向量存储后端
RAG 的本质不是绑定 Redis,而是“检索增强生成”这条流程。以后你换成 Chroma、FAISS、Milvus、PgVector,整体思路并不会变。
章节思考题:
一个完整 RAG 系统里,哪些步骤属于离线建库,哪些属于在线问答?
参考思路: 离线建库包括文档加载、清洗、切分、Embedding、入向量库;在线问答包括问题向量化、召回、重排、上下文组装、模型生成和答案返回。两段分清,排障会简单很多。
文本切分为什么不是越细越好,也不是越大越好?
参考思路: 太细会丢上下文,太大又会引入噪声并浪费 token。好的切分要兼顾语义完整、召回精度和上下文成本,必要时还要保留标题、层级和来源信息。
如果 RAG 答案出现幻觉,你会如何判断是检索问题还是生成问题?
参考思路: 先看召回片段是否包含答案依据。如果没召回到,查文档、切分、Embedding 和检索参数;如果召回到了但模型乱答,查 Prompt、引用约束、上下文排序和输出要求。
为什么 RAG 需要保留来源和 metadata?
参考思路: 来源能让答案可追溯,metadata 能支持过滤、排序、权限控制和排障。企业场景里,只答对还不够,还要知道依据来自哪里、是否有权限使用。
本章小结:
- RAG 的本质:不是训练模型,而是先检索外部知识,再让模型基于知识生成答案。
- RAG 的两阶段:索引阶段负责“准备知识库”,检索阶段负责“每次提问时动态查资料”(与官方文档中的 2-Step RAG 一致)。
- 本章的新增重点:相比 第 18 章,这一章补上了“文档从哪来、如何切块、如何把检索结果喂给模型”。
- 本章案例主线:从多格式文档加载,到文本切分,再到 Redis 检索和智能运维助手,构成完整的入门版 RAG 系统;但真正上线时,效果还取决于文档质量、切块策略、召回、重排、Prompt 约束和评估闭环是否做扎实。
- 学完本章后,你应当能够:用“索引阶段”和“检索与生成阶段”两段式描述 RAG 的标准流程;知道文档加载器、文本分割器、Embedding、向量库、检索器、Prompt、聊天模型在 RAG 中各自负责哪一步;区分 管道式 RAG 和 Agent 式 RAG 的差别,并理解 RAG 质量由“文档、检索、生成、评估”共同决定。
建议下一步: 先把本章的文档加载、文本切分、综合案例至少各跑通一个,再回头对照 第 18 章 向量数据库与 Embedding 实战,把“向量化 → 入库 → 检索”这条底层链路重新连一遍;如果你准备继续学习“什么时候固定写死检索流程,什么时候让系统自己决定是否检索”,就顺势进入 第 21 章 Agent 智能体。