Dayward AI
Week 4 · D24About 6 hours

RAG, Level Up: Hybrid Search, Reranking, Citations, Recall Evaluation

Upgrade day twelve's memory retrieval into a hybrid-search-plus-rerank combination, add source citations, and evaluate retrieval quality with a recall metric.

Today's goals 0/3

Sign in to tick these off and save your progress.

今日目标

  1. 能实现关键词检索和向量检索结合的 hybrid search
  2. 能给检索结果接入一个 rerank 步骤,提升排序质量
  3. 能设计一套 recall 评估方法,衡量检索是否召回了应该召回的内容

昨天把「接一个能力」变便宜了,于是你会很自然地接进来一堆知识库和检索服务。今天回头修上游——因为接得再多,也救不回一个召回率本身就低的检索。读完回到页面顶部把三条目标勾掉。

小白版讲解

招聘那两轮筛选,恰好就是检索的两轮

一家公司要招一个人,收到八百份简历。HR 不会逐份细读——那要花两周。真实流程是两轮:

第一轮量大且宽,同时开两条渠道。一条是关键词粗筛:在系统里搜「Kubernetes」,命中的进池子。它很准,但有个致命毛病——候选人如果把它写成「K8s」,就整份漏掉了。另一条是猎头推荐:他见过这个人,觉得「感觉像」,说不出具体命中了哪个词。他懂同义、懂上下文,但你让他找「用过 SF-3000 这个型号的人」,他会给你一堆做过类似型号的人,唯独漏掉那个真正写了 SF-3000 的。

两条渠道各有各的盲区,而且盲区不重叠——所以把两边的人合到一起进第二轮。第二轮是技术面试官逐份细看,一份看二十分钟。准,但贵,所以只能对少量候选人做。

检索是一模一样的两轮。关键词检索(BM25 那一路)就是 HR 的搜索框:字面命中,对型号、错误码、订单号极准,对同义词完全无能。向量检索就是猎头:它把文本压成一串数字,比的是语义距离,懂「运费」和「邮费」是一回事,但对 E4032 这种没有语义的字符串,它只能给你一堆「看起来像编号」的东西。重排(rerank)就是那位技术面试官:拿到几十个候选,一条一条和问题细对,重新排个序。

这个类比最值钱的一点在最后:召回率(recall)就是「真正合适的人有没有进到第一轮」。 第一轮漏掉的人,第二轮再厉害也救不回来——面试官只能在给到他的名单里排序,不能凭空变出一个没进池子的人。

所以整条链路的形状是固定的:上游要宽,下游才收紧。 两路各取 20 条,融合,重排之后只留 5 条进上下文。

两路的盲区不重叠,而它们的分数不能相加

D12 已经把向量检索这一路做完了:切块、算 embedding、余弦相似度、包成 memory_search 工具。那些今天全部沿用,一个字不重讲——1536 维、text-embedding-3-small、0.02 美元每百万 token,还是那套。

今天要做的是在旁边加一路关键词检索,然后回答一个问题:它到底补上了什么?

今天实验里的知识库是 54 篇电商客服文档、切成 64 块。拿一条真实 query 去打:

TextText
query:E4032 是什么意思,可以让买家重试吗
  向量 top5 —— 没有 s07#0(那篇讲支付错误码的文档)
  关键词 top1 —— s07#0

这就是盲区的样子:向量那一路把正确答案挤出了前五名,而关键词那一路把它排在第一。 反过来的例子也有:问「邮费什么时候可以免掉」,文档里写的是「运费」,关键词那一路一个字都对不上,只有向量能捞回来。

先说清楚关键词那一路算的是什么。BM25 的直觉只有两条:一个词在这篇文档里出现得越多,越相关;但这个词在整个语料里越常见,它就越不值钱。 所以「退款」在一个电商知识库里几乎没有区分度,而 E4032 只出现在一篇文档里,命中它几乎等于确定答案。向量检索恰恰对这类低频、无语义的字符串最不敏感——它被压进 1536 维空间之后,和其他一堆编号挤在一起。两种方法的强弱正好互补,这不是巧合,是它们的计算原理决定的。

关键词那一路怎么做?教学实现直接用 Postgres 的全文检索(tsvectorts_rank),不额外引一个搜索引擎。但中文有个坑必须提前说破:Postgres 默认的分词器对中文等于不分词——整句话会被当成一个 token,于是你搜什么都搜不到。教学实现的兜底是按相邻两字切开(bigram),「运费规则」切成「运费」「费规」「规则」;英文单词和编号(sf-3000e4032)按整词保留,不能切碎。生产环境要上专门的中文分词扩展,别拿 bigram 当终点。

bigrams.js
// 中文按相邻两字切;英文与编号按整词保留——切碎了 e4032 就再也搜不到了
export function bigrams(text) {
  const lowered = text.toLowerCase()
  const tokens = []
  // 先把连续的字母数字(含连字符)整段抠出来当一个 token
  for (const word of lowered.match(/[a-z0-9][a-z0-9-]*/g) ?? []) tokens.push(word)
  const han = lowered.replace(/[^一-鿿]/g, ' ')
  for (const run of han.split(/\s+/).filter(Boolean)) {
    if (run.length === 1) tokens.push(run)
    for (let i = 0; i + 1 < run.length; i++) tokens.push(run.slice(i, i + 2))
  }
  return tokens
}

现在有两份候选名单,怎么合成一份?

直觉做法是加权求和:0.6 × 余弦相似度 + 0.4 × BM25 分数这条路走不通,原因不是权重难调,是两个分数根本不可比:余弦相似度落在 0 到 1 之间、分布密集,同一批候选之间常常只差 0.02;BM25 没有上界,一条命中三个稀有词的文档可以拿到 12 分。把它们相加,等于让 BM25 单方面决定结果——你调权重调到天亮,也只是在调「BM25 说了算的程度」。

正确做法是只用名次,不用分数:RRF(Reciprocal Rank Fusion,倒数排名融合)。每一路给出的名次 rank 换算成 1 / (k + rank),同一个文档在几路里的得分相加,k 固定取 60

k 为什么是 60?它是这个方法被提出时用实验定下来的经验值,作用是压平前几名之间的差距:没有它(k = 0)时第一名拿 1 分、第二名 0.5 分,一路陡降,等于让每一路的冠军独裁;k 取到 60 之后,第一名 1/61、第十名 1/70,差距只有一成多,于是「两路都排进前二十」比「一路排第一」更有分量。这正是我们想要的加权方式——共识优先于单点自信。60 不是什么魔法数字,但在你有自己的评估集之前,没有理由动它。

为什么这么做有效:名次是无量纲的。第一名就是第一名,不管它的原始分是 0.83 还是 12.6。于是「两路都排进前列」的文档自然浮到最上面,而「只有一路认得它」的文档也能靠那一路的高名次挤进来——这正是我们要的,因为两路的盲区不重叠。

实验里有个可以手算的例子:两路的排名分别是 [a, b, c][c, d, a],融合后第一名是 a,得分 1/61 + 1/63 ≈ 0.03227。而 starter 里那个「分数直接相加」的版本,同一组输入算出的第一名是 c,得分 12.635——c 只是在第二路里排第一而已,它的 BM25 原始分把一切都压过去了。

这个例子里还藏着一个必须处理的细节:a 和 c 是同分的(两者都是「一路第一、一路第三」,1/61 + 1/63 完全相等)。同分时谁在前,如果交给哈希表的遍历顺序去决定,同一份输入在不同语言、不同运行里会给出不同的结果——今天的四份实现里,只有显式加了「同分按 id 排」这一步的版本才处处一致。检索结果必须可复现,否则你的评估集量出来的数字每次都不一样。

rrf.js
export const RRF_K = 60
 
// 输入是若干路排好序的 id 列表(每路已按自己的分数降序)。分数一律丢掉,只用名次。
export function rrfFuse(rankings) {
  const scores = new Map()
  for (const ranking of rankings) {
    ranking.forEach((id, index) => {
      // index 从 0 开始,名次从 1 开始
      scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + index + 1))
    })
  }
  // 同分时按 id 定序:不加这一步,结果就取决于 Map 的遍历顺序,不可复现
  return [...scores.entries()]
    .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
    .map(([id]) => id)
}

重排:贵,所以只对少量候选做

融合之后的名单还是「按检索信号排的」,不是「按和问题的相关程度排的」。重排这一步就是把候选逐条和问题细对一遍。

教学实现用 LLM 批量打分:把 40 条候选和问题一次性发给模型,让它给每条打 0 到 10 分,按分数重排。这么做的好处是不用额外引一个模型,坏处是慢、贵,而且分数会随提示词的措辞漂移。

生产环境的做法不一样,这个口径差别一定要说清楚:生产用的是专门训练的 cross-encoder 重排模型(把问题和候选拼在一起送进同一个编码器,直接输出相关度)。代价是多一次 100 到 300 毫秒的调用,外加一台推理机器——它不是 API 调用那种「按 token 付费」的东西,你得为它准备资源。所以重排的候选数要卡住:40 条是教学量级,生产里常见的是 50 到 100 条封顶。

别把教学版当成标准做法。 面试里说「我用 LLM 给检索结果打分做重排」,追问一定是「延迟和成本怎么算的、为什么不用专用重排模型」——答不上来就露馅了。

引用:模型会引用一个你没给过它的编号

检索到的东西进了上下文,模型给出回答——然后你要让它标出每句话的依据来自哪一条。做法很朴素:给每个候选块编号,上下文里写成 [1] 正文……,让模型在回答里用 [1] 标注,末尾列来源清单。

固定的形状是每个块带三样:chunkId(块的唯一标识,形如 s07#0)、sourceId(它属于哪篇文档)、sourceTitle(文档标题,用来渲染来源清单)。

这一节真正要讲的坑是幻觉引用:模型会引用一个根本不存在的编号。 今天实验里就有一条真实输出,你给了它 8 条候选,它在回答里写了 [2][9]——9 号从来不存在。这不是罕见现象,它在长回答和候选数多的时候相当稳定地出现。

后处理要做两件事,缺一不可:

  1. 把不存在的编号从回答里剔除,别原样发给用户。用户点开来源清单发现只有 8 条,会认为整个系统在胡说。
  2. 把「这一条回答里出现过幻觉引用」计进一个指标(呼应 D21 的可观测面板)。剔除只是止血,指标才能告诉你这件事最近是变多还是变少——它的曲线一抬头,通常意味着检索质量或提示词最近被改坏了。

没有评估集,你连「改好了」都不知道

前面几节的每一个改动,凭什么说它是改进?靠感觉试几条 query,是这个领域最常见的自欺。

评估集的形状很简单:20 条 query,每条人工标注 1 到 3 个「必须召回」的 chunkId 注意标注的是不是文档——检索的粒度就是块,标到文档级会让指标虚高。

三个指标各回答一个不同的问题:

  • recall@5:进上下文的 5 条里,覆盖了多少应该召回的内容。这是你真正关心的数,因为模型只看得到这 5 条。
  • recall@20:第一轮捞上来的 20 条里覆盖了多少。它是天花板——recall@20 上不去,说明问题在召回侧,重排再强也没用。
  • MRR(平均倒数排名):第一个正确结果平均排在第几位。它对排序质量敏感,recall 一样时用它分高下。

今天实验里三档的真实数字,三行放在一起看最有说服力:

TextText
只用向量                       recall@5 57%  recall@20 83%  MRR 0.677
向量 + 关键词 + RRF            recall@5 80%  recall@20 95%  MRR 0.732
向量 + 关键词 + RRF + 重排     recall@5 91%  recall@20 98%  MRR 0.908

这三行要横着读,也要竖着读。 横着读:加了关键词那一路,recall@5 从 57% 涨到 80%——那 23 个百分点就是向量的盲区。竖着读第二列:recall@20 从 83% 涨到 95%,天花板被抬高了;而重排那一行 recall@20 只从 95% 到 98%(重排不召回新东西,它只重排已有的 20 条),它涨的是 recall@5(80% → 91%)和 MRR(0.732 → 0.908)

这就是三个指标分工的意义:混合检索抬天花板,重排把天花板上的东西挪到前五名。 两件事不能互相替代,也不能用一个数字概括。

三个还没说的坑:切块、条数、拼接顺序

切块。 固定 512 字符、重叠 64 字符是一个够用的起点(今天实验里 54 篇文档切成 64 块,最长的一块 509 字符)。重叠是为了防止一个完整的意思正好被切在中间——一条退款规则的前半句在第 1 块、后半句在第 2 块,两块都不完整,两块都召不回来。更好的做法是按语义段落切(按标题、按列表项),代价是要写解析逻辑,而且不同来源的文档结构不一样。先用固定长度跑通、拿评估集量出来,再决定值不值得上语义切分。

条数。 进上下文的条数不是越多越好。多了三件事同时变坏:token 成本线性上升、无关内容稀释注意力、以及编号变多之后幻觉引用也跟着变多。5 条是一个经验起点,调它的依据应该是 recall@5 和 recall@10 的差值——差值很小就说明加条数没用。

拼接顺序。 把最相关的一条放在最后、紧挨着问题。原因是模型对长上下文中间部分的注意力明显弱于头尾(业内叫 lost-in-the-middle)。既然要选一头,就选离问题最近的那一头。这条改动一行代码,不花钱,属于白捡的收益。

源码导读

动手实验

🧪 D24 实验:记忆检索升级 + 评估

Code location: labs/agent-30days/day-24-rag-advanced

验收标准:

  1. MOCK=1 SELFTEST=1 pnpm start 五项自检全部 ✅、退出码 0(starter/ 原样跑是 1/5)。
  2. 第 2 项证明两路的盲区不重叠:E4032 那条 query 向量 top5 里没有 s07#0,而关键词 top1 就是它;中文 query 在 bigram 实现之前关键词那一路一条都召不回。
  3. 第 3 项证明 RRF 融合的是名次不是分数:[a,b,c][c,d,a] 融合后第一名是 a(0.03227);分数直接相加的版本给出的是 c(12.635)。
  4. 第 4 项打出三档 recall 的严格递进:recall@5 从 57%80%91%,且 recall@20 从 83% → 95% → 98%。
  5. 第 5 项证明引用后处理生效:回答里出现的不存在编号被剔除,幻觉引用计数从 0/1 变成 1/1(计进指标而不是原样发出)。

starter/ 挖了 4 个练习点,MOCK=1 下完全离线跑通,不需要 Postgres、Docker 或 API key——基础设施走内存实现,embedding 由文本内容确定性生成,所以召回率是真算出来的,改一条 query 数字就会变。想连真 pgvector 就 docker compose up -d 之后设置 DATABASE_URL(宿主端口 5524),跑的是同一份检索代码。

  1. 原样跑一次 MOCK=1 SELFTEST=1 pnpm start,记下基线:五项里只有第 1 项 ✅,recall@5 三档是 57% → 69% → 69%(后两档涨不动,因为关键词那一路和重排都还没接上)。
  2. 实现 bigrams(练习 1),让中文 query 在关键词那一路真的能召回,第 2 项变 ✅。
  3. 把融合改成 RRF(练习 2)——丢掉两路的原始分数、只用名次,第 3 项变 ✅,同时 recall@5 的第二档跳到 80%。
  4. 接上 rerankTopK(练习 3),第 4 项的三档递进补齐到 57% → 80% → 91%。
  5. 实现 stripHallucinatedCitations(练习 4),把不存在的编号剔除并计进指标,第 5 项变 ✅;顺手看一眼那条被剔除的 [9] 长什么样。

面试题

今天 4 道题在下方题库区,侧重混合检索为什么必要、重排解决什么、召回率怎么量。展开后先看"分析过程"再看要点——第 1 题里「两路盲区不重叠」是题眼,第 4 题的评估集设计是国内面试的极高频追问,别跳过。

检查清单与明日预告

  • 能实现关键词检索和向量检索结合的 hybrid search
  • 能给检索结果接入一个 rerank 步骤,提升排序质量
  • 能设计一套 recall 评估方法,衡量检索是否召回了应该召回的内容
  • 能说清为什么两路的分数不能加权求和,只能用 RRF 融合名次
  • 能说清 recall@5、recall@20、MRR 三个指标各自回答什么问题
  • 实验的 5 条验收标准全部通过
  • 4 道面试题不看要点也能答出至少 3 道

明天(D25)我们把这套后端第一次接到人眼前。到今天为止它会分诊、会拆任务、会自我评审、会主动关怀、找得准、还挡得住攻击——但你从头到尾没有从浏览器里看过它一眼。明天写一个 React 聊天前端:流式渲染、把工具调用过程可视化、以及打断和重试这两个看起来简单、实际上前后端必须配合才做得对的交互。顺序是有意的:先把后端做对,再谈怎么把它呈现出来——反过来做,你会把一堆后端缺陷用前端的加载动画盖住。

Interview questions

  • Why isn't pure vector search enough — what does keyword search add?为什么单纯的向量检索不够,还要加一路关键词检索?
    Common in ChinaCommon overseasBasic#rag#hybrid-search#retrieval

    How to reason about it · think before answering

    1. The discriminator is not whether you know the term 'hybrid search' — it is whether you can name a concrete query that vector search will always miss. No example means you have only read architecture diagrams.
    2. One causal chain: vector search compares semantic distance, so both its strength and its weakness come from that compression step. Synonyms match (shipping fee vs postage), but strings with no semantics collapse together — error codes, SKUs, order ids, person names.
    3. BM25 has the mirror-image profile: a term matters more when it is frequent in this document and rare across the corpus. So it nails low-frequency literals and fails completely on paraphrase.
    4. State the conclusion as 'their blind spots do not overlap, and that follows from how each one computes' — not the vague 'two channels are safer'. A measured example lands best: for 'what does E4032 mean', the correct doc is absent from the vector top-5 and is the keyword top-1.
    5. Expected follow-up 1: how do you merge the two rankings? Answer RRF, and explain why weighted sums fail (see q02).
    6. Expected follow-up 2: how do you do keyword search over Chinese? Postgres's default parser effectively does not tokenize Chinese; the cheapest workable fallback is character bigrams, keeping ASCII words and codes whole. Production needs a real Chinese tokenizer extension. Answering this usually proves you actually built it.

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

    1. 这题的区分度不在「你知不知道有 hybrid search」,而在**你能不能说出一个向量检索一定会漏的具体例子**。答不出例子的,一听就是只看过架构图。
    2. 推导链只有一句:向量检索比的是语义距离,所以它的强项和弱项都来自「压缩成语义」这一步——同义词能对上(运费 / 邮费),而没有语义的字符串会被压到一起(E4032、SF-3000、订单号、人名)。
    3. 关键词那一路(BM25)的性质正好相反:一个词在本文档里越频繁越相关、在全语料里越常见越不值钱,所以它对低频稀有词极准,对同义改写完全无能。
    4. 结论要说成「两者的盲区不重叠,而且是由计算原理决定的不重叠」——不是「多一路更保险」这种模糊说法。举一个实测例子最有说服力:查「E4032 是什么意思」,向量 top5 里没有那篇讲支付错误码的文档,关键词 top1 就是它。
    5. 可预期的追问一:那怎么合并两路结果?答 RRF,并说清为什么不能加权求和(见 q02)。
    6. 可预期的追问二:中文怎么做关键词检索?答 Postgres 默认分词器对中文等于不分词,最简可用的兜底是 bigram(相邻两字切开),但英文与编号必须整词保留;生产要上专门的中文分词扩展。这一条能答出来,基本就说明你真动手做过。

    Key points

    • Vector search compares semantic distance: strong on paraphrase, weak on SKUs, error codes and order ids that carry no semantics.
    • BM25 is strong on rare literal terms and weak on paraphrase — the blind spots follow from the algorithms and do not overlap.
    • So run both channels wide (top 20 each) and fuse with RRF so each covers the other's gap.
    • Give a measured example: for the E4032 query the correct chunk is missing from vector top-5 but is keyword top-1; a 'postage vs shipping fee' query is the reverse.
    • Chinese keyword search needs tokenization: character bigrams as the cheap fallback, ASCII words kept whole, a real tokenizer extension in production.

    答题要点

    • 向量检索比的是语义距离,强在同义改写,弱在型号、错误码、订单号这类没有语义的字符串。
    • BM25 强在低频稀有词的字面命中,弱在同义改写——两者的盲区由各自的计算原理决定,不重叠。
    • 所以第一轮开两路、各取 20 条,用 RRF 融合,把两边的盲区互相补上。
    • 举实测例子:E4032 那条 query 向量 top5 漏掉正确文档,关键词 top1 就是它;「邮费」那条反过来只有向量能召回。
    • 中文关键词那一路要处理分词,最简兜底是 bigram,字母数字整词保留,生产上专门的中文分词扩展。
  • How do you merge two retrieval rankings, and why not just take a weighted sum of the scores?两路检索结果怎么合并?为什么不能直接加权求和?
    Common in ChinaCommon overseasIntermediate#rag#rrf#ranking

    How to reason about it · think before answering

    1. The second half is the real question. Anyone can say 'RRF'; explaining why weighted sums fail is what separates people who have looked at the score distributions.
    2. Decompose it: are the two scores even the same unit? Cosine similarity is bounded in 0 to 1 and tightly clustered — candidates often differ by 0.02. BM25 is unbounded and a few rare-term hits reach 12. Adding them lets the larger-magnitude channel decide everything; the weight only tunes how much it dominates.
    3. Worse, it is unstable. Weights tuned on one corpus drift on the next, so you re-tune forever.
    4. Conclusion: fuse ranks, not scores. RRF maps each rank to 1/(k + rank) and sums, with k = 60. Ranks are unitless and need no calibration. k flattens the head of the list so that 'top-ranked in both channels' beats 'first in one channel' — consensus over single-source confidence.
    5. A hand-checkable example helps: rankings [a,b,c] and [c,d,a] give a = 1/61 + 1/63 ≈ 0.0323, while a raw score sum promotes c on the strength of its BM25 12.
    6. Expected follow-up: what about ties? You must break them explicitly, e.g. by id. Otherwise ordering depends on hash-map iteration order and differs across languages and runs, which makes your evaluation numbers irreproducible. Mentioning this signals you actually ran it more than once.

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

    1. 题眼在后半句。前半句答「RRF」谁都会,后半句「为什么不能加权求和」才是筛人的地方——它考的是你有没有真的看过两路分数的分布。
    2. 怎么拆:先问自己两个分数是不是同一个量纲。余弦相似度有界(0 到 1)且分布密集,同一批候选常常只差 0.02;BM25 无上界,命中几个稀有词就能到 12 分。**不同量纲的数相加,等于让量纲大的那一路单方面决定结果**,权重只是在调「它说了算的程度」。
    3. 更麻烦的是它不稳定:权重在这批语料上调好了,换一批语料分布就变了,得重调。这是一个永远还不完的技术债。
    4. 结论:改用名次。RRF 把每一路的名次折算成 `1/(k + rank)` 再相加,k 取 60。名次是无量纲的,不需要任何标定。k 的作用是压平头部差距,让「两路都进前列」压过「一路排第一」——共识优先于单点自信。
    5. 一个能当场手算的例子很加分:两路排名 [a,b,c] 与 [c,d,a],a 得 1/61 + 1/63 ≈ 0.0323;而分数直接相加的版本会把 BM25 里 12 分的 c 顶到第一。
    6. 可预期的追问:同分了怎么办?必须显式定序(比如按 id),否则结果取决于哈希表遍历顺序,同一份输入在不同语言、不同运行里给出不同排序——评估集量出来的数字也就不可复现了。这一条答出来会非常加分,因为它说明你真的跑过多次。

    Key points

    • Use RRF: map each channel's rank to 1/(k + rank) and sum, with k = 60.
    • Weighted sums fail because the scores are different units — bounded, tightly clustered cosine versus unbounded BM25, so BM25 decides the outcome.
    • Weights also do not transfer: tuned on one corpus, they drift on the next.
    • Ranks are unitless and need no calibration; k flattens the head so cross-channel consensus outweighs single-channel confidence.
    • Break ties explicitly (by id) or ordering depends on hash iteration order and your evaluation numbers stop being reproducible.

    答题要点

    • 用 RRF:每一路的名次折算成 1/(k + rank) 再相加,k 取 60。
    • 不能加权求和是因为两个分数量纲不同——余弦有界密集、BM25 无上界,相加等于让 BM25 单方面决定结果。
    • 而且权重不可迁移:这批语料调好,换一批就得重调,是还不完的债。
    • 名次是无量纲的,不需要标定;k 压平头部差距,让两路共识压过单路自信。
    • 同分必须显式定序(按 id),否则结果依赖哈希表遍历顺序,评估数字不可复现。
  • How is reranking usually implemented, what problem does it solve, and what does it cost?重排(rerank)一般怎么实现?它解决了初步检索的什么问题,代价是什么?
    Common in ChinaCommon overseasIntermediate#rag#rerank#latency

    How to reason about it · think before answering

    1. The lazy answer is 'sort again, more accurately'. What the interviewer wants is why the first pass cannot rank well, and why reranking cannot run over the whole corpus.
    2. Decompose: the first pass ranks by retrieval signals — cosine distance or term statistics — which are designed to scan millions of items fast, and coarseness is the price. Reranking changes the algorithm: query and candidate go into one model together (a cross-encoder), which is far more accurate but costs one forward pass per candidate. Hence it must sit behind a wide recall stage.
    3. Distinguish two implementations. For teaching or prototypes, batch-score with an LLM (0-10 for 40 candidates in one call). Production uses a trained cross-encoder reranker. Name the cost: an extra 100-300 ms hop plus an inference box — it is not a per-token API, it consumes capacity.
    4. Framing it as a funnel is clearest: recall sets the ceiling, reranking decides whether what is under the ceiling reaches the top five. Measured: adding the keyword channel lifts recall@20 from 83% to 95%; adding reranking moves recall@20 only to 98%, but recall@5 jumps from 80% to 91% and MRR from 0.732 to 0.908.
    5. Expected follow-up 1: does reranking improve recall? No. It introduces no new candidates, so recall@20 is the wrong metric to judge it by.
    6. Expected follow-up 2: why not ship LLM scoring to production? Unpredictable latency, per-token cost, scores that drift with prompt wording, and no clean path to offline distillation.

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

    1. 这题最容易答成「再排一次序,更准」。面试官想听的是**为什么第一轮不能直接排准**,以及**为什么重排不能对全库做**。
    2. 怎么拆:第一轮的排序依据是「检索信号」——余弦距离或词频统计,它们是为了能在百万条里快速筛选而设计的,代价就是粗。重排换了一种算法:把 query 和候选**拼在一起**送进同一个模型算相关度(cross-encoder),精度高得多,但复杂度是每条候选一次前向,没法对全库做。所以它必须跟在一个宽召回后面。
    3. 结论要区分两种实现:教学 / 原型可以用 LLM 批量打分(一次调用给 40 条打 0 到 10 分),生产用专门训练的 cross-encoder 重排模型。**代价说清楚:多一次 100 到 300 毫秒的调用,外加一台推理机器**——它不是按 token 计费的 API,是要占资源的。
    4. 把它放进漏斗里说最清楚:召回决定天花板,重排决定天花板上的东西能不能排到前五。实测的样子是——加了关键词那一路,recall@20 从 83% 涨到 95%(天花板抬高);再加重排,recall@20 只到 98%,但 recall@5 从 80% 跳到 91%、MRR 从 0.732 到 0.908。
    5. 可预期的追问一:重排能不能提高召回?不能。它不引入新候选,只重排已有的那批——所以看 recall@20 判断重排效果是错的指标。
    6. 可预期的追问二:为什么不用 LLM 打分上生产?延迟不可控、成本按 token 走、分数会随提示词措辞漂移,而且没法做批量离线蒸馏。

    Key points

    • The first pass ranks by retrieval signals so it can scan a large index fast; coarseness is the trade.
    • Reranking feeds query and candidate through one model together (cross-encoder): much sharper, but one forward pass per candidate, so only tens of items.
    • Batch LLM scoring works for teaching; production uses a dedicated reranker, costing an extra 100-300 ms hop plus an inference box.
    • Reranking does not raise recall — it raises recall@5 and MRR (measured 80% to 91%, 0.732 to 0.908) while recall@20 barely moves from 95% to 98%.
    • So judge a reranker by small-k metrics, never by recall@20.

    答题要点

    • 第一轮按检索信号粗排(余弦、词频),为的是能在大库里快速筛,代价是粗。
    • 重排把 query 和候选拼在一起过同一个模型(cross-encoder),精度高但每条一次前向,只能对几十条做。
    • 教学版可用 LLM 批量打 0 到 10 分;生产用专用重排模型,代价是多一次 100 到 300 毫秒的调用加一台推理机器。
    • 重排不提高召回,它提高的是 recall@5 与 MRR——实测 80% → 91%、0.732 → 0.908,而 recall@20 只从 95% 到 98%。
    • 所以判断重排效果要看前 k 小的指标,不要看 recall@20。
  • How do you evaluate retrieval quality in a RAG system, and how should the evaluation set be built?怎么评估一个 RAG 系统的检索效果?评估集应该怎么构造?
    Common in ChinaCommon overseasDeep dive#rag#evaluation#recall

    How to reason about it · think before answering

    1. This is a very common question in the Chinese market and the fastest way to expose someone who has assembled RAG but never tuned it. The test: does your answer contain concrete metric names and an annotation granularity?
    2. First separate what is being evaluated — the step people most often conflate. Retrieval evaluation asks 'was it found'; generation evaluation asks 'was the answer right'. Keep two separate sets. Merge them and, when the score drops, you cannot tell whether retrieval missed or the model fumbled — and those have completely different fixes.
    3. Shape of the set: about 20 queries, each annotated with 1-3 chunk ids that must be retrieved. Annotate at chunk level, not document level — chunks are the retrieval unit, and document-level labels inflate the numbers. Cover the real query mix, especially the types you know break: codes, paraphrase, cross-document.
    4. Three metrics, three questions. recall@5 is what actually reaches the model, so it is the number you care about. recall@20 is the ceiling — if it does not move, the problem is on the recall side and no reranker will save you. MRR is sensitive to ordering and breaks ties when recall is equal.
    5. Production view: freeze the set once agreed, because changing samples destroys comparability — the same reason a factory keeps fixed reference samples. Pair it with online counterparts (empty-citation rate, hallucinated-citation rate, escalation rate), since passing offline does not mean passing in production.
    6. Expected follow-up: is 20 enough given the labelling cost? Not for statistical significance, but enough for regression — its job is to stop retrieval silently getting worse. Scale up before you settle an A/B, and grow it from failure cases rather than random additions.

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

    1. 这题是国内面试的极高频题,也是最容易暴露「只搭过没调过」的一题。判据很简单:你的回答里有没有出现**具体的指标名和标注粒度**,没有就是没做过。
    2. 先把评估对象分清楚——这是最容易混的一步:**检索评估问「找得到找不到」,生成评估问「答得对不对」**。两套评估集要分开维护。混成一套的后果是分数掉了你分不清是检索漏了还是模型答砸了,而这两件事的修法完全不同。
    3. 评估集的形状:20 条左右的 query,每条**人工标注 1 到 3 个必须召回的 chunkId**。注意标注粒度是**块**不是文档——检索的单位就是块,标到文档级会让指标虚高。query 要覆盖真实分布,尤其要包含那些你知道会翻车的类型(编号、同义改写、跨文档)。
    4. 三个指标各回答一个问题:recall@5 是「进上下文的那几条覆盖了多少」,也就是你真正关心的数;recall@20 是天花板,它上不去说明问题在召回侧、重排再强也没用;MRR 对排序质量敏感,recall 打平时用它分高下。
    5. 生产视角:评估集一旦定下来就要冻结,换了样本分数就没有可比性——这和产线质检必须用固定的标准样品是同一个道理。同时线上要有对照指标(引用为空率、幻觉引用率、转人工率),因为离线过了不等于线上没事。
    6. 可预期的追问:标注成本这么高,20 条够吗?答:20 条不够做统计显著性,但足够做**回归**——它的作用是「改了检索之后别悄悄变差」。要做 A/B 定论再上规模,而且优先扩充失败案例,不是随机加样本。

    Key points

    • Retrieval and generation evaluation are two separate sets: 'was it found' versus 'was the answer right'.
    • Around 20 queries, each labelled with 1-3 chunk ids that must be retrieved — chunk level, not document level.
    • recall@5 is what the model actually sees, recall@20 is the ceiling, MRR measures ordering quality.
    • Freeze the set once agreed or scores stop being comparable; pair it with online empty-citation and hallucinated-citation rates.
    • Twenty cases is a regression guard, not a significance test; grow it from failure cases, not random samples.

    答题要点

    • 检索评估和生成评估是两套:前者问「找得到找不到」,后者问「答得对不对」,分开维护。
    • 评估集是 20 条左右的 query,每条人工标 1 到 3 个必须召回的 chunkId——标到块级,不是文档级。
    • recall@5 是真正关心的数(模型只看得到这几条),recall@20 是天花板,MRR 衡量排序质量。
    • 评估集一旦定下来就冻结,否则分数没有可比性;线上再配引用为空率、幻觉引用率做对照。
    • 20 条不够做显著性但够做回归;扩充时优先补失败案例,不是随机加样本。

Comments

Sign in to join the discussion

No comments yet — be the first.