14.13-Rerank重排序
要点
- 向量检索是「双塔模型」——查询和文档独立编码,交互不够深
- Rerank 模型是交叉编码器——同时看查询和文档,判断更精细
- 检索流程变成:向量检索 top50 → Rerank top10 → 取 top5 进 Prompt
- 主流 Rerank 模型:Cohere Rerank、BGE Reranker、Jina Reranker、Workers AI
- Rerank 增加延迟和成本——只在检索质量不够用时才加
- Rerank 不是万能的——如果检索阶段完全没召回正确答案,Rerank 救不回来
1. 为什么需要 Rerank
向量检索用的是双塔模型(bi-encoder)——查询和文档分别编码成向量,然后算距离。
查询: "退款多久到账"
→ embed → [0.12, -0.34, ...] (768 维)
文档 A: "退款一般在 3-5 个工作日到账"
→ embed → [0.11, -0.32, ...] (768 维)
文档 B: "退货流程需要 7 天处理"
→ embed → [0.15, -0.30, ...] (768 维)
cosine(query, A) = 0.92
cosine(query, B) = 0.88
双塔模型的问题是:查询和文档在编码时完全独立,不知道对方长什么样。只有编码完成后才算距离。
这意味着模型无法捕捉「查询和文档之间的细粒度交互」。比如:
查询: "支持微信支付吗"
文档 A: "我们支持多种支付方式,包括支付宝和微信支付"
→ cosine = 0.86
文档 B: "我们接受微信支付、支付宝、银行卡等多种支付方式"
→ cosine = 0.85
文档 C: "支付方式说明:目前支持支付宝,微信支付即将上线"
→ cosine = 0.88 ← 分数最高,但实际上「即将上线」≠「已支持」
双塔模型被「微信支付」这个关键词的语义相似度误导了。
Rerank 用的是交叉编码器(cross-encoder)——把查询和文档拼在一起输入模型:
输入: "[CLS] 支持微信支付吗 [SEP] 支付方式说明:目前支持支付宝,微信支付即将上线 [SEP]"
输出: relevance_score = 0.45 ← 模型理解「即将上线」不等于「已支持」
输入: "[CLS] 支持微信支付吗 [SEP] 我们支持多种支付方式,包括支付宝和微信支付 [SEP]"
输出: relevance_score = 0.92
交叉编码器能看到查询和文档的每一个词之间的交互,判断更精确。
2. 检索 + Rerank 流程
原始检索流程:
查询 → 向量检索 topK → 拼进 Prompt
加了 Rerank 后的流程:
查询 → 向量检索 topN(N >> K,通常 N=50, K=5)
→ Rerank topN → 取 topK → 拼进 Prompt
async function searchWithRerank(
query: string,
queryVector: number[],
options: { topK?: number; topN?: number } = {}
): Promise<SearchResult[]> {
const { topK = 5, topN = 50 } = options
// 第一步:向量检索,多取一些
const candidates = await vectorDB.search(queryVector, { topK: topN })
// 第二步:Rerank
const reranked = await reranker.rerank(query, candidates)
// 第三步:取 topK
return reranked.slice(0, topK)
}为什么先取 topN 再 Rerank?因为 Rerank 模型比 embedding 模型重很多——逐条打分很慢。如果直接对所有文档 rerank,成本太高。先用向量检索做粗筛,再用 Rerank 做精选。
3. 主流 Rerank 模型
Cohere Rerank
import { CohereClient } from 'cohere-ai'
const cohere = new CohereClient({ token: process.env.COHERE_API_KEY })
async function rerankWithCohere(
query: string,
documents: Array<{ id: string; content: string }>
): Promise<Array<{ id: string; score: number }>> {
const response = await cohere.rerank({
query,
documents: documents.map((d) => d.content),
model: 'rerank-multilingual-v3.0',
topN: 5,
})
return response.results.map((r) => ({
id: documents[r.index].id,
score: r.relevance_score,
}))
}Cohere Rerank 的特点:
- 多语言支持好(中英文都强)
- 返回 0-1 的分数,可以直接比较
- 价格:$1/1000 次搜索(每次搜索最多 50 个文档)
BGE Reranker(开源)
import { pipeline } from '@xenova/transformers'
const reranker = await pipeline('text-classification', 'BAAI/bge-reranker-v2-m3')
async function rerankWithBGE(
query: string,
documents: Array<{ id: string; content: string }>
): Promise<Array<{ id: string; score: number }>> {
const results = []
for (const doc of documents) {
const output = await reranker(`${query} [SEP] ${doc.content}`, {
pooling: 'cls',
})
results.push({
id: doc.id,
score: output[0].score,
})
}
results.sort((a, b) => b.score - a.score)
return results
}BGE Reranker 的特点:
- 开源免费,可以本地部署
- 中文效果好(bge-reranker-v2-m3 是多语言版)
- 缺点:推理速度比 API 慢——没有 GPU 的话,50 个文档要几秒
Workers AI Rerank
async function rerankWithWorkersAI(
query: string,
documents: Array<{ id: string; content: string }>,
ai: Ai
): Promise<Array<{ id: string; score: number }>> {
const response = await ai.run('@cf/baai/bge-reranker-base', {
query,
documents: documents.map((d) => d.content),
})
return response.results.map((r, i) => ({
id: documents[i].id,
score: r.score,
}))
}Workers AI 的好处是和 Vectorize 集成方便,延迟低。模型选择有限。
Jina Reranker
async function rerankWithJina(
query: string,
documents: Array<{ id: string; content: string }>
): Promise<Array<{ id: string; score: number }>> {
const response = await fetch('https://api.jina.ai/v1/rerank', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.JINA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'jina-reranker-v2-base-multilingual',
query,
documents: documents.map((d) => d.content),
top_n: 5,
}),
})
const data = await response.json()
return data.results.map((r: any) => ({
id: documents[r.index].id,
score: r.relevance_score,
}))
}4. 延迟和成本
Rerank 模型的推理速度比 embedding 模型慢一个数量级。
| 模型 | 50 文档延迟 | 成本 |
|---|---|---|
| BGE Reranker(CPU) | ~3-5s | 免费(本地) |
| BGE Reranker(GPU) | ~200-500ms | 电费 |
| Cohere Rerank API | ~300-800ms | $1/1000 次 |
| Jina Reranker API | ~200-600ms | $0.15/1M token |
| Workers AI Rerank | ~100-300ms | Workers AI 计费 |
优化策略:
- 减少候选数量:topN 不要太大,20-50 足够
- 截断文档长度:Rerank 只看查询和文档的相关性,不需要看全文——截断到前 500 字符
- 并行调用:如果 Rerank 支持批量,用批量接口
- 缓存:同一个查询短时间内多次请求,复用 Rerank 结果
async function rerankWithTruncation(
query: string,
documents: Array<{ id: string; content: string }>,
maxLength = 500
): Promise<Array<{ id: string; score: number }>> {
// 截断文档——Rerank 不需要看全文
const truncated = documents.map((d) => ({
...d,
content: d.content.slice(0, maxLength),
}))
return reranker.rerank(query, truncated)
}5. 什么时候不需要 Rerank
Rerank 不是万能的。以下场景可以跳过:
场景一:检索质量已经足够
如果向量检索 top5 的 Recall@5 已经有 0.9 以上,加 Rerank 的提升有限——不值得增加延迟和成本。
场景二:检索阶段完全没召回
Rerank 只能在已召回的候选里重新排序——如果正确答案不在 top50 里,Rerank 也救不回来。
向量检索 top50 里没有正确答案 → Rerank 无从选择 → 最终结果依然不对
这种情况应该优化 embedding 模型或切分策略,而不是加 Rerank。
场景三:延迟敏感
实时聊天场景,用户期望秒级响应。如果加 Rerank 导致延迟超过 2 秒,体验会变差。
替代方案:
- 用更快的 Rerank 模型(比如 base 版代替 large 版)
- 减少 topN(从 50 降到 20)
- 流式返回——先返回向量检索结果,Rerank 完成后更新排序
6. 统一 Rerank 服务
封装统一的 Rerank 接口:
// src/services/rag/rerank/index.ts
export type RerankProvider = {
name: string
rerank(
query: string,
documents: Array<{ id: string; content: string }>
): Promise<Array<{ id: string; score: number }>>
}
export class RerankService {
private provider: RerankProvider
constructor(provider: RerankProvider) {
this.provider = provider
}
async rerank(
query: string,
candidates: SearchResult[],
options: { topK?: number; maxLength?: number } = {}
): Promise<SearchResult[]> {
const { topK = 5, maxLength = 500 } = options
// 截断文档长度
const documents = candidates.map((c) => ({
id: c.id,
content: (c.content ?? '').slice(0, maxLength),
}))
// 调用 Rerank
const scores = await this.provider.rerank(query, documents)
// 按分数排序
const scoreMap = new Map(scores.map((s) => [s.id, s.score]))
return candidates
.map((c) => ({
...c,
score: scoreMap.get(c.id) ?? 0,
}))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
}
}7. Rerank 效果评估
评估 Rerank 是否有效,对比加 Rerank 前后的 Recall:
async function evaluateRerankImpact(
testCases: Array<{ query: string; relevantDocIds: string[] }>,
searchFn: (q: string) => Promise<SearchResult[]>,
searchWithRerankFn: (q: string) => Promise<SearchResult[]>,
k = 5
): Promise<{ before: number; after: number }> {
let beforeRecall = 0
let afterRecall = 0
for (const { query, relevantDocIds } of testCases) {
const before = await searchFn(query)
const after = await searchWithRerankFn(query)
const beforeCorrect = before.slice(0, k).filter((r) => relevantDocIds.includes(r.id)).length
const afterCorrect = after.slice(0, k).filter((r) => relevantDocIds.includes(r.id)).length
beforeRecall += beforeCorrect / k
afterRecall += afterCorrect / k
}
return {
before: beforeRecall / testCases.length,
after: afterRecall / testCases.length,
}
}如果 Rerank 后 Recall 提升 < 5%,可能不值得加——延迟成本高于收益。
总结
回顾这一节的要点:
- 向量检索是双塔模型,查询和文档独立编码,交互不够深
- Rerank 是交叉编码器,同时看查询和文档,判断更精细
- 流程:向量检索 topN → Rerank → 取 topK 进 Prompt
- 主流模型:Cohere Rerank、BGE Reranker、Jina Reranker、Workers AI
- Rerank 增加延迟(200ms-5s)和成本——按场景决定是否值得
- 截断文档长度可以减少延迟
- Rerank 不能救回完全没召回的情况——embedding 和切分策略才是基础
- 统一 Rerank 服务屏蔽底层模型差异
下一篇讲上下文拼接——把检索到的 chunk 组装成 Prompt。