文章解释两阶段检索的分工:双编码器快速召回候选文档,交叉编码器结合查询与文档进行精细重排。该架构以少量额外延迟换取更高的头部排序精度,可减少 RAG 因选错上下文产生的偏差。
检索负责把正确的文档放进候选集合;reranker 负责把它们排到正确的位置。大多数 RAG 流水线止步于第一步,然后疑惑为什么答案总有些微妙的偏差。解决办法是增加一个成本低廉的第二阶段,对候选短名单重新排序——而要理解它为什么有效,关键在于两类 encoder 之间的一项架构差异。
每个 RAG 系统最初使用的 retriever 都是 bi-encoder:它分别将 query 转换成一个向量,并将每篇文档独立转换成一个向量,然后按照 cosine similarity 排序。文档向量只需离线计算一次并建立索引,之后 approximate-nearest-neighbour index 就能在几毫秒内搜索数百万个向量。
bi = SentenceTransformer("all-MiniLM-L6-v2")
doc_emb = bi.encode(corpus) # PRECOMPUTED, offline, indexed
q_emb = bi.encode(query)
scores = util.cos_sim(q_emb, doc_emb)[0]
topN = scores.topk(N).indices.tolist() # a broad shortlist, fast
这种速度的代价是存在一个盲区:每篇文档在看到 query 之前,就已经被压缩成了单个向量,因此评分无法体现当前 query 与这篇文档之间如何交互。结果就是召回能力很强——有用的文档通常位于 top-N 中的某个位置——但最前面的排序精度比较粗糙:一篇只是表面词汇相似的文档,可能排在真正回答了问题的文档之前。而在 RAG 中,模型读取的是排在最前面的 chunk,因此错误的第 1 名会导致错误答案,并且下游再怎么调整 prompt 都无法补救。
Cross-encoder 会拼接 [query] [SEP] [doc],并让这一输入对共同通过 transformer,因此每个 query token 都能关注每个文档 token。最终,它会输出一个经过校准的 relevance score。
ce = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(query, corpus[i]) for i in topN]
rel = ce.predict(pairs) # e.g. [0.41, 0.96, 0.34, 0.04]
这种 joint attention 恰好能建模 bi-encoder 所丢弃的 query-doc 交互,因此准确得多。但它无法预计算任何内容,因为评分取决于 query,所以你不能让它遍历整个语料库。每个 query 都执行一百万次 forward pass,显然不可行。
将两者的优势结合起来。阶段 1:bi-encoder 扫描整个语料库,得到范围较广的 top-N(召回率高、成本低)。阶段 2:cross-encoder 只对这 N 个输入对重新评分,将它们重新排列成精确的 top-k(精度高、排序准)。这样,每个 query 只需要运行 N 次成本高昂的模型。
def retrieve_then_rerank(query, N=50, k=5):
# STAGE 1 — cheap, over the whole corpus
scores = util.cos_sim(bi.encode(query), doc_emb)[0]
shortlist = scores.topk(N).indices.tolist()
# STAGE 2 — expensive, only over N pairs
rel = ce.predict([(query, corpus[i]) for i in shortlist])
order = sorted(range(N), key=lambda j: rel[j], reverse=True)
return [shortlist[j] for j in order[:k]]
原本被 bi-encoder 埋在第 2 或第 3 名的文档,现在升到了第 1 名;靠关键词混淆视听的文档则掉了下去。经过重新排序的 top-k,才是 LLM 真正会读取的内容。
N 是至关重要的调节参数。Reranker 只能重新排列阶段 1 已经找出的内容:如果真实答案位于 bi-encoder 排名的第 60 位,而你只对前 50 名进行 rerank,它就永远没有入选的机会——N 越大,召回率越高。但候选列表中每增加一篇文档,就会多执行一次 cross-encoder forward pass,因此延迟会随 N 线性增长。
N 的典型取值是 50~200;你应该在自己的数据上测量 recall@N,并选择越过曲线拐点后的最小 N。具体来说,阶段 1 是一次不到一毫秒的 ANN 查询;阶段 2 处理每篇文档需要几毫秒,而且非常适合批处理。因此,对大约 100 篇文档进行 rerank,会增加几十毫秒的延迟——这是一个有界且可预测的代价,换来的则是最前排结果质量的大幅提升。
在生产环境中,你通常不需要自行托管 cross-encoder:可以选择 Cohere Rerank、Voyage rerank 或开源的 bge-reranker,将它放在 vector store 与 LLM 之间,再把精确的 top-k 交给模型。由于排在第 1 位的 chunk 现在就是真正的答案,模型也就获得了可靠的事实依据。
选择一个 query,滑动调整 top-N 的截断位置,并在以下页面中观察两种排名如何并排重新排序:
https://dev48v.infy.uk/ai/days/day56-rerankers.html
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。