2026-07-24T15:41:44.399Z / RAG
混合检索策略详解:向量检索与关键词检索的融合实践
深入解析RAG系统中的混合检索策略,包括向量检索、关键词检索和RRF融合算法的实现细节。
混合检索RRFPgvector全文检索
混合检索策略详解:向量检索与关键词检索的融合实践
为什么需要混合检索?
在RAG系统中,检索质量直接决定最终回答的质量。单一检索方式都有局限:
向量检索的优缺点
优点:
- 理解语义相似性
- 能处理同义词和不同表达方式
- "RDL"能找到"罗马尼亚硬拉"
缺点:
- 可能忽略精确关键词
- 对专业术语的精确匹配不够可靠
- 相似度分数难以设定统一阈值
关键词检索的优缺点
优点:
- 精确匹配专业术语
- 可解释性强
- 分数有明确含义
缺点:
- 无法理解同义词
- "卧推"找不到"bench press"
- 依赖分词质量
FitAtlas的混合检索架构
用户问题:"卧推时手肘应该保持多少度?"
│
▼
┌───────────────────┐
│ 查询规范化 │
│ - 清洗空白字符 │
│ - 转换小写 │
│ - 别名扩展 │
└───────────────────┘
│
┌───────┴───────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ 向量检索 │ │ 关键词检索│
└─────────┘ └─────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│pgvector │ │PostgreSQL│
│cosine │ │全文检索 │
│Top 20 │ │Top 20 │
└─────────┘ └─────────┘
│ │
└───────┬───────┘
▼
┌───────────────┐
│ RRF 融合 │
│ 排名合并 │
└───────────────┘
│
▼
┌───────────────┐
│ 最终 Top 8 │
│ 带引用的片段 │
└───────────────┘
查询规范化
在检索之前,系统会规范化查询文本:
1. 基础清洗
" 卧推 时 手肘 " → "卧推时手肘"
2. 别名扩展
健身领域有大量中英文混用的术语:
"RDL" → "rdl 罗马尼亚硬拉 romanian deadlift"
"深蹲" → "深蹲 squat"
"卧推" → "卧推 bench press"
扩展后的查询同时包含中英文,能同时命中两种表达的文档。
向量检索实现
查询向量生成
# 1. 规范化查询
normalized_query = normalize_query("卧推时手肘应该保持多少度?")
# 2. 生成查询向量
query_vector = await embedding_provider.embed([normalized_query])
# 3. L2归一化(保证余弦相似度计算正确)
query_vector = normalize_l2(query_vector[0])
pgvector余弦检索
SELECT
id,
content,
heading_path,
embedding <=> :query_vector AS distance
FROM chunks
JOIN documents ON chunks.document_id = documents.id
WHERE documents.status = 'published'
AND 1 - (embedding <=> :query_vector) >= :min_similarity
ORDER BY distance
LIMIT :vector_candidates;
关键点:
<=>是pgvector的余弦距离运算符cosine_similarity = 1 - cosine_distance- 只检索
published状态的文档 - 过滤低相似度结果(默认阈值0.40)
HNSW索引
CREATE INDEX ix_chunks_embedding_hnsw
ON chunks
USING hnsw (embedding vector_cosine_ops);
HNSW(Hierarchical Navigable Small World)是近似最近邻算法,在精度和速度之间取得良好平衡。
关键词检索实现
中文分词
PostgreSQL的simple配置不会自动进行中文分词,需要在应用层处理:
import jieba
def tokenize_for_search(text: str) -> str:
# 1. jieba分词
tokens = jieba.cut(text)
# 2. 英文和数字补充
# 3. 去重
return " ".join(tokens)
索引构建
每个chunk的关键词索引由以下内容组合:
文档标题 + 文档标签 + Markdown标题路径 + chunk正文
组合后经过jieba分词,写入search_text字段:
-- search_text是应用层分词后的文本
-- search_vector是PostgreSQL自动生成的tsvector
ALTER TABLE chunks
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('simple', search_text)) STORED;
GIN索引
CREATE INDEX ix_chunks_search_vector_gin
ON chunks
USING gin (search_vector);
全文检索查询
SELECT
id,
content,
ts_rank_cd(search_vector, query) AS rank
FROM chunks
JOIN documents ON chunks.document_id = documents.id
WHERE documents.status = 'published'
AND search_vector @@ websearch_to_tsquery('simple', :keyword_query)
ORDER BY rank DESC
LIMIT :keyword_candidates;
RRF融合算法
为什么用RRF?
向量检索和关键词检索的原始分数量纲不同:
- pgvector使用cosine distance(0-2)
- PostgreSQL全文检索使用ts_rank_cd(0-1)
不能直接相加混合,RRF只使用排名:
RRF公式
RRF分数(d) = Σ 1/(k + rank_i(d))
其中:
d是文档片段k是平滑参数(FitAtlas使用60)rank_i(d)是文档在第i路检索中的排名
计算示例
示例一:两路排名都高
向量排名 = 2
关键词排名 = 1
RRF = 1/(60+2) + 1/(60+1)
= 1/62 + 1/61
≈ 0.016129 + 0.016393
≈ 0.032522
该片段同时具备较强的语义相关性和字面相关性,排在前面。
示例二:向量很高、关键词较低
向量排名 = 1
关键词排名 = 17
RRF = 1/(60+1) + 1/(60+17)
= 1/61 + 1/77
≈ 0.0164 + 0.0130
≈ 0.0294
虽然是向量检索第一名,但关键词排名较低,融合后可能落到第六或第七。
示例三:只有一路召回
向量排名 = 1
关键词排名 = 无
RRF = 1/(60+1) = 0.0164
只有向量检索的贡献。
检索质量调试
页面指标解读
| 表现 | 含义 |
|---|---|
| VECTOR高、KEYWORD高 | 语义和字面都相关,最可靠 |
| VECTOR高、KEYWORD低 | 表达方式不同,但语义接近 |
| VECTOR低、KEYWORD高 | 出现相同词,整体语义可能较弱 |
| 两者都较低 | 候选池尾部结果 |
| 只有VECTOR | 纯语义召回 |
| 只有KEYWORD | 纯字面召回 |
推荐调试顺序
- 检查入库状态:确认文档状态为published,chunks数量不为零
- 使用明确答案的问题:避免"胸部"这种宽泛查询
- 检查Top 5:正确证据是否出现?排在第几?
- 比较两路排名:定位是向量还是关键词的问题
- 最后检查生成:只有检索正确才调试LLM提示词
宽泛查询的问题
查询"胸部"会导致:
- 文档标题"胸部训练"被加入每个chunk的关键词索引
- 大量来自同一文档的片段进入关键词候选
- "胸部"语义过于宽泛,无法判断用户具体需要什么
更适合评测的查询:
"卧推时手肘应该保持多少度?"
"上斜卧推应该设置多少角度?"
"卧推时怎样保持肩胛稳定?"
参数调优
| 参数 | 默认值 | 作用 |
|---|---|---|
| VECTOR_CANDIDATES | 20 | 向量召回候选数 |
| KEYWORD_CANDIDATES | 20 | 关键词召回候选数 |
| CONTEXT_LIMIT | 8 | 最终结果数 |
| MIN_VECTOR_SIMILARITY | 0.40 | 最低向量相似度 |
| RRF k | 60 | 排名融合平滑参数 |
调优原则
- 不要只根据单个查询调整参数
- 使用固定黄金问题集比较调整前后结果
- 增加候选数会提高召回,也会增加噪声
- 提高最低相似度会减少弱相关结果,也可能降低Recall
- RRF只能融合已经召回的候选,不能找回两路都没有召回的知识
经验总结
- 混合检索是必要的:向量和关键词互补,不是竞争关系
- RRF是简单有效的融合方案:只使用排名,避免混合不同量纲的分数
- 查询规范化很重要:别名扩展能显著提高召回率
- 中文需要应用层分词:PostgreSQL的simple配置不适合中文
- 调试要系统化:先确认检索正确,再优化生成
- 参数调优要基于数据集:不能只看几个演示案例
检索质量指标
建议追踪的指标:
- Recall@5:Top 5中是否包含正确答案(目标≥80%)
- MRR:正确答案的平均排名倒数
- 无答案问题误召回:知识库中没有答案时是否误召回
- 同文档结果占比:Top N中来自同一文档的比例
- 引用可回溯率:引用能否定位到原始来源
本文基于FitAtlas健身知识库的实践经验。FitAtlas使用BGE-M3 + PostgreSQL pgvector构建可追溯的RAG系统。