从零开始的LLM 6. RAG(检索增强生成)(一)
前言
从零开始学习ai文章系列已完成《动手学深度学习》和《磨菇书》两本书的学习,新开的LLM系列课本来自《Happy-LLM》和《Hello-Agents》和 《all-in-rag》,但是内容和排版等个人重新进行整理,因此不会按照原来课本中的章节来写。如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。
由于 RAG 部分内容较多,单篇草稿已超过 1.4 万字,因此将拆分为上下两篇进行介绍。
整体内容基于对 all-in-rag 项目的学习与实践总结。
接下来,文章将以原项目中的 “第八章:项目实战一” 为主线,从整体流程入手,再逐步深入到各个模块的实现细节。
- 篇章一:主要介绍 RAG 搜索引擎之外的内容,包括整体流程、系统架构以及各模块的作用等概述性内容。
- 篇章二:重点讲解 RAG 搜索引擎的具体实现,以及其中涉及的关键技术与实现细节。
1. 什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种在大语言模型(LLM)生成回答之前,先从外部知识库中检索相关信息,再结合这些信息进行回答的技术。
你可以将 RAG 理解为一个搜索工具。
当用户提出一个问题时,它会先从知识库中搜索与问题相关的内容,然后再把这些内容提供给 LLM,帮助模型生成更加准确、更加符合事实的回答。
需要特别强调的是,RAG 是一个检索系统,而不是记忆系统。
很多初学者容易误以为,RAG 能够"记住"大量知识,或者能够"回忆"以前见过的内容。实际上并非如此。
RAG 的核心能力始终都是 Retrieval(检索),而不是 Memory(记忆)。它没有主动记忆能力,每一次用户发起查询时,都会重新执行一次完整的检索流程:
- 将用户的问题转换为向量表示(Embedding)。
- 在向量数据库(或其他检索系统)中进行相似度搜索。
- 找到最相似的文档片段(Chunk)。
- 将这些文档片段与用户问题一起发送给 LLM。
- 由 LLM 综合上下文生成最终回答。
整个过程中,RAG 不会回忆过去检索过什么内容,也不会因为之前检索过而记住某些知识。每一次查询都是一次全新的检索过程。
此外,还需要理解一个非常重要的概念:
RAG 检索到的是"相似"的内容,而不一定是真正"相关"的内容。
对于向量检索而言,它依据的是向量空间中的距离(相似度)进行匹配;对于关键词检索而言,它依据的是关键词之间的匹配关系进行搜索。
因此,相似(Similar)并不等于相关(Relevant)。
例如,用户询问的是某个算法的实现方式,而检索结果可能只是包含大量相同专业术语的另一篇文章,却并没有真正回答用户的问题。
这也是 RAG 中最核心的问题之一——检索质量(Retrieval Quality)。如果检索阶段没有找到真正有价值的内容,那么即使后续使用再强大的 LLM,也很难生成高质量的回答。因此,RAG 的大量研究工作实际上都是围绕如何提高检索质量、提高召回率以及排序质量展开的。
2. RAG 在整个流程中的位置
理解了 RAG 的作用之后,我们再来看它在整个问答流程中的位置。
在没有使用 RAG 时,流程非常简单,用户的问题会直接发送给 LLM:
1 | response = LLM(query) |
此时,LLM 只能依赖模型参数中已经学到的知识进行回答。
而加入 RAG 后,流程会多出一个检索步骤。用户的问题首先交给 RAG 系统,由它从知识库中搜索相关文档:
1 | relevant_docs = RAG(query) |
随后,再将用户问题和检索得到的相关文档一起交给 LLM:
1 | response = LLM(query, relevant_docs) |
整个流程可以表示为:

也正因为如此,RAG 并不是用于替代 LLM,而是作为 LLM 的一个外挂知识系统,在回答问题之前,为模型提供更加丰富、更具时效性的上下文信息,从而提升回答的准确性和可靠性。
3.RAG内部怎么实现呢。
3.1 查询路由(Query Routing)
来自原文:查询重构与分发
当用户的 query 进入 RAG
系统后,首先经过的通常不是检索,而是查询路由(Query
Routing)。
查询路由可以理解为 RAG 系统中的智能调度中心。
如果一个 RAG 系统只连接一个知识库,那么所有查询都可以直接进入检索阶段;但在实际项目中,一个系统往往会接入多个数据源、多个检索器,甚至拥有不同的处理流程。例如:
- 不同类型的知识库(文档、代码、数据库等)
- 不同的检索策略(向量检索、关键词检索、混合检索)
- 不同的 Prompt 模板
- 不同的 Agent 或工具
这时,并不是所有查询都应该走同一条流程,而是需要先分析用户的意图,再决定最适合的处理路径,这就是查询路由的作用。
简单来说,它负责回答这样一个问题:
"这个问题应该交给谁来处理?"
因此,查询路由本质上是一个分类(Classification)问题。它根据用户问题的语义,将查询分发到最合适的数据源、检索器、处理组件或 Prompt 模板,从而提高系统效率,并提升最终回答的准确性。
两种主流实现方式
目前,查询路由主要有两种实现方式。
方法一:基于 LLM 的意图识别(LLM Routing)
这是目前最常见、也是最灵活的一种方式。
其核心思想是:让 LLM 自己判断用户的问题应该走哪条路线。
开发者只需要在 Prompt 中预先定义好所有可选路由,并说明每条路由适用于什么场景,然后让 LLM 根据用户的问题输出对应的路由标签。
整个流程如下:
- 定义所有可选的路由(例如数据源、检索器或 Prompt 模板)。
- 编写 Prompt,描述每条路由的适用范围。
- 将用户问题发送给 LLM。
- LLM 输出对应的路由标签。
- 程序根据标签调用相应的检索器或处理链。

这种方式最大的优点是理解能力强、扩展性高。由于 LLM 具备较强的语义理解能力,即使用户的表达方式发生变化,它通常也能够做出正确判断。
当然,它也存在一定缺点:每次查询都需要额外调用一次 LLM,因此会增加一定的响应时间和调用成本。
方法二:基于嵌入相似度的路由(Embedding Routing)
另一种方式是不使用 LLM,而是直接利用向量相似度完成路由选择,因此延迟更低、成本也更低。
它的核心思想是:
将每个路由都表示成一段描述文本,然后比较用户问题与这些描述之间的语义相似度。
整个流程如下:
- 为每条路由编写一段能够描述其适用场景的文本。
- 使用 Embedding 模型将这些描述转换成向量,并提前保存。
- 用户查询到来后,同样计算其向量表示。
- 计算查询向量与各路由向量之间的相似度。
- 选择相似度最高的路由,并执行对应的处理流程。

这种方式无需调用 LLM,因此速度更快,非常适合对实时性要求较高的场景。
不过,它的分类能力通常受限于 Embedding 模型本身以及路由描述的质量,在面对复杂意图时,一般不如 LLM 路由灵活。
本项目使用的是基于 LLM 的意图识别。
项目中的食谱知识库支持多种不同类型的问题,例如:
- list:仅返回菜名列表;
- detail:查询某道菜的详细制作方法;
- general:咨询与食谱相关的一般性问题。
不同类型的问题会使用不同的 Prompt 模板以及对应的检索流程。

因此,在真正开始检索之前,系统会首先调用 LLM
对用户问题进行分类,判断其属于 list、detail
还是
general,然后再将查询分发到对应的处理链,最终完成后续的 RAG
检索与回答生成。
3.2 查询重写(Query Transformation)
经过查询路由之后,用户的问题已经被分发到了对应的检索链路。但此时,系统通常不会立即开始检索,而是会先对用户的
query 进行一次查询重写(Query
Transformation)(原文中称为查询翻译(Query
Translation))。
需要说明的是,查询重写并不一定发生在路由之后,它也可以发生在路由之前。例如,在前面介绍的基于嵌入相似度的路由(Embedding Routing)中,为了获得更加准确的路由结果,也可以先对查询进行重写,再计算其向量表示。
查询重写的目标只有一个:
弥合用户自然语言与知识库文档之间的语义鸿沟(Semantic Gap)。
现实中的用户提问通常存在以下特点:
- 表达过于简短;
- 省略了大量上下文;
- 使用口语化表达;
- 一个问题中包含多个子问题;
- 使用知识库中不存在的术语。
而知识库中的文档往往具有更加正式、完整、结构化的表达方式。
如果直接拿原始 Query 去检索,很可能无法找到真正相关的文档。因此,在检索之前,通常会先利用 LLM 对 Query 进行优化,使其更加符合知识库中的表达形式,从而提升召回率和检索准确率。
方法一:基于 Prompt 的查询重写
这是目前最常见,也是实现成本最低的一种方法。
核心思想十分简单:利用 Prompt 引导 LLM 对用户的问题进行重新表达。
根据不同的需求,Prompt 可以设计成多种不同的重写方式,例如:
(1)补充问题细节
用户的问题往往十分简短,而 LLM 可以根据语义补充必要的信息,使 Query 更加完整。
例如:
1 | 用户: |
这样生成的 Query 通常能够匹配到更多相关文档。
(2)查询分解(Query Decomposition)
有些问题实际上包含多个子问题,一次检索很难覆盖全部内容。
例如:
1 | Qwen3 和 DeepSeek-R1 有什么区别?各自适用于哪些场景? |
可以重写为:
- Qwen3 的主要特点是什么?
- DeepSeek-R1 的主要特点是什么?
- 两者有哪些区别?
- 两者分别适用于哪些应用场景?
随后分别检索,再合并结果。
这种方法尤其适用于复杂问答(Complex QA)。
(3)查询扩展(Query Expansion)
有时候,用户使用的关键词与知识库中的表达方式并不一致。
例如:
1 | 微调 |
可以扩展为:
1 | 模型微调、Fine-tuning、SFT、Supervised Fine-Tuning、Instruction Tuning |
通过增加同义词、近义词、专业术语等关键词,可以提高文档的召回率。
(4)生成结构化查询(Structured Query Generation)
对于结构化数据源,LLM 还可以将用户的自然语言直接转换为数据库能够执行的查询语句,而不是继续进行文本检索。
例如,假设系统连接的是一个 MySQL 数据库,其中存储着商品信息。
用户提出问题:
1 | 价格低于 100 元且销量最高的耳机有哪些? |
LLM 可以先将其转换为对应的 SQL 语句:
1 | SELECT * |
随后,系统直接执行该 SQL,并返回查询结果。
需要注意的是,这里的检索与 RAG 中常见的向量检索(Vector Retrieval)完全不同。
这里查询的是传统关系型数据库(如 MySQL、PostgreSQL),执行的是 SQL 查询,依赖的是数据库的索引、过滤条件以及排序等能力;而向量检索则是在向量数据库中,根据向量之间的相似度(如余弦相似度、欧氏距离等)寻找语义最接近的文档。
因此,生成结构化查询更适用于具有明确结构的数据源,例如:
- MySQL、PostgreSQL 等关系型数据库;
- Neo4j 等图数据库(生成 Cypher 语句);
- Elasticsearch(生成 DSL 查询);
- 各类提供 REST API 的业务系统(生成对应的请求参数)。
这种方式本质上属于 Text-to-SQL、Text-to-Cypher 等自然语言到结构化查询的转换,而不是传统意义上的语义相似度检索。
本项目中,仅对经过 detail 路由的 Query
执行查询重写,所使用的 Prompt 如下图所示。

方法二:HyDE(Hypothetical Document Embeddings)
另一种经典的查询改写方法是 HyDE(Hypothetical Document Embeddings,假设性文档嵌入)。
HyDE 由 Luyu Gao 等人在论文中提出,是一种无需微调模型即可提升向量检索效果的方法。
它主要解决的是向量检索中的一个典型问题:
用户的 Query 往往非常短,而知识库中的文档却十分完整,两者在向量空间中的语义分布存在较大的差距。
例如,用户可能只输入一句:
Transformer 为什么比 RNN 好?
但知识库中的文档却可能包含数百甚至数千字,对 Transformer 的背景、结构、优势等进行了完整介绍。
直接使用这句 Query 计算向量,往往无法准确定位到真正相关的文档。
HyDE 的思路非常巧妙:
它不直接使用 Query 去检索,而是先让 LLM 生成一篇"假设已经回答了该问题"的文档,然后再利用这篇假设文档进行向量检索。
这样,就把原本困难的:
Query → Document
匹配问题,
转化为了更加容易的:
Document → Document
匹配问题。
由于两侧都是完整的文档,因此向量空间中的语义分布更加接近,检索效果通常会更好。
HyDE 的整体流程如下:
生成(Generate)
将用户 Query 输入给生成式 LLM,让模型生成一篇能够回答该问题的"假设性文档"。这篇文档不要求完全真实,但必须与理想答案保持较高的语义一致性。
编码(Embed)
使用 Embedding 模型对这篇假设文档进行向量化,得到其语义表示。
检索(Retrieve)
使用该向量在向量数据库中进行相似度搜索,找到与这篇假设文档最接近的真实文档。
最终,系统将检索到的真实文档作为 RAG 的上下文,交给后续的 LLM 生成回答。
相比直接对 Query 进行向量检索,HyDE 往往能够获得更高的召回率,尤其适用于用户问题较短、语义信息不足的场景。不过,它也需要额外调用一次 LLM 来生成假设文档,因此会带来一定的时间和计算成本。在实际项目中,需要根据检索效果与系统性能之间的权衡决定是否采用。
3.3 构建上下文并生成回答
经过检索后,搜索引擎会返回与用户问题最相关的 Top-K 个结果。这些结果可以是文档片段(Chunk),也可以是完整文档(Document),具体取决于知识库的组织方式以及检索策略。
随后,RAG 会将这些检索结果作为上下文(Context),与用户的原始问题共同构造成 Prompt,并发送给 LLM,由模型生成最终回答。
如下图所示:

从整个流程可以看出,RAG 并不负责生成答案,它的职责只是从外部知识库中检索出与问题相关的信息,并将这些信息作为上下文提供给 LLM。真正完成理解、推理以及回答生成工作的,始终是大语言模型本身。
因此,可以将 RAG 理解为 LLM 的一个外部知识提供者。它通过动态检索外部知识,弥补模型参数中知识有限、知识过时等问题,使模型能够基于最新、最相关的信息生成更加准确、更加可靠的回答。
当然,检索返回的文档数量(Top-K)也是一个需要权衡的参数。K 值过小,可能遗漏关键知识,导致回答信息不足;K 值过大,则可能引入大量无关内容,不仅增加 Token 消耗,还可能使模型受到无关上下文的干扰。因此,在实际项目中,通常需要结合知识库规模、文档长度以及模型上下文窗口大小,对 Top-K 参数进行调优。
本项目中的回答生成
前面介绍查询路由时提到,本项目将用户问题划分为三种类型:
- list:菜谱列表查询;
- detail:菜谱详情查询;
- general:一般性问答。
不同类型的问题,在获得检索结果后,会采用不同的处理方式。
(1)List 模式
对于 list 类型的问题,并不需要 LLM 参与生成。
系统会直接从检索结果中提取符合条件的菜名,并组成列表返回给用户。这种方式不仅响应速度更快,还能够避免 LLM 在生成过程中产生幻觉(Hallucination)。
如下图所示:

(2)Detail 模式
对于 detail 类型的问题,则需要将检索得到的相关文档填充到对应的 Prompt 模板中,再交由 LLM 生成回答。
为了保证回答格式统一、内容完整,本项目采用了结构化 Prompt,对回答内容进行了明确约束,例如要求模型按照菜品介绍、所需食材、制作步骤、注意事项等固定结构进行输出,从而提升回答的规范性、可读性以及实用性。
对应的 Prompt 模板如下:

(3)General 模式
对于 general 类型的问题,其处理方式与 detail 基本一致,同样会将检索得到的上下文填充到对应的 Prompt 模板中,再交由 LLM 进行回答生成。不同之处在于,两者使用的 Prompt 模板不同:detail 更侧重于生成结构化、步骤清晰的菜谱内容,而 general 则更适用于开放式的食谱相关问答。

4. 小结
本章节主要介绍了 RAG 的基本概念、整体工作流程,以及检索引擎之外的关键技术,包括查询路由(Query Routing)、查询重写(Query Transformation)和回答生成等内容。
通过这些模块可以看到,RAG 并不仅仅是"检索 + LLM"这么简单,而是由多个环节协同工作,共同提升最终回答的准确性与可靠性。
下一章节将聚焦于 RAG 检索引擎 本身,详细介绍其内部实现,包括文档切分、向量化、向量数据库、检索策略等核心技术,进一步理解 RAG 是如何从知识库中高效、准确地检索出相关信息的。