RAG深入解读:文本分块、混合检索、重排序、bge微调(工程落地实践)
大家好,我是此林。
如今,我们惊叹 LLM 基模能力越来越强的同时,工程师越来越多地把目光转向了如何让 LLM 落地于实际生产中。比如 RAG 智能客服、知识库助手就一个落地案例。
1. RAG 流程概要
一般我们眼中认为的 RAG 可能是这样的。
1. 输入阶段:用户查询 “世界上最高的山是哪座?”
2. 系统将查询转换为 向量(embedding),用查询向量去 向量数据库(Vector DB)中查找最相似的文档或片段。
3. 将检索到的文档片段与用户问题拼接成 prompt,比如:
Context: [文档1内容] [文档2内容] ...
Question: [用户问题]
4. 使用 大语言模型(LLM) 生成答案。
综上,可以看出,RAG 最重要的部分其实就是 文本召回阶段。如何根据用户的提问提高文本召回的准确度成为了一个值得考量的研究点。
2. 检索技术
目前 RAG 的 Retrieval 存在一些弊端,比如召回率低,准确率低,噪声大,存在冗余查询,效率和鲁棒性差等。
因此我们需要Hybrid Search (混合搜索)。目前比较通用的混合搜索是多路混合检索:
全文搜索 with BM25 + 稠密向量(语义匹配)
2.1. 全文搜索 with BM25
这个其实也就是关键词搜索,先对用户的 query 进行分词、去除停用词标点符号等噪声,然后去检索文档,根据文档相关性检索出 TopN。
像 ElasticSearch,为了提高检索速度,常使用倒排索引(Inverted Index)等技术。将文档中的每个单词映射到包含该单词的文档列表,从而实现高效的查询。查询速度快,适合精确匹配。支持复杂的查询语法(如布尔查询、通配符查询)。
相关性排序,常用算法为 BM25算法。
BM25 在传统 TF-IDF 基础上做了两点改进:
对词频的饱和处理:避免高频词权重过大。
考虑文档长度:避免长文档天然得分高。
它通过结合词频、逆文档频率和文档长度归一化,提供了一种高效评估文档与查询相关性的方法。
全文搜索优点:响应快速,关键词精准性高。
全文搜索缺点:无法理解语义,仅依赖字面匹配。对同义词、语义相似性等处理能力有限,当然这可以通过归一化等预处理来解决。
2.2. 稠密向量(语义匹配)
向量检索是一种基于向量空间的相似度搜索方法,将文本、图像或其他数据通过编码器转换为固定维度的向量表示,再通过计算 向量相似度(如余弦相似度或欧氏距离)来实现快速检索。
相比传统关键词检索,向量检索能捕捉语义信息,支持模糊匹配和跨语言检索,广泛应用于智能问答、推荐系统和知识管理。常用向量搜索技术为 ANN(近似最近邻)算法。
常见向量数据库有:chroma、Milvus、Qdrant 等。
但是需要预训练模型生成向量,计算复杂度较高。对硬件资源(如GPU)要求较高。
2.3. 二者结合
在 RAG 系统中,采用多种召回策略能够显著提升结果质量。例如,将向量搜索、全文搜索结合使用,往往能获得最佳召回效果。
这是因为向量能够捕捉文本的语义信息:无论是一句话还是整篇文章,都可以通过向量来表示其核心含义,本质上向量传递了文本在上下文中出现的概率分布的压缩信息。
然而,向量检索往往难以完全精确匹配查询。
比如,当用户提问:“2025年第二季度的市场推广预算分配是多少?”时,向量搜索可能会返回其他年份或季度的数据或与预算无关的内容,如销售计划或产品策略。
而全文搜索则更侧重精确匹配查询中的关键词和语义。因此,将多种检索方式结合使用,可以兼顾语义理解能力与检索精度,满足实际应用需求。
3. 文本分块技术
既然我们用了向量检索,文本分块肯定少不了。
文本分块是将原始文档切分成较小的片段,以便更高效地进行检索和生成。通常会根据语义或长度将文档切成固定字数或句子为单位的块,比如每块500字左右,然后通过向量模型转为向量存储到向量数据库中。
传统的分块方式通常采用固定长度的切分策略,例如每500个字符或每若干句子进行一次分割。这种方法虽然实现简单,但在实际应用中容易割裂完整的语义单元,导致后续的信息检索与理解受到影响。
目前比较流行的分块技术可以参考 unstructured.io
下面介绍几种常见的分块策略。
3.1. 基础分块策略
如果强制按照固定字符数来切分文档,比如每500个字切分一次,那么某些语句可能就会中间截断,导致语义缺失。
那我们是不是可以通过正则匹配识别末尾句号来优化呢?
如果检测到当前 chunk 被截断了,那么当前 chunk 文本就回退到最近的一个句号;这个被截断的文本放到下一个 chunk 当中。
3.2. 基于标题分块 + 重叠上下文
在基础分块的上,我们还可再进行标题分块,如下图所示:
可以看到,图中 title1 下的文本块字数超出了硬限制,所以 Element5 就放到了 chunk2,同时因为 Element5 的文本块字数也超出了硬限制,所以被切分到了 chunk3。
title 2 因为和 title1 不一样,所以即使 Element5 Part2 字数不足,Title 2 的内容也依然被切分到了 chunk 4。
概念图说明了这一点,因为许多 Title 元素可以生成许多相对较小的块。
重叠上下文,什么意思呢?看下图:
每个 chunk 都不是独立的个体,所以在部分 chunk 中可以重叠加入上一个 chunk 的末尾内容,以增强语义的准确性和连贯性。
3.3. 语义相似性分块
按相似性分块策略使用如: sentence-transformers/multi-qa-mpnet-base-dot-v1 嵌入模型来识别主题相似的顺序元素并将它们组合成块。
与其他分块策略一样,块永远不会超过“最大字符数”设置的绝对最大块大小。因此,并非所有共享主题的元素都一定会出现在同一个块中。但是,使用此策略,可以保证两个相似度低的元素不会组合成一个块。
当然语义分块策略也可以使用:百分位法、标准差法、四分位距法等。
语义分块在工程上用的可能不是很多,因为其实现复杂度高,开销比较大。
而且由于文档的非结构化,相比于其他分块方法,语义分块对于 RAG 文本召回的提升水平可能并不是很显著。
4. 意图改写
4.1. 查询重写
在很多情况下,用户提出的问题可能比较模糊或简短,例如:
“介绍下公司的业务?”
这种提问虽然明确,但不够具体,系统在中搜索时可能会返回过于宽泛或不相关的结果。
查询重写的作用就是根据原始问题的意图,生成一个更清晰、更详细的版本,帮助系统找到更相关的信息。
一般来说,我们会基于大模型做query rewrite,例如:
system_prompt:你是一个擅长搜索策略的AI助手。你的任务是将具体查询扩展为更通用的形式,以获取相关的背景信息。你是一个擅长搜索策略的AI助手。你的任务是将具体查询扩展为更通用的形式,以获取相关的背景信息。
user_prompt:
请将以下查询语句进行改写,使其更加具体并包含有助于精准检索的相关术语和概念。
原始查询: {original_query}
改写后的查询:
4.2. 子查询拆解
子查询拆解是在 RAG 系统中常用的一种查询优化方法。用户的问题往往复杂或包含多个意图,若直接送入检索模块,容易出现召回不足或结果不精确。
通过子查询拆解,可以将一个复合问题分解为多个独立、明确的小问题,再分别进行检索,最后由生成模型融合回答。
这样做的好处是覆盖率更高,能够避免遗漏用户意图的某些方面;同时粒度更细,检索结果更精准。例如 “比较 RAG 和传统问答系统在召回率和速度上的差异”,可拆解成四个子问题,分别关注召回率和速度的对比。最终系统将多个答案整合,给出结构化的综合回复,从而显著提升问答质量和用户体验。
用户原问题 | 拆解后的子查询 |
---|---|
RAG 的优势和劣势分别是什么? | 1. RAG 的优势是什么? 2. RAG 的劣势是什么? |
RAG 和传统问答系统在召回率和速度上的区别? | 1. RAG 的召回率特点是什么? 2. 传统问答系统的召回率特点是什么? 3. RAG 的响应速度特点是什么? 4. 传统问答系统的响应速度特点是什么? |
RAG 的完整流程是什么?每一步如何协同? | 1. RAG 的第一步是什么? 2. RAG 的第二步是什么? 3. RAG 各步骤如何协同工作? |
prompt:你是一个智能查询优化助手。用户可能会提出复杂、复合或模糊的问题。
你的任务是将用户问题拆解为若干个可以独立检索的子问题。
要求:
- 保持语义覆盖,不遗漏任何意图
- 子问题必须清晰、完整、独立
- 输出JSON格式:{"subqueries": ["子问题1", "子问题2", ...]}用户问题:{query}
5. 重排序 Reranker
前文已经说到,混合检索结合了向量检索与稀疏检索(如 BM25),既能捕捉语义相似度,又能利用关键词匹配提高覆盖率。但这一步得到的候选文档往往仍然包含噪声:有的结果虽然语义接近,但缺乏关键事实;有的结果关键词命中率高,却与查询语境不符。
因此,仅靠混合检索无法保证返回结果的排序最优。
这时 reranker 的作用就凸显出来。reranker 通常基于 Cross-Encoder 架构,它会将查询与候选文档成对输入模型,逐一计算相关性分数。这种方式比 Bi-Encoder(用于向量检索)更精细,因为它能充分建模查询与文本之间的交互,从而更准确地判断相关性。比如 bge-m3 适合做初步检索,而 bge-reranker-v2-m3 则在候选集合内进行更高质量的筛选。
下面这张图就很清楚地展现了 Cross-Encoder(交叉编码器)和 Bi-Encoder(双编码器)之间的区别。
Bi-Encoder:一般来说,Embedding 模型会使用双编码器(例如:bge-m3)。
它会为每个句子生成一个嵌入(向量)。
Cross-Encoder:reranker 模型通常使用交叉编码器架构(例如:bge-reranker-m3-v2)。
我们将两个句子同时传递给 Transformer 网络。然后它会产生一个介于 0 和 1 之间的输出值,表示输入句子对的相似度。
Cross-Encoder 比 Bi-Encoder 精度更好,因为它接受两句话拼接输入,同一模型能逐词比对,理解上下文逻辑,因此排序更准确。
那既然这样,为什么还要 Embedding 模型,直接用 Reranker 不就好了吗?
虽然 Reranker(Cross-Encoder)精度高,但直接用它来检索全部文档在实际场景中是不现实的,这就是为什么仍然需要 Embedding(Bi-Encoder)模型的原因。
首先,Embedding 模型可以将文档和查询映射到向量空间,通过向量相似度快速筛选候选文档。这种方法非常高效,能够在海量文档中即时找到相关内容。
而 Reranker 的 Cross-Encoder 需要将查询和每个文档拼接后输入模型进行评分,计算量与文档数量线性相关,对于百万级别甚至更大的文档库,计算成本非常高,几乎不可承受。
再通俗一点来说,就是 Embedding 模型可以通过向量建立查询索引, Reranker 模型每次都要全表扫描,速度可想而知。
所以在 RAG 整体流程上,混合检索保证了“召回率”,尽量把可能有用的文档找回来;而 reranker 则保证“精确率”,把真正最相关的结果排到前面。
这种 粗排 + 精排 的两阶段设计,大幅提高了 RAG 最终生成答案的可靠性和准确性。
6. 微调 Reranker 模型
微调 Reranker 是为了让模型更精准地理解特定任务的相关性。例如,在特定领域的问答或文档检索中,默认模型可能无法捕捉领域术语或用户意图,通过微调,可以让 Reranker 学会更准确地排序候选文档,提高检索结果的准确性和实用性,同时减少错误匹配。
一般我们可以使用 sentence_transformer 来微调。
微调其实比较简单,代码量不大,最重要的其实是数据集的质量和分布。
微调要点
训练 CrossEncoder 模型的成功通常取决于负例的质量,即查询-负例得分应该很低的段落。负例可以分为两种类型:
-
软负例 (Soft negatives):完全不相关的段落。
-
难负例 (Hard negatives):那些看起来可能与查询相关,但实际上不相关的段落。
一个简洁的例子是:
-
查询:苹果公司在哪里成立的?
-
软负例:凯奇河大桥是一座帕克小马桁架桥,横跨阿肯色州核桃岭和帕拉古尔德之间的凯奇河。
-
难负例:富士苹果是一种在 1930 年代后期开发并于 1962 年上市的苹果品种。
通常 epochs 设置为个位数即可,数据集最好可以多一点,但要保证高质量。
warm_steps 的值通常设为 总训练步数的 5%–10% 比较合理。
(学习率预热(learning rate warm-up) 的作用是让优化器在训练开始的几个步骤里,从很小的学习率逐渐增加到设定的学习率,然后再按原计划训练。这样做可以让模型训练更加稳定,尤其是大模型或者在小批量数据上训练时,能减少梯度爆炸或训练不稳定的风险。)
今天的分享就到这里了。
我是此林,关注我吧,带你看不一样的世界!