面试官问:“RAG 为什么要同时使用 BM25 和向量检索?”
一个常见回答是:“BM25 找关键词,向量检索找语义,两路一起用,召回效果更好。”这个回答讲到了两种检索方式,但还没有回答三个问题:两路分别会漏掉什么、找回的内容怎样排到一起,以及如何证明多跑一路确实有价值。
面试官通常会顺着这三个问题继续问:
- 只用 BM25 或只用向量检索,各自会在哪类问题上漏掉正确原文?
- 两路分数不是同一把尺子,结果怎样合并,为什么还要重新排序?
- 你怎样用一批带正确原文的问题,证明混合检索确实比单路更合适?
先看两个教学用的问题。假设知识库里有一篇“保险销售技巧”,用户问的却是“如何推销保险产品”;另一份文档的标题是“报销制度”,用户也只输入这四个字。前一个问题需要系统理解“推销”和“销售”意思接近,后一个问题则希望原词命中时,标题能直接排到前面。
这道题考的不是两个名词的定义,而是能否把“为什么要混合、怎样混合、怎样验证”连成一条完整答案。下面就按面试官的追问顺序拆开。
如果第一次接触 RAG,可以把它理解成“先从资料中找到相关原文,再把原文交给大模型回答”。Chunk 就是一段被切分出来的原文,后文直接称为“原文段落”。
2 分钟完整作答
如果面试官没有打断,可以这样回答:
RAG 同时使用 BM25 和向量检索,是因为两种方法解决的漏找问题不同。BM25 适合标题、编号和专业术语这类原词明确的问题,例如“报销制度”;向量检索适合用户换了一种说法的情况,例如用“如何推销保险产品”去找“保险销售技巧”。
两路会先各找一批原文段落,再合并并重新排序。融合负责把两路结果放进同一份候选清单,重排负责再次比较问题与候选,把更能回答问题的内容放到前面。搜索前还要限定知识库、文档和可用状态,避免在错误资料中查找。
验证时,我会固定同一批问题和正确原文,分别测试只用 BM25、只用向量、两者合并、合并后再重排,比较正确原文是否出现、排得是否靠前,以及无关和重复内容是否减少。这样可以看出变化来自哪一路,也能定位问题出在搜索范围、单路召回、融合还是重排。
面试官会从哪四层判断你的回答
回答这道题时,可以沿着四层展开。
第一层是互补性。BM25 依赖词面重合,向量检索依赖语义接近;它们对不同问法的反应不同。第二层是链路。两路先负责“把可能有用的内容找回来”,融合与重排再负责“把更合适的内容放到前面”。
第三层是范围。系统只能在指定知识库、指定文档和可用内容中搜索,范围不对时,算法再复杂也找不到正确证据。第四层是验证。效果要落到一批带正确证据的问题上,而不是只展示一两个恰好答对的例子。

图 1:在这个教学例子中,“报销制度”更依赖原词命中,“如何推销保险产品”更依赖“推销—销售”的语义联系。两路检索解决的是不同类型的漏召回。
追问一:只用一种检索,会分别漏掉什么
回答主线是:只用 BM25 容易漏掉同义改写,只用向量检索容易在短标题、编号和专业术语上发生语义漂移。下面分开看。
BM25 常被简称为“关键词检索”,但它并不是简单地数一数出现了几个相同字。搜索引擎会结合词项在当前文档里的出现情况、词项在整个语料中的稀有程度,以及文档长度来计算相关度。因此,清楚而少见的标题词、保险术语、产品名和编号,往往能形成很有区分度的信号。
面试官追问:为什么“报销制度”更适合 BM25
用户只输入“报销制度”,问题很短,信息也不多。如果知识库里正好有一份文档或一个章节使用了这个标题,原词命中就是很有价值的线索。向量模型知道它大致与费用、财务、流程有关,却可能同时找回报销申请、差旅标准、费用审批等相近内容。BM25 对“报销制度”四个字的直接匹配,能帮助标题或正文中含有原词的片段靠前。
不过,词面匹配也有明显短板。文档写“保险销售技巧”,用户问“如何推销保险产品”,两边共享了“保险”之类的词,却没有共享最关键的动作“销售”。如果没有同义词规则、查询改写或领域词典,BM25 可能给正确片段的分数不够高,甚至没有把它带进候选集。
面试官追问:向量检索补的是什么
向量检索会先用 Embedding 模型把一句话变成一组数字。可以把这组数字理解成文本在“语义地图”上的坐标:意思越接近,两组坐标通常也越接近。它不要求问题与原文逐字一致,所以有机会把“推销保险产品”与“保险销售技巧”联系起来。
向量检索也会失手。以教学案例中的“现金价值”为例:通用向量模型对领域词义掌握不够时,可能把它和普通财务表达混在一起。此时先看正确片段的向量排名,再判断是补充领域样本、调整模型,还是让词面检索承担更多精确术语的召回。
短问题也容易出现语义漂移。“报销制度”没有人物、条件和动作,与报销、费用、审批相关的段落都可能靠得很近。BM25 的原词信号能给它一个锚点。反过来,“如何推销保险产品”包含完整意图,但表达与原文不同,向量候选更有补充价值。
所以,混合检索的意义并不是宣布哪一路更高级。它把两路各自擅长的候选带到同一个后续排序环节,让系统有机会同时保留精确词项和语义改写。
追问二:两种检索怎样在一次搜索中配合
这里的 Query 是用户的问题,Chunk 是从资料中切出来的一段原文,候选集则是检索系统认为“可能有用”、准备继续排序的一批原文段落。
一次混合检索可以分成四步:先接收问题;再分别按原词和按意思找资料;同时限定只在当前知识库和允许使用的文档中搜索;最后把两路结果合并,得到一批候选原文。这个阶段只负责找资料,还没有让大模型开始回答。

图 2:同一个问题会进入关键词路线和语义路线,资料范围会同时限制两路搜索。两路结果合并并重新排序后,才留下可供回答的原文。
追问到实现时,可以先用白话说明对应关系:一层负责生成关键词条件,一层负责生成向量条件,一层负责限定知识库和文档范围,再由搜索层执行并合并候选。具体代码名称统一放在文末。
这里的“两路”指两种找原文的方法,并不是让两个接口各生成一段答案。它们返回的都是候选原文,后面还要合并、重新排序和截断,大模型最后看到的是筛选后的证据。
搜索结果中还带着候选 ID、来源文档和两路分数。后面的重排需要问题与候选内容,页面展示需要文档名与正文,排查问题则需要 ID 和分数。这些中间信息能帮助我们判断一条候选在哪里进入、又在哪里被淘汰。
面试官追问:为什么先多找一些候选,再做重排
原因很直观:重排只能在已经找回的内容里换顺序。若一开始只取最终要展示的几条,正确证据排在稍后位置时,重排根本看不到它。这套实现会先取回更多候选,再截取最终需要展示的数量,后面的重排才有选择空间。具体参数只代表当前实现如何处理候选,不能直接当成其他数据集的固定答案。
追问三:检索前为什么要先限定资料范围
搜索前还要先限定资料范围。例如,用户正在查询 A 知识库,就不应从 B 知识库带回内容;已经停用的文档,也不应继续参与搜索。这一步不是判断哪段资料更相关,而是先决定哪些资料有资格进入候选集。
实现中的过滤步骤会整理所属知识库、来源文档和内容是否可用等条件。它们不是给相关内容加分,而是决定本次搜索能看哪些资料。若知识库选错、文档范围过窄,或者内容状态不符合条件,两种检索都会在错误范围里工作。对应代码放在文末。
另一个教学情景是“最新的车险理赔流程”。新旧文档内容高度相似,两路检索都可能把旧版流程找回来。相关性排序能回答哪一段更像问题,却不能仅凭“像”判断哪个版本仍然有效。若两版都保留,还要确认版本信息是否写进候选内容或可用的排序字段,再观察重排结果。
追问四:融合与重排,分别在做什么
融合,是把两种方法找到的内容放进同一张候选清单;重排,是重新阅读用户问题和每段候选内容,把更能回答问题的段落往前放。前者解决“两路结果怎样合在一起”,后者解决“候选已经找回,谁更应该排在前面”。
融合时首先会遇到分数问题。关键词检索和向量检索使用的不是同一把尺子,就像百分制成绩和五分制评价,数字大小不能直接比较。这套实现采用加权求和来分配两路权重,具体比例仍要放到同一批测试问题中验证。
在概念上,可以把加权结果写成“文本信号乘文本权重,加上向量信号乘向量权重”。这只是表达组合关系,不能据此假设两个原始分数已经天然可比。配置融合前,可以先观察两路分数的范围和分布,再选择归一化与权重。若一边的数值跨度远大于另一边,表面上各占一半,排序时仍可能长期由其中一路主导。
验证权重时,可以同时查看关键词分数、语义分数和综合分数。若关键词分数有明显差异,综合分数却几乎只跟语义分数同步,就要检查融合配置是否让文本信号失去了作用;反过来也一样。权重不是页面上的装饰参数,它应当能在候选排序变化中被观察到。

图 3:关键词分数与语义相似度不是同一把尺子。系统可以为两路分配权重,但权重是否合适,需要看同一批问题上的排序结果。
无论走哪条路径,重排都只能处理第一次搜索已经找回的内容。若“保险销售技巧”没有进入候选集,重排看不到它,也就没有机会把它提到前面。这也是面试中常见的追问:召回解决“有没有找到”,重排解决“找到后排得好不好”。
场景追问:下面四种失败分别先查哪里
“效果不好”太宽了。拿前面的四个问题逐一看,排查入口并不相同。
1. “报销制度”:标题原词没有排在前面
我会按“关键词召回 → 融合 → 重排”三步排查。先确认问题处理后是否还保留“报销制度”,搜索范围是否包含标题和正文,最小匹配比例是否对短问题设得过严。再看正确原文有没有进入候选,以及关键词分数能否与其他内容拉开差距。
若只用 BM25 时正确标题排在前面,合并后却掉下去,再比较两路权重、语义候选和重排结果。这样可以判断问题出在关键词召回、融合还是重排,而不是直接更换向量模型。
2. “如何推销保险产品”:原文写的是“保险销售技巧”
我会先单独看向量候选,再看合并和重排有没有把正确原文淘汰。这是典型的表达差异:先确认“保险销售技巧”是否进入前几条。如果语义路线已经找回,关键词路线没找回并不意外,接下来观察合并后是否保留该候选、重新排序后它是否靠前。
如果语义路线也没有找到它,再核对原文段落是否包含完整的销售技巧内容、问题和文档是否使用对应的向量模型,以及候选数量和最低相似要求是否把它截掉。问题到了这一步,才与模型的语义能力直接相关。
3. “现金价值”:专业词被普通财务语义带偏
我会同时比较两路候选和分数,先判断是术语切分出了问题,还是语义检索发生了漂移。“现金价值”不能只按“现金”和“价值”的日常含义理解。先看关键词检索能否精确找回包含该术语的片段,再看语义结果中混入了哪些相近但无关的内容。如果关键词路线稳定、语义路线漂移,可以在对照实验里调整权重或补充领域样本;若分词已经把术语拆坏,则先修复关键词路线。
这里的关键是保留两路分数和候选。只看大模型最后生成的一段话,很难知道它引用的是术语定义,还是从普通财务内容里拼出了一个听起来合理的解释。
4. “最新的车险理赔流程”:旧版本排得更靠前
我会先查资料范围和文档状态,而不是一上来就调向量权重。先确认新旧版本是否同时可检索。若旧文档本来就不该参与,处理入口在搜索范围和状态过滤;若两版都保留,再确认版本信息是否写进候选内容或可用的排序字段,并观察重排结果。
这类问题也说明,相关度高不等于资料可用。新旧流程往往共享大量文字,BM25 和向量都可能认为旧版很相似。评测时要把正确版本的证据 Chunk 标出来,不能只标“来自车险流程文档”。
四个案例对应四种检查顺序:原词题先看关键词路线,同义题先看语义候选,专业术语对比两路偏差,版本题先核对搜索范围与文档状态。沿着“搜索范围 → 两路各自找到什么 → 合并后怎样排序 → 最后留下哪些原文”逐层看,才能知道问题具体出在哪一步。
追问五:怎样证明混合检索有效
我会固定同一批问题、正确原文和查看条数,只改变检索配置,依次比较 BM25、向量检索、混合检索、混合检索加重排。先看正确原文能否进入前几条,再看第一条正确原文排在哪里、结果是否重复,最后按原词题、改写题、术语题和版本题分组复盘。
最小可用的检索测试样本,至少包含三部分:用户问题、参考答案、能直接支持答案的原文。这里的“证据”指的就是这段原文;Top K 则表示只查看排在最前面的 K 条,例如前 5 条或前 10 条。只有答案、没有证据位置,无法判断检索器是否真的把资料找了回来。
测试问题还要覆盖不同问法。可以把前面的四类问题放进集合:原词标题、同义改写、领域术语和版本条件,再补充事实题、原因题、流程题与需要多条证据的问题,并清理重复或只做表面改写的样本。这样测到的是多种检索难点,而不是某一种模板问句。
证据标注也要落到足够细的粒度。如果答案来自文档中的一个具体段落,只标文档名会把结果看得过于乐观:系统找到了同一份长文档,却未必找到了能支持答案的 Chunk。对需要多段信息才能回答的问题,可以分别记录每条证据包含的关键信息点。这样计算 Recall@K 时,才能发现系统只找回了其中一半。
测试集还需要人工校验。自动生成问题可以扩大初稿,但参考答案是否能由证据推出、证据位置是否准确、问题之间是否只是换了几个词,都需要检查。若测试集本身把错误段落标成正确证据,检索器越贴合这个标注,离用户需要的结果反而越远。
在同一份数据、同一份证据标注和同一个 K 值下,依次运行:
- 只使用 BM25;
- 只使用向量检索;
- BM25 与向量加权融合;
- 混合召回后再做 Rerank。

图 4:四组配置共享同一批 Query 和证据标注,只改变检索与排序方式。指标先留空,跑完自己的数据再填写,避免用示例数字代替项目结果。
对比时可以先看四项指标。
前 K 条有没有找全正确原文(Recall@K)。它适合判断混合检索有没有补回单路漏掉的片段。一个问题需要两条证据时,只找回一条,覆盖仍然不完整。
前 K 条里有多少内容直接相关(Precision@K)。扩大候选通常容易提高覆盖,也可能带来更多噪声。若找全程度上升、准确程度却下降,后面的上下文会塞进一批主题相近却无用的内容。
第一条正确原文排在第几位(MRR)。同样都在前 10 条找回,排第 1 与排第 10 对后续截断和生成的影响不同。需要多条证据时,还可以结合 MAP 观察多个相关结果的整体排序。
返回内容是否反复讲同一件事(冗余率)。连续切分的相邻段落可能高度相似,表面上前几条都相关,实际只提供了一份信息。可以记录证据包含的关键信息点,或检查近重复段落。
评测表除了总分,还可以保留每个问题在四组配置下的前几条原文、首条正确证据排名,以及关键词分数、语义分数和综合分。这样才能回答三个具体问题:BM25 漏掉了哪些问法,向量检索补回了哪些证据,重排又把哪些正确候选提到了前面。
还可以为每次实验固定一份配置记录:知识库版本、Embedding 模型、topk、minimum_should_match、融合权重、相似度阈值和是否启用 Rerank。否则今天改了向量模型,明天又换了候选数量,最后只剩一个更高或更低的总分,很难知道变化来自哪里。一次只比较一个主要改动,结果会更容易解释,也方便把有效配置复现出来。
如果混合检索的总指标变好,也要回看分组结果。它可能改善了“推销—销售”这样的同义问法,却让“报销制度”这类短标题题下降。下一步到底调文本字段、向量权重还是 Rerank,要由这类差异决定。没有证据标注,只凭几次演示问答,很难区分检索改进与偶然命中。
三个高频追问,怎样继续接
面试官如果继续问“权重怎么定”,可以接着说:先固定数据、向量模型、候选数量与重排配置,只调整融合参数,在验证集上按问题类型比较;选定后再用独立测试集检查,避免为了几道展示题反复调参。
如果继续问“重排能不能解决漏召回”,回答是不能。重排的输入是初次搜索拿到的候选,它能改顺序,却看不到候选集之外的正确原文。先判断证据有没有进入候选,再判断它排得是否靠前,这两类问题要分开看。
如果问“混合检索是不是一定更好”,可以回到测试结果:它提供了词面与语义互补的机会,也会增加候选和参数。是否保留、权重如何设置,要看自己的证据标注与分组指标。这样既交代了参数依据,也留下了复现路径。
加分追问:这些流程在实现里对应哪一步
如果面试官继续问到代码,可以把实现拆成五个通用接口:构造关键词查询、生成查询向量、限定资料范围、融合两路候选、重排并截取最终证据。下面只用框架无关的伪代码解释职责,不对应任何真实系统的类名、字段名与配置。
1. 关键词检索怎样控制字段和匹配比例
fields 表示搜索范围,best_fields 表示优先采用匹配效果最好的字段,minimum_should_match 表示最少要匹配多少词。最后一行给文本信号分配权重,为两路融合留出位置。
keyword_query = build_keyword_query(
query=user_query,
fields=["title", "body"],
minimum_match=config.minimum_match,
)
keyword_candidates = keyword_index.search(
keyword_query,
filters=scope_filters,
limit=config.keyword_candidate_limit,
)
2. 问题怎样变成向量检索条件
get_vector 先生成问题向量,再指定余弦相似度、候选数量和最低门槛。向量字段名由维度拼出,因此查询侧模型与入库时的向量配置需要对应。
query_vector = embedding_model.encode(user_query)
vector_candidates = vector_index.search(
vector=query_vector,
metric="cosine",
filters=scope_filters,
limit=config.vector_candidate_limit,
minimum_score=config.vector_minimum_score,
)
3. 为什么第一次要多取一些候选
初次召回数量通常应大于最终展示数量,给融合和重排留下选择空间。扩展倍数与最低候选量都只是待调参数,应通过同一份验证集确定,不能从示例代码直接照搬。
candidate_limit = max(
final_limit * config.candidate_expansion_factor,
config.minimum_candidate_count,
)
search_options = {
"candidate_limit": candidate_limit,
"final_limit": final_limit,
"minimum_score": config.minimum_score,
}
4. 资料范围怎样进入检索条件
范围过滤先决定哪些资料有资格进入候选集,不参与相关度加分。过滤字段应来自业务权限和文档状态,而不是偷偷混进排序权重。
scope_filters = {
"tenant_id": request.tenant_id,
"collection_ids": request.allowed_collection_ids,
"document_ids": request.selected_document_ids,
"status": "active",
}
scope_filters = remove_empty_values(scope_filters)
5. 重排怎样接在召回之后
下面是简化后的控制流程,只表达分支关系:有重排模型时用模型比较,没有时使用已有信号;之后再排序、按阈值筛选并截取最终内容。
if search_result.total > 0:
if rerank_model is not None:
scores = rerank_by_model(question, candidates)
else:
scores = rerank(question, candidates)
ranked = sort_by_score(scores)
final_chunks = filter_and_slice(ranked, threshold, page_size)
6. 排查时需要保留哪些返回字段
除了原文,还要保留来源、所属知识库和三类分数。这样才能反向检查候选是被哪一路找回、又在哪一步被调整了顺序。
{
"chunk_id": candidate.chunk_id,
"source_id": candidate.source_id,
"collection_id": candidate.collection_id,
"content": candidate.content,
"keyword_score": candidate.keyword_score,
"vector_score": candidate.vector_score,
"rerank_score": candidate.rerank_score,
"source_position": candidate.source_position,
}
回到这道题
BM25 保留原词、术语和标题信号;向量检索处理用户问法与文档措辞之间的差异。把搜索范围、两路查找、合并排序和证据验证串起来,这道混合检索面试题就从概念回答落到了可以检查的工程过程。