ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RAG策略三层检索:召回、精排、增强,面试高分指南

RAG策略三层检索:召回、精排、增强,面试高分指南 面试官问RAG策略说清这三层检索直接拿分很多人在准备大模型相关面试时最怕遇到这类开放式问题“讲讲你的 RAG 策略”如果只回答“文档切块 - Embedding - 向量检索 - 拼接 Prompt - 让大模型回答”表面看流程完整但面试官大概率不会满意。因为这套流程任何一个看过教程的人都能背出来它没有体现你对 RAG 系统的真实理解。面试官真正想听到的是你在检索链路里做了哪些层次化的设计以及你知不知道每一层分别解决什么问题。说得直白一点RAG 的核心不是生成而是检索检索的核心不是单次向量匹配而是分层递进地逼近用户真正想要的那段文本。本文用一套可以落地的“三层检索”策略来拆解这个问题——召回层、精排层、增强层。每一层是什么、解决什么问题、常用方案有哪些、代码怎么实现、面试被追问时怎么答全部展开讲清楚。读完这篇文章你既能在面试时把 RAG 策略讲出层次感也能在实际项目中把检索效果真正提上去。1. 为什么 RAG 策略成了面试必考题先说一个观察这两年大模型岗位的面试题正在从“背模型架构”转向“考工程系统”。原因很简单模型能力本身已经高度同质化企业更关心的是你怎么把模型用起来、用得好、用得稳。RAG 就是“把模型用起来”最典型的场景。它不依赖重新训练模型也不需要昂贵的微调成本而是通过外挂知识库的方式让模型能回答私有领域问题、减少幻觉、支持知识更新。这套思路几乎适用于所有企业级 LLM 应用所以面试官几乎必问。但面试官问 RAG 策略往往还会带一句潜台词你只是用过 LangChain 的 vectorstore 做检索还是真的理解检索质量怎么控制如果你只停留在“向量检索”这一步说明你还没有遇到真实世界的检索问题。真实世界的问题包括用户问“苹果公司今年营收怎么样”你库里存的是“Apple Inc. 2024 Q4 财报”向量相似度可能没那么高单纯向量检索可能召回不了用户问的是多个条件叠加的复合问题一次检索往往不够向量检索召回的 Top-K 里真正有用的可能只有一两条其余全是“意思相近但不对题”的内容用户问题里带了特定术语、缩写、拼写变体检索层很可能直接把关键信息过滤掉了。这些问题的共同点是单靠一层向量检索无法同时满足“召回全”和“排得准”。于是就有了分层的检索策略。三层检索的结构可以这样理解层级核心任务解决的核心问题第一层召回层从大规模文档库中快速找到候选集高召回不能漏掉关键信息第二层精排层对候选集重新排序让最相关的内容排前面高精度让真正有用的上下文进入 Prompt第三层增强层对用户问题和检索结果做改写、扩展、压缩解决复杂查询、术语不一致、上下文超长等问题这套结构的核心思想其实很好理解用便宜快速的手段扩大候选范围再用更精细的手段缩小答案范围。它和搜索引擎的“召回 排序”架构一脉相承只是放在 RAG 场景里做了一些适配。2. RAG 基础链路与面试官真正想听的答案在展开三层检索之前先把 RAG 整体链路交代清楚。这既是给基础稍弱的读者补背景也是后面分析每层检索的起点。一个完整的 RAG 流程通常分两阶段离线阶段知识库构建加载文档支持 PDF、Word、Markdown、HTML 等格式文本清洗去掉页眉页脚、乱码、无关内容文本切块chunking把长文档切成适当大小的文本块对每个文本块做 Embedding生成向量把向量和原始文本一起写入向量数据库。在线阶段查询问答用户输入问题对问题做 Embedding在向量数据库中执行相似度检索取 Top-K 候选把候选文本作为上下文拼接用户问题构造 Prompt交给大模型生成回答。很多面试者答到这里就停了但这只是“标准流程”不是“策略”。策略意味着你要在每一个环节做决策并解释决策理由。面试官真正想听的答案可以概括成一句话“RAG 的检索环节我做了分层设计。召回层用向量检索保证 recall精排层用重排序模型提升 precision增强层用查询改写和混合检索处理复杂查询。这样既控制成本又能把最终的上下文质量提上去。”下面逐层展开。3. 第一层召回层——向量检索与切块策略3.1 召回层解决什么问题召回层的目标非常明确从海量文本块中快速找到与用户问题相关的候选集。这一层的核心指标是召回率Recall而不是精确率。场景化解释你的知识库有 10 万条文本块用户问了一个问题。召回层的任务是挑出最有可能包含答案的 20 条或 50 条交给下一层精排。这一层如果漏召回了正确答案后面无论怎么精排都救不回来。所以召回层的核心矛盾是候选集太小容易漏候选集太大后续精排成本高而且噪声多。通常的折中是控制在 20 到 100 条之间具体取决于你的文档规模和业务场景。3.2 向量检索不是唯一手段一说到召回很多人默认就是向量检索。实际上召回层可以组合多种方式纯向量检索把问题和文档都变成向量计算余弦相似度或内积适合语义匹配场景。关键词检索BM25基于词频和逆文档频率匹配适合包含专有名词、编号、准确术语的查询。向量 关键词混合检索两种方式的结果做融合兼顾语义和相关词匹配。纯向量检索的典型弱点是“语义相近但关键词不重合”。比如用户问“怎么申请退款”文档里写的是“退货流程”向量检索大概率能匹配上。但如果用户问的是“SKU-2024-001 的库存”文档里恰好用“货号 2024001”表示向量可能匹配不准确关键词检索反而更可靠。所以现在主流的召回方案都会引入混合检索而不是只依赖向量。3.3 切块策略RAG 里最容易忽略的细节如果说召回层有一个最容易被忽略但又影响巨大的配置那就是切块策略。这也是最近行业里讨论很多的话题。为什么切块重要因为向量检索的基本单位是“文本块”而不是整篇文档。你切出的每个块的质量直接决定向量检索的上限。切块核心参数有三个Chunk Size块大小每个文本块包含多少字符或 token。太小语义不完整太大向量表示被稀释检索精度下降还会浪费大模型上下文窗口。Chunk Overlap块重叠相邻块之间重叠多少内容。没有重叠时如果关键信息恰好被切断这段内容就废了。加上重叠等于给关键信息“上保险”。切分粒度按固定长度切还是按段落、句子、Markdown 标题切。固定长度最简单但容易把语义完整的段落拦腰截断。按结构切往往效果更好。从实际项目经验看切块并没有一个“一劳永逸”的万能参数。更靠谱的做法是把切块策略当成一个可调参数配合评测集去验证。推荐一个保守的起步方案文档类型建议 chunk_size建议 overlap技术文档、说明书500-800 字50-100 字问答对、FAQ200-300 字20-30 字长文章、报告800-1200 字100-150 字但是这里要强调一点不要照抄网上推荐值直接上生产。文本类型不同、Embedding 模型不同最优参数都会变。要建立自己的验证集用检索命中率评估不同参数组合。3.4 召回层的代码示例下面是一个基于 LangChain 的文档加载与切块示例。这里重点演示的是切块思路而不是某个特定库的使用。# 文件路径src/rag_retrieval/chunking.py from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载原始文档 loader TextLoader(data/company_manual.txt, encodingutf-8) documents loader.load() # 2. 配置切块策略 text_splitter RecursiveCharacterTextSplitter( chunk_size600, # 每个块约 600 字 chunk_overlap80, # 块与块之间重叠 80 字 separators[\n\n, \n, 。, , , , , ], # separators 的优先级先按段落切再按句子切最后按字符切 ) # 3. 执行切块 chunks text_splitter.split_documents(documents) print(f原始文档数: {len(documents)}) print(f切块后文本块数: {len(chunks)}) print(f第一个文本块前 100 字:\n{chunks[0].page_content[:100]})说明几个关键点RecursiveCharacterTextSplitter是 LangChain 里最推荐的通用切分器它会按 separators 列表里的优先级递归切分尽量保证语义完整。chunk_size600和chunk_overlap80只是起步值后续需要根据评测结果调整。如果文档本身有明确的标题结构Markdown 标题、HTML 标签可以考虑MarkdownHeaderTextSplitter或按标题拆分的方案让每个块自带语义边界。切块之后再由 Embedding 模型对每个块生成向量写入向量数据库。这一步的代码如下# 文件路径src/rag_retrieval/indexing.py from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS # 初始化 Embedding 模型 # 生产环境建议用独立的 embedding 服务或本地模型降低延迟和成本 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) # 基于切块结果构建向量索引 vectorstore FAISS.from_documents( documentschunks, embeddingembeddings, ) # 保存索引到本地方便后续加载 vectorstore.save_local(data/faiss_index)这个阶段输出的就是一个可检索的向量索引。后续在线查询时直接加载索引做相似度检索即可。3.5 召回层面试高频追问面试官在这个环节最常追问的几个问题问chunk_size 设置多大合适不要直接说一个固定值。更好的回答是分两层先说一个经验范围比如 500 到 800 字然后强调这个值需要根据文档类型、Embedding 模型和评测结果来调整最后补一句“我会用验证集分别测试不同 chunk_size 下的召回命中率选最优值”。问overlap 有什么用overlap 是为了防止文本切分把关键上下文切断。如果某句话的上半句在一个块里、下半句在下一个块里单独检索任一块都可能语义不完整。加了 overlap 之后关键信息会以完整形式同时出现在相邻块中提高召回概率。问为什么不用更大的 chunk_sizechunk_size 越大单个向量表示的语义越模糊检索精度下降。而且大块会占更多上下文窗口导致可放入 Prompt 的块数变少。所以要在“语义完整性”和“向量精度”之间取平衡。4. 第二层精排层——重排序Rerank4.1 为什么需要精排层向量检索返回的 Top-K 只是“候选”不是“答案”。它的问题在于向量相似度高的文本不一定真的能回答用户的问题。举个例子。用户问“RAG 中向量检索和关键词检索有什么区别”向量检索可能召回一段讲“向量检索的原理是……”“关键词检索的优点是……”的文本这些文本单独看都和问题相关但真正直接回答问题的可能只有一段。如果你把 Top-5 全部塞进 Prompt既浪费上下文还可能引入干扰信息导致大模型答非所问。精排层的任务就是解决这个问题对召回层返回的候选集做一次更精细的相关性排序把最可能包含答案的文本排到最前面。4.2 重排序模型的原理精排层最常用的方案是重排序模型Reranker。与 Embedding 模型不同重排序模型通常使用 Cross-Encoder 架构。它会把“用户问题 候选文本”作为一个整体输入模型一次性计算它们之间的相关性得分。这样模型能看到问题和文档之间更细粒度的交互效果比向量相似度更准但代价是计算量大。Embedding 模型和重排序模型的关键区别维度Embedding 模型Bi-Encoder重排序模型Cross-Encoder输入方式问题和文档分别编码问题和文档拼接后一起编码计算效率高文档向量可离线预计算低需要在线逐一计算语义匹配精度一般更高适用场景大规模召回小规模精排正因为重排序模型的精度高但成本高它不适合对全量文档做计算只适合在召回层已经缩小候选集的基础上对几十条候选做精排。这也正是 RAG 链路中“粗排 精排”两层配合的典型架构。4.3 精排层的代码实现下面用一个完整的示例演示“召回 - 精排”的流程。这里使用 sentence-transformers 库加载 Embedding 模型和重排序模型并用 FAISS 做向量检索。# 文件路径src/rag_retrieval/retrieve_rerank.py from sentence_transformers import SentenceTransformer, CrossEncoder import faiss import numpy as np # ---------- 1. 加载模型 ---------- # Embedding 模型用于向量召回 embed_model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 重排序模型用于精排 rerank_model CrossEncoder(BAAI/bge-reranker-base) # ---------- 2. 模拟候选文档 ---------- documents [ RAGRetrieval-Augmented Generation通过检索外部知识增强大模型生成能力。, 向量检索是将文本映射为高维向量在向量空间中计算相似度。, 关键词检索BM25基于词频匹配适合专有名词查询。, 重排序模型使用 Cross-Encoder 架构精度更高但计算量更大。, RAG 的检索策略通常分为召回层、精排层和增强层。, ] # ---------- 3. 离线向量化 ---------- doc_embeddings embed_model.encode(documents, normalize_embeddingsTrue) dimension doc_embeddings.shape[1] # 构建 FAISS 索引 index faiss.IndexFlatIP(dimension) # IP内积配合归一化向量等价于余弦相似度 index.add(doc_embeddings) # ---------- 4. 在线查询召回 ---------- query RAG 检索策略包含哪些层次 query_embedding embed_model.encode([query], normalize_embeddingsTrue) # 召回 Top-4 候选 top_k 4 scores, indices index.search(query_embedding, top_k) candidates [(documents[i], float(score)) for i, score in zip(indices[0], scores[0])] print( 向量召回结果 ) for idx, (doc, score) in enumerate(candidates): print(fTop{idx 1} 相似度{score:.4f} | {doc}) # ---------- 5. 精排 ---------- # 构造 rerank 模型需要的输入格式 rerank_inputs [(query, doc) for doc, _ in candidates] rerank_scores rerank_model.predict(rerank_inputs) # 按精排分数排序 reranked sorted( zip(candidates, rerank_scores), keylambda x: x[1], reverseTrue, ) print(\n 重排序结果 ) for idx, ((doc, vec_score), rerank_score) in enumerate(reranked): print(fRerank Top{idx 1} 向量分{vec_score:.4f} 重排分{rerank_score:.4f} | {doc})这个示例的精髓在最后一步向量检索负责快速缩小范围重排序负责重新排序。如果你的候选集是 Top-20经过重排序后通常只取前三到五条进入 Prompt这样上下文质量会明显提升。4.4 精排层的成本控制面试官一定会问重排序那么贵线上扛得住吗一个成熟的回答是控制精排的候选数量。召回层返回 Top-50精排层只对 Top-50 做计算最后取 Top-3。这样每查询一次只做 50 次 Cross-Encoder 推理延迟通常在几十毫秒级别可以接受。另一个常见优化是只在候选数量多或者检索置信度不高的时候才启用重排序。如果简单查询一次向量检索的 Top-1 相似度已经很高可以直接跳过重排序大幅降低平均延迟。5. 第三层增强层——查询改写与混合检索5.1 用户问题没那么“干净”实际场景里用户的问题往往不是理想化的简洁问句。你可能遇到口语化表达“那个退款的流程是啥来着”指代不清晰“它的效果怎么样”——这里的“它”指什么复合问题“对比一下 A 方案和 B 方案的成本和效果。”拼写错误或术语变体“SKU-2024 怎么同步”、“sku2024 价格”问题缺少上下文“推荐一下。”——没有任何领域背景。如果直接拿用户原始问题去做向量检索效果经常不好。因为 Embedding 模型面对口语、缩写、无上下文的问题时很难准确映射到文档库里规范化的表述上。增强层的作用就是在这里在向量检索之前和之后对问题和检索结果做额外的处理提高最终上下文的匹配度。5.2 查询改写把问题变得更适合检索查询改写Query Rewriting的思路是让大模型把用户的原始问题改写成更适合向量检索的查询语句。最典型的场景用户问“它支持并发吗”——改写为“系统架构支持并发处理吗”用户问“怎么部署”前面聊的是某个具体系统——改写时补全主语用户问“mac 上怎么装”——改写为“在 macOS 系统上安装的步骤是什么”改写的常见实现方式是用大模型做一次轻量调用。代码示例# 文件路径src/rag_retrieval/query_rewrite.py from openai import OpenAI import json client OpenAI() # 根据实际环境配置 base_url 和 api_key def rewrite_query(original_query: str) - str: 使用大模型对用户原始问题进行改写使其更适合向量检索。 prompt f你是一个检索查询改写助手。请把用户的问题改写成更适合文档检索的形式。 要求 1. 补全缺失的主语和上下文 2. 把口语表达转换为书面表达 3. 保留原问题中的专有名词和关键数字 4. 只输出改写后的查询不要解释 用户原始问题{original_query} 改写后的查询 response client.chat.completions.create( modelgpt-4o-mini, # 实际模型名以项目配置为准 messages[{role: user, content: prompt}], temperature0, ) return response.choices[0].message.content.strip() # 示例 original 它支持并发吗 rewritten rewrite_query(original) print(f原始问题{original}) print(f改写后{rewritten})需要注意的是查询改写不是每次都要做。它本身有成本一次额外的大模型调用少则几百毫秒多则几秒。如果用户的查询已经非常明确、包含准确的专有名词直接检索即可。常见的策略是先直接检索如果置信度低再触发查询改写并重新检索。5.3 混合检索与分数融合增强层的另一个重要手段是混合检索。它的思路是把向量检索和关键词检索BM25结合起来各取所长。BM25 对专有名词、编号、精确匹配非常友好。比如用户查询“SKU-2024-001 库存”BM25 能精确命中包含“SKU-2024-001”的文档块而向量检索可能因为语义向量距离不够近而漏掉。混合检索的实现方案很多最简单的方式是分别用向量检索引擎和 BM25 检索各自返回 Top-N然后把两路结果合并去重按融合分数重新排序。常见的分数融合方法是Reciprocal Rank FusionRRF。它的核心思想是不看具体分数只看排名。一个文档在两路检索中都排前面那它的综合排名应该靠前。# 文件路径src/rag_retrieval/hybrid_search.py from rank_bm25 import BM25Okapi def rrf_fusion(ranked_lists, k60): Reciprocal Rank Fusion 分数融合。 ranked_lists: 多个排序结果列表每个列表是文档 id 列表 k: RRF 常数通常取 60 scores {} for ranked_list in ranked_lists: for rank, doc_id in enumerate(ranked_list): if doc_id not in scores: scores[doc_id] 0 scores[doc_id] 1.0 / (k rank 1) # 按融合分数降序排序 return sorted(scores.items(), keylambda x: x[1], reverseTrue) # 模拟两路检索返回的文档 id 列表 vector_results [doc_3, doc_1, doc_5, doc_2] bm25_results [doc_2, doc_4, doc_3, doc_1] fused rrf_fusion([vector_results, bm25_results]) print(混合检索融合结果:) for doc_id, score in fused: print(f{doc_id}: {score:.4f})RRF 最大的好处是不需要对向量分数和 BM25 分数做归一化因为两者量纲不同直接相加没有意义。通过排名融合天然规避了分数分布不一致的问题在实际项目中非常实用。5.4 上下文压缩增强层还有一项容易被忽视的工作检索结果的上下文压缩。向量检索返回的文本块往往包含大量无关内容。比如用户问“退款的到账时间”检索到的文本块可能有 500 字但核心答案只有一句话。如果直接把整个文本块塞进 Prompt大模型容易被无关信息干扰。上下文压缩的思路是用轻量模型或大模型对检索到的文本块做摘要或抽取只保留与用户问题最相关的部分再拼接进 Prompt。这样既减少了 token 消耗又提高了大模型生成答案的准确率。6. 完整示例三层检索整合流程上面三节分别介绍了每一层的思路和代码这一节把三层整合成一个完整流程方便你直接参考或在自己的项目里改造。整体流程如下用户输入问题查询改写模块判断是否需要改写召回层执行向量检索 BM25 混合检索得到候选集精排层用 Reranker 对候选集重新排序取排序后的 Top-3 作为上下文构造 Prompt大模型生成答案。# 文件路径src/rag_retrieval/pipeline.py from sentence_transformers import SentenceTransformer, CrossEncoder from rank_bm25 import BM25Okapi import faiss import numpy as np class ThreeLayerRetriever: def __init__(self, documents: list[str]): 初始化三层检索器。 documents: 已完成切块的文档列表 self.documents documents # 向量检索模型 self.embed_model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 重排序模型 self.rerank_model CrossEncoder(BAAI/bge-reranker-base) # 建立向量索引 doc_embeddings self.embed_model.encode(documents, normalize_embeddingsTrue) self.dimension doc_embeddings.shape[1] self.index faiss.IndexFlatIP(self.dimension) self.index.add(doc_embeddings) # 建立 BM25 索引 tokenized_docs [self._tokenize(doc) for doc in documents] self.bm25 BM25Okapi(tokenized_docs) def _tokenize(self, text: str) - list[str]: 简单中文分词实际项目可替换为 jieba 等专业分词器 # 这里用最简单的方式按字符切分 保留英文单词 import re return re.findall(r[\u4e00-\u9fff]|[a-zA-Z0-9], text) def recall(self, query: str, top_k: int 20) - list[int]: 召回层向量检索 BM25 混合召回 # 向量检索 query_embedding self.embed_model.encode([query], normalize_embeddingsTrue) vec_scores, vec_indices self.index.search(query_embedding, top_k) vector_rank_list [int(i) for i in vec_indices[0]] # BM25 检索 query_tokens self._tokenize(query) bm25_scores self.bm25.get_scores(query_tokens) bm25_rank_list list(np.argsort(bm25_scores)[::-1][:top_k]) # RRF 融合 fused rrf_fusion([vector_rank_list, bm25_rank_list]) return [doc_id for doc_id, _ in fused[:top_k]] def rerank(self, query: str, candidate_ids: list[int], top_n: int 3) - list[int]: 精排层Cross-Encoder 重排序 inputs [(query, self.documents[i]) for i in candidate_ids] scores self.rerank_model.predict(inputs) ranked sorted(zip(candidate_ids, scores), keylambda x: x[1], reverseTrue) return [doc_id for doc_id, _ in ranked[:top_n]] def retrieve(self, query: str, final_top_k: int 3) - list[str]: 三层检索完整流程 candidate_ids self.recall(query, top_k20) final_ids self.rerank(query, candidate_ids, top_nfinal_top_k) return [self.documents[i] for i in final_ids] def rrf_fusion(ranked_lists, k60): scores {} for ranked_list in ranked_lists: for rank, doc_id in enumerate(ranked_list): if doc_id not in scores: scores[doc_id] 0 scores[doc_id] 1.0 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue) # ---------- 使用示例 ---------- documents [ RAGRetrieval-Augmented Generation通过检索外部知识增强大模型生成能力减少幻觉。, 向量检索将文本映射到高维向量空间通过余弦相似度衡量语义相关性。, BM25 是一种基于词频的关键词检索算法对专有名词和精确匹配更友好。, 重排序模型 Cross-Encoder 将问题和文档拼接输入输出相关性分数精度高于向量检索。, RAG 检索策略通常分为召回层、精排层、增强层每层解决不同问题。, 查询改写可以补充上下文、修正表达使检索更准确。, ] retriever ThreeLayerRetriever(documents) query RAG 三层检索架构的分工是什么 results retriever.retrieve(query) print(最终用于生成的上下文) for doc in results: print(f- {doc})这段代码虽然简化了很多工程细节但完整演示了三层检索的核心流程。实际项目中你会在以下几个方面做扩展向量数据库换成 Milvus、Qdrant 或 Elasticsearch 的向量索引而不是本地 FAISSEmbedding 模型和 Reranker 可能部署成独立服务查询改写模块接入大模型调用增加缓存层避免相同或相似查询重复执行完整链路。7. 三层检索的效果验证方法面试能讲出来是一回事项目里能不能用又是另一回事。聊完方案之后面试官大概率会追问你怎么证明你的检索效果是好的这是一个很容易拉开差距的问题。很多候选人只会说“效果还行”而真正有工程经验的人会给出具体的评测方法。7.1 建立检索评测集无论做什么优化第一步都是建立一个评测集。评测集的格式可以很简单每一条记录包含一个用户查询、一个期望命中的文档 ID 列表。例如[ { query: 如何配置 Apollo 配置中心, relevant_doc_ids: [doc_1024, doc_1025] }, { query: 订单超时自动关单的机制是什么, relevant_doc_ids: [doc_2087] } ]评测集的数据来源可以是历史日志中用户真实问过的问题也可以由业务方整理。数量不需要特别多但一定要覆盖典型场景简单查询、复杂查询、专有名词查询、口语化查询等。7.2 检索效果指标对于 RAG 场景建议关注的指标有RecallK在 Top-K 结果中是否包含期望命中的文档。这是召回层最重要的指标。MRRMean Reciprocal Rank期望命中的文档排在第几位。排名越靠前MRR 越高。上下文利用率检索结果中有多少文本最终被大模型回答引用。这个指标需要结合生成结果判断但能从侧面上反映检索质量。在项目中通常的做法是先固定切块策略和检索参数跑出一版基线指标然后单独调整某一个变量比如 chunk_size、是否开启混合检索、是否加 Reranker对比指标变化保留有效优化、回滚无效改动。7.3 用 LangChain 自带工具快速验证如果不想从零写评测代码LangChain 官方提供了检索评测相关模块但也完全可以自己写一个简单的评测脚本# 文件路径src/rag_retrieval/evaluate.py def evaluate_recall(retriever, eval_set, top_k5): 简单评估 RecallK hit_count 0 total len(eval_set) for item in eval_set: query item[query] relevant_ids set(item[relevant_doc_ids]) retrieved retriever.recall(query, top_ktop_k) if relevant_ids set(retrieved): hit_count 1 recall hit_count / total print(fRecall{top_k}: {recall:.2%} ({hit_count}/{total})) return recall这个脚本虽然简单但足以支撑你在开发阶段快速验证切块参数、检索策略的调整效果。8. RAG 策略常见问题与面试追问这一节把开发者和面试者最容易出问题的点汇总成表并给出排查思路和标准回答。问题现象可能原因排查方式解决方案检索结果完全不相关Embedding 模型与领域不匹配抽查几个 query 的向量检索结果换领域适配的 Embedding 模型或增加混合检索专有名词检索不到向量检索对精确匹配不敏感用 BM25 单独测同一 query启用 BM25 向量混合检索检索结果相关但答案不准上下文太多噪音干扰大模型查看最终进入 Prompt 的文本块加 Reranker只保留 Top-3 高质量上下文切块后语义断裂chunk_size 太小或按固定长度切查看切块结果中相邻块是否语义完整调整 chunk_size增加 overlap按结构切分精排后结果反而变差Reranker 模型领域不匹配对比有 Reranker 和无 Reranker 的评测结果换 Reranker 模型或调整候选集大小查询延迟过高每轮都调用查询改写和 Reranker链路加耗时统计定位瓶颈增加缓存、启用条件触发、降低候选集大小多轮对话指代不清未处理对话历史检查输入的 query 是否包含完整上下文在改写阶段结合对话历史补全指代这里重点讲几个面试官特别喜欢深挖的点。追问一向量检索到底用什么相似度度量常见的有余弦相似度、内积、欧氏距离。使用前提不同如果你的 Embedding 模型输出的是归一化向量内积和余弦等价FAISS 的IndexFlatIP配合归一化向量计算的就是余弦相似度。实际项目里很多中文 Embedding 模型如 BGE 系列官方推荐直接用内积因为模型在训练时已经做了归一化。追问二Reranker 和向量检索的区别为什么不能只用 RerankerReranker 精度更高但计算复杂度与候选文档数量成正比。如果全库 10 万条文本都走 Reranker单次查询延迟会到秒级以上完全无法接受。向量检索可以通过 ANN近似最近邻索引在毫秒级扫完几百万向量所以要先粗排后精排。追问三如果检索效果不好你会先优化哪一块合理的顺序是先看召回是否命中——如果 RecallK 都上不去再多精排也没用确认召回没问题后再看 Top-1、Top-3 的排序质量最后才是调 Prompt 和生成策略。很多人一上来就换大模型、改 Prompt这是本末倒置。9. RAG 检索链路的工程最佳实践最后补充一些工程落地时的建议。这些点不一定会在面试里被问到但真实项目里非常关键。建议一把检索链路做成可观测的。每一层都要打印耗时和关键指标。线上出现问题的时候你才能快速判断是召回没命中、精排排错还是生成环节出了问题。建议至少记录查询改写是否触发改写前后的 query召回层的候选数量与 Top-K 范围精排层的候选数量与最终保留数量每一层的耗时。建议二切块策略要纳入版本管理。你可能会惊讶很多团队改 chunk_size 全凭感觉改完没有记录、没有对比。建议把切块参数、Embedding 模型、Reranker 模型、评测指标都纳入配置管理每次调整都跑一遍评测集用数据说话。建议三不能忽视缓存层。RAG 系统的检索链路里Embedding 计算、Reranker 推理、大模型调用都有成本。对于高频重复的查询做一个语义缓存非常划算。命中缓存时直接返回历史答案可以省掉大量计算资源。建议四安全和权限必须前置。企业级 RAG 一定会遇到权限问题不同角色能检索的文档范围不同。如果你在检索层就把无权限的文档过滤掉而不是等生成完再过滤效果和安全性都会好很多。另外Prompt 注入、恶意查询等安全问题也要在链路设计时考虑进去不要等上线后再补。建议五建立回滚机制。检索策略调整本质上是线上行为。任何模型或参数的变更都要能快速回滚。建议在向量索引服务和重排序服务上保留上一版本配合灰度发布逐步放量。一旦线上检索质量下降可以快速切回旧版本而不是紧急回滚代码。10. 总结三层检索策略怎么讲才能拿分最后回到面试场景本身。如果面试官问你“RAG 策略”你可以这样组织回答框架先一句话总述我理解的 RAG 策略核心是检索质量我会把检索拆成三个层次来设计和优化。然后依次展开召回层用向量检索 BM25 混合召回配合合理的切块策略目标是高召回率精排层用 Cross-Encoder Reranker 对候选集精排目标是提升上下文精度增强层用查询改写、上下文压缩解决复杂查询和长上下文问题。最后补一句每一层都可以独立评测、独立调优我也会用检索评测集来验证改动是否有效而不是靠感觉。这个回答框架有层次、有细节、有工程思维比单纯背流程要高出几个段位。对于正在准备面试的读者建议你把文中代码在本地跑一遍亲手感受一下切块参数变化对检索结果的影响再把三层检索的取舍逻辑用自己的话讲出来。面试的时候能讲清楚“为什么这样设计”比“用了什么库”重要得多。对于已经在做 RAG 项目的读者建议从切块策略和评测集入手先用数据找出当前检索链路里最薄弱的一环再有针对性地引入混合检索或 Reranker不要一上来就堆技术。检索是 RAG 的地基地基稳了楼上大模型的生成能力才能真正发挥出来。
返回列表