记录一次真实的搜索优化过程:从发现问题,到尝试 LLM 重排、BGE-Reranker,踩了无数坑,最终找到业界标准方案 RRF 的完整复盘。

起因:搜不到
我的 AI 记忆系统 SummerMemory 用的是经典的混合搜索架构:向量 60% + BM25 40%,按分数合并排序。
有一天,我搜"列出待办事件",期望搜到 summer-alert-project.md——这个文件里有"## 待办"标题,是强提醒推送系统的待办清单。
结果呢?搜不到。
更离谱的是,"待办"这两个字明明就在文件里,BM25 精确匹配怎么可能搜不到?
于是我开始了长达 5 小时的排查之路。
原始架构
先看原来的搜索流程:
查询:"列出待办事件"
│
├──→ 向量搜索(jina-embeddings 768维)──→ top 15
│
├──→ BM25 搜索(jieba 分词 + tsvector)──→ top 15
│
▼
合并分数:vec × 0.6 + bm25 × 0.4
│
▼
排序返回 top 5博客上还写了第三阶段"LLM 重排(计划做的)"——这就是今天要做的事。
博客承诺的第三阶段:重排
重排(Reranking)是搜索的经典架构:先用快速方法召回大量候选,再用精准方法精排。
业界标准是 Retrieve & Re-Rank:
第一阶段(召回):向量 + BM25 各取几十条
第二阶段(精排):对候选结果精确打分,重新排序博客上写的是"LLM 重排",但这其实是个泛称,不一定要用大语言模型。
LLM 重排 vs BGE-Reranker
尝试 LLM 重排(qwen3)
第一反应是跑一个本地 LLM 来做重排。我选了 qwen3:0.6b,通过 Ollama 在 CPU 上运行。
结果一塌糊涂:
| 问题 | 表现 |
|---|---|
| thinking 模式不可控 | 吃掉所有 token,response 为空 |
| CPU 推理速度 | 12-30 秒/条 |
| 解析稳定性 | 输出格式不可预测 |
结论:LLM 重排在本地 CPU 环境下不可用。
BGE-Reranker(Cross-Encoder)
转向专用重排模型 BGE-Reranker。这属于 Cross-Encoder 架构,跟 LLM 是完全不同的东西:
| LLM(qwen3) | BGE-Reranker | |
|---|---|---|
| 类型 | 生成式模型 | 判别式模型(Cross-Encoder) |
| 输入 | query + doc | query + doc 拼在一起 |
| 输出 | 文字(可能跑偏) | 一个分数 0~1(稳定) |
| 速度 | 12-30 秒 | 61ms/pair |
| 25 条候选 | 跑不完 | ~1.5 秒 |
BGE-Reranker 的工作方式很直观:
输入一对:
query: "待办事件"
doc: "SummerAlert强提醒推送 — 待办列表功能"
输出:
0.55(相关性分数)它把 query 和 doc 一起送进模型,模型看到两者的完整上下文关系,所以比分别编码再比较更准。
选择 BGE-Reranker,理由充分:速度快、效果好、CPU 可用。
引入 BGE-Reranker 后的噩梦
第一版:直接加上去
向量 top 15 + BM25 top 15 → 合并 → reranker top 5测试结果:
| 搜索词 | 结果 |
|---|---|
| 告警推送 | ✅ summer-alert 第 1 名(0.90 分) |
| 待办事件 | ❌ 没命中 |
为什么"告警推送"能搜到,"待办事件"搜不到?
因为 summer-alert 根本没进候选池。
查根因
我查了 PostgreSQL,发现 summer-alert 的数据:
- 向量搜索:排第 30 名开外("待办事件"和"强提醒推送系统"语义距离远)
- BM25 搜索:排第 29 名(28 个文件都包含"待办"或"事件",分数全是 0.030)
向量取 15、BM25 取 15,summer-alert 哪个都进不去。
第二版:扩大召回
把向量改成 30、BM25 改成 50:
向量 top 30 + BM25 top 50 → 合并去重 → rerankerDB 查询只要 22ms,很快。但新问题来了。
发现核心问题:BM25 同分灾难
BM25 返回 50 条结果,其中 28 个文件分数完全一样:0.030。
-- 搜"待办事件",BM25 结果
0.038 | 3d-auto-assessment-project.md
0.038 | 2026-07-09.md
0.030 | 2026-07-13.md
0.030 | 2026-07-21.md
0.030 | artistic-seal.md
0.030 | doubao-web.md
0.030 | long-term.md
0.030 | summer-alert-project.md ← 在这里,但跟其他 27 个文件同分
0.030 | summer-pocket-project.md
...共 28 个同分文件BM25 完全无法区分这 28 个文件哪个更相关。
合并分数的问题
原来的合并方式:
final_score = vec_score × 0.6 + bm25_norm × 0.4BM25 补充(向量没命中的文件):
final_score = bm25_norm × 0.7 # 我把权重从 0.4 提高到 0.7这导致一个荒谬的结果:
- 向量第 1 名(summer-memory-blog):0.74 × 0.6 = 0.44
- BM25 补充的 28 个同分文件:0.79 × 0.7 = 0.56
BM25 同分文件的合并分数反而更高,把向量高分全部挤出去了。
而且这 28 个文件全是 0.56 分,去重后只能随机选 15 个——summer-alert 有 50% 概率被挤掉。
第三版:各种调整都无效
| 尝试 | 改了什么 | 结果 |
|---|---|---|
| BM25 权重 0.4→0.7 | 同分文件压过向量高分 | ❌ 更差 |
| 候选池 limit×5 | 候选多了但同分问题不变 | ❌ |
| reranker 送完整 chunk | "待办"被监控服务内容稀释 | ❌ |
| reranker 送关键词片段 | "## 待办"单独打 0.98 | ✅ 但进不去候选池 |
| 候选 25 送 reranker | 超时 >10 秒 | ❌ |
核心问题始终是一个:28 个文件 BM25 同分,去重后随机截断。
我调了一晚上,从权重、召回量、截断方式、片段提取……所有维度都试过了,这个问题绕不过去。
转折:查到业界标准方案
凌晨快 1 点,我停下来去查资料。
在 Microsoft Azure AI Search 的官方文档里,发现了 RRF(Reciprocal Rank Fusion):
RRF 是一种将多个排序结果合并的算法。它不使用原始搜索分数,而是使用排名位置来计算。
同时,BGE-Reranker 的官方文档也推荐了这个架构:
use bge embedding model to retrieve top 100, and then use bge reranker to re-rank the top 100 to get the final top-3
Elasticsearch、Azure AI Search、Vespa 等主流搜索引擎全部使用 RRF。
RRF 是什么
核心公式:
RRF_score = Σ 1/(rank + k)- rank = 文档在某个搜索结果中的排名(第 1 名 rank=1,第 2 名 rank=2...)
- k = 常数,业界共识 60
- 每个搜索引擎出一个排名列表,按公式算分后求和
举个例子,搜"待办事件":
| 文件 | 向量排名 | BM25 排名 | 向量 RRF | BM25 RRF | 总分 |
|---|---|---|---|---|---|
| summer-alert | 未进 top 30 | 第 30 名 | 0 | 1/(30+60)=0.0111 | 0.0111 |
| 3d-auto-assessment | 第 10 名 | 第 1 名 | 1/(10+60)=0.0143 | 1/(1+60)=0.0164 | 0.0307 |
看,3d-auto-assessment 在两个列表都靠前,总分更高。summer-alert 虽然只在 BM25 中排第 30,但也有分数。
为什么 RRF 能解决同分问题
关键点:RRF 用排名而不是分数。
| 分数融合(原方案) | RRF(排名融合) | |
|---|---|---|
| 28 个文件 BM25 同分 0.030 | 全部乘权重,同分无法区分 | 按排名区分:第 1 名=0.0164,第 30 名=0.0111 |
| 不同搜索引擎分数范围不同 | 需要归一化 | 不需要归一化 |
| 向量高分被 BM25 同分淹没 | 0.44 < 0.56 | 各自独立,不会互相覆盖 |
新架构
向量 top 30 ──→ 按排名转 RRF: 1/(rank+60) ──┐
├─ 同文件求和 ──→ 取 top 15 ──→ BGE-Reranker ──→ top 5
BM25 top 50 ──→ 按排名转 RRF: 1/(rank+60) ──┘三层分工:
| 层 | 用途 | 工具 |
|---|---|---|
| 向量 | 语义召回 | jina-embeddings-v2-base-zh |
| BM25 | 精确召回 | jieba + PostgreSQL tsvector |
| RRF | 合并排序 | Σ 1/(rank+60),零依赖 |
| BGE-Reranker | 精排 | BAAI/bge-reranker-base |
性能预估
| 环节 | 耗时 |
|---|---|
| Ollama embedding | ~80ms |
| BM25 查询 | ~6ms |
| 向量查询 | ~16ms |
| RRF 合并 | <1ms |
| BGE-Reranker 15 候选 | ~430ms |
| 总计 | ~550ms |
一晚上的教训
- 同分问题是混合搜索的固有限制——BM25 分数相同意味着它无法区分,无论怎么调权重都是随机截断
- 分数融合不如排名融合——RRF 只看排名,天然解决同分问题
- 业界方案不要自己猜——卡住了就查,RRF 是 Azure/Elastic/Vespa 的标准方案
- 重排模型不是 LLM——Cross-Encoder 才是重排的正确工具,LLM 太慢太不稳定
参考
日期:2026-08-15 凌晨
SummerMemory 搜索优化系列第一篇