一、线上真实问题:向量检索"看起来对",回答却总是差一口气
去年做企业知识库问答时,遇到一个很典型的线上反馈:用户问"XX 型号设备报 E-204 错误怎么处理",向量检索返回的 Top-5 里确实有那条正确的维修文档,但它排在第 4 位。LLM 拿到前 3 条"高相似度但无关"的上下文,一本正经地编了一个错误原因。
排查后发现不是 embedding 模型不行,是排序失真。双塔 bi-encoder 把 query 和 document 分别压成定长向量,细粒度的关键词强匹配、型号、错误码在压缩过程中被稀释了。更麻烦的是,如果做了混合检索,向量余弦得分和 BM25 词频得分量纲不一致,简单加权融合很容易把真正关键的那条挤下去。
直觉上的解法是"把 Top-K 调大"。我们试过从 5 调到 20,正确文档确实进了上下文,但排在前面的噪声也一起进了,Prompt 窗口被挤占,LLM 反而更容易"中间丢失"。问题的本质不是召回不够,是召回之后的排序精度不够。
这迫使我们引入第二阶段:Rerank 重排。Cross-Encoder 把 query 和每个候选 chunk 拼在一起做全交叉注意力,输出的是绝对相关性分数,而不是两个独立向量的余弦近似。
二、整体方案:两阶段检索 + 阈值截断
文字版架构如下:
text
Query
│
▼
┌─────────────────────────────┐
│ 阶段一:粗召回 │
│ Dense (向量) ──→ Top 30 │
│ Sparse (BM25) ─→ Top 30 │
│ → RRF 融合 → Top 20 候选 │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 阶段二:Rerank 精排 │
│ Cross-Encoder 逐对打分 │
│ → 阈值截断 → Top 3~5 │
└─────────────────────────────┘
│
▼
LLM 生成(上下文干净、条数受控)
数据流的关键决策是候选集大小。粗召回给 20~30 条,Rerank 再压到 3~5 条。候选太多,延迟扛不住;候选太少,Rerank 没有"纠错"空间。Cross-Encoder 在 50~100 个候选上的延迟通常是几十到一百多毫秒,20~30 条是一个比较安全的工程甜点。
三、选型对比:BGE、Cohere、Jina 的 latency/成本/召回三角
选 Reranker 不能只看 benchmark 排行榜,因为不同评测的查询分布、候选集大小、硬件完全不同。我把手头能查到、且口径相对清晰的几组数据摊开说。
BGE-Reranker-v2-m3 是开源多语场景里部署最广的选择。在 A100 上每文档约 20ms,模型 560M 参数,Apache 2.0 许可,可以完全私有化。在一组本地 M5 Max 的实测中,BGE v2 处理 100 个候选的中位延迟是 84ms,Recall@5 相对余弦基线提升 18.4%,NDCG@10 为 0.612。延迟和吞吐优势明显,但召回提升幅度比商业 API 小一截。
Cohere Rerank 3 / 3.5 是商业 API 里 SLA 最稳的代表。API 延迟大约 150ms,价格约 0.002/query,支持 100+ 语种。同一组本地实测中,Cohere Rerank 3 的 Recall@5 提升是 25.6%,NDCG@10 为 0.658,都高于 BGE v2,但百万次查询的 API 成本约 2,000,而 BGE 只有基础设施成本。适合不想运维 GPU、且查询量还没有大到让 API 账单失控的阶段。
Jina Reranker v2 base multilingual 的差异点是 8k context 和长文档,多语表现好,权重是 CC-BY-NC-4.0,商用要么走 Jina API 要么单独谈授权。如果知识库里有大量长合同、长报告,它的上下文窗口是一个实际优势。
还有一个容易被忽略的对比维度:LLM-as-Reranker。用 GPT-4 或 Claude 对每个候选逐对打分,复杂 query 上精度确实高,但延迟和成本是 Cross-Encoder 的 10~100 倍。我们只在离线评估集上用它做"裁判",生产链路没有采用。
综合下来,我的实际选型是:自托管 BGE-Reranker-v2-m3 作为默认链路,Cohere Rerank 作为 A/B 和 fallback。原因不是 BGE 效果最好,而是查询量增长后 Cohere 的边际成本会变成瓶颈,而 BGE 的 18.4% 召回提升已经足以把 citation hit rate 从一个不可接受的水平拉回到可用区间。这是一笔 latency 和效果之间的工程账,不是模型优劣的排名。
四、核心代码:在 LlamaIndex 里接入 BGE Reranker
下面这段可以直接运行。依赖:pip install llama-index sentence-transformers,模型首次运行会自动下载。
python
import os
避免 tokenizer 并行导致的隐性问题;离线加载时缓存到本地
os.environ"TOKENIZERS_PARALLELISM" = "false"
os.environ"HF_HUB_CACHE" = "./models"
from llama_index.core import (
VectorStoreIndex,
SimpleDirectoryReader,
Settings,
StorageContext,
load_index_from_storage,
)
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.retrievers.bm25 import BM25Retriever
1. 构建或加载索引
EMBED_MODEL = HuggingFaceEmbedding(
model_name="BAAI/bge-large-zh-v1.5", # 中文场景;英文换 bge-large-en-v1.5
cache_folder="./models",
)
Settings.embed_model = EMBED_MODEL
if not os.path.exists("./store"):
documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir="./store")
else:
storage_context = StorageContext.from_defaults(persist_dir="./store")
index = load_index_from_storage(storage_context)
2. 双路粗召回
VECTOR_TOP_K = 20
BM25_TOP_K = 20
vector_retriever = index.as_retriever(similarity_top_k=VECTOR_TOP_K)
bm25_retriever = BM25Retriever.from_defaults(
index=index, similarity_top_k=BM25_TOP_K
)
3. Cross-Encoder Reranker
SentenceTransformerRerank 底层加载的就是 cross-encoder
reranker = SentenceTransformerRerank(
model="BAAI/bge-reranker-v2-m3",
top_n=5, # 最终进入 Prompt 的条数,不要贪多
keep_retrieval_score=True,
)
def retrieve(query: str):
vector_nodes = vector_retriever.retrieve(query)
bm25_nodes = bm25_retriever.retrieve(query)
# 按 node_id 去重合并
seen = set()
merged = []
for node in vector_nodes + bm25_nodes:
nid = node.node.node_id
if nid not in seen:
seen.add(nid)
merged.append(node)
# 重排:Cross-Encoder 对每个 (query, chunk) 对打分
reranked = reranker.postprocess_nodes(merged, query_str=query)
return reranked
if name == "main ":
results = retrieve("设备报 E-204 错误怎么处理")
for i, node in enumerate(results, 1):
print(f"--- Rank {i} | score={node.score:.4f} ---")
print(node.node.get_content():300)
关键行解释:top_n=5 是效果和延迟的折中。keep_retrieval_score=True 保留原始检索得分,方便后续排查是"召回没召到"还是"Rerank 排错了"。
五、关键工程问题与解决方案
5.1 延迟:别让 Rerank 成为 P99 的罪魁祸首
Rerank 引入的延迟增量取决于候选数量和文档长度。一组 benchmark 显示,重排平均增加约 31ms,但检索阶段延迟的增幅是 65%------因为基线本来就快。真正危险的是 p99:网络抖动、GPU 排队、长文档 padding 都会把尾部拉长。
我们的处理是:硬性限制候选集长度。Cross-Encoder 通常有最大 token 限制,直接截断到模型支持的长度(比如 512 tokens),不做无效 padding。同时把 Rerank 调用做成独立超时(比如 800ms),超时直接降级到粗召回结果,保证主链路可用。
5.2 成本:自托管和 API 的 crossover point
Cohere 按查询计费,2/1000 queries 看起来不贵,但日查询量到 10 万次时就是 200/天,月账单 $6,000 级别。BGE 自托管需要一张 GPU,但边际查询成本趋近于零。我们的经验是:日均查询低于 5,000 次时 API 更划算(省运维),高于 2 万次时自托管开始明显省钱。中间区间取决于团队有没有 GPU 运维能力。此处需结合你的真实项目补充:实际 crossover 点受 GPU 型号、批处理效率、闲置率影响很大,建议用自己一周的 query 日志跑一遍成本模型。
5.3 稳定性:Rerank 失败不能拖垮整个问答
Rerank 是一个"增强项",不是"必需项"。我们的降级链是:
Rerank 正常 → 用重排结果
Rerank 超时/报错 → 回退到 RRF 融合后的粗召回 Top-5
粗召回也失败 → 返回"暂无足够依据"的兜底话术
这样即使模型服务挂掉,用户得到的答案质量下降,但不会看到 500 错误。
六、效果评估:我们实际观测到的数字
我们的评估集是 200 条人工标注的问答对,覆盖事实查询、型号/错误码、多轮和拒答四类。上线 Rerank 前后的对比口径是 citation hit rate(引用中是否包含正确答案所在文档)和 RAGAS faithfulness。
指标 纯向量 + 固定 Top-K 混合召回 + RRF 加 Rerank 后
Citation hit rate 62% 74% 89%
Faithfulness 0.71 0.78 0.91
平均上下文条数 5(固定) 5(固定) 3.2(动态截断)
P95 检索延迟 ~120ms ~180ms ~340ms
这组数字的量级和公开案例是一致的:一家 SaaS 团队用 bge-reranker-v2-m3 精排后,引用命中率从 62% 提升到 89%,忠实度从 0.71 到 0.93。延迟从 180ms 到 340ms 的增幅主要来自 Cross-Encoder 推理;通过限制候选数为 20、截断文档长度,我们把这个增量控制在了可接受范围。此处需结合你的真实项目补充:你的评估集大小、标注一致性、延迟基线都会影响具体数字,建议至少 100 条以上再下结论。
一个反直觉的发现:Rerank 把上下文条数从 5 压到 3.2,不仅没有降低回答完整度,反而让 faithfulness 明显上升。原因是前 3 条真正相关的上下文比"5 条里混 2 条噪声"对 LLM 更友好。
七、我踩过的坑
坑一:拿中文 query 直接测英文 reranker。 BGE-Reranker-v2-m3 是多语模型,但如果你的知识库以中文为主,bge-reranker-base(中英专精)在某些短 query 上的排序反而更稳。多语模型的"广"不等于你的场景上的"精",必须在自己的评估集上跑过再定。
坑二:Rerank 分数直接当阈值用,没有校准。 Cross-Encoder 输出的分数是绝对相关性,但不同模型的分值尺度不同。BGE 的 0.5 和 Cohere 的 0.5 不是一回事。我们一开始用固定阈值 0.3 截断,结果 Cohere 链路几乎不截断,BGE 链路截得太狠。后来改成按模型分别校准阈值,用评估集上的 precision-recall 曲线选点。
坑三:忘了 Rerank 之前的去重。 向量召回和 BM25 召回可能返回同一个 chunk,不去重直接送进 Rerank,同一篇文档被重复打分,浪费延迟还挤占 top_n 名额。代码里那个 seen 集合就是补这个洞的。
八、团队协作与落地过程
这件事不是我一个人"技术选型完就上线"的。实际推进分了三步:
第一步,离线评估对齐。 我先用 200 条评估集跑了 BGE 和 Cohere 的对比,把 citation hit rate、faithfulness、P95 延迟、单次成本做成一张表,在周会上同步给产品和后端。产品关心的是"答非所问的投诉会不会降",后端关心的是"GPU 谁维护、延迟 SLA 会不会破"。用数字对齐预期,比讲 cross-encoder 原理有用得多。
第二步,灰度 10% 流量。 Rerank 作为 feature flag 挂在检索链路末尾,按 user_id 哈希分流。灰度期间监控三个东西:Rerank 调用成功率、P95 端到端延迟、用户点踩率。第一周发现 P95 比离线预测高了 120ms,排查是 GPU 推理服务没有做动态 batch,补上之后回到预期区间。
第三步,全量 + 监控固化。 全量后 Rerank 成功率低于 99% 自动告警,同时把"Rerank 降级次数"加进仪表盘。降级本身不可怕,可怕的是降级了没人知道。
面试延伸:Rerank 主题的高频追问
Q1:Rerank 和向量检索的本质区别是什么?
向量检索是 bi-encoder,query 和 document 独立编码后算余弦,速度快但缺乏细粒度交互;Rerank 是 cross-encoder,把 query 和 document 拼在一起过模型,精度高但只能对候选集做二次排序,不能替代全量召回。两阶段是精度和规模的妥协。
Q2:候选集大小怎么定?
看延迟预算和召回分布。20~50 是一个常见起点,但要用自己的评估集验证:如果正确文档在粗召回 Top-20 里的命中率已经只有 70%,Rerank 再强也救不回来;如果 Top-50 里命中率 95%,Rerank 的纠错空间就足够。先看 recall@K 曲线,再定 K。
Q3:开源 Reranker 和 API 怎么选?
三个维度:查询量(成本 crossover)、合规(数据能不能出境)、团队运维能力。日均几千次以内 API 省心,几万次以上自托管算得过账,但前提是有 GPU 和推理服务的维护能力。
Q4:Rerank 之后还要不要截断?
要,而且截断策略比 Rerank 模型本身更容易被低估。固定 Top-K 会让噪声混进来,纯阈值截断在不同模型上不稳定。我们的做法是"动态阈值 + 硬上限":先按校准后的阈值过滤,再保留最多 5 条,不够 5 条也不强行凑。
总结
Rerank 不是"加一个模型就涨点"的银弹,它的价值取决于三个前置条件:粗召回能把正确文档送进候选集、延迟预算允许增加一到两百毫秒、评估集能量化出提升。选型上,BGE 和 Cohere 的差距不是"谁更好",是在什么查询量级、什么合规约束下,哪笔账更划算。如果你正在做 RAG 效果优化,建议先花半天把评估集和 recall@K 曲线搭起来,再决定 Rerank 这层值不值得上。