掌握RAG(检索增强生成)检索策略,能让你的知识库在海量数据中精准、快速地定位所需信息。本教程将深入解析RAG检索的三大核心环节,助你轻松驾驭检索全流程,显著提升知识库的响应质量与效率。

检索前优化:让提问更精准
在正式检索知识库之前,我们首先需要优化用户提出的问题(query)。一个模糊或不完整的问题,很可能导致检索结果不理想。以下是三种常见且高效的优化策略:
1. 原始问题改写
当用户提出一个问题后,调用一个大语言模型,结合对话上下文对这个原始问题进行改写,使其具备更完整的语义。
# 原始对话
Q: 请给我推荐一门编程语言
A: python
Q: 我该如何开始?
----
# 说明
如果直接使用"我该如何开始"去检索知识库
显然效果是很不好的
# 基于上下文进行问题改写
Q: 我应该如何开始学习Python?
# 说明
根据上下文改写后的问题,具备了比较好的语义完整度。
小提示: 问题改写是提升检索召回率的第一步,尤其适用于用户提问不够完整或带有指代关系的场景。
2. 多查询生成 (Multi-Query)
将单个用户问题扩展为多个角度不同、表达方式不同的子问题,分别进行检索,从而覆盖更广的相关信息。
# 用户问题
Q: 我想学习PYTHON
# 提示词
你是一个AI语言模型助手。
你的任务是针对给定的用户问题生成五个不同版本的表述,以便从向量数据库中检索相关文档。
通过对用户问题生成多种角度的表述,你的目标是帮助用户克服基于距离的相似性搜索的一些局限性。
将这些替代问题用换行符分隔开。
原始问题:{{ question }}
# 改写后的问题
Q1: 如何高效掌握Python编程?
Q2: 学习Python的最佳路径是什么?
Q3: Python入门教程推荐有哪些?
Q4: 从零开始学Python应该怎么做?
Q5: Python编程技能提升方法有哪些?
-----
使用改写后的5个问题,分别检索知识库
得到5组不同的检索结果
最终再通过大模型把5种结果进行筛选合并
生成最佳答案
3. 子问题拆解 (Sub-Question)
当用户提出的问题比较复杂,包含多个方面时,可以将其拆解成一系列更简单、更具体的子问题。然后分别检索每个子问题的答案,最后综合所有信息形成最终回答。
# 用户原始问题
Q: Coze和dify有啥区别?
# 改写后的问题
Q1: Coze的基本介绍
A1: Coze 是字节跳动旗下...
Q2: Dify基本介绍
A2: Dify 是一个开源的LLM应用平台...
Q3: Coze和Dify有啥区别?
A3: 基于上面的两个检索结果生成最佳答案
小提示: 以上只是几种基础优化策略。实际应用中,检索词的优化程度直接决定了后续检索的成败,你需要根据业务场景灵活选择或组合使用。
常见问题: 检索前优化是否总是必要的?
- 不一定。对于已经非常明确、完整且简单的问题(如“北京故宫有多少件文物?”),可以直接进行检索。但对于不完整、模糊或有歧义的问题,优化能显著提升检索效果。
知识检索:高效召回相关知识
问题优化后,接下来就是利用这些检索词,从知识库中找出最相关的知识片段。目前主流的方法有四种:
1. 关键词检索(传统检索)
这是最基础的检索方式,使用检索词对文本或文本标签进行精确的模糊匹配(如正则匹配)。
优点: 能够精准匹配关键词。
缺点: 无法理解同义词、近义词,容易造成漏检。
# 知识原文
北京故宫中存放了超过180万件珍贵文物。
# 关键词
北京、故宫、文物
# 用户问题
Q: 故宫中有多少件文物?
# 匹配了关键词 "故宫"和"文物"
# 能够召回原文片段
A: 北京故宫中存放了超过180万件珍贵文物。
# 但问题如果变成
Q:紫禁城中有多少件宝贝?
# 这时候由于无法匹配关键词,召回就失败了
A: 不知道
注意: 当用户使用“紫禁城”代替“故宫”,或用“宝贝”代替“文物”时,传统的关键词检索就会失效。
2. 语义检索(向量检索)
语义检索通过Embedding模型将检索词和文档都转换为多维向量(密集向量),然后在向量空间中计算它们的相似度。语义上越接近的内容,其向量距离就越近。
核心步骤: 检索词向量化 ——> 在向量数据库中搜索最相似的向量。
以下是一个使用openai-sdk、Milvus向量数据库、阿里text-embedding-v4嵌入模型的代码示例:
# Step1: 检索词向量化
const search_vector = await embedding(query);
// 调用Embedding Model进行向量化
const embedding = async (text) => {
const response = await openai.embeddings.create({
model: "text-embedding-v4",
input: text,
});
return response.data[0].embedding;
};
# Step2: 密集向量检索
const vector_res = await milvusService.search(search_vector, 3);
# 查询向量数据库
async search(vector, limit = 10) {
return await this.client.search({
collection_name: 'test',
vector: vector,
limit: limit,
output_fields: ['id', 'block_id', 'name'],
metric_type: 'L2',
params: { nprobe: 10 }
});
}
检索结果如下:
# 检索词
`The founding time of Snow Beer`
# 检索词向量化
[
0.03072373941540718, -0.010284383781254292, -0.006375404074788094,
-0.06936439871788025, 0.0009934211848303676, -0.04058944061398506,
... 924 more items
]
# 密集向量检索结果
[
{
score: 0.46093612909317017,
block_id: '94',
name: 'Below is a detailed introduction to Snow Beer:',
id: '460215000564761333'
},
{
score: 0.517002284526825,
block_id: '95',
name: '1. Brand History & Development Origins: Snow Beer traces its roots to Shenyang Beer Factory, established in 1957.',
id: '460215000564761323'
},
{
score: 0.790127158164978,
block_id: '94',
name: "Snow Beer is one of China's most renowned beer brands, owned by China Resources Snow Breweries (China) Co., Ltd. (CR Snow), a joint venture between the state-owned China Resources Group and the global beer giant SABMiller.",
id: '460215000564761331'
}
]
注意: 示例中使用的是L2欧氏距离作为度量单位,L2值越小,代表两个向量越相似。
常见的向量度量单位对比表:
| 度量单位 | 说明 | 取值范围 |
|---|---|---|
| L2(欧氏距离) | 值越小表示相似度越高。 | [0, ∞) |
| IP(内积) | 值越大,表示相似度越高。 | [-1, 1] |
| COSINE(余弦相似度) | 数值越大,表示相似度越高。 | [-1, 1] |
| JACCARD | 值越小,表示相似度越高。 | [0, 1] |
| MHJACCARD | 根据 MinHash 签名位估算 Jaccard 相似度;距离越小 = 越相似 | [0, 1] |
| HAMMING | 值越小表示相似度越高。 | [0,dim(向量) |
| BM25 | 根据术语频率、反转文档频率和文档规范化对相关性进行评分。 | [0, ∞) |
3. 全文检索(稀疏检索)
全文检索通过BM25算法或SPLADE等模型将文档转换为稀疏向量,利用TF-IDF等统计方法,评估文档与检索词之间的相关性。 与语义检索不同,它无需进行Query向量化,因此不消耗Embedding模型的Token。
代码示例:
# query不需要向量化,所以也不消耗token
const query = 'The founding time of Snow Beer';
# 直接调用向量数据库进行查询
const sparse_res = await milvusService.search(query, 3);
async search(query, limit = 10) {
return await this.client.search({
collection_name: 'test',
anns_field: 'sparse_vector',
limit: limit,
data: [query],
metric_type: 'BM25',
output_fields: ['id', 'block_id', 'name']
});
}
检索结果:
# 检索词
'The founding time of Snow Beer'
# 稀疏向量检索结果:
[
{
score: 4.728769302368164,
block_id: '94',
name: "Snow Beer is one of China's most renowned beer brands, owned by China Resources Snow Breweries (China) Co., Ltd. (CR Snow), a joint venture between the state-owned China Resources Group and the global beer giant SABMiller.",
id: '460215000564761331'
},
{
score: 3.9664859771728516,
block_id: '108',
name: 'Domestic Dominance: Snow has been the world’s top-selling single beer brand since 2006, with 2022 sales reaching ~11 billion liters (30% of China’s market share).',
id: '460215000564761291'
},
{
score: 3.210528612136841,
block_id: '100',
name: 'Bra ve the World: Flagship product for mass markets, known for its slogan "Snow Beer, Bra ve the World."',
id: '460215000564761309'
}
]
注意: 全文检索只能使用BM25作为度量单位,BM25值越大,代表相关性越高。 它可以在一定程度上替代传统的关键词检索,实现更灵活的精确匹配。很多场景会结合全文检索和语义检索一起使用。
4. 混合检索
混合检索是结合了语义检索和全文检索两种方式的综合方案,旨在取长补短。
流程: 分别执行语义和全文检索,得到两组结果,然后通过一定的策略(如RRF或加权)进行结果融合与排序。
许多向量数据库(如Milvus)都提供了封装好的混合检索接口。 以下是Milvus的使用示例:
const hybrid_res = await milvusService.search(search_vector, query, 10);
async hybrid_search(vector, query, limit = 10) {
return await this.client.hybridSearch({
collection_name: 'test',
limit: limit,
output_fields: ['id', 'block_id', 'name'],
data: [{
// 密集向量检索
anns_field: 'vector',
data: [vector],
metric_type: 'L2',
params: { nprobe: 10 }
}, {
// 稀疏向量检索
anns_field: 'sparse_vector',
data: [query],
metric_type: 'BM25'
}],
// 排序策略
rerank: {
strategy: "rrf", // 使用RRF(Reciprocal Rank Fusion)策略
params: {
k: 60, // RRF参数k,默认60
},
}
})
}
常见问题: 我应该选择哪种检索方式?
- 关键词检索: 适用于需要完全精确匹配的场景,如代码片段、专有名词检索。
- 语义检索: 推荐作为默认选择,能处理同义词、近义词,理解能力更强。
- 全文检索: 适合处理包含大量罕见词的查询,或在不消耗Embedding Token的情况下进行统计相关性分析。
- 混合检索: 综合了语义和全文的优势,通常在复杂场景下效果最好,是生产环境的首选。
重排序:优中选优,排出最佳顺序
通过上述多种策略(多路召回)得到大量候选结果后,我们需要一个“裁判”来对这些结果进行重新排序和筛选,确保最相关的结果排在第一位。这个过程叫重排序。
1. RRF(Reciprocal Rank Fusion)
这是一种仅依赖不同检索结果中每个项目的排名的算法,不关心具体的分数。
核心思想: 如果一个项目在所有检索结果中的排名都很靠前,那么它的综合排名就高。
原理示例:
想象你和你的朋友都在找一本丢失的书,每个人都有一个关于书可能在哪里的猜测排名。
人员和地方 书架 床下 桌上
朋友A 1 2 3
朋友B 3 1 2
朋友C 2 1 3
计算每个地方的综合得分:
桌上:1/(1+3) + 1/(1+2) + 1/(1+3) = 1/4 + 1/3 + 1/4 = 0.833
床下:1/(1+2) + 1/(1+1) + 1/(1+1) = 1/3 + 1/2 + 1/2 = 1.333
书架:1/(1+1) + 1/(1+3) + 1/(1+2) = 1/2 + 1/4 + 1/3 = 1.083
得出最终结论:书最有可能在床下(得分最高)。
优点: 简单、快速、不消耗Token,且能有效消除不同检索方法因打分尺度不同带来的偏差。
常见问题: RRF的k参数有什么作用?
k是一个常数,用于平滑排名带来的影响。默认值通常是60。它的作用是防止某个项目因为排名靠后而得分过低。增大k值会使得排名较低的项目对最终结果的影响力变大,而减小k值则会放大排名靠前项目的影响力。
2. Weighted加权策略
这种算法需要为每个检索策略设置一个权重,然后对每个策略返回的结果分数进行归一化处理,最后计算加权总分。
一个简单的代码实现示例:
/**
* 合并结果并应用加权排序
* @param {array} resultsList 各个搜索的结果列表
* @param {number[]} weights 权重数组
* @param {number} limit 最终返回结果数量
* @returns {object} 格式化后的搜索结果
*/
function mergeAndWeightResults(resultsList, weights, limit) {
// 结果合并表 {id: {data: 文档数据, scores: [各搜索的分数]}}
const merged = {};
// 归一化权重
const sumWeights = weights.reduce((sum, w) => sum + w, 0);
const normalizedWeights = weights.map(w => w / sumWeights);
// 收集所有结果并记录各搜索的分数
resultsList.forEach((results, searchIndex) => {
results.results.forEach(item => {
if (!merged[item.id]) {
merged[item.id] = {
data: item,
scores: newArray(resultsList.length).fill(0)
};
}
merged[item.id].scores[searchIndex] = item.score;
});
});
// 计算加权分数并排序
const weightedResults = Object.values(merged).map(item => {
// 计算加权分数
let weightedScore = 0;
for (let i = 0; i < normalizedWeights.length; i++) {
weightedScore += normalizedWeights[i] * normalizeScore(item.scores[i], i);
}
return {
...item.data,
weightedScore, // 加权后的综合分数
originalScores: item.scores // 保留原始分数供调试
};
});
// 按加权分数降序排序
weightedResults.sort((a, b) => b.weightedScore - a.weightedScore);
// 返回格式与Milvus原生搜索一致
return {
status: { error_code: "Success", reason: "" },
results: weightedResults.slice(0, limit),
recalls: resultsList.map(r => r.recalls?.[0] || 0)
};
}
/**
* 分数归一化处理(不同搜索的分数可能尺度不同)
* @param {number} score 原始分数
* @param {number} searchIndex 搜索索引
* @returns {number} 归一化后的分数
*/
function normalizeScore(score, searchIndex) {
// 实际应用中应根据不同搜索类型调整归一化方法
// 这里使用简单的sigmoid归一化示例
return1 / (1 + Math.exp(-score));
}
常见问题: RRF和加权策略,我该选哪个?
- RRF: 当你对各个检索策略的评分置信度不高,或者各策略的评分尺度差异巨大时(如语义检索得分在0-1之间,而BM25得分可达10以上),RRF是不错的选择,因为它只看排名。
- 加权策略: 当你对各个检索策略的效果有明确认知,并愿意为其分配不同的重要性时,加权策略更灵活,能精细调优。
3. Rerank Model(重排序模型)
RRF和加权策略虽然速度快,但精确度有限。目前最主流的方案是:先使用RRF或加权策略进行初步筛选,再调用一个专门的Rerank Model做最终的精细筛选和排序。
这里以阿里的gte-rerank-v2模型为例,演示如何使用Rerank Model:
// rerank方法,直接使用https的方式
const rerank = async (query, documents) => {
const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openai.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gte-rerank-v2',
input: {
query: query,
documents: documents
},
parameters: {
top_n: 1 // 只返回最相关的一个结果
}
})
});
return await response.json();
};
// 调用重排序算法
const rerankRes = await rerank(query, allResults.map(item => item.name));
// 找到最优解
const topIndex = rerankRes?.output?.results?.[0]?.index ?? 0;
console.log('重排序结果:', allResults[topIndex])
小提示: Rerank Model 能够更准确地衡量查询和文档之间的相关性,但会消耗Token和响应时间。在实践中,建议在可接受的性能和成本范围内,对重要的查询使用Rerank Model。
结语
本教程系统地介绍了从检索前优化到多路召回,再到最终重排序的完整RAG检索流程。掌握这些常见的优化策略,已经能够应对大多数知识库的检索需求。
然而,每个实际的生产环境都可能面临独特的挑战。真正的价值在于掌握调优的思路。建议你在理解每种策略的原理和优缺点后,根据自己业务的特点(如数据类型、查询模式、性能要求),灵活组合并定制一套专属的优化方案,以实现最佳的检索效果。
