最近在准备大模型方向面试的同学大概率会遇到一个高频问题“讲讲你对 RAG 的理解检索策略你是怎么设计的”很多人的回答会卡在“向量相似度检索”这一步然后被面试官追问“如果向量检索召回不准怎么办”“query 本身有歧义怎么办”“多路召回结果怎么融合”“纯向量检索能处理精确匹配吗”——一旦接不上话分就丢了。这篇文章把 RAG 里最容易拿来考察的“检索策略”拆成三层来讲查询理解层、多路召回层、精排与上下文构造层。只要能把这层逻辑讲清楚再配合一个可落地的代码示例面试时基本就能稳稳拿住这道题。文章适合正在准备大模型/算法岗位面试的读者也适合刚接触 RAG 的后端同学做知识梳理。全文从概念到代码、从原理到排错尽量讲透。1. 面试官到底在问什么RAG 策略1.1 先给 RAG 一个明确的定义RAG 全称是 Retrieval-Augmented Generation检索增强生成。它的核心思路是在让大模型回答问题之前先从外部知识库中检索出和问题相关的文档片段把这些片段拼进 Prompt 里再让大模型基于这些片段生成答案。换句话说RAG 解决的是大模型“不知道”的问题大模型的训练数据有截止日期它不知道最新信息。大模型的参数记忆有限无法容纳企业私有文档。大模型原生没有“引用来源”概念经常一本正经地胡说八道。RAG 的流程可以概括为用户提问 - 查询理解/改写 - 多路召回 - 精排 - 拼接上下文 - LLM 生成 - 返回答案很多教程会把 RAG 简化成“向量检索 Prompt”但实际工程里真正决定系统效果上限的恰恰是检索这条链路。面试官问“RAG 策略”本质上是在考察你有没有系统设计思维而不只是会不会调库。1.2 面试官考察的三个层次我总结过这类问题的考察点大致分三层考察层次面试官想听到的内容基础层知道 RAG 是什么能画出整体流程图知道向量检索的基本原理进阶层能讲清楚检索中存在的问题比如 query 歧义、召回不精准、结果重复加分项有实际落地经验能说出查询改写、多路召回、重排、切块策略等细节并且踩过坑这篇文章要重点展开的就是“进阶层”和“加分项”两个部分。你能说出“三层检索”本质上就是在告诉面试官我不只是会跑通 Demo我还认真想过怎么让 RAG 在真实数据上变得可用。1.3 为什么检索策略是 RAG 的核心有一句经验之谈RAG 的效果上限取决于检索质量。这句话背后的逻辑很简单——大模型只能基于你给它的上下文生成答案。如果检索出来的 TopK 文档本身就不相关那后面 Prompt 写得多花哨、模型参数多大都救不回来。检索错误是“输入侧错误”生成模型没有能力自行纠正。所以RAG 项目优化的第一步不是换大模型而是优化检索链路。2. RAG 系统整体架构先看清检索在哪个位置2.1 RAG 的标准流程一个完整的 RAG 系统通常分成两个阶段离线索引阶段Indexing收集文档比如 PDF、Word、Markdown、网页、数据库记录。文档清洗去掉页眉页脚、水印、乱码。文本切块Chunking把长文档切成固定大小的片段。向量化Embedding把文本片段转成向量。写入向量数据库同时建立关键词倒排索引可选。在线推理阶段Retrieval Generation用户输入 query。对 query 进行改写、扩展、意图识别。执行检索从向量数据库、关键词索引甚至知识图谱中获取候选文档。对候选文档进行重排筛选出最相关的 TopK。和原始 query 一起拼成 Prompt。调用大模型生成答案。从流程里可以看出检索策略不是单点操作而是“从 query 进入系统到上下文构造完成”之间的一整段链路。把它拆成三层来设计是工程上最清晰的做法。2.2 三层检索策略的总体框架我这里说的“三层”不是指三个并列的检索工具而是三个功能层次层级核心任务常见手段第一层查询理解与改写指代消解、query 扩展、HyDE、多查询生成第二层多路召回向量检索、BM25 关键词检索、知识图谱检索、SQL 检索第三层精排与上下文构造Rerank 模型、MMR 去重、上下文压缩、引用溯源下面逐一展开。3. 第一层查询理解与改写3.1 为什么一上来就要改 query很多人做 RAG 会忽略 query 预处理直接拿用户输入去向量数据库里检索。这在 Demo 里够用但真实场景下会遇到几个明显问题用户提问有口语化表达“那个啥上次说的那个报错怎么解决”这里的“那个啥”和“上次”都是无效信息。用户提问有指代“它的源码在哪”——这里的“它”通常指代上一轮对话里提到的东西。用户提问包含反问、情感词“这破接口到底怎么调啊真的烦死了。”——这些词会污染向量化结果。用户提问太简短“注册流程”直接检索往往召回不到有价值的文档。这些情况下直接把原始 query 送去检索向量化之后的关键语义可能被噪声淹没。3.2 查询改写的常见做法核心思路是在进入检索之前先用一个轻量模型或规则把 query 处理成更适合检索的形式。做法一指代消解与补全如果是多轮对话场景可以把上文信息拼进当前 query用户当前问题它的鉴权方式是什么 历史上文项目里用到了 Spring Security 和 JWT。 改写后 querySpring Security 和 JWT 的鉴权方式是什么做法二HyDEHypothetical Document EmbeddingsHyDE 的思路比较巧妙先让大模型基于当前 query 生成一段“假设的理想答案”然后用这段假设答案去做向量检索而不是直接用 query 检索。为什么要这么做因为 query 往往是简短的、不完整的而文档片段往往是陈述性的、完整的。query 和文档之间存在“表示差异”直接算相似度效果可能不好。但如果先生成一段“假设答案”这段假设答案在句式和长度上都更接近文档检索效果会更好。原始 query如何配置 Nginx 反向代理 HyDE 生成Nginx 反向代理的配置方式是在 server 块中使用 location / { proxy_pass http://backend; }并设置 proxy_set_header 参数…… 然后用这段文字去向量库检索。做法三多查询扩展Multi-Query把一个 query 改写成多个不同角度、不同措辞的子 query分别检索然后合并结果。这样做可以降低单一 query 召回不全的风险。原始 queryMySQL 索引失效怎么办 子查询1MySQL 索引失效的常见原因 子查询2SQL 查询不走索引怎么优化 子查询3explain type 为 ALL 时如何解决3.3 查询改写层的代码思路实际项目中查询改写通常通过 LLM 调用完成。下面给一个简化示例生产环境可以替换成批量处理或异步调用。# 文件query_processor.py from openai import OpenAI client OpenAI() # 根据你的实际部署方式调整 base_url 和 api_key def rewrite_query(history, current_query): prompt f 你是一个查询改写助手。用户的任务是 1. 结合历史对话补全当前问题中的指代信息。 2. 去掉口语化、情绪化表达。 3. 生成 1 个适合知识库检索的正式 query。 历史对话 {history} 当前问题 {current_query} 请只输出改写后的 query不要解释。 resp client.chat.completions.create( modelqwen2-7b-instruct, messages[{role: user, content: prompt}], temperature0.2, ) return resp.choices[0].message.content.strip()这个示例只是一个思路演示不同版本的 SDK 可能 API 略有差异。关键点是查询改写层解决的是“用户问题不便于检索”的问题可以用 LLM 做也可以先用规则过滤高频停用词。4. 第二层多路召回4.1 不要把宝全押在向量检索上很多 RAG 教程默认检索就是“向量相似度 topk”实际工程里这是不够的。向量检索适合语义相似比如“怎么修电脑”能召回“电脑无法开机如何处理”。但向量检索在以下场景表现不佳精确匹配比如订单号、姓名、身份证号、报错码“ORA-00942”这些 token 级别的精确匹配向量检索未必能排到最前面。专有名词比如“XK-2024-001”这种编号embedding 模型不熟悉向量距离不理想。冷门实体某些内部系统名称、地方术语向量模型训练时没见过。所以工程上更稳妥的做法是“多路召回”同时跑向量检索、关键词检索甚至结构化查询最后把多路结果合并起来。4.2 向量检索语义召回向量检索的原理不复杂用 Embedding 模型把文本变成向量然后计算向量之间的余弦相似度或内积取 topk。实际开发中关注几个点Embedding 模型选择中文场景常见的有 BGE 系列、M3E、text2vec 等英文场景常用 OpenAI embedding、E5 等。具体选型要看评测效果不能只看名气。向量数据库Milvus、Qdrant、Weaviate、pgvector 都可以。自建或使用云服务都可以关键看部署成本和规模。topk 设置一般召回 20~50 条候选留给重排层去过滤。4.3 关键词检索稀疏检索关键词检索用的是 BM25 或 Elasticsearch 的 query_string。它的优势在于精确匹配能力强。对专有名词、编号、报错码更友好。实现成本低ES 本身就能做。# 伪代码BM25 检索 from rank_bm25 import BM25Okapi # 假设已经完成分词 tokenized_corpus [doc.split( ) for doc in corpus] bm25 BM25Okapi(tokenized_corpus) scores bm25.get_scores(query.split( ))实际项目里如果文档量很大一般用 Elasticsearch 集群来做关键词召回而不是内存版 BM25。4.4 多路召回结果的融合多路召回后各路分数尺度不同向量相似度是 0~1 之间的浮点数BM25 分数可能是几十上百直接比较没有意义。常见的融合方式有加权融合Weighted Sumfinal_score w1 * normalized_vector_score w2 * normalized_bm25_score归一化一般用 min-max 或 rank 归一化。rank 归一化的做法是把每路结果的排序序号当分数比如第一名得 1 分第二名得 2 分然后做加权。RRF 融合Reciprocal Rank FusionRRF 是业界常用的融合算法公式大致是score(d) sum( 1 / (k rank_i(d)) )其中 rank_i(d) 是文档 d 在第 i 路检索中的排名k 是常数一般取 60。名字“倒数排名融合”就是这个含义。RRF 的好处是不需要归一化各路分数直接用排名。# 伪代码RRF 融合 def rrf_fusion(ranked_lists, k60): scores {} for ranked in ranked_lists: for rank, doc_id in enumerate(ranked): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)4.5 切块策略对召回的影响多路召回的底层依赖文档切块。切块策略会影响检索粒度和信息完整性切块方式优点缺点固定长度切块如 512 token实现简单可能切断语义完整的段落按段落/标题切块语义完整块大小差异大可能超出模型长度递归切块按分隔符层级切兼顾结构与长度还需要再调参数父子块切块父块上下文完整子块用于检索检索精度和上下文完整可兼得实现稍复杂建双份索引面试时如果能主动讲出切块策略对检索效果的影响会是一个明显的加分项。5. 第三层精排与上下文构造5.1 为什么不能把召回结果直接给大模型第一层和第二层做的是“海选”目的是尽量把相关文档捞进来。但海选结果通常有噪声召回的 50 条文档里可能只有 3~5 条真正有价值。文档之间可能存在信息重复。把一堆低质量片段直接塞进 Prompt不仅浪费 token还会干扰模型判断。所以第三层要做“精排”从候选文档中挑出最应该被大模型看到的内容。5.2 Rerank 重排重排的常见做法是训练一个 Cross-Encoder 模型把 query 和文档拼在一起过一遍模型输出相关性分数。这个和 Embedding 阶段的 Bi-Encoder 不一样模型类型方式效果速度Bi-EncoderEmbeddingquery 和 doc 分别编码一般快Cross-EncoderRerankquery 和 doc 拼接编码更好慢因为 Rerank 模型慢所以只对第一轮召回的 topk比如 20~50 条做精排。线上推荐的组合是粗召回多路取 50 条- 重排Rerank取 5~10 条- 进入上下文中文环境经常用 BGE-reranker 系列模型。下面是调用思路# 伪代码Rerank 重排需按实际依赖调整 from sentence_transformers import CrossEncoder model CrossEncoder(BAAI/bge-reranker-base) pairs [(query, doc) for doc in candidate_docs] scores model.predict(pairs) # 按分数排序取 topk top_indices sorted(range(len(scores)), keylambda i: scores[i], reverseTrue)[:5] final_docs [candidate_docs[i] for i in top_indices]这里要提醒一下不同版本的 sentence-transformers API 有差异实际运行时请参照你部署环境的官方示例调整。5.3 MMR 去重Rerank 之后还有一个常见问题候选文档可能大量重复。比如 10 条文档都在讲同一个知识点如果全放进去既浪费 token又可能让模型产生重复表达。MMRMaximal Marginal Relevance的思路是在保证相关性的同时惩罚与已选文档相似的候选。MMR argmax( lambda * 相关性分数 - (1-lambda) * max(与已选文档的相似度) )lambda 越大越看重相关性lambda 越小越看重多样性。5.4 上下文压缩有时候召回的文档虽然相关但太长了。比如一段文档有 2000 token而真正有用的只有中间一句话。这时可以做上下文压缩用一个轻量模型对文档片段做摘要。或者只抽取与 query 最相关的句子。或者按 token 上限截断。压缩的目的是让有限的上下文窗口装下更多有效信息。5.5 引用溯源与 groundedness精排之后还要为生成内容准备引用信息。这是 RAG 系统在企业场景中必须考虑的一环。用户不只需要答案还需要知道答案来自哪份文档。比较常见的做法是检索时保留每个 chunk 的原始文档 ID、页码、段落号。生成 Prompt 时要求模型在引用处标注来源编号。后端返回答案时附带来源列表前端可以做“引用角标”点击跳转。对模型生成内容做 groundedness 校验判断生成内容是否忠于检索文档避免模型自由发挥。这一部分在面试中属于“高阶亮点”如果能主动提出来会明显区别于只会调 Demo 的候选人。5.6 第三层的代码思路下面给出一个从召回结果到最终上下文的简化流程# 文件retrieval_pipeline.py def build_context(query, recall_docs, topk5): # recall_docs 是字典列表每个元素包含 text、source、score # 1. Rerank 打分 scored rerank_score(query, recall_docs) # 调用 CrossEncoder # 2. 按分数取 topk selected sorted(scored, keylambda x: x[score], reverseTrue)[:topk] # 3. MMR 去重这里用简化实现实际建议用独立模块 selected mmr_dedup(query, selected, lambda_value0.7) # 4. 构造上下文 context_parts [] for idx, doc in enumerate(selected): context_parts.append(f[{idx}] {doc[text]}) context \n\n.join(context_parts) # 5. 记录引用来源 sources [{id: str(i), source: doc[source]} for i, doc in enumerate(selected)] return context, sources这段代码是一个框架示意图重点在于表达“先精排、再去重、再构造上下文、再记录引用”的处理顺序。6. 完整实战案例一个简化版三层检索问答系统为了帮助大家把上面的内容串起来这里用一个可运行的最小项目来做演示。项目不依赖重型框架重点展示三层检索的代码组织方式。6.1 项目结构rag_demo/ ├── config.py ├── data/ │ └── docs.txt ├── offline_index.py ├── online_query.py ├── reranker.py └── requirements.txt6.2 环境依赖# requirements.txt # 以下依赖需要根据实际环境调整版本 openai rank-bm25 sentence-transformers numpy安装命令pip install -r requirements.txt如果只需要跑通 “向量检索 BM25 RRF 融合”部分可以暂时不装 sentence-transformers用现成的 Embedding API 替代。6.3 离线索引与切块# 文件offline_index.py # 注意示例代码具体实现需根据你的向量库和 Embedding 服务调整 import hashlib def simple_chunk(text, chunk_size200, overlap30): 按固定长度切块并保留重叠部分避免切断语义。 words text.split() chunks [] start 0 while start len(words): end start chunk_size chunk .join(words[start:end]) chunks.append(chunk) if end len(words): break start end - overlap return chunks def build_index(file_path): with open(file_path, r, encodingutf-8) as f: raw_text f.read() docs [] for idx, chunk in enumerate(simple_chunk(raw_text)): doc_id hashlib.md5(f{idx}-{chunk[:20]}.encode()).hexdigest() docs.append({doc_id: doc_id, text: chunk, source: file_path}) # 生产环境将 docs 写入向量数据库并建立 ES 倒排索引 return docs6.4 在线检索向量 BM25 融合# 文件online_query.py from rank_bm25 import BM25Okapi import numpy as np # 假设 docs 是离线索引阶段生成的列表 docs [] bm25 None def init_retriever(all_docs): global docs, bm25 docs all_docs tokenized [d[text].split() for d in docs] bm25 BM25Okapi(tokenized) def vector_search(query, topk20): # 此处伪代码调用 embedding 服务生成 query 向量 # 和向量数据库做相似度检索返回 doc_id 列表和相似度分数 # 考虑到不同部署环境差异较大这里演示返回空列表 return [] def bm25_search(query, topk20): tokenized_query query.split() scores bm25.get_scores(tokenized_query) top_indices np.argsort(scores)[::-1][:topk] results [] for i in top_indices: if scores[i] 0: continue results.append({ doc_id: docs[i][doc_id], text: docs[i][text], score: scores[i] }) return results def rrf_fusion(ranked_lists, k60): scores {} for ranked in ranked_lists: rank 0 for item in ranked: doc_id item[doc_id] scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) rank 1 return scores6.5 重排# 文件reranker.py # 如果环境不支持加载模型可改为调用在线 rerank API from sentence_transformers import CrossEncoder model None def load_reranker(): global model model CrossEncoder(BAAI/bge-reranker-base) def rerank(query, candidates, topk5): pairs [(query, c[text]) for c in candidates] scores model.predict(pairs) for i, s in enumerate(scores): candidates[i][rerank_score] float(s) sorted_candidates sorted(candidates, keylambda x: x[rerank_score], reverseTrue) return sorted_candidates[:topk]6.6 完整回答流程# 文件main.py 核心逻辑 from online_query import init_retriever, vector_search, bm25_search, rrf_fusion from reranker import load_reranker, rerank def answer(question, topk_recall20, topk_rerank5): # 第一层查询改写这里用最简单的直接返回生产环境接 LLM rewrite_query question # 第二层多路召回 vec_results vector_search(rewrite_query, topktopk_recall) bm25_results bm25_search(rewrite_query, topktopk_recall) fused_scores rrf_fusion([vec_results, bm25_results]) # 把 doc_id 映射回文档对象 doc_map {d[doc_id]: d for d in docs} candidates [] for doc_id, score in fused_scores.items(): if doc_id in doc_map: candidates.append({**doc_map[doc_id], recall_score: score}) # 第三层精排 final_docs rerank(rewrite_query, candidates, topktopk_rerank) context \n\n.join([f[{i}] {d[text]} for i, d in enumerate(final_docs)]) # 拼接到 Prompt 并调用 LLM prompt f 请根据以下参考资料回答问题。如果资料中不存在相关信息请明确说明“知识库中未找到相关内容”。 参考资料 {context} 问题{rewrite_query} # 这里调用 LLM return prompt这里面“向量检索”只是个占位线上需要结合具体的向量数据库来实现。重点是看到三层结构的组织方式。6.7 运行与验证python offline_index.py # 构建索引 python main.py # 查询预期效果对同一份知识库单路向量检索可能召回不全但加上 BM25 和 RRF 融合后相关文档的召回排序更稳定Rerank 之后进入 Prompt 的文档相关性明显提升。7. 常见问题与排查思路问题现象常见原因排查思路向量检索召回结果乱七八糟Embedding 模型与领域不匹配评估多个 embedding 模型在自建验证集上的效果BM25 检索结果很少分词不合理、文档碎片化检查分词器扩充同义词检索有结果但答案不对召回片段不完整或被截断调整切块策略尝试父子块结构答案重复、车轱辘话多个召回文档内容高度重叠加入 MMR 去重Rerank 耗时过高候选集太大限制重排数量先粗排到 20~50 条Prompt 太长召回的文档太长做上下文压缩或截断模型回答天马行空检索结果不相关检查 Rerank 分数阈值低分文档不要进入 Prompt用户 query 含指代没有做查询改写增加 query 改写和上下文补全引用来源对不上引用信息没有随 chunk 传递在切块、检索、重排全链路保留 source 字段8. RAG 策略最佳实践与工程建议8.1 检索链路的调优优先级我的经验是先保证“能召回”再保证“召得准”然后才是“排得好”。如果多路召回环节就漏掉了关键文档后面的 Rerank 再强也没用。建议调优顺序检查切块策略是否合理文档有没有被切断关键信息。检查召回结果在验证集上的 Recall20如果 recall 低优先提升召回。再调 Rerank 阈值和 topk观察 Precision 变化。最后优化 Prompt 和答案生成的风格。8.2 评估指标怎么定RAG 项目的评估不要只看“答得对不对”要分层看检索层RecallK、PrecisionK、MRR、NDCG。生成层答案忠实度Faithfulness、答案相关性Relevance、引用准确性Groundedness。工程层检索 P99 延迟、Rerank 延迟、整体吞吐量。平时可以准备一份百条左右的标准问答测试集每次改动后跑一遍观察指标变化避免“修好一个 bug 引入两个新问题”。8.3 多轮对话与记忆管理如果 RAG 系统要支持多轮对话query 改写几乎是必须的。否则用户第二轮问“它怎么部署”系统根本不知道“它”指什么。工程上通常把最近几轮对话打包发给 LLM让 LLM 输出改写后的独立 query。同时要注意控制历史轮数避免把太多无关历史塞进改写 Prompt浪费 token 且引入噪声。8.4 安全与权限边界RAG 系统在企业内部落地时要特别注意检索权限问题。不同角色能看到的知识库范围可能不同。比如普通员工不该检索到薪资文档外包人员不该检索到核心代码库。建议在检索前先做用户权限过滤把当前 user 的可见文档范围确定下来再在这个范围内执行多路召回。而不要等检索完了再过滤否则可能造成越权信息泄露。8.5 成本控制Embedding 可以离线批量算好线上只缓存 query 向量。Rerank 模型比生成模型便宜但也费时间可以设置候选集上限。高频相似 query 可以做缓存命中缓存直接返回。文档更新时做增量索引不要每次都全量重建。8.6 从“三层检索”走向更复杂的 RAG 形态三层检索是当前大多数 RAG 系统的骨架但在这个基础上行业里已经演进出了更多模式Agentic RAG把检索拆成多步让大模型根据中间结果决定下一步查什么支持工具调用和迭代检索。GraphRAG把实体和关系抽取成知识图谱在向量检索之外增加图谱检索适合多跳问答和全局性提问。混合 RAG向量数据库 知识图谱 关键词索引三种能力同时在线路由层决定走哪条路。如果面试聊到这些可以点出它们与三层检索的关系这些形态并没有推翻三层结构而是在查询理解、多路召回或精排层增加了更复杂的策略。9. 面试回答话术参考最后给一个可以直接参考的回答框架方便大家面试时组织语言RAG 的检索策略我会把它分成三层来设计。第一层是查询理解与改写。用户问题往往带有指代、口语化表达或者信息不全直接拿去检索效果不好。所以我会先用 LLM 做指代消解和查询改写有时也会用 HyDE 或多查询扩展来丰富检索入口。第二层是多路召回。我不会只用向量检索因为向量检索对精确匹配、专有名词不友好。常规做法是向量检索 关键词检索BM25/ES并行再用 RRF 或加权融合把多路结果合并起来。召回数量一般控制在 20~50给后面的精排留出余地。第三层是精排与上下文构造。召回结果噪声很大直接塞给大模型会影响生成质量。我会用 Cross-Encoder 对候选集做 Rerank然后用 MMR 去掉重复内容再按 token 上限做上下文压缩。构造上下文时保留来源信息方便生成内容做引用溯源和 groundedness 校验。综合来看检索质量决定了 RAG 的上限所以这三层每一步都值得认真优化。这段话不算长但把三层逻辑讲得很清楚。面试官如果继续追问细节就往切块策略、混合权重、Rerank 阈值、评估指标这些方向深入聊。如果你正在准备大模型方向面试可以和 RAG 一起复习的还有Embedding 模型选型、向量数据库原理、Prompt 设计、Agent 工具调用、幻觉评估方法。把“三层检索”这条主线吃透RAG 相关的题基本都能接住。文章里的示例代码可以在本地环境跑通后再逐步替换成真实的向量数据库和生产模型效果会直观很多。