2026-07-24T15:41:44.399Z / RAG

混合检索策略详解:向量检索与关键词检索的融合实践

深入解析RAG系统中的混合检索策略,包括向量检索、关键词检索和RRF融合算法的实现细节。

混合检索RRFPgvector全文检索

混合检索策略详解:向量检索与关键词检索的融合实践

为什么需要混合检索?

在RAG系统中,检索质量直接决定最终回答的质量。单一检索方式都有局限:

向量检索的优缺点

优点

  • 理解语义相似性
  • 能处理同义词和不同表达方式
  • "RDL"能找到"罗马尼亚硬拉"

缺点

  • 可能忽略精确关键词
  • 对专业术语的精确匹配不够可靠
  • 相似度分数难以设定统一阈值

关键词检索的优缺点

优点

  • 精确匹配专业术语
  • 可解释性强
  • 分数有明确含义

缺点

  • 无法理解同义词
  • "卧推"找不到"bench press"
  • 依赖分词质量

FitAtlas的混合检索架构

text
用户问题:"卧推时手肘应该保持多少度?"
            │
            ▼
    ┌───────────────────┐
    │   查询规范化       │
    │  - 清洗空白字符    │
    │  - 转换小写        │
    │  - 别名扩展        │
    └───────────────────┘
            │
    ┌───────┴───────┐
    ▼               ▼
┌─────────┐   ┌─────────┐
│ 向量检索 │   │ 关键词检索│
└─────────┘   └─────────┘
    │               │
    ▼               ▼
┌─────────┐   ┌─────────┐
│pgvector │   │PostgreSQL│
│cosine   │   │全文检索   │
│Top 20   │   │Top 20    │
└─────────┘   └─────────┘
    │               │
    └───────┬───────┘
            ▼
    ┌───────────────┐
    │  RRF 融合      │
    │  排名合并      │
    └───────────────┘
            │
            ▼
    ┌───────────────┐
    │  最终 Top 8    │
    │  带引用的片段   │
    └───────────────┘

查询规范化

在检索之前,系统会规范化查询文本:

1. 基础清洗

text
"  卧推  时  手肘  " → "卧推时手肘"

2. 别名扩展

健身领域有大量中英文混用的术语:

text
"RDL" → "rdl 罗马尼亚硬拉 romanian deadlift"
"深蹲" → "深蹲 squat"
"卧推" → "卧推 bench press"

扩展后的查询同时包含中英文,能同时命中两种表达的文档。

向量检索实现

查询向量生成

python
# 1. 规范化查询
normalized_query = normalize_query("卧推时手肘应该保持多少度?")

# 2. 生成查询向量
query_vector = await embedding_provider.embed([normalized_query])

# 3. L2归一化(保证余弦相似度计算正确)
query_vector = normalize_l2(query_vector[0])

pgvector余弦检索

sql
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索引

sql
CREATE INDEX ix_chunks_embedding_hnsw 
ON chunks 
USING hnsw (embedding vector_cosine_ops);

HNSW(Hierarchical Navigable Small World)是近似最近邻算法,在精度和速度之间取得良好平衡。

关键词检索实现

中文分词

PostgreSQL的simple配置不会自动进行中文分词,需要在应用层处理:

python
import jieba

def tokenize_for_search(text: str) -> str:
    # 1. jieba分词
    tokens = jieba.cut(text)
    
    # 2. 英文和数字补充
    # 3. 去重
    return " ".join(tokens)

索引构建

每个chunk的关键词索引由以下内容组合:

text
文档标题 + 文档标签 + Markdown标题路径 + chunk正文

组合后经过jieba分词,写入search_text字段:

sql
-- search_text是应用层分词后的文本
-- search_vector是PostgreSQL自动生成的tsvector
ALTER TABLE chunks 
ADD COLUMN search_vector tsvector 
GENERATED ALWAYS AS (to_tsvector('simple', search_text)) STORED;

GIN索引

sql
CREATE INDEX ix_chunks_search_vector_gin 
ON chunks 
USING gin (search_vector);

全文检索查询

sql
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公式

text
RRF分数(d) = Σ 1/(k + rank_i(d))

其中:

  • d 是文档片段
  • k 是平滑参数(FitAtlas使用60)
  • rank_i(d) 是文档在第i路检索中的排名

计算示例

示例一:两路排名都高

text
向量排名 = 2
关键词排名 = 1

RRF = 1/(60+2) + 1/(60+1)
    = 1/62 + 1/61
    ≈ 0.016129 + 0.016393
    ≈ 0.032522

该片段同时具备较强的语义相关性和字面相关性,排在前面。

示例二:向量很高、关键词较低

text
向量排名 = 1
关键词排名 = 17

RRF = 1/(60+1) + 1/(60+17)
    = 1/61 + 1/77
    ≈ 0.0164 + 0.0130
    ≈ 0.0294

虽然是向量检索第一名,但关键词排名较低,融合后可能落到第六或第七。

示例三:只有一路召回

text
向量排名 = 1
关键词排名 = 无

RRF = 1/(60+1) = 0.0164

只有向量检索的贡献。

检索质量调试

页面指标解读

表现 含义
VECTOR高、KEYWORD高 语义和字面都相关,最可靠
VECTOR高、KEYWORD低 表达方式不同,但语义接近
VECTOR低、KEYWORD高 出现相同词,整体语义可能较弱
两者都较低 候选池尾部结果
只有VECTOR 纯语义召回
只有KEYWORD 纯字面召回

推荐调试顺序

  1. 检查入库状态:确认文档状态为published,chunks数量不为零
  2. 使用明确答案的问题:避免"胸部"这种宽泛查询
  3. 检查Top 5:正确证据是否出现?排在第几?
  4. 比较两路排名:定位是向量还是关键词的问题
  5. 最后检查生成:只有检索正确才调试LLM提示词

宽泛查询的问题

查询"胸部"会导致:

  • 文档标题"胸部训练"被加入每个chunk的关键词索引
  • 大量来自同一文档的片段进入关键词候选
  • "胸部"语义过于宽泛,无法判断用户具体需要什么

更适合评测的查询:

text
"卧推时手肘应该保持多少度?"
"上斜卧推应该设置多少角度?"
"卧推时怎样保持肩胛稳定?"

参数调优

参数 默认值 作用
VECTOR_CANDIDATES 20 向量召回候选数
KEYWORD_CANDIDATES 20 关键词召回候选数
CONTEXT_LIMIT 8 最终结果数
MIN_VECTOR_SIMILARITY 0.40 最低向量相似度
RRF k 60 排名融合平滑参数

调优原则

  1. 不要只根据单个查询调整参数
  2. 使用固定黄金问题集比较调整前后结果
  3. 增加候选数会提高召回,也会增加噪声
  4. 提高最低相似度会减少弱相关结果,也可能降低Recall
  5. RRF只能融合已经召回的候选,不能找回两路都没有召回的知识

经验总结

  1. 混合检索是必要的:向量和关键词互补,不是竞争关系
  2. RRF是简单有效的融合方案:只使用排名,避免混合不同量纲的分数
  3. 查询规范化很重要:别名扩展能显著提高召回率
  4. 中文需要应用层分词:PostgreSQL的simple配置不适合中文
  5. 调试要系统化:先确认检索正确,再优化生成
  6. 参数调优要基于数据集:不能只看几个演示案例

检索质量指标

建议追踪的指标:

  • Recall@5:Top 5中是否包含正确答案(目标≥80%)
  • MRR:正确答案的平均排名倒数
  • 无答案问题误召回:知识库中没有答案时是否误召回
  • 同文档结果占比:Top N中来自同一文档的比例
  • 引用可回溯率:引用能否定位到原始来源

本文基于FitAtlas健身知识库的实践经验。FitAtlas使用BGE-M3 + PostgreSQL pgvector构建可追溯的RAG系统。

混合检索策略详解:向量检索与关键词检索的融合实践 | Remi Resume