想象一家生意火爆的私房菜馆:客人一进门,一楼领位员小李快速问一句“想吃清爽的、带点辣味的”,然后从几百道菜里秒选出 15 道递上菜单;客人点头后,二楼大厨老王接过这 15 道菜,细细琢磨食材、火候、上菜顺序,最后排出一桌完美宴席。如果让大厨一个人从几百道菜里慢慢挑,客人早就饿跑了;如果只让领位员拍板,菜品又未必精致。于是“一楼快筛、二楼精排”成了餐厅的铁律——这正是搜索引擎与向量检索里大名鼎鼎的重排序两阶段架构:召回阶段从千万级文档里快速捞出候选集,精排阶段对候选集精细化排序,兼顾效率与精度。这篇文章,我们就跟着小李和老王,把两阶段架构的每一道工序拆开看明白。
学懂重排序两阶段架构,是你从“会调 API”走向“能设计检索系统”的关键一步——无论是做电商搜索、RAG 知识库还是人岗匹配,这套“先粗筛、再精排”的骨架都是工业界最通用的答案。
可视化演示
展示两阶段检索流水线:召回阶段快速筛选大规模候选集,精排阶段精细化排序,兼顾效率与精度。包含多路召回融合和分数标准化策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
两阶段检索流水线 召回阶段从百万级库中快速筛选候选集 精排阶段用复杂模型重排Top-K recall@K -> rerank@TopN 效率与精度分层优化
两阶段架构概览
这家餐厅的运作方式,就是一套完美的漏斗:一楼领位员小李负责“从几百道菜里筛出 15 道”,二楼大厨老王负责“把这 15 道菜安排得明明白白”。搜索系统也一样——召回(Recall)阶段面对的是可能上千万甚至上亿的文档,目标只有一个:快。哪怕多放进来一些“看起来还行”的候选也没关系,因为后面还有大厨把关。精排(Rerank)阶段拿到的候选集往往只有几十到几百条,此时算力充裕,可以上复杂模型做精细化排序。
有人会问:为什么不能只用一个超级模型,一次性对全量文档排序?想象让老王一个人把几百道菜全部细细品一遍,再决定上菜顺序——客人早就拍桌子走人了。全量精确排序的时间复杂度与文档总量成正比,在真实搜索引擎里根本无法接受。所以两阶段架构本质上是用“漏斗”换时间:每一层只处理上一层筛出的子集,层层收窄,让最贵的计算只作用在最少的数据上。
这个漏斗里还有一个容易被忽略的位置:多路召回融合。现代系统通常不会只让小李一个人筛菜——语义向量召回、关键词稀疏召回、热门统计召回各派一个“领位员”,他们的结果合并后再送给大厨。这个融合环节,我们后面专门用一节来讲。理解了“先粗筛、后精排、中间融合”这条主线,两阶段架构的地图就已经刻在你脑子里了。
召回效率优化
小李凭什么能在几秒内从几百道菜里选出 15 道?因为他手里有一本厚厚的“索引手册”:按菜系分门别类,按辣度贴上标签,客人说“清爽”,他直接翻到“清淡”那一章,根本不用逐页翻菜单。召回阶段的效率优化,核心就是建索引。
最经典的手段是 ANN(近似最近邻)索引。向量检索里,每篇文档被映射成一个高维向量,查询向量和文档向量的“相似程度”用余弦相似度衡量。但全量计算相似度太慢,ANN 的做法是提前把向量空间“切块”——比如 IVF 倒排索引先做聚类,查询时只搜索最近的几个聚类桶;再比如 HNSW 图索引,把向量建成多层跳表式的图,从粗粒度层快速下探。此外还有 PQ 乘积量化,把高维向量压缩成短码,用查表代替浮点运算,内存和速度双双优化。
召回阶段的“容忍漏检”原则也很重要:小李不需要保证 15 道菜一定是最完美的,只要保证好菜大概率在里面。这就是 ANN 用“近似”换速度的底气——允许少量误伤,换取毫秒级响应。
- 分子是逐维乘积之和把查询向量的每一维与文档向量的对应维度相乘后累加,相当于把两份“口味列表”逐项核对。
- 分母是长度的乘积两个向量各自求模长再相乘,作用是消除向量长短带来的偏差,只保留方向上的相似。
- 整体是归一化的点积分子除以分母后,结果被压缩到 [-1,1] 区间,1 表示完全同向,-1 表示完全相反。
精排模型选择
菜送到二楼,大厨老王开始较真了。他有两种工作方式:一种是“隔桌看菜单”——先看客人的口味偏好列表,再看菜品标签列表,两列对照打分;另一种是“把客人请进厨房”——让客人和菜面对面,细致观察客人的表情、菜品的色泽,综合判断。这两种方式,对应精排阶段两大主流模型:双塔模型和交叉编码器。
双塔模型把查询(Query)和文档(Doc)分别编码成两个向量,再用点积或余弦算相似度。因为文档向量可以离线预计算,在线时只需算一次查询向量,响应极快——它其实更适合放在召回阶段做粗筛。交叉编码器则把查询和文档“拼”在一起,喂给同一个深层模型(如 BERT 类模型),让每一层都能充分捕捉两者之间的细粒度交互,精度显著更高,但必须在线逐对计算,开销大得多。精排阶段通常选用交叉编码器,正是因为候选集已经被压缩到几十条,算得动。
精排模型训练常用交叉熵损失:
选模型本质是选“精度与速度的折中点”:大厨再厉害,也不能让每桌客人都等一小时。双塔负责快,交叉编码器负责准,两者搭配,正好承接两阶段各自的任务。
- 真实标签 y_j第 j 个样本是否为“好结果”,1 代表相关,0 代表不相关。
- 预测概率 ŷ_j模型输出的匹配概率,取值范围在 0 到 1 之间。
- 对数惩罚项当 y=1 时看 -log(ŷ),预测越接近 1 损失越小;当 y=0 时看 -log(1-ŷ),越接近 0 损失越小。
- 对所有样本求和取负把 M 个样本的惩罚加总,整体损失越小,代表模型排得越准。
候选集规模权衡
现在到了餐厅老板最头疼的问题:到底让领位员送多少道菜给大厨?送 10 道,大厨排得快,但万一漏掉客人最爱的招牌菜怎么办?送 200 道,好菜是基本不会漏了,可大厨精排的时间翻倍,客人等得起吗?候选集大小(N_cand)就是这样一个需要精确拿捏的数字。
从召回角度看,N_cand 越大,相关文档被捞进来的概率越高,召回率(recall)单调上升。但从精排角度看,候选越多,精排时延线性增长,而且大量“凑数”的低质量候选会稀释排序模型的注意力,甚至引入噪声。实践中 recall 曲线会先陡升后趋于平缓,而精排质量往往存在一个拐点:超过某个阈值后,继续扩 N_cand 带来的收益微乎其微,时延和资源开销却实打实地增加。
工业界的经验区间通常在 50~300 之间,具体要看精排模型的算力和时延预算。找到那个“拐点”的方法也不难:离线跑一组 N_cand 的梯度实验,观察 recall 与精排指标(如 NDCG)的变化曲线,选取边际收益开始骤降的位置。
- 相关文档集合 R所有与查询真正相关的文档,是评估用的“标准答案”。
- 前 k 条候选集合 C_k召回阶段捞出的候选按某种顺序排前 k 条。
- 交集大小标准答案中恰好出现在前 k 条候选里的数量。
- 除以相关总数交集数量占所有相关文档的比例,就是召回率。
多路召回融合
小李不是一个人在战斗。餐厅里还有小张——他专门记熟客们的口味档案(统计召回);小赵——他负责按菜名关键词快速定位(稀疏关键词召回);小李自己则是靠“感觉对味”的语义匹配(稠密向量召回)。三个领位员各筛出一份候选清单,最后必须合并成一份送给大厨。多路召回融合要解决的就是“怎么合并意见”。
最简单的是加权融合:先把每一路的得分做归一化,再按权重相加。权重可以人工调,也可以离线学习。但加权融合有个隐患——如果某一路打分普遍偏高,即便归一化后也可能主导结果。所以更稳健的替代方案是 RRF(Reciprocal Rank Fusion,倒数排名融合):它不看分数绝对值,只看文档在每一路里的排名。
融合之后还有一道工序:去重剪枝。同一道菜可能被三个领位员同时推荐,菜单上只保留一份即可;而对那些只被一个人提名且排名靠后的菜,果断划掉,免得浪费大厨的精力。
- rank_i(d) 是文档 d 在第 i 路的排名第一名记 1,第二名记 2,以此类推。
- 常数 k 防止除零通常取 60 左右,让排名带来的分数差异平滑一些。
- 对每一路取倒数排名越靠前,1/(k+rank) 越大,贡献越高。
- 所有路求和把 R 路召回的贡献累加,得到文档的最终融合分。
精排特征工程
大厨老王能排出一桌好菜,靠的可不只是菜本身。他还看季节——夏天少上油腻的;看食材新鲜度——今天刚到的鱼优先推荐;看客人历史偏好——这位客人每次来都点辣,那就把辣菜往前排。这些“菜之外的功夫”,在精排阶段就是特征工程。
精排特征大致分三类:交互特征刻画查询与文档之间的细粒度关系,比如“查询词在标题中出现的次数”“用户点击过该商品的同类目商品数量”;统计特征刻画文档的全局热度,比如点击率、收藏数、销量;时序特征刻画新鲜度,新闻搜索里一小时前的文章显然比一个月前的更值得排前面。时序特征常用半衰期衰减建模:
特征工程还有一道关键工序:归一化。各个特征的数值范围差异巨大——点击率是 0 到 1 的小数,销量可能是几万的大整数,若不归一化,数值大的特征会粗暴地压过其他特征,就像食材味道失衡的菜。分桶、对数变换、Z-score 归一化都是常用的调味手法。
- t 是内容年龄从文档发布时间到现在的时长,单位可以是小时或天。
- T 是半衰期衰减速度的调节旋钮,T 越小,新鲜度权重掉得越快。
- t/T 是半衰期个数算算内容经历了几个“保鲜周期”。
- 2 的负幂次每过一个周期权重变一半,所以权重始终落在 (0,1] 区间。
工程实践部署
最后,老板要给整个餐厅定一条规矩:从客人点菜到上菜,全流程不能超过 100 秒。这条规矩就是时延预算——两阶段架构从理论走向工程落地时,一切设计都要先服从这条“军令状”。
一条典型检索链路的时延拆解如下:
压时延的常用招数包括:特征缓存——把文档的静态特征和预计算向量放缓存,避免重复计算;动态阈值——高峰时段把 N_cand 从 200 临时降到 100,牺牲一点精度换取吞吐;异步化——某些非关键特征可以“迟到”补上,不让它拖慢主链路。每一步优化,都是在“菜的质量”和“上菜速度”之间找平衡。
- T_recall 召回时延从发出查询到各路召回返回候选的耗时,通常靠 ANN 索引压到 10~30ms。
- T_fuse 融合时延多路候选合并、去重、剪枝的耗时,一般在个位数毫秒。
- T_rerank 精排时延复杂模型对候选逐条打分的耗时,是预算的大头,通常占一半以上。
- 总时延不超过预算整条链路必须满足端到端约束,否则要反过来压缩各环节。
AI 应用场景
电商搜索“红色连衣裙”
阶段:召回 + 精排 + 特征工程
解决的问题:传统电商搜索只靠关键词匹配,用户搜“红色连衣裙”时,标题里没有“红”但图片是红色的商品会被漏掉;而全量向量检索又太慢。两阶段架构先用稠密向量召回语义相近的商品,再用稀疏关键词召回精确匹配的商品,融合后由精排模型结合销量、点击率、用户历史偏好等特征排出最终 10 个结果。
具体案例:某电商平台将候选集从 5000 压到 200,精排阶段使用交叉编码器对 200 个候选打分,端到端时延控制在 80ms 以内,搜索转化率提升 12%。
企业知识库 RAG 问答
阶段:多路召回融合 + 精排重排序
解决的问题:RAG 系统如果只用向量检索,专有名词和精确编号容易匹配错;只用关键词检索,语义泛化又不行。两阶段架构先用“稠密向量 + BM25 稀疏”双路召回,再用精排模型(交叉编码器)对召回片段重排序,把最相关的 3~5 个片段送给大模型。
具体案例:某金融企业把 10 万份研报构建成知识库,问答系统采用两阶段检索后,答案引用准确率从 58% 提升到 81%,大模型幻觉明显减少。
招聘网站人岗匹配
阶段:召回效率优化 + 精排特征工程
解决的问题:候选人简历和职位描述都是长文本,直接全量匹配计算量巨大。系统先用 ANN 索引对简历向量做快速召回,再结合技能标签稀疏召回,融合后精排模型加入“技能重合度”“工作年限匹配”“公司职级”等特征,输出人岗匹配分。
具体案例:某招聘平台每日处理 200 万次岗位推荐请求,两阶段架构将单次匹配耗时从 1.2 秒降到 90ms,同时 Top10 录用转化率提升 9%。
Python 演示
import randomimport math def cosine_sim(q, d): return sum(a * b for a, b in zip(q, d)) def recall_stage(q, docs, top_n): scored = [(cosine_sim(q, d), i) for i, d in enumerate(docs)] scored.sort(key=lambda x: x[0], reverse=True) return [idx for _, idx in scored[:top_n]] def rerank_score(q, doc_idx, docs): sim = cosine_sim(q, docs[doc_idx]) heat = 0.5 + 0.5 * docs[doc_idx][0] return 0.7 * sim + 0.3 * heat def rerank_stage(q, docs, cand): cand.sort(key=lambda i: rerank_score(q, i, docs), reverse=True) return cand def recall_at_k(relevant, ranked, k): hit = len(set(relevant) & set(ranked[:k])) return hit / len(relevant) if relevant else 0.0 def build_corpus(num_docs=600, dim=32): docs = [] for _ in range(num_docs): vec = [random.gauss(0, 1) for _ in range(dim)] norm = math.sqrt(sum(v * v for v in vec)) docs.append([v / norm for v in vec]) return docs def main(): random.seed(42) dim = 32 q = [random.gauss(0, 1) for _ in range(dim)] q_norm = math.sqrt(sum(v * v for v in q)) q = [v / q_norm for v in q] docs = build_corpus(600, dim) relevant = [i for i in range(600) if cosine_sim(q, docs[i]) > 0.5] for n_cand in [20, 50, 100, 200]: cand = recall_stage(q, docs, n_cand) rerank_stage(q, docs, cand) rk = recall_at_k(relevant, cand, 10) print("N_cand=%3d recall@10=%.3f top1_score=%.3f" % ( n_cand, rk, rerank_score(q, cand[0], docs))) if __name__ == "__main__": main()
import random` / `import math引入随机数生成与数学函数库,用于构造模拟向量。def cosine_sim(q, d)定义余弦相似度计算,作为召回阶段的核心打分函数。scored.sort(key=lambda x: x[0], reverse=True)按相似度从高到低排序,取前 top_n 个作为候选集。heat = 0.5 + 0.5 * docs[doc_idx][0]构造一个假想的“热度”特征,取值范围固定在 [0,1]。return 0.7 * sim + 0.3 * heat精排打分 = 70% 相似度 + 30% 热度,体现特征融合。def recall_at_k(relevant, ranked, k)计算召回率,衡量候选集中相关文档的覆盖率。for n_cand in [20, 50, 100, 200]遍历不同候选集大小,观察性能变化趋势。
常见误区
误区既然召回已经能把最相关的文档捞出来,那精排不就是多此一举?直接把召回结果按相似度排序输出不就行了?
正解召回阶段的相似度是“粗匹配”,往往只用了轻量向量或关键词信号,很难捕捉细粒度匹配。就像领位员只看“辣不辣”来推荐菜,但大厨还要考虑火候、搭配、上菜顺序。精排模型能综合几十路特征做精细排序,是排序质量的天花板所在。只靠召回,Top 结果的准确率通常会明显下降。
为何常见:因为召回结果看起来“已经很相关”,小白容易忽略排序质量与召回质量是两回事——召回解决“有没有”,精排解决“排得对不对”。
误区反正精排模型很强,把候选集从 100 扩到 1000,recall 更高,效果肯定更好。
正解候选集增大确实会提升 recall,但精排时延线性增长,且大量弱相关候选会引入噪声,可能让排序模型的决策边界被干扰,NDCG 等指标反而下降。正确做法是离线做 N_cand 的梯度实验,找到边际收益拐点,而不是无脑加大。
为何常见:人直觉上认为“更多选择 = 更好结果”,忽略了计算资源和噪声成本,这是典型的“只见收益、不见代价”。
误区既然 RRF 只看排名不看分数,那就不用做分数归一化了,直接把各路的原始分数拿来用。
正解RRF 的优势恰恰在于对分数尺度不敏感,因为它只取排名。但这里有个前提:各路召回的结果必须是“同一个候选集”的有效排序。如果某一路的召回结果质量极差,排名全是噪声,RRF 依然会按名次给分,污染融合结果。所以实践中仍应结合每路召回的置信度做加权或过滤,而不是完全放任。
为何常见:很多教程只强调“RRF 不看分数”,初学者便误以为可以完全省略质量把控,忽略了融合的整体设计。
误区精排模型这么强,把能想到的特征全部塞进去,效果肯定更好。
正解特征不是越多越好。冗余特征会增大训练和推理开销,未归一化的大数值特征可能压制其他特征,同时相关性弱的特征还会引入噪声。特征工程的核心是“每加一个特征都要有明确的业务含义与线上收益验证”,就像大厨加调料,贵在精准而非量大。
为何常见:深度学习模型“自动学习特征”的印象让新手误以为特征工程不再重要,实际在精排阶段,人工特征工程仍是工业界的核心壁垒。
面试题
Q1. 两阶段检索架构中,“召回”阶段的首要目标是什么?
- A.训练一个最精确的排序模型
- B.从全量集合中快速筛出候选集← 正确答案
- C.计算每条文档的复杂交互特征
- D.对候选集做最终精细化排序
答案:B。召回阶段的核心目标是“快而全”地从全量文档中筛出可能相关的候选集,允许粗粒度但不允许漏太多。精确排序和复杂特征计算属于精排阶段的任务,B 正确。
Q2. 以下哪项不是召回阶段常用的效率优化手段?
- A.ANN 近似最近邻索引
- B.向量量化压缩
- C.对全量文档逐条计算精排模型打分← 正确答案
- D.IVF 倒排索引加速
答案:C。ANN 索引、向量量化、IVF 倒排都是为了加快粗筛速度的手段。逐条计算精排模型打分成本极高,违背召回阶段“快”的原则,通常只在精排阶段对少量候选使用。
Q3. 关于双塔模型与交叉编码器的对比,下列说法正确的是?
- A.双塔模型能捕捉 query 与 doc 的深层交互,精度更高
- B.交叉编码器可离线预计算 doc 向量,适合大规模召回
- C.交叉编码器将 query 与 doc 拼接编码,精度高但速度慢← 正确答案
- D.双塔模型在线时必须对每对 query-doc 联合编码
答案:C。交叉编码器把 query 和 doc 拼接后送入深层模型,交互充分、精度高,但必须在线逐对计算,速度慢;双塔模型可以预计算 doc 向量,速度快但交互弱。C 正确,其余说法均与事实相反。
Q4. 增大候选集 N_cand 时,下列哪种现象最可能发生?
- A.召回率 recall 一定下降
- B.精排时延单调下降
- C.recall 先上升后趋于平缓,精排时延持续上升← 正确答案
- D.N_cand 对精排效果毫无影响
答案:C。N_cand 越大,相关文档越容易被捞进候选,recall 上升;但候选增多后精排计算量线性增长,时延上升,且弱相关候选会引入噪声。C 描述符合实际曲线,A、B、D 均错误。
Q5. RRF(倒数排名融合)中,文档 d 在第 i 路召回排名为 r 时,它的融合分数贡献是?
- A.score += 1 / (k + r)← 正确答案
- B.score += r / k
- C.score += w_i × r
- D.score += k / r
答案:A。RRF 的核心公式是 1/(k + rank),k 通常取 60。它只依赖排名而非原始分数,对量纲不敏感。B、C 是加权融合的思路,D 的形式会导致排名越靠后分数反而越高。
Q6. 多路召回融合前,各路分数直接相加的最大风险是什么?
- A.某一路分数范围大,完全主导融合结果← 正确答案
- B.所有文档的融合分数相同
- C.融合后候选文档数自动减少
- D.召回结果必然包含大量重复
答案:A。不同召回路的分数尺度可能差异巨大,比如语义相似度在 0~1,而统计分数可能是千量级。直接相加会让大数值那一路“一票否决”其他路,融合失去意义,所以融合前必须归一化或用 RRF 这类排名融合。
Q7. 在精排特征工程中,“用户过去 30 天点击该商品类目的次数”属于哪类特征?
- A.交互特征
- B.统计特征← 正确答案
- C.时序衰减特征
- D.向量相似度特征
答案:B。这类特征来自用户历史行为的聚合统计,属于统计特征。交互特征强调 query 与 doc 的交叉关系,时序特征强调时间衰减,相似度特征则由向量模型输出,均与题干不符。
Q8. 关于两阶段检索架构的工程实践,下列说法正确的有?
- A.可以给整条链路设置端到端时延预算← 正确答案
- B.特征缓存能显著降低精排重复计算开销← 正确答案
- C.高峰时段可以动态缩小候选集阈值← 正确答案
- D.精排模型越复杂越好,无需考虑时延
答案:ABC。工程落地必须有时延预算意识,A 正确;特征缓存避免重复计算,B 正确;动态调整 N_cand 是应对流量高峰的常见手段,C 正确。D 错误,模型复杂度必须服从时延预算,不能只看精度。
Q9. 下列哪些属于召回阶段常见的效率优化手段?
- A.IVF 倒排索引← 正确答案
- B.PQ 乘积量化← 正确答案
- C.交叉编码器在线逐对打分
- D.HNSW 图索引← 正确答案
答案:ABD。IVF 通过聚类分桶缩小搜索范围,PQ 通过向量压缩降低存储与计算量,HNSW 通过图结构快速逼近最近邻,三者都是经典 ANN 手段。交叉编码器在线打分成本高,属于精排阶段的方法。
Q10. 当候选集 N_cand 从 50 增大到 200 时,以下哪些现象是合理的?
- A.召回率 recall@k 可能上升← 正确答案
- B.精排时延必然上升← 正确答案
- C.排序质量可能不升反降← 正确答案
- D.精排模型的参数量必然需要增大
答案:ABC。候选越多,相关文档越容易被包含,recall 上升,A 正确;候选数增加,精排打分次数线性增加,时延上升,B 正确;弱相关候选的噪声可能干扰排序模型,导致指标下降,C 正确。D 错误,候选集大小与模型参数量没有必然联系。