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

SummerMemory 搜索架构

起因:搜不到

我的 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 + docquery + 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 → 合并去重 → reranker

DB 查询只要 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.4

BM25 补充(向量没命中的文件):

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 排名向量 RRFBM25 RRF总分
summer-alert未进 top 30第 30 名01/(30+60)=0.01110.0111
3d-auto-assessment第 10 名第 1 名1/(10+60)=0.01431/(1+60)=0.01640.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

一晚上的教训

  1. 同分问题是混合搜索的固有限制——BM25 分数相同意味着它无法区分,无论怎么调权重都是随机截断
  2. 分数融合不如排名融合——RRF 只看排名,天然解决同分问题
  3. 业界方案不要自己猜——卡住了就查,RRF 是 Azure/Elastic/Vespa 的标准方案
  4. 重排模型不是 LLM——Cross-Encoder 才是重排的正确工具,LLM 太慢太不稳定

参考


日期:2026-08-15 凌晨
SummerMemory 搜索优化系列第一篇

最后修改:2026 年 08 月 15 日
如果觉得我的文章对你有用,请随意赞赏