Dayward AI
Week 2 · D9About 5 hours

Hybrid Search and Reranking: Two-Path Retrieval, Reciprocal Rank Fusion, Then Re-Ranking the Top Results With a Cross-Encoder

Keyword search and vector search each have their own blind spots; combining both paths with Reciprocal Rank Fusion is often an instant win. Then use a cross-encoder to precisely re-rank the top few dozen results, and use day eight's evaluation to prove how much each step actually contributed.

Today's goals 0/3

Sign in to tick these off and save your progress.

今日目标

  1. 能说清关键词检索与向量检索各自的盲区,并举出一个只有对方能命中的查询
  2. 能实现倒数排名融合并解释为什么它比按分数加权更稳
  3. 能说明双编码器与交叉编码器的结构差异,以及重排为什么只能用在前几十条上

昨天你给第一周那套系统装了一杆秤,量出了 87.5% 的召回率和一个已经顶格的平均倒数排名。今天开始用这杆秤称东西:把两路检索合起来、再加一层重排,逐档量它们各自值多少钱。读完回到页面顶部把三条目标勾掉。

小白版讲解

两个侦探:一个查字面线索,一个查动机

一桩案子交给两位侦探。第一位只认物证:现场留下的指纹、鞋印、写着名字的收据,一个字对得上就锁定,对不上就当没看见。第二位不看物证,专看动机:谁最近缺钱、谁跟死者有过节、谁的说辞前后不一致——他说不出哪一条铁证指向凶手,但他能圈出一批"这类人"。

关键词检索就是第一位侦探。你问"年假当年没休完最多能结转几天到次年",它去倒排索引里找"年假""结转""次年"这几个词,谁包含得多、谁的这些词更罕见,谁就排前面。公式和两个修正项在 第 1 天 已经拆过,今天不重讲。它的强项是精确:你说"周敏",它就找"周敏",绝不会给你返回一个"李强"。它的盲区也在这里——换个说法它就瞎了

向量检索是第二位侦探。它把查询和文档各自压成一串数字,比的是方向而不是字面。"口令"和"密码"在向量空间里离得很近,哪怕两个词一个字都不重合。它的盲区是不精确:你问"周敏",它可能给你一批"某某组长"的段落,因为"组长的名字"这件事在语义上确实相近。

这不是"哪个更好"的问题,是两个人擅长的案子不一样。我在本天实验的这份 30 篇语料上验过一对(块级检索,134 块,MOCK=1 的哈希向量):

  • 问"排障用的记录会存多长时间才被清掉",答案在 doc-022(讲日志保留期)。关键词一路把它排到第 3 名,向量一路前 10 名里根本没有它
  • 问"员工改用公司统一身份进系统以后,原来的口令还有效吗",答案在 doc-002(讲单点登录与密码策略)。关键词一路前 10 名里一条都没有——因为"口令"这个词整份语料里就没出现过,而"统一身份""员工"这些词又太常见、分不出高下;向量一路把它排到了第 3 名。

同一份语料、同一套索引,两个查询各自把一位侦探问倒了。那就别让他们单干——问题在于,怎么把两个人的结论合到一张纸上

分数没法直接相加:两把尺子刻度不一样

最自然的想法是把两路的分数加起来排个序。这个想法一动手就会碎。

BM25 的分数是一堆对数项累加出来的,没有上界。在本实验这份语料上,标准答案集那 20 个问题的第一名从 16.37 到 94.46 都有,换成更冷僻的问法还能低到 6.49——取决于你问的词有多罕见、命中了几次。余弦相似度则被钉死在负一到正一之间,MOCK=1 的哈希向量下,相关块也就 0.2 出头。一个是没有刻度的橡皮尺,一个是刻度密到看不清的游标卡尺,把两个读数相加没有任何意义。

那先归一化呢——各自除以本路最高分,压到 0 到 1 再加权?第一周那套系统(第 7 天 的实现)就是这么做的,它能用,但它有个静默的毛病:分母是随查询浮动的。假设某个问题在语料里压根没有答案,向量那一路捞回来的全是不相干的块,最高分只有 0.09——归一化之后,这条最不相干的结果照样拿到满分 1.0,然后带着权重堂堂正正地进了融合。你以为你在比"有多相关",其实你在比"在本路矮子里有多高"。

更麻烦的是权重本身。1 : 0.6 这个配比是从哪来的?第 7 天是拍脑袋定的。要调,就得对每一组权重跑一遍完整评估——两路是二维,三路是三维,加上查询改写的多路召回(第 10 天 会做)就是四维五维。这是一个会随路数指数膨胀的调参问题,而且每换一个 embedding 模型就要从头调一遍。

结论很干脆:分数是不可比的,名次才是。 无论 BM25 给了 52 分还是 5 分,"它在关键词那一路排第一"这句话的含义是稳定的。

倒数排名融合:只看名次,一个常数就够了

倒数排名融合(reciprocal rank fusion,RRF)的做法简单到有点不讲道理:每一路给出一份有序名单,一篇文档在某一路排第 i 名,就在那一路拿 1 / (k + i) 分;几路的分加起来,谁高谁排前面。k 是一个常数,全课统一取 60。

fuse.js
export const RRF_K = 60
 
export function rrfFuse(rankings, k = RRF_K) {
  const scores = new Map()
  for (const ranking of rankings) {
    ranking.forEach((id, index) => {
      // 名次从 1 起算
      scores.set(id, (scores.get(id) ?? 0) + 1 / (k + index + 1))
    })
  }
  return [...scores.entries()]
    .map(([id, score]) => ({ id, score }))
    .sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : 1))
}

注意函数签名:输入是每一路各自的有序 id 列表,不是分数列表。分数在进这个函数之前就被扔掉了,这不是省事,是设计——扔掉不可比的东西,才不会被它误导。

k 的作用是"压平器"。k 取 0 时,第 1 名得 1.0、第 2 名得 0.5,差了一倍,等于"谁在自己那一路排第一谁就赢";k 取 60 时,第 1 名得 0.0164、第 2 名得 0.0161,只差 1.6%,于是"两路都排进前列"反而比"一路排第一"更有分量。这正是我们想要的:被两位侦探交叉验证过的嫌疑人,优先。 实验第五段会让你把 k 从 0 扫到 600,看同一个问题的前 5 名怎么随之变形。

召回数量怎么定:这个旋钮比你想的狠

融合之前,每一路各取多少条?本课统一取前 50,融合后留前 20 交给重排。

但这个默认值有前提。在百万级语料里,50 条是万分之一,保守得几乎没有副作用;而本实验的语料只有 134 块,每路取 50 等于把 37% 的库灌进了融合——相当于让两位侦探把半个城市的人都列成嫌疑人。噪声也是有名次的,进了名单就有票。

实验第六段把这件事跑成了一张表(30 篇语料、134 块、D8 的 20 题、600 token 上下文预算):

TextText
每路取   召回率   multi    nDCG@10   拒答率
5        93.8%    75.0%    0.6281    0.0%
10       87.5%    50.0%    0.6826    0.0%
20       87.5%    50.0%    0.7054    0.0%
50       87.5%    50.0%    0.7186    0.0%

两个指标在这里公开打架:召回深度收到 5,召回率和多跳档都回来了,nDCG@10 却掉了将近 0.1。原因不难想——取得浅,进候选的垃圾少,答案更容易挤进 600 token 的预算;取得深,前 10 名的整体质量更好看,但答案被更多"看起来也相关"的块挤出了上下文。

这就是为什么"该取几条"没有标准答案,只有评估结果。 你的业务如果是问答,召回率优先;如果是给人看的搜索结果列表,排序质量优先。先想清楚要哪个,再去看表。

双编码器与交叉编码器:一个提前算好,一个当场算

融合完了还有一层可做。到这里为止,所有的相似度都是双编码器(bi-encoder)算出来的:查询编码成一个向量,文档也编码成一个向量,两个向量做一次内积。关键在于它们从头到尾没见过面——文档向量可以离线算好、建索引、几毫秒查完全库。这是它能扛住海量数据的全部原因。

代价是信息在压缩时丢了。一篇 500 字的文档被压成 1536 个数,"周敏是平台组组长"这句话里"周敏"和"组长"的绑定关系,在这个压缩里未必留得下来。

交叉编码器(cross-encoder)换了个结构:把查询和文档拼成一段文本一起送进模型,每一层注意力都能让查询里的词去看文档里的词。"组长是谁"这个问句可以直接对齐到"周敏"这两个字上。它准得多——代价是没有任何东西可以预先算好。N 篇文档就要跑 N 次前向,每次都要等模型。

所以它只能站在召回后面,做一层重排(rerank):召回负责在全库里捞出一小批,重排负责把这一小批的顺序改对。用图书馆打比方,召回是按卡片目录把一摞书搬到桌上,重排是管理员翻开每一本,帮你挑出最该读的那三本——他不可能翻遍整个书库。

pipeline.js
const RRF_K = 60, PER_ROUTE_K = 50, FUSE_KEEP = 20
 
export async function hybridSearch(query, routes, rerank) {
  // 两路并行:关键词那一路是纯内存计算,向量那一路要等一次 embedding
  const [keyword, vector] = await Promise.all([
    routes.bm25(query, PER_ROUTE_K),
    routes.vector(query, PER_ROUTE_K),
  ])
  const fused = rrfFuse([keyword.map((h) => h.id), vector.map((h) => h.id)], RRF_K)
  // 只把前 20 条交给交叉编码器,不是全库
  const candidates = fused.slice(0, FUSE_KEEP).map((f) => ({ id: f.id, text: routes.text(f.id) }))
  return rerank(query, candidates)
}

重排的成本账:三个数决定该不该上

重排是本课第一个明确要花钱、也明确要花时间的优化,所以它必须过三笔账——指标、延迟、成本,外加一笔常被忘掉的第四笔。

第一笔,指标——但先确认你看的是哪个指标。 昨天那份基线里平均倒数排名已经是 1.0000,本天四档配置里也有三档是 1.0000。这不是系统满分,是指标饱和:题目是从语料反向出的、字面重合度太高,第一名永远是答案文档,它不可能再涨。指标撞天花板时该做的是把题目出难一点,而不是宣布胜利。 所以今天的证据只能来自召回率(尤其多跳档)、nDCG 和拒答率这三个还有空间的量。

按这三个看,重排的贡献可以精确归因到一道题上。q07 要同时命中 doc-029doc-021 才算数:纯关键词那一档命中了,融合之后反而丢了(doc-021 被稀释、掉出 600 token 的预算),重排又把它排了回来——多跳档因此从 50.0% 回到 75.0%。这才是一条干净的、可归因的改进:不是"混合检索让指标涨了",而是"重排修好了融合弄坏的那一道题"。至于同样是多跳的 q06,四档全部未命中,因为答案文档从头到尾没进过候选池——融合和重排都只能重排已有的候选,变不出新的,那笔债要留到后面还。

拒答率则完全是另一回事:重排在本实现里一点忙都没帮上。准入判定卡在每一路的原始分上,重排只改顺序、不改准入,四档里唯一把拒答率提上去的是纯向量那一档(25.0%,靠 0.2 的余弦门槛真的挡住了一道无答案的题)。重排救不了"该闭嘴的时候没闭嘴",那是 第 6 天 生成侧拒答的活。

第二笔,延迟。 重排是一次同步的网络往返,卡在检索之后、生成之前,用户全程在等。实验里这一列用的是可覆盖的占位值:一次 embedding 记 120 毫秒、一次重排记 180 毫秒。于是纯 BM25 那一档的建模延迟接近 0,混合那一档是 120 毫秒,混合加重排是 300 毫秒。这 180 毫秒买的是排序质量,不是答案质量——如果你的上下文预算足够大、答案本来就进得去,这笔钱就是白花的。

第三笔,钱。 重排普遍按"检索次数"计价,不按 token:一次提问不管送 20 条还是 100 条都算一次。这解释了两件事——为什么重排的成本是按问计费而不是按块计费,以及为什么"多送几条给它排"几乎不增加费用,真正贵的是提问次数本身。实验里这一档的重排输入是每问 2922 个 token、调用 1 次;embedding 那一路只有每问 31.6 个 token。两个数量级的差距,说明检索侧的账单基本上就是重排的账单。

第四笔,能不能自己部署。 这一笔常被忘掉,但它经常是决定性的。交叉编码器不像大模型那么庞大:本实验默认接的托管后端是 Cohere 的 rerank-v3.5,而自部署那条路用的是 BAAI/bge-reranker-v2-m3——Apache-2.0 许可、多语言、一张消费级显卡放得下。数据不能出内网、或者调用量大到按次付费不划算,就切到自部署,rerank.ts 里两个后端实现的是同一个接口,换一行环境变量的事。

元数据过滤放在检索前还是检索后

最后一块拼图。现实里的检索很少是"在全库里找",而是"在这个部门、这个时间段、这个人有权看的范围里找"。过滤放在哪一步,答案取决于过滤之后还剩多少

先过滤再检索(前置过滤):范围小的时候这么做。剩 200 篇,直接在这 200 篇里跑 BM25 和向量都很快,而且召回的 50 条全是合法的

先检索再过滤(后置过滤):范围大的时候这么做。但它有一个必须防的坑——召回 50 条、过滤掉 45 条,你手上就只剩 5 条了,等于把召回深度悄悄砍掉了 90%。补救办法是按过滤通过率放大召回数量:估计通过率是 20%,就召回 250 条再过滤。

filter.js
// 先估一下这个过滤条件能留下多少,再决定放在哪一步
export function planFilter(estimatedKept, totalDocs, want = 50) {
  const ratio = estimatedKept / totalDocs
  // 剩不到一成,先缩范围更划算:候选池小,两路都跑得更快
  if (ratio < 0.1) return { mode: 'pre', topK: want }
  // 否则后置过滤,但要按通过率放大召回数,否则过滤完手上没剩几条
  return { mode: 'post', topK: Math.ceil(want / Math.max(ratio, 0.05)) }
}

权限过滤是这件事最要命的场景,因为它错一次就是事故——那一整套怎么做,留到 第 13 天。这里你只需要记住判据:先算通过率,再决定顺序。

源码导读

动手实验

🧪 D9 实验:混合检索加重排的实现与三档配置在同一评估集上的指标对比

Code location: labs/rag-14days/day-09-hybrid-and-rerank

验收标准:

  1. MOCK=1 pnpm start 跑完,"八、验收"四项全部 ✅
  2. 第一段的四档对照表里,混合那一档的排序与纯 BM25 那一档不同,说明融合真的发生了
  3. 第五段的 k 扫描表里,k 取 0 与 k 取 60 的前 5 名不同,且能看出 k 越大越奖励"两路都排进前列"
  4. 第七段显示重排改写了前 10 名里的若干个位置,参考实现是 151 个
  5. 第三段里混合加重排那一档的"重排调用/问"是 1.00,"每千问费用"明显高于其他三档

starter/ 里挖了 4 个练习:倒数排名融合、k 的扫描、重排替身的打分、成本账。原样跑起来四项验收全是 ❌,每做完一个就会有一项翻绿——把它当进度条用,不要一次性看答案。四个练习都不需要网络和 API key。

  1. 先原样跑一遍 starter,记下四档对照表的数字,注意混合那一档此刻与纯 BM25 逐字相同——因为融合还没实现。
  2. 实现 rrfFuse 与 sweepRrfK,再跑一遍:第五段的 k 扫描表出现了,看 k 从 0 到 600 前 5 名怎么变形。
  3. 实现重排替身的打分,第七段会显示有多少个位置被改写;对比重排前后的 nDCG,判断它是帮了忙还是添了乱。
  4. 实现成本账,第三段的费用列不再是 0;把 PRICE_RERANK_PER_KSEARCH 调到十分之一再跑一遍,看四档的费用占比怎么翻转。
  5. 用 PER_ROUTE_K 换几个召回深度重跑,把召回率与 nDCG 打架的那一幕看清楚,然后写下你的推荐配置和理由。

面试题

今天 4 道题在下方题库区,侧重多路召回的融合策略、重排模型的成本与收益、指标驱动的取舍。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。

检查清单与明日预告

  • 能说清关键词检索与向量检索各自的盲区,并举出一个只有对方能命中的查询
  • 能实现倒数排名融合并解释为什么它比按分数加权更稳
  • 能说明双编码器与交叉编码器的结构差异,以及重排为什么只能用在前几十条上
  • 能说出同分不做兜底排序会导致什么后果
  • 实验的 5 条验收标准全部通过
  • 4 道面试题不看要点也能答出至少 3 道

今天我们把功夫下在"检索发生之后":两路怎么合、合完怎么精排。但 q06 那道题已经画出了这条路的上限——四档配置全军覆没,因为没有任何一路把答案文档捞进候选池。既然出口这一侧已经榨得差不多了,明天(D10)我们换到入口:用户的问题写得又短又含糊,检索不到往往不是索引的错。改写、假设文档嵌入、多路查询、后退提问、意图路由——先把问题问对,再谈怎么检索。顺带一提,多路查询生成的那几路结果,用的正是今天这个 rrfFuse,一行都不用改。

如果你学过 30 天课,第 24 天 讲的是把这一套接进 Agent 之后的形态,可以对照着看。

Interview questions

  • Why do hybrid retrieval systems usually use reciprocal rank fusion instead of normalizing both scores and adding them with weights? When does the weighted approach break down?混合检索为什么普遍用倒数排名融合,而不是把两路分数归一化之后加权相加?加权那条路在什么情况下会失控?
    Common in ChinaCommon overseasIntermediate#hybrid-search#rank-fusion

    How to reason about it · think before answering

    1. The hinge word is `scores`. Answering `RRF is simpler` is reciting a concept; the interviewer wants to hear that you know why the two scores are not comparable in the first place.
    2. Start with scale: BM25 is an unbounded sum of log terms, and on one index the top hit can range from 5 to 50 depending on the query; cosine is pinned between -1 and 1. Adding those two readings is meaningless.
    3. Then name the silent failure of normalization: dividing by the per-route maximum makes the denominator float with the query. For a question with no answer in the corpus, the vector route's best hit may score 0.09 and still normalize to a perfect 1.0, entering the fusion at full weight. You think you are comparing relevance; you are comparing `tallest among the short`.
    4. Then the maintenance cost of weights: a 1-to-0.6 ratio has to be tuned against an eval set, tuning two routes is a 2-D search, adding multi-query retrieval makes it 4-D or 5-D, and swapping the embedding model invalidates all of it. RRF has a single k, and the default of 60 rarely needs touching.
    5. Conclusion: rank is the only thing the two routes share. RRF throws the scores away on purpose so that an incomparable quantity cannot mislead it.
    6. Expected follow-up: what does k do? It flattens — the larger k is, the smaller the gap between the top few ranks, so `ranked well by both routes` outweighs `ranked first by one route`, which is exactly the cross-validation effect hybrid retrieval is after. A second follow-up on ties: you must fall back to sorting by document id, or ranks drift between runs and every eval number wobbles with them.

    分析过程 · 先想清楚再作答

    1. 这题的题眼在「分数」两个字。只答「RRF 更简单」是背概念,面试官想听的是你知道分数为什么不可比。
    2. 先给量纲差异:BM25 是一堆对数项累加,没有上界,同一套索引里不同查询的第一名可以从 5 分到 50 分;余弦被钉死在负一到正一。两个读数相加没有意义。
    3. 再点出归一化的静默失败:除以本路最高分之后,分母随查询浮动。一个语料里根本没有答案的问题,向量那一路最高分只有 0.09,归一化之后照样是满分 1.0 带权重进融合——你以为在比相关性,其实在比「本路矮子里有多高」。
    4. 然后是权重的维护成本:1 比 0.6 这个配比要靠跑评估调出来,两路是二维搜索,加上多路查询就是四维五维,而且换一个 embedding 模型全部作废。RRF 只有一个 k,而且 60 这个默认值几乎不用动。
    5. 结论:名次是两路唯一可比的东西。RRF 主动扔掉分数,是为了不被不可比的量误导。
    6. 可预期的追问:那 k 是干什么的?答 k 是压平器——k 越大,头几名之间的差距越小,于是「两路都排进前列」比「一路排第一」更有分量,这正是混合检索想要的交叉验证效果。再追问同分怎么办,答必须按文档 id 兜底排序,否则跨次运行名次会飘、评估数字跟着抖。

    Key points

    • BM25 is unbounded, cosine is bounded; the two scales are not comparable, so adding them is meaningless.
    • Per-route max normalization has a denominator that floats with the query, so the least relevant hit of an unanswerable query still normalizes to 1.0.
    • Weights must be tuned against an eval set, the search is high-dimensional once you add routes, and swapping models invalidates it; RRF has a single constant k.
    • RRF consumes only the ordered id list from each route, because rank is the one thing the routes share.
    • Larger k rewards `ranked well by both routes`; ties must fall back to document id so results are reproducible.

    答题要点

    • BM25 无上界、余弦有界,两个量纲不可比,直接相加没有意义。
    • 按本路最高分归一化的分母随查询浮动,无答案的查询里最不相干的结果也能拿到满分。
    • 权重要跑评估调,路数一多就是高维搜索,换模型还得重来;RRF 只有一个常数 k。
    • RRF 只吃每一路的有序 id 列表,名次是两路唯一可比的东西。
    • k 越大越奖励「两路都排进前列」;同分必须按 id 兜底排序才可复现。
  • Why is a cross-encoder more accurate than a bi-encoder? And if it is more accurate, why not just use it to search the whole corpus directly?交叉编码器为什么比双编码器准?既然更准,为什么不干脆拿它直接检索全库?
    Common in ChinaCommon overseasBasic#cross-encoder#bi-encoder

    How to reason about it · think before answering

    1. This is a giveaway question, but the discriminating half is the second part. Saying `cross-encoders are slow` is not enough; you have to point at the structural reason.
    2. Start with the structure: a bi-encoder encodes query and document **separately** into vectors that never meet until a single dot product at the end; a cross-encoder concatenates query and document into one sequence, so every attention layer lets query tokens attend to document tokens.
    3. That yields the accuracy gap: a bi-encoder must compress a document into one fixed-length vector, and compression loses information — the binding between `Zhou Min` and `platform team lead` may not survive. A cross-encoder does not compress; it aligns them on the spot.
    4. The answer to the second half hides in the same structure: bi-encoder document vectors can be computed **offline** and indexed, so query time is just a vector search. A cross-encoder has nothing to precompute — N documents means N forward passes. Reranking a 100k-chunk corpus means pushing the entire corpus through a model on every question.
    5. So the engineering split is a division of labor: recall pulls a small batch out of the whole corpus (cheap, indexable), reranking fixes the order of that batch (expensive, accurate). The default is to rerank only the top 20 after fusion.
    6. Expected follow-up: is there a middle path? Yes — late interaction, where token-level document representations are precomputed and the interaction happens at query time. Accuracy and cost land between the two, at the price of a much larger index.

    分析过程 · 先想清楚再作答

    1. 这是一道送分题,但送分题的区分度在第二问。只答「交叉编码器慢」是不够的,要说清慢在结构上的哪一处。
    2. 先给结构差异:双编码器把查询和文档**各自**编码成向量,两者从头到尾没有见过面,最后只靠一次内积凑到一起;交叉编码器把查询和文档拼成一段文本一起过模型,每一层注意力都能让查询的词去看文档的词。
    3. 由此推出准确率差异的来源:双编码器要把一篇文档压成一个固定长度的向量,压缩必然丢信息,「周敏是平台组组长」里两个词的绑定关系未必留得下来;交叉编码器不压缩,它当场对齐。
    4. 第二问的答案就藏在同一个结构里:双编码器的文档向量**可以离线算好**,查询时只做向量检索;交叉编码器没有任何东西能预先算好,N 篇文档就要跑 N 次前向。十万块的语料重排一遍,等于每次提问都把整个库过一遍模型。
    5. 所以工程上的定位是分工:召回负责在全库里捞出一小批(便宜、可索引),重排负责把这一小批的顺序改对(贵、准)。默认只重排融合后的前 20 条。
    6. 可预期的追问:有没有中间路线?答有——后期交互(late interaction)那一类,文档侧提前算好词级表示、查询侧当场做交互,精度和成本都在两者之间,代价是索引体积大得多。

    Key points

    • A bi-encoder encodes both sides separately and joins them with one dot product; a cross-encoder concatenates them so attention can align across the pair.
    • The accuracy gap comes from compression: a bi-encoder squeezes a whole document into one vector and loses bindings; a cross-encoder does not compress.
    • Bi-encoder document vectors can be computed offline and indexed; a cross-encoder has nothing to precompute.
    • Reranking the full corpus means running every chunk through a model on every question, so cost scales linearly with corpus size.
    • The standard split is recall plus rerank, with reranking applied only to the top few dozen after fusion.

    答题要点

    • 双编码器各自编码、最后一次内积;交叉编码器把查询和文档拼在一起过模型,注意力可以跨两者对齐。
    • 准确率差异来自压缩:双编码器把整篇文档压成一个向量,绑定关系会丢;交叉编码器不压缩。
    • 双编码器的文档向量能离线算好并建索引,交叉编码器没有任何东西可以预先算好。
    • 全库重排等于每次提问把整个语料过一遍模型,成本随语料规模线性增长。
    • 标准分工是召回加重排,重排只作用于融合后的前几十条。
  • You replaced pure vector retrieval with hybrid search plus reranking, and after shipping it your eval metrics went down. How do you investigate?你把纯向量检索换成了混合检索加重排,上线之后评估指标反而掉了。你会怎么排查?
    Common in ChinaCommon overseasDeep dive#hybrid-search#evaluation

    How to reason about it · think before answering

    1. This question tests whether you have actually done stage-by-stage attribution. Answering `I would tune the weights and see` loses — that is guessing, not investigating.
    2. Step one is to run the stages apart, not to change code: pure keyword, pure vector, hybrid, and hybrid plus rerank, all on the **same eval set with the same context budget**. Whichever stage the drop appears in is where you look, and this alone separates `fusion is broken` from `reranking is broken`.
    3. Step two asks a specific question: did recall drop, or did the ranking metrics drop? A recall drop means the answer never entered the context at all — a candidate-pool or budget problem. Ranking metrics dropping while recall holds means the answer is still there but pushed down — a fusion-weight or rerank-model problem. The two failures have completely different fixes.
    4. A third common root cause is recall depth. This knob runs against intuition: going deeper is not safer, it lets noise vote too. On a 134-chunk corpus I measured that narrowing each route from 50 to 5 took hybrid recall from 87.5% back to 93.8% and multi-hop from 50% to 75%, while nDCG fell by almost 0.1. The metrics fight each other, so decide which one the product needs first.
    5. A fourth root cause is that the eval protocol quietly changed. Touch the context budget, the hit rule, or the candidate depth, and the old and new numbers stop being comparable — in which case the `drop` may not be a drop at all.
    6. Expected follow-up: how do you avoid this next time? Make the four-way comparison a single command, store the previous report as a baseline, and fail the build with a non-zero exit code on regression. That is precisely why evaluation comes before optimization.

    分析过程 · 先想清楚再作答

    1. 这题考的是你有没有真的做过分阶段归因。答「调一下权重再看看」就输了——那是在猜,不是在查。
    2. 第一步是拆档跑,不是改代码:纯关键词、纯向量、混合、混合加重排四档在**同一份评估集、同一个上下文预算**下各跑一遍。指标掉在哪一档就在哪一档找原因,这一步能立刻区分「融合坏了」和「重排坏了」。
    3. 第二步问一个具体问题:掉的是召回率还是排序指标?召回率掉说明答案根本没进上下文,是候选池或者预算的问题;排序指标掉而召回率没动,说明答案还在、只是被挤到了后面,那是融合权重或重排模型的问题。这两类故障的解法完全不同。
    4. 第三个常见根因是召回深度。每路取多少条这个旋钮方向反直觉:取深了不是更保险,是把噪声也一起投了票。我在一份 134 块的语料上实测过,每路从取 50 收到取 5,混合那一档的召回率从 87.5% 回到 93.8%、多跳档从 50% 回到 75%,而 nDCG 反而掉了近 0.1——两个指标会打架,先想清楚业务要哪个。
    5. 第四个根因是评估口径被悄悄改了。上下文预算、命中判定、候选池深度只要动过一个,新旧数字就不可比,这时候「掉了」可能根本不是真的掉了。
    6. 可预期的追问:怎么防止下次再踩?答把四档对照做成一条命令、把上一版报告存成基线、指标退步就以非 0 退出码拦住合并——这就是评估要先于优化的原因。

    Key points

    • Run all four configurations separately for attribution, on one eval set with one context budget, before touching any parameter.
    • Separate a recall drop from a ranking drop: the first is a candidate-pool or budget issue, the second is a fusion or rerank issue.
    • Check recall depth: taking too many per route lets noise vote, and narrowing it can bring recall back.
    • Confirm the eval protocol did not change; touching budget, hit rule, or candidate depth makes old and new numbers incomparable.
    • Freeze the four-way comparison into one command plus a baseline report, and block merges on regression.

    答题要点

    • 先拆档跑四种配置,在同一份评估集和同一个上下文预算下归因,不要一上来就调参。
    • 区分召回率掉与排序指标掉:前者是候选池或预算问题,后者是融合或重排问题。
    • 查召回深度:每路取太深会把噪声也投进融合,收窄反而可能救回召回率。
    • 确认评估口径没被改:预算、命中判定、候选池深度动过一个,新旧数字就不可比。
    • 把四档对照固化成一条命令加一份基线报告,指标退步直接拦住合并。
  • Adding a reranker costs you 200 ms of extra latency per question plus a per-search fee. How do you decide whether that spend is worth it?加上重排之后每次提问多了两百毫秒延迟,还多了一笔按次计费的开销。你怎么判断这笔钱该不该付?
    Common in ChinaCommon overseasDeep dive#rerank#cost-tradeoff

    How to reason about it · think before answering

    1. This question tests whether you can translate a technical choice into a business judgment. Answering `check whether the metrics went up` covers only a third of it.
    2. Split it into three ledgers: how much the metrics moved, how much latency grew, and how much money it costs. All three must be reported together; a proposal with only the first will not survive review.
    3. For the first ledger, be specific about **which** metric reranking improves. Reranking changes the order, not the candidate set — it cannot fix `the answer was never retrieved`. If your recall is the bottleneck, add a retrieval route or adjust recall depth first; the 200 ms buys nothing.
    4. For the second, ask where those 200 ms land. They sit synchronously between retrieval and generation, with the user waiting; but if a streaming generation follows and time-to-first-token is already a second or two, the relative cost is small. In an as-you-type search box, 200 ms is fatal.
    5. For the third, note the billing unit: rerankers usually charge per search rather than per token, so sending a few more candidates barely changes the bill — what is expensive is the number of questions. That points optimization at reducing query volume (caching, intent routing) rather than at trimming the candidate list.
    6. Expected follow-up: what if you simply cannot afford it? Three paths — rerank only queries classified as hard (intent routing), cache results, or self-host an open-weights cross-encoder to convert per-call fees into fixed compute cost.

    分析过程 · 先想清楚再作答

    1. 这题考的是你会不会把技术选择翻译成业务判断。只答「看指标涨没涨」只答了三分之一。
    2. 先把账拆成三笔:指标涨了多少、延迟涨了多少、钱涨了多少。三笔必须一起报,只报第一笔的方案在评审会上过不去。
    3. 第一笔要问清楚重排改善的是**哪个**指标。重排改的是顺序,不是候选集合——它救不了「答案压根没被召回」这种故障。如果你的召回率本来就不够,先去加召回路数或者调召回深度,重排这两百毫秒是白花的。
    4. 第二笔要看这两百毫秒落在哪。它是同步卡在检索之后、生成之前的,用户全程在等;但如果后面接的是一个流式生成、首字节本来就要一两秒,这两百毫秒的相对占比就小得多。反过来,如果这是一个自动补全式的即时搜索框,两百毫秒就是致命的。
    5. 第三笔要注意计价单位:重排普遍按检索次数计价而不是按 token,所以「多送几条给它排」几乎不涨钱,真正贵的是提问次数本身。这直接决定了优化方向是压提问量(缓存、意图路由)而不是压候选数。
    6. 可预期的追问:如果就是付不起怎么办?答三条路——只对判定为复杂的查询走重排(意图路由)、把结果缓存起来、或者换成自部署的开源交叉编码器把按次付费变成固定的算力成本。

    Key points

    • Report all three ledgers together: metric gain, latency growth, cost growth; a proposal missing one is incomplete.
    • Confirm whether the bottleneck is ordering or recall first; reranking only reorders and cannot rescue an answer that was never retrieved.
    • Judge the latency by where it lands: it is small relative to a streaming generation, but fatal in an as-you-type search box.
    • Rerankers bill per search rather than per token, so cost scales with question volume, not candidate count.
    • If it is unaffordable: route only hard queries to the reranker, cache results, or self-host an open-weights cross-encoder.

    答题要点

    • 三笔账一起报:指标增量、延迟增量、成本增量,缺一笔方案就不完整。
    • 先确认瓶颈是排序还是召回:重排只改顺序,救不了没被召回的答案。
    • 延迟要看落在哪:流式生成场景下相对占比小,即时搜索框里两百毫秒就是致命的。
    • 重排按检索次数计价而不是按 token,涨钱的是提问量而不是候选条数。
    • 付不起时的三条路:意图路由只对难查询重排、结果缓存、换自部署的开源交叉编码器。

Comments

Sign in to join the discussion

No comments yet — be the first.