前言
做 RAG 时最常见的体感是:向量召回「看起来有了」,最终回答却经常答非所问。根因多半不在大模型,而在召回排序:embedding 相似度擅长「语义近」,不擅长「对这个问题有没有用」。
本文用一套可复现的最小闭环,把 召回(Retrieve)→ 重排(Rerank)→ 再交给 LLM 跑通,并给出阈值阈值与落地注意点。适合已有向量库、想用交叉编码器把 Top-K 精度拉回来的同学。
关键词:RAG、Rerank、bge-reranker、交叉编码器、向量检索、Top-K
为什么只靠向量相似度不够
双塔 / embedding 检索把 query 和 doc 分别编码,再算余弦或内积。优点是快、可预计算;缺点是:
- 缺少交互信息:query 与 doc 没有在同一模型里做 token 级交互,细节对齐弱。
- 分数难校准:相似度高 ≠ 可回答该问题(常见于同主题但切题偏了的段落)。
- 长尾 query:术语、否定、约束条件(「不要用 GPU」「只要 Python」)容易被语义近邻淹没。
交叉编码器(cross-encoder)把 (query, doc) 拼在一起打相关性分,慢但准,适合对召回的前几十条做重排。
最小架构
text
用户问题
→ Embedding 召回 Top-N(如 N=50)
→ Reranker 打分,取 Top-K(如 K=5)
→ 拼进 Prompt,交给 LLM 生成
经验值(可按延迟预算调):
| 阶段 | 建议规模 | 说明 |
|---|---|---|
| 向量召回 | Top-30 ~ Top-100 | 宁多勿少,交给 rerank 筛 |
| 重排输出 | Top-3 ~ Top-8 | 再多会稀释上下文、涨延迟 |
| 单段长度 | 300~800 字 / 512~1024 token | 过长先切片再检索 |
环境与模型
以开源 BAAI/bge-reranker-base(或 bge-reranker-v2-m3)为例,本地或内网推理均可。
bash
pip install torch transformers sentence-transformers
# 若用 FlagEmbedding:
# pip install FlagEmbedding
下面用 sentence-transformers 的 CrossEncoder 接口示意(也可换成 FlagEmbedding / vLLM / 自建 HTTP 服务)。
代码:召回后重排
假设你已经有一批候选文档(来自 FAISS / Milvus / ES 等):
python
from sentence_transformers import CrossEncoder
# 首次会下载权重;生产环境建议预热并固定版本
reranker = CrossEncoder("BAAI/bge-reranker-base", max_length=512)
query = "无人机正射影像接缝处色差怎么用羽化减弱?"
candidates = [
"正射 DOM 局部修补:图章修复与亮度对比度调整步骤......",
"大模型 Function Calling 参数校验与重试策略......", # 噪声
"镶嵌线 shp + 羽化接缝:色差过渡的实操思路......",
"OSGB 白膜重建中的三角面分离......",
]
pairs = [[query, doc] for doc in candidates]
scores = reranker.predict(pairs) # 越高越相关
ranked = sorted(
zip(candidates, scores),
key=lambda x: float(x[1]),
reverse=True,
)
top_k = ranked[:3]
for i, (doc, score) in enumerate(top_k, 1):
print(f"{i}. score={score:.4f} | {doc[:40]}...")
把 top_k 的原文拼进 prompt 即可。注意:
- 截断策略 :
max_length=512时,过长 doc 会被截断;切片时尽量保留「问句能命中的关键句」。 - 批量推理:生产上对 50 条做 batch,比逐条 for 循环稳得多。
- 分数阈值 :不同模型分数区间不同,用相对排序即可,不要写死「>0.8 才采用」这类绝对阈值。
和 LLM 的衔接(推荐写法)
python
def build_prompt(query: str, passages: list[str]) -> str:
blocks = []
for i, p in enumerate(passages, 1):
blocks.append(f"[资料{i}]\n{p}")
ctx = "\n\n".join(blocks)
return (
"你只能依据下列资料回答;资料不足时明确说「资料不足」,不要编造。\n\n"
f"{ctx}\n\n"
f"用户问题:{query}\n"
"请给出可操作的步骤,并标注引用了哪条资料编号。"
)
重排的价值在这里体现:同一 token 预算下,更切题的段落进上下文,幻觉和跑题会明显下降。
评估:别只看「感觉好了」
至少做一小组离线集(20~50 条真实问题):
- Hit@K:标准答案段落是否进入重排后的 Top-K。
- MRR / nDCG:排序质量。
- 端到端:人工或 LLM-as-judge 看最终回答是否可用。
对比实验建议固定召回集合,只开关 rerank,避免把「换了向量模型」和「加了重排」混在一起。
常见坑
- N 太小:召回只有 5 条再 rerank,重排几乎没空间。
- 重复近重复段落:切片重叠过大,Top-K 全是同一段的变体------重排前做简单去重(标题/hash/simhash)。
- 延迟爆炸:每次请求实时加载模型;应常驻进程或独立 rerank 服务。
- 中英混排语料 :优先选多语 reranker(如
bge-reranker-v2-m3),并在评测集里覆盖真实语言比例。 - 把 rerank 分数写进业务硬阈值:模型一换阈值全废;用排序位次 + 业务规则更稳。
小结
- 向量召回解决「候选从哪来」,交叉编码器重排解决「谁更该进上下文」。
- 落地时固定:Top-N 召回 → Rerank → Top-K → LLM,并用 Hit@K 验证,而不是凭感觉调参。
- 下一篇可以继续写:多路召回(向量 + 关键词)融合后再 rerank,以及如何把重排服务化。
互动提问:你现在的 RAG 流水线里,重排是用交叉编码器、LLM 打分,还是根本没做?如果做过,Top-N / Top-K 大概取多少、延迟能否接受?欢迎在评论区贴一下你的配置,我可以帮你看有没有明显浪费算力的点。