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.

Today's Goals

  1. Explain the blind spots of keyword search and vector search respectively, and give a query only one of them can hit
  2. Implement Reciprocal Rank Fusion and explain why it's more stable than weighting by raw score
  3. Explain the structural difference between bi-encoders and cross-encoders, and why reranking can only be applied to the top few dozen results

Yesterday you fitted week one's system with a scale and measured 87.5% recall and an already-saturated mean reciprocal rank. Today starts weighing things with it: fuse two retrieval paths, add a reranking layer, and measure tier by tier what each is worth. Come back and tick off the three goals.

Plain-Language Walkthrough

Two detectives: one works the literal clues, one works the motive

A case goes to two detectives. The first recognizes only physical evidence: fingerprints at the scene, shoe prints, a receipt with a name on it, locking on when one character matches and looking away when it does not. The second ignores physical evidence and studies motive: who has been short of money lately, who had a grievance with the deceased, whose account keeps changing — they cannot name one piece of hard evidence pointing at the killer and they can circle a set of that kind of person.

Keyword search is the first detective. Ask "how many unused annual leave days can carry into next year at most" and it goes to the inverted index for those terms, ranking by who contains more of them and whose terms are rarer. The formula and its two correction terms were taken apart on day 1 and are not repeated today. Its strength is precision: say a name and it finds that name and never returns a different one. Its blind spot is right there — rephrase and it goes blind.

Vector search is the second detective. It compresses query and document each into a string of numbers and compares direction rather than characters. "Passphrase" and "password" sit close in vector space even sharing no characters. Its blind spot is imprecision: ask for a person by name and it may hand you a batch of "the team lead" passages, because the name of the lead is genuinely close in meaning.

This is not which is better but that the two are good at different cases. I verified one pair on this day's thirty-document corpus (chunk-level retrieval, 134 chunks, MOCK=1 hash vectors):

  • Asked how long troubleshooting records are kept before being purged, with the answer in doc-022 (log retention), the keyword path ranks it 3rd and the vector path does not have it in the top 10 at all.
  • Asked whether an employee's old passphrase still works after switching to the company's unified identity, with the answer in doc-002 (single sign-on and password policy), the keyword path has not one entry in the top 10 — because "passphrase" never appears in the corpus while "unified identity" and "employee" are too common to discriminate — and the vector path ranks it 3rd.

Same corpus, same indexes, and two queries each stumped one detective. So do not let them work alone — the question is how two people's conclusions get onto one sheet of paper.

Scores cannot simply be added: two rulers with different graduations

The most natural idea is adding the two paths' scores and sorting. That idea shatters on contact.

A BM25 score is a sum of logarithmic terms with no upper bound. On this corpus, the golden set's 20 questions have first-place scores from 16.37 to 94.46, and a more obscure phrasing goes as low as 6.49 — depending on how rare your terms are and how often they hit. Cosine similarity is pinned between minus one and one, and under MOCK=1 hash vectors even a relevant chunk sits just past 0.2. One is a rubber ruler with no graduations and the other a vernier caliper graduated too finely to read, and adding the two readings means nothing.

What about normalizing first — divide each by its own path's top score, squeeze into 0 to 1, and weight? Week one's system (day 7's implementation) did exactly that; it works, and it has a silent defect: the denominator floats with the query. Suppose a question has no answer in the corpus at all, so the vector path fetches only irrelevant chunks with a top score of 0.09 — after normalization that least relevant result scores a full 1.0 and marches into the fusion with its weight. You believe you are comparing how relevant, and you are comparing how tall among this path's dwarves.

The weights themselves are worse. Where did 1 : 0.6 come from? Day 7 guessed it. Tuning it means running a full evaluation per weight combination — two paths are two-dimensional, three are three-dimensional, and adding query rewriting's multi-path recall (day 10) makes four or five. That is a tuning problem expanding exponentially with the number of paths, and it must be redone from scratch with every embedding model change.

The conclusion is blunt: scores are incomparable and ranks are not. Whether BM25 gave 52 or 5, the sentence "it ranked first on the keyword path" has stable meaning.

Reciprocal rank fusion: only ranks, and one constant is enough

Reciprocal rank fusion (RRF) is almost unreasonably simple: each path gives an ordered list, a document ranked i on a path scores 1 / (k + i) there, the paths' scores are summed, and the highest ranks first. k is a constant, uniformly 60 across this course.

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) => {
      // Ranks start at 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))
}

Note the signature: the input is each path's ordered list of ids, not a list of scores. The scores were discarded before entering this function, which is not laziness but design — discarding what is incomparable prevents being misled by it.

k acts as a flattener. At k of 0, first place scores 1.0 and second 0.5, a factor of two, meaning whoever ranks first on their own path wins; at k of 60, first scores 0.0164 and second 0.0161, a difference of 1.6%, so ranking near the top on both paths carries more weight than ranking first on one. That is exactly what we want: a suspect cross-validated by both detectives goes first. The lab's fifth section has you sweep k from 0 to 600 and watch one question's top 5 deform accordingly.

Setting the recall depth: this knob is harsher than you think

Before fusing, how many does each path take? This course uniformly takes the top 50, keeping the top 20 after fusion for reranking.

But that default has a precondition. In a million-chunk corpus, 50 is one ten-thousandth, conservative to the point of harmlessness; and this lab's corpus is 134 chunks, so 50 per path pours 37% of the store into the fusion — rather like having two detectives list half the city as suspects. Noise has ranks too, and being on the list earns votes.

The lab's sixth section runs that into a table (30 documents, 134 chunks, D8's 20 questions, a 600-token context budget):

TextText
per path   recall   multi    nDCG@10   refusal
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%

Two metrics openly fight here: pull recall depth back to 5 and both recall and the multi-hop tier return while nDCG@10 falls by nearly 0.1. The reason is not hard — take fewer and less garbage enters the candidates, so the answer squeezes into the 600-token budget more readily; take more and the top 10's overall quality looks better while the answer gets pushed out of the context by more chunks that also look relevant.

That is why how many to take has no standard answer and only an evaluation result. If your business is question answering, recall comes first; if it is a search results list for a human, ranking quality does. Decide which you want and then read the table.

Bi-encoders and cross-encoders: one precomputed, one computed on the spot

There is one more layer after fusion. Everything up to here computed similarity with a bi-encoder: the query encodes to a vector, the document encodes to a vector, and the two take one dot product. The key is that they never met — document vectors can be computed offline, indexed, and searched across the whole store in milliseconds. That is the entire reason it withstands massive data.

The cost is information lost in compression. A 500-word document squeezed into 1,536 numbers may not retain the binding between a person's name and "team lead" in the sentence stating it.

A cross-encoder changes the structure: query and document are concatenated into one text and sent through the model together, with every attention layer letting the query's words look at the document's. The question "who is the lead" can align directly onto the name. It is far more accurate — at the cost that nothing can be precomputed. N documents mean N forward passes, each waiting on the model.

So it can only stand behind recall as a reranking layer: recall fetches a small batch from the whole store, and reranking fixes that batch's order. In the library metaphor, recall is carrying an armful of books to the desk by the card catalog, and reranking is the librarian opening each one to pick the three you should read — they cannot open the whole collection.

pipeline.js
const RRF_K = 60, PER_ROUTE_K = 50, FUSE_KEEP = 20
 
export async function hybridSearch(query, routes, rerank) {
  // Two paths in parallel: the keyword path is pure in-memory computation, the vector path awaits one 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)
  // Only the top 20 go to the cross-encoder, not the whole store
  const candidates = fused.slice(0, FUSE_KEEP).map((f) => ({ id: f.id, text: routes.text(f.id) }))
  return rerank(query, candidates)
}

Reranking's cost accounting: three numbers decide whether to adopt it

Reranking is this course's first optimization that explicitly costs money and explicitly costs time, so it must clear three bills — metric, latency, cost — plus a fourth that is often forgotten.

Bill one, the metric — but first confirm which metric you are reading. Yesterday's baseline already had mean reciprocal rank at 1.0000, as do three of today's four configurations. That is not full marks but metric saturation: questions written backwards from the corpus with such high literal overlap always put the answer document first, so it cannot rise further. When a metric hits the ceiling the response is writing harder questions, not declaring victory. So today's evidence can come only from recall (especially the multi-hop tier), nDCG, and the refusal rate, the three with room left.

By those three, reranking's contribution attributes precisely to one question. q07 requires hitting both doc-029 and doc-021: the pure keyword tier hits it, fusion loses it (doc-021 diluted and pushed out of the 600-token budget), and reranking brings it back — taking the multi-hop tier from 50.0% back to 75.0%. That is a clean, attributable improvement: not "hybrid search raised the metric" but "reranking fixed the one question fusion broke." As for q06, also multi-hop, all four tiers miss it, because the answer document never entered the candidate pool at all — fusion and reranking can only reorder existing candidates and cannot conjure new ones, and that debt is paid later.

The refusal rate is another matter entirely: reranking helps not at all in this implementation. Admission is gated on each path's raw score, and reranking changes order rather than admission; the only tier raising the refusal rate is pure vector (25.0%, with the 0.2 cosine gate genuinely blocking one unanswerable question). Reranking cannot rescue failing to stay quiet when it should, which is day 6's generation-side refusal work.

Bill two, latency. Reranking is one synchronous network round trip wedged between retrieval and generation, with the user waiting throughout. The lab's column uses overridable placeholder values: 120 milliseconds per embedding and 180 per rerank. So the pure BM25 tier's modeled latency is near zero, hybrid is 120 milliseconds, and hybrid plus reranking is 300. Those 180 milliseconds buy ranking quality, not answer quality — if your context budget is large enough that the answer got in anyway, that money was wasted.

Bill three, money. Reranking is generally priced per search rather than per token: one question counts as one search whether it sends 20 candidates or 100. That explains two things — why reranking's cost is per question rather than per chunk, and why sending a few more candidates barely raises the fee, since what is genuinely expensive is the number of questions. The lab's reranking input for that tier is 2,922 tokens per question across 1 call, while the embedding path is only 31.6 tokens per question. Two orders of magnitude apart, meaning the retrieval side's bill is essentially the reranking bill.

Bill four, whether it can be self-hosted. This one is often forgotten and is frequently decisive. A cross-encoder is not as vast as a large model: the lab's default hosted backend is Cohere's rerank-v3.5, and the self-hosted path uses BAAI/bge-reranker-v2-m3 — Apache-2.0, multilingual, and fitting on one consumer graphics card. When data may not leave the network, or the volume makes per-call pricing uneconomical, switch to self-hosting; rerank.ts's two backends implement one interface and it is one environment variable.

Metadata filtering before or after retrieval

The last piece. Real retrieval is rarely across the whole store and is usually within this department, this time range, or what this person may see. Where the filter goes depends on how much remains after it.

Filter first, then retrieve (pre-filtering): do this when the range is small. With 200 documents left, running BM25 and vectors over those 200 is fast, and all 50 recalled are legitimate.

Retrieve first, then filter (post-filtering): do this when the range is large. But it has a pothole to guard: recall 50, filter out 45, and you hold 5 — quietly cutting the recall depth by 90%. The remedy is scaling the recall count by the pass rate: an estimated 20% pass rate means recalling 250 and then filtering.

filter.js
// Estimate what this filter leaves before deciding which step it belongs to
export function planFilter(estimatedKept, totalDocs, want = 50) {
  const ratio = estimatedKept / totalDocs
  // Under a tenth remaining, narrowing first is better value: a small candidate pool runs both paths faster
  if (ratio < 0.1) return { mode: 'pre', topK: want }
  // Otherwise post-filter, scaling the recall count by the pass rate, or little is left afterwards
  return { mode: 'post', topK: Math.ceil(want / Math.max(ratio, 0.05)) }
}

Permission filtering is the deadliest case of this, because one mistake is an incident — and that whole subject waits for day 13. Here you only need the criterion: compute the pass rate first, then decide the order.

Source Reading

Hands-On Lab

🧪 D9 lab: implementing hybrid search plus reranking and comparing three configurations' metrics on one evaluation set

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

Acceptance criteria:

  1. MOCK=1 pnpm start finishes with all four items of section eight's acceptance ✅
  2. In section one's four-tier comparison table, the hybrid tier's ordering differs from pure BM25's, showing fusion genuinely happened
  3. In section five's k sweep table, k of 0 and k of 60 give different top 5s, and it is visible that larger k rewards ranking highly on both paths
  4. Section seven shows reranking rewrote several positions in the top 10; the reference implementation has 151
  5. In section three, the hybrid-plus-reranking tier's rerank calls per question is 1.00 and its cost per thousand questions is markedly above the other three tiers

starter/ has 4 exercises cut out: reciprocal rank fusion, the k sweep, the rerank stand-in's scoring, and the cost ledger. Run as-is, all four acceptance items are ❌ and each one finished turns one green — use it as a progress bar and do not read the answers all at once. None of the four needs a network or an API key.

  1. Run the starter as-is first, note the four-tier table's numbers, and observe that the hybrid tier is currently identical to pure BM25 — because fusion is not implemented.
  2. Implement rrfFuse and sweepRrfK, run again: section five's k sweep table appears, showing how the top 5 deforms as k goes from 0 to 600.
  3. Implement the rerank stand-in's scoring, and section seven shows how many positions were rewritten; compare nDCG before and after to judge whether it helped or hurt.
  4. Implement the cost ledger and section three's cost column stops being 0; set PRICE_RERANK_PER_KSEARCH to a tenth and rerun to see how the four tiers' cost shares invert.
  5. Rerun with a few different recall depths via PER_ROUTE_K, watch the scene where recall and nDCG fight, then write down your recommended configuration and why.

Interview Questions

Today's 4 questions are in the question bank below, weighted toward fusion strategies for multi-path recall, a reranking model's cost and benefit, and metric-driven trade-offs. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.

Checklist and Tomorrow

  • Explain the blind spots of keyword search and vector search respectively, and give a query only one of them can hit
  • Implement Reciprocal Rank Fusion and explain why it's more stable than weighting by raw score
  • Explain the structural difference between bi-encoders and cross-encoders, and why reranking can only be applied to the top few dozen results
  • Say what happens when ties are not broken deterministically
  • All 5 acceptance criteria of the lab pass
  • Answer at least 3 of the 4 interview questions without looking at the key points

Today's effort went into what happens after retrieval: how two paths fuse and how the fusion is refined. But q06 already drew this road's ceiling — all four configurations fail, because no path ever fetched the answer document into the candidate pool. With the exit side largely wrung out, tomorrow (D10) moves to the entrance: a user's question is short and vague, and failing to retrieve is often not the index's fault. Rewriting, hypothetical document embeddings, multi-query, step-back prompting, and intent routing — ask the question properly before discussing how to retrieve. Incidentally, the several paths multi-query generates are fused by today's rrfFuse with not one line changed.

If you took the 30-day course, day 24 covers what this looks like once wired into an agent, and is worth reading alongside.

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