RAG是一种结合了信息检索 和文本生成 的AI技术。它解决了大型语言模型(LLM)的一个关键限制:知识截止日期 和领域专业知识的缺乏。
RAG的整体架构分为三部分:索引,检索,生成
索引:
指的是在离线状态下,从数据来源处获取数据并建立索引的过程。具体而言,构建数据索引包括以下步骤:
- 数据索引: 包括清理和提取原始数据,将 PDF、HTML、Word、Markdown 等不同格式的文件转换成纯文本。
- 分块: 将加载的文本分割成更小的片段。由于语言模型处理上下文的能力有限,因此需要将文本划分为尽可能小的块。
- 嵌入和创建索引: 这一阶段涉及通过语言模型将文本编码为向量的过程。所产生的向量将在后续的检索过程中用来计算其与问题向量之间的相似度。由于需要对大量文本进行编码,并在用户提问时实时编码问题,因此嵌入模型要求具有高速的推理能力,同时模型的参数规模不宜过大。完成嵌入之后,下一步是创建索引,将原始语料块和嵌入以键值对形式存储,以便于未来进行快速且频繁的搜索。
检索:
根据用户的输入,采用与第一阶段相同的编码模型将查询内容转换为向量。系统会计算问题向量与语料库中文档块向量之间的相似性,并根据相似度水平选出最相关的前 K 个文档块作为当前问题的补充背景信息。
生成:
将给定的问题与相关文档合并为一个新的提示信息。随后,大语言模型(LLM)被赋予根据提供的信息来回答问题的任务。根据不同任务的需求,可以选择让模型依赖自身的知识库或仅基于给定信息来回答问题。如果存在历史对话信息,也可以将其融入提示信息中,以支持多轮对话。
这三个阶段是rag的简单的整体架构。可以作为一个最简单的rag。但是以目前的架构来说有两个明显的问题等待着我们:检索质量、回应生成质量。
检索质量
检索质量则是通过索引检索出来的文档是低精度 ,即检索集中的文档块并不都与查询内容相关 ,这可能导致信息错误或不连贯。其次是低召回率问题 ,即未能检索到所有相关的文档块 ,使得大语言模型无法获取足够的背景信息来合成答案。此外,过时信息也是一个挑战 ,因为数据冗余或过时可能导致检索结果不准确。
出现上述这三个问题的原因有着以下几个方面:数据层(文档质量,切分策略不当,元数据丢失),向量化与索引层(嵌入模型不适配,检索算法不当)
文档质量:
解决方案:
-
预处理流水线:建立标准化的数据清洗流程。
-
格式统一 :使用
pandas、pdfplumber、unstructured等库,将所有文档(PDF、Word、HTML、扫描件OCR后)转换为纯文本,并统一编码和换行符。 -
清理噪音:用正则表达式移除无用的页眉、页脚、页码、广告、乱码字符。
-
结构化提取:对于半结构化文档(如报告),尝试提取标题、作者、日期等字段作为元数据。
-
-
人工审核与规则制定:对核心数据源进行抽样审核,制定质量规则(如"必须包含产品名称和版本号"),甚至建立"黄金文档集"。
-
源头治理:如果可能,推动文档生产流程的规范化。
切分策略不当
解决方案 :放弃"一刀切",采用"量体裁衣"。
目前的分块策略有以下四种
固定大小分块,也称为滑动窗口分块,是一种将文本机械地、均匀地切割为预定长度片段的预处理方法。它完全忽略文本内在的语义、句法和结构信息,仅依据字符数、词元数或单词数等单一度量标准进行操作,是文本分割领域最直观、最易实现的"基线"方法。
递归分块:递归分块是一种分层、自适应的文本切分方法 ,它通过在多个粒度级别上使用分隔符,智能地保留文本的自然结构,从而生成语义上更连贯的文本块。其核心思想是:优先使用最符合语义边界的"大"分隔符进行切分,如果切分出的块仍然过大,则再使用更细粒度的分隔符进行递归切分,直到所有块的大小都满足预设要求。
为何优于固定分块?
最大程度保留语义完整性:通过优先使用段落、句子等分隔符,它能确保一个完整的句子或段落尽可能被保留在同一个块内,避免了将一句完整的话从中切断的尴尬,显著提升了单个文本块的语义凝聚力和可读性。
尊重文档固有结构 :对于结构清晰的文档(如Markdown、带标题的PDF、代码文件),使用
["#", "##", "```", "\n\n", "\n"]这样的分隔符列表,可以自然地按照章节、代码块进行划分,使生成的块具有更清晰的上下文。自适应性与灵活性 :它不强制所有块的大小严格一致,而是允许在一个目标范围内(
chunk_size上下)波动。这种灵活性使得它既能处理结构规整的文本,也能应对结构松散的内容。
基于文档的分块:在这种分块方法中,我们根据文档的固有结构来分割文档 。这种方法考虑了内容的流程和结构,但可能不是缺乏清晰结构的有效文档。带有 Markdown 的文档:Langchain 提供了 MarkdownTextSplitter 类来分割包含 Markdown 的文档作为分隔符 。Python/JS 文档:Langchain 提供了 PythonCodeTextSplitter 来根据类、函数等拆分 python 程序 ,我们可以将语言提供给 RecursiveCharacterTextSplitter 类的 from_language 方法。带表格的文档:处理表格时,基于级别 1 和级别 2 的拆分可能会丢失行和列之间的表格关系。要保留这种关系,请以语言模型可以理解的方式格式化表内容(例如,在 HTML、CSV 格式中使用 <table> 标记,并用";"分隔等)。在语义搜索过程中,直接从表中匹配嵌入可能具有挑战性。开发人员通常在提取后对表进行汇总,生成该汇总的嵌入,并将其用于匹配。带有图像的文档(多模态):图像和文本的嵌入内容可能不同(尽管 CLIP 模型支持这一点)。理想的策略是使用多模态模型(如 GPT-4 视觉)来生成图像摘要并存储其嵌入。 Unstructed.io 提供了partition_pdf 方法来从pdf 文档中提取图像。
语义分块:所有上述三个级别都涉及文档的内容和结构,并且需要保持块大小的恒定值。这种分块方法旨在从嵌入中提取语义,然后评估这些块之间的语义关系。核心思想是将语义相似的块放在一起。Llamindex 具有 SemanticSplitterNodeParse 类,允许使用块之间的上下文关系将文档拆分为块。这使用嵌入相似性自适应地选择句子之间的断点。
| 策略 | 核心理念 | 最佳适用场景 | 何时使用 | 工具/示例 |
|---|---|---|---|---|
| 固定分块 | 机械均匀切割:按固定长度切分,无视内容。 | 1. 完全非结构化文本墙 (日志、OCR脏数据) 2. 快速原型/基准测试 3. 信息分布均匀的语料 | 当你需要极致的简单性、速度 ,或处理毫无结构可言的文本时。 | RecursiveCharacterTextSplitter(chunk_size=500) (设为超大分隔符) |
| 递归分块 | 结构感知切割:按分隔符优先级递归切分,保留段落句子边界。 | 1. 通用性最强的日常场景 (博客、文章、一般文档) 2. 混合格式文档 3. RAG系统的默认首选 | 当你需要平衡智能度与复杂度 ,处理大多数常见文档 且希望有较好效果时。推荐作为大多数项目的起点。 | RecursiveCharacterTextSplitter (LangChain) |
| 基于文档分块 | 逻辑单元切割:按文档固有结构(标题、章节、表格)切分。 | 1. 高度结构化文档 (技术手册、论文、法律合同) 2. 需要精确出处引用的场景 3. 代码、Markdown等有明确语法结构的文件 | 当你的文档有清晰、可靠的结构标记 ,且保持逻辑单元完整性比均匀块大小更重要时。 | MarkdownHeaderTextSplitter PythonCodeTextSplitter |
| 语义分块 | 含义驱动聚类:根据相邻句子的语义相似度动态切分。 | 1. 高度非结构化但语义连贯的文本 (访谈、对话、会议记录) 2. 创意/叙述性文本 (小说、故事) 3. 作为其他方法的"优化器" | 当你处理没有明显结构标记,但有内在话题流 的文本,且追求最高质量语义完整性时。 | SemanticChunker (LangChain) SemanticSplitterNodeParser (LlamaIndex) |
检索算法不当
目前的rag有着多种多样的检索方式,那么选择恰当的检索方式则是直接影响检索的chunk质量,因此我将阐述几种当前市面上比较流行的检索方式并将这些检索方式进行归类。
Single-query (单一查询)
流程 :用户查询 → 检索 → LLM生成答案
这种方式则是最简单最直接的检索方式,将用户查询语句转化为向量随后进行相似性检索,检索出k个相关的chunk来丢给llm生成答案
Multi-query (多角度查询)
用户提问的方式千差万别,但背后的意图 和所需知识是固定的。单一查询容易受限于:
词汇不匹配:用户用词和文档用词不同(如"AI" vs "人工智能")。
表达简略:问题不完整,依赖上下文(如"它怎么工作?"中的"它"指代不明)。
意图多义:一个问题可能有多个解释方向(如"苹果"指水果还是公司?)。
多查询就是为了解决这些问题,从多个"角度"去"包围"真实意图。
把一个用户问题,改写成多个不同表达的查询,一起去检索。
流程:
用户查询: "什么是Agent?"
↓
使用LLM生成变体:
├─ "Agent系统的定义是什么?"
├─ "AI Agent的核心概念"
└─ "什么是自主智能体?"
↓
并行检索每个变体
↓
合并并去重结果
↓
生成最终答案
优点
✅ 覆盖面大,降低"问法不匹配"导致的漏检
✅ 对语义检索(向量检索)特别友好
✅ 实现简单,性价比高
缺点
❌ 查询数量多 → 检索成本上升
❌ 可能引入噪声(有些改写偏题)
❌ 不解决"问题本身太复杂"的问题
适合场景:问题比较短,文档表达风格多样,向量召回为主的 RAG
RAG-Fusion(多查询 + 重排序融合)
如果说多查询 是"派出多支侦察队",那么 RAG-Fusion 就是:
-
派出多支不同任务的侦察队(多角度查询)
-
每支队都有自己的评估标准(独立检索排序)
-
回来后进行联合军情研判(交叉评分重排序)
-
形成统一的战略报告(融合结果)
本质区别 :多查询只是查询扩展 ,而RAG-Fusion是整个检索流程的重构。
不只是多查,而是 多路检索 + 智能合并排序。
流程:
生成多个 query
每个 query 单独检索
用算法(如 Reciprocal Rank Fusion)融合结果
这里的重排序的算法则是有多种:
RRF传统算法
这是一种基于排名位置 而非相关性分数的无监督融合算法。它的核心思想是:如果一个文档在多个不同查询的检索结果中都出现,并且排名靠前,那么它很可能非常相关。RRF会给每个文档一个融合分数(公式:总分 = Σ 1/(k+排名)),最后将所有文档按此总分重新排序。其最大优点是简单、无需训练、计算成本极低,特别适合作为多路检索结果的快速融合器。
交叉编码器
交叉编码器将查询和文档拼接在一起 ,输入同一个Transformer模型进行深度交互计算,直接输出相关性分数。这种完全交互的方式让模型能捕捉到最细微的语义关联,因此准确率通常最高。但代价是计算成本巨大,且无法预先计算文档表示,每次推理都需要实时处理,因此通常只用于对少量(如Top 100)候选文档进行最终的精排。
LLM重排序
这种方法直接使用大语言模型作为"评委" ,通过设计提示词(如"给相关性打分0-10")来评估查询和文档的相关性。LLM凭借其强大的语义理解和指令遵循能力,可以处理非常复杂、依赖背景知识的判断任务。其优势是灵活、理解力强、无需训练 ,但主要缺点是推理速度慢、成本高、结果可能不稳定,常用于对最终结果的最终验证或对精度要求极高的关键环节。
Decomposition(问题分解)
问题分解是将复杂问题拆解成多个较简单、可独立解决的子问题,然后分别检索答案,最后综合的思维过程。这模仿了人类专家处理复杂问题的方式。
传统RAG:
用户问题 → 一次性检索 → 生成答案
风险:问题太复杂,单次检索无法覆盖所有方面
带分解的RAG:
用户问题 → 拆解为子问题 → 并行检索每个子问题 → 综合生成答案
优势:每个子问题更聚焦,检索更精准,答案更全面
优点:
答案质量大幅提升:复杂问题回答完整度提升40-60%
检索效率优化:每个子问题检索更精准,减少无关信息
可解释性增强:可以看到推理过程,用户更易理解
处理能力扩展:能处理传统RAG无法处理的超复杂问题
缺点:
分解质量依赖LLM能力:分解不好会导致后续全错
计算成本增加:N倍检索 + N倍生成 + 综合生成
错误传播风险:一个子问题答错会影响最终答案
时间延迟:串行分解可能增加响应时间
HyDE(Hypothetical Document Embeddings)
HyDE 的核心思想很巧妙:不直接检索用户问题,而是先让LLM"想象"一个理想答案,然后用这个想象文档去检索真实文档。
传统RAG:
用户问题 → 向量化 → 直接检索相似文档 → 生成答案
HyDE:
用户问题 → LLM生成"假设答案" → 向量化假设答案 → 用假设答案检索真实文档 → 生成最终答案
关键洞察 :问题本身和答案在语义空间可能很远,但两个答案之间(即使一个是假设的)在语义空间会很近。
| 问题类型 | 传统RAG问题 | HyDE如何解决 |
|---|---|---|
| 抽象概念 | "解释存在主义哲学" - 文档可能用具体哲学家名字,不匹配 | 假设文档会包含"萨特"、"加缪"、"自由选择"等具体词,匹配更好 |
| 专业术语 | 用户用通俗说法,文档用专业术语 | 假设文档会"翻译"成专业术语 |
| 多语言 | 中文问题检索英文文档 | 假设文档可以用英文生成,直接匹配英文文档 |
| 问题vs答案 | 问题在向量空间远离答案 | 假设答案在向量空间靠近真实答案 |
优点:
突破词汇壁垒:用户怎么说都行,LLM会"翻译"成专业表述
提升相关性:假设文档更接近答案的"样子",匹配更准
处理抽象问题:能把模糊需求具体化
零样本适应:无需训练,直接使用现有LLM
创意类查询:对写作、设计等创意任务特别有效
主要局限:
LLM幻觉风险:假设文档可能包含错误信息
计算成本:多了一次LLM生成
不适合简单事实:问"珠穆朗玛峰多高?"没必要生成假设
领域特异性差:专业领域可能生成不准确的假设
延迟增加:比直接检索多100-500ms
Hybrid Retrieval (多路召回策略)
流程:
用户问题
→ LLM 生成一段"假想答案文档"
→ 对这段文档做 embedding
→ 用这个 embedding 去向量检索
| 维度 | Hybrid | HyDE |
|---|---|---|
| 类型 | 检索策略 | Query 构造策略 |
| 发生位置 | Retrieval | Retrieval(更靠前) |
| 是否用 LLM | 可选 | 必须 |
| 是否增加召回路数 | 是 | 否 |
| 是否改变 Query | 否 | 是 |
| 主要提升 | 召回覆盖率 | 语义匹配质量 |
一张总览对比表
| 方法 | 核心目标 | 最大优点 | 最大缺点 | 所属 RAG 阶段 | 是否依赖 LLM | 成本等级 |
|---|---|---|---|---|---|---|
| Multi-Query | 提高召回 | 多角度查询,覆盖面广 | 成本高,易引噪声 | Retrieval(Query 增强) | ✅ 是 | 中~高 |
| RAG-Fusion | 稳定高质量结果 | 多路结果融合,排序更准 | 架构复杂,延迟高 | Retrieval(融合召回) | ❌ 否 | 中 |
| Decomposition | 解决复杂问题 | 子问题清晰,理解深入 | 流程长,链路复杂 | Retrieval(任务分解) | ✅ 是 | 高 |
| HyDE | 弥补查询过短 | 冷启动友好,语义命中强 | 假设文档可能带偏 | Retrieval(Query 重写) | ✅ 是 | 中 |
| Hybrid(关键词 + 语义) | 提高召回稳定性 | 精确命中 + 语义泛化 | 权重需调,非最强单点 | Retrieval(召回策略) | ❌ 否 | 低~中 |