Appearance
四、RAG(检索增强生成)
本主题题目目录
点击题号跳转;右侧目录会高亮你正在阅读的题目
125. 什么是 RAG?RAG 的主要流程是什么?126. RAG 和微调的区别是什么?各自适用什么场景?127. RAG 有哪些优点?有哪些局限性?130. RAG 在数据处理上会遇到哪些挑战?131. 从复杂 PDF 提取数据为什么难?如何解决?132. 结构化数据查询在 RAG 中为什么更难?如何处理?133. 文档切分中如何保持语义完整性(Semantic Chunking)?134. 如何选择合适的 chunk size?有什么最佳实践?135. 如何优化 RAG 的检索效果?136. 什么是混合检索(Hybrid Search)?如何结合稠密检索和稀疏检索?137. 什么是重排(Reranking)?常用的重排模型有哪些?140. 什么是上下文查询增强?如何处理无关问题?141. 为什么需要 RAG-Fusion?它解决了什么问题?153. 什么是向量数据库?它主要解决什么问题?154. FAISS 和 Milvus 各有什么特点?如何选型?155. ANN 算法有哪些?各有什么优缺点(HNSW / IVF / PQ)?156. Milvus 的索引类型有哪些?如何选择?157. 向量检索中如何平衡召回率和查询速度?
覆盖 RAG 基础、文档处理、检索优化、问题诊断、向量数据库,共 33 题(125-157)。
125. 什么是 RAG?RAG 的主要流程是什么?
👔面试官:解释一下 RAG,它的主要流程是什么?为什么需要它?
🙋♂️我:RAG 是检索增强生成,把检索到的相关文档和问题一起喂给模型就行了。
👔面试官:「喂给模型就行了」?文档是怎么存进去的?Embedding 怎么做的?离线和在线两个阶段各干了什么?你只说了结论,流程呢?
好,这道题看起来人人都能说几个字,但一追问流程细节,大多数人就露馅了。下面把两个阶段完整捋一遍。
💡 简要回答
RAG,全称 Retrieval-Augmented Generation,检索增强生成。它解决的核心问题是:LLM 的知识在训练完之后就冻住了,遇到私有数据或最新信息就答不上来。RAG 的思路是在生成答案之前,先去外部知识库检索相关内容,把检索结果和用户问题一起塞进 prompt,让 LLM 基于这些「参考资料」来回答,而不是靠死记硬背。
整个系统分离线和在线两个阶段:
- 离线阶段(索引构建):文档 → 清洗 → 切块(Chunking)→ 向量化(Embedding)→ 存入向量数据库,只做一次
- 在线阶段(检索生成):用户问题 → 向量化 → 相似度检索 → 召回 Top-K 片段 → 拼入 Prompt → LLM 生成答案,每次提问都跑一遍
📝 详细解析
LLM 为什么需要 RAG?
LLM 训练完之后,知识就固定了。训练截止日期之后发生的事它一无所知,你们公司内部的文档它更不可能知道。
那靠微调来更新知识行不行?理论上可以,但微调成本高、周期长,而且知识一旦写进参数,以后更新还得重新训练。RAG 走了一条完全不同的路:把知识存在外面,用的时候实时检索注入,不动模型本身。本质上是给 LLM 开了一个「开卷考试」的口子。
离线阶段:提前把知识建好
第一步:文档加载。把各种格式的原始数据读进来——PDF、Word、Markdown、网页都行,LangChain 的 DocumentLoader 基本上支持你能想到的所有格式。
第二步:切块(Chunking)。这是最容易被忽视却最影响效果的步骤。为什么不把整篇文档直接存进去?原因有两个:一是 Embedding 模型有输入长度限制,整篇塞不进去;二是把几千字压缩成一个向量,细节信息全被「平均掉」了,检索时根本定位不到具体那段话。所以要切成小片段,每个片段代表一段聚焦的语义内容。
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 每块最大 token 数
chunk_overlap=64, # 相邻块重叠,防止把完整语义从中间切断
separators=["\n\n", "\n", "。", "!", "?", " "]
)
chunks = splitter.split_documents(docs)第三步:向量化(Embedding)。把每段文字转成高维向量,比如 1536 维的浮点数列表。核心在于:语义相近的文本,在这个向量空间里的距离就近;语义不相关的,距离就远。「苹果手机怎么截图」和「iPhone 如何截屏」,用词完全不同,但向量会非常接近。这是语义检索的基础,比的是意思,不是关键词。
第四步:入库。把每个 chunk 的向量和原始文本一起存进向量数据库。向量负责「找到」,原文负责「阅读」,两者缺一不可。
python
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
vectorstore = FAISS.from_documents(chunks, OpenAIEmbeddings())
vectorstore.save_local("my_index")在线阶段:用户提问时实时检索
Query 处理:用户的问题往往口语化或者含有代词,直接拿去检索效果不好。实际工程里会加一步查询改写,让 LLM 把问题改写成更适合检索的形式。
向量检索(粗排):把用户问题也转成向量,去向量库里做相似度搜索,找出最近的 Top-K 个 chunk。这一步速度极快,百万量级的向量库也能在几十毫秒内返回,但精度有限。
Rerank(精排):用 Cross-Encoder 对粗排结果重新打分,过滤掉「看着近但其实不相关」的结果。粗排就像扫一眼书架,精排就是一本一本翻开看目录。
生成:把用户问题 + 精排后的 chunk 拼成 prompt,交给 LLM 生成最终答案。Prompt 里通常会明确告诉 LLM「只根据提供的资料回答,资料里没有就说不知道」,有效抑制乱编。
python
vectorstore = FAISS.load_local("my_index", OpenAIEmbeddings())
query = "公司的年假政策是什么?"
retrieved_docs = vectorstore.similarity_search(query, k=4)
context = "\n\n".join([doc.page_content for doc in retrieved_docs])
prompt = f"""请只根据以下背景信息回答问题,背景中没有的内容请说「不知道」。
背景信息:
{context}
问题:{query}
回答:"""
answer = llm(prompt)RAG 最核心的价值有两点:知识可以随时热更新,往知识库里加新文档就立刻生效,不需要重新训练模型;答案有溯源,每条回答都能追溯到来自哪个 chunk,可解释性强。这也是企业落地 AI 问答系统首选 RAG 的根本原因。
🎯 面试总结
面试官问这道题,最怕的是只说「检索+生成」五个字就完了。你要说清楚两件事:第一,RAG 解决的是什么问题——LLM 知识冻结、无法覆盖私有数据;第二,完整的两阶段流程——离线阶段(文档加载 → 切块 → 向量化 → 入库,只做一次),在线阶段(Query 改写 → 向量检索 → Rerank → 拼 prompt → 生成,每次提问都跑)。每个环节干什么、为什么需要,都要能说清楚。最后再补一句 RAG 的核心价值:知识可热更新、答案可溯源,面试官基本就没什么好追问的了。
126. RAG 和微调的区别是什么?各自适用什么场景?
👔面试官:什么时候用 RAG,什么时候用微调(Fine-tuning)?如何选择?
🙋♂️我:知识更新用 RAG,风格定制用微调。
👔面试官:就这一句话?那具体判断标准是什么?两者能不能结合?什么场景下需要同时用?
这道题的陷阱在于:很多人觉得这是个非此即彼的选择题,但实际工程里往往是两者互补,甚至三者叠加(CPT + SFT + RAG)。先搞清楚各自擅长什么,再说什么时候叠。
💡 简要回答
一张表说清楚核心差异:
| 维度 | RAG | 微调(Fine-tuning) |
|---|---|---|
| 知识更新 | 实时(改知识库即可) | 需重新训练 |
| 知识量 | 无上限(按需检索) | 受参数量限制 |
| 幻觉风险 | 低(有证据支撑) | 较高 |
| 来源可溯 | 支持 | 不支持 |
| 输出格式 | 不擅长 | 擅长(SFT 塑造) |
| 语言风格 | 不擅长 | 擅长 |
| 训练成本 | 无 | 高(GPU + 数据) |
| 推理延迟 | 增加(检索耗时) | 不增加 |
📝 详细解析
什么时候选 RAG
有一个判断标准特别好用:如果你的「知识」是数据,不是能力,选 RAG。
知识频繁更新:公司的规章制度每季度修订一次,产品文档每次发版都要更新,竞品信息随时在变。微调的话每次都要重新训练,RAG 只需要把新文档加进知识库,几分钟内就能生效。
知识量巨大:一个大型企业的内部知识库可能有几百万篇文档,没有任何 LLM 的参数能装得下这些知识。只能用 RAG,让模型在需要的时候去检索,而不是把所有东西都背下来。
需要来源可溯:法律、医疗、金融这类高风险场景,每个答案都要标注来源,用户要能验证。RAG 天然做到这一点,微调的模型说出来的话你根本不知道它从哪学来的。
开发速度优先:RAG 配合 LangChain 这类框架,几个小时内就能出第一个可演示的版本。微调至少需要准备数据集、调参、训练、评估,周期以天计。
什么时候选微调
有另一个判断标准:如果你想改变的是模型「怎么说话」而不是「知道什么」,选微调。
特定输出格式:你需要模型每次都输出一个结构严格的 JSON,或者固定模板的报告,或者严格的步骤编号。用 prompt 控制这件事并不稳定,微调可以让格式成为模型的「本能」。
特定语言风格:客服场景要求品牌口吻,技术文档要求简洁专业,内容创作要求某种文风。这些是模型的「表达方式」,不是知识,要用 SFT 数据来训练。
推理能力增强:某些专业领域有独特的推理模式,比如法律推理的论证结构、医学诊断的鉴别逻辑。这种「思维方式」不是往上下文塞几篇文档就能学会的,需要微调来改变模型的推理模式。
延迟极敏感:实时对话、游戏 NPC、语音交互场景,多一步检索就是明显的延迟感。如果知识量不大且相对稳定,可以全微调进参数,省去检索这一跳。
最优工程实践:三者叠加
真正成熟的 AI 应用往往是三层架构:
① Continue Pretraining(领域语言建模)
→ 让模型「读懂」领域语言(医学术语、法律条文、代码语法)
② SFT 微调(任务格式 + 风格对齐)
→ 让模型「学会」目标任务的回答方式和输出格式
③ RAG(实时知识注入)
→ 让模型「查到」最新的、最私有的具体知识三者分工明确,互不替代:CPT 管语言能力基础,SFT 管行为方式对齐,RAG 管知识内容时效。一个领域医疗问答系统,往往就是这三层叠起来的。
🎯 面试总结
判断选 RAG 还是微调,核心看一点:想改的是「知道什么」还是「怎么说话」。知识性的(时效性强、量大、要溯源)选 RAG;能力性的(输出格式、语言风格、推理模式)选微调;生产级应用往往是 CPT + SFT + RAG 三者叠加,各司其职。能画出这个三层架构并说清每层解决什么问题,面试官会觉得你真的做过完整的 AI 系统,不只是调过 API。
127. RAG 有哪些优点?有哪些局限性?
👔面试官:RAG 的优缺点分别是什么?
这道题不要只背优点,局限性才是面试官真正想考的——他想知道你有没有在生产环境里踩过坑,知不知道 RAG 不是万能药。
💡 简要回答
优点:知识实时更新、减少幻觉(有据可查)、答案可溯源、成本低(无训练开销)、可处理超长文档
局限:检索失败会放大错误(垃圾进垃圾出)、上下文窗口限制了能塞的片段数量、多跳推理很弱、表格和图表处理差
📝 详细解析
RAG 的四大优势
减少幻觉(Hallucination):LLM 编造事实,根本原因是训练时没有见过相关知识,只能靠参数的「模糊记忆」来猜。RAG 把真实文档塞进上下文,让模型「有据可查」,再配合 prompt 里的「只基于以下资料回答」,幻觉大幅降低。这是企业最关心的点,没有之一。
知识实时热更新:知识库更新只需要把新文档向量化之后存进去,立刻生效,不需要重新训练或微调 LLM。产品更新了一个功能、政策改了一条条款,知识库里改一下,系统的回答立刻就对了。
答案可溯源,可解释:可以在返回答案的同时附上「来自哪个文档的第几页」,用户和审核人员可以直接核对原文。在法律、医疗、金融这类高风险场景,可解释性是合规要求,不是加分项。
没有训练成本:不需要 GPU 集群,不需要准备标注数据,不需要等训练完成。从零开始搭一个 RAG 原型,一两天就能出来。
RAG 的四大局限
1. 垃圾进,垃圾出
这是最严重的局限。RAG 的质量上限取决于检索的质量。如果用户问的问题太模糊,或者文档切块策略不合理,检索召回的片段可能和问题毫无关系。更糟的是,LLM 拿到一堆无关片段之后,往往还是会「努力生成一个答案」,但这个答案根本没有任何依据,比不用 RAG 直接回答还糟糕。
2. 多跳推理能力弱
单次检索只能找到一个「知识点」,但很多问题需要跨多个文档综合推理。比如「A 公司的 CEO 本科就读于哪所大学」,你要先从一个文档里查出 CEO 的名字,再用这个名字去另一个文档里查他的教育背景,这种两步推理单次 RAG 很难做到。需要引入 Agent 或者 Query Decomposition 来处理。
3. 上下文窗口限制
Top-K=4,每个 chunk 512 token,加起来就是 2048 token,再加 System Prompt 和用户问题,8K 的 context window 很容易撑满。不够用了就要截断,而截断可能正好把关键信息丢掉。虽然现在 128K 的长 context 越来越普及,但对应的成本也成倍增加。
4. 表格、图表处理差
一个 PDF 里有一张数据表格,里面的数字和行列关系是有结构的。普通的文本 Embedding 会把这张表格当作一段普通文本来处理,完全丢失了「这个数字在哪一行、哪一列、和哪个标签对应」的结构信息。这是目前 RAG 的技术短板,需要专门的表格解析或者多模态模型来处理。
🎯 面试总结
优点四条:减幻觉(有据可查)、知识热更新(改库不改模型)、答案可溯源(高风险场景必备)、无训练成本(快速上线)。局限四条:垃圾进垃圾出(检索失败会放大错误)、多跳推理弱(需要 Agent 或 Query Decomposition 补充)、上下文窗口限制(塞进去的片段有上限)、表格图表处理差(结构信息丢失)。说出局限性并能给出对应的工程解法,面试官会觉得你真的在生产里用过 RAG。
128-129. RAG 的文档处理流程(ETL)是怎样的?
👔面试官:RAG 知识库构建的 ETL 流程是什么?每步有哪些关键点?
🙋♂️我:把文档切块,然后向量化存进去。
👔面试官:「切块」之前呢?「向量化」之后呢?元信息怎么处理?ETL 三步各干了什么?
RAG 的 ETL 经常被当成「顺手就做完的事」,但实际上这一阶段的工程质量直接决定整个 RAG 系统的效果上限。每步做什么、为什么要做,都要说清楚。
💡 简要回答
RAG 的 ETL 三步:
- E(Extract,抽取):从各种格式(PDF/Word/网页/数据库)中提取出可以被处理的文本
- T(Transform,转换):清洗噪声、按语义切块(Chunking)、附加元信息标注
- L(Load,加载):向量化(Embedding)后存入向量数据库
📝 详细解析
Extract:多格式文档解析
这一步听起来简单,但往往是藏坑最多的地方。不同格式的文档,解析难度天差地别:
- 普通文本/Markdown:直接读,没什么难度
- Word 文档:用 python-docx 读,基本上解决了
- 网页:用 BeautifulSoup 或 Playwright 抓,注意动态加载的内容
- PDF:最麻烦,下一题专门讲
python
from langchain.document_loaders import PyMuPDFLoader, WebBaseLoader, Docx2txtLoader
docs_pdf = PyMuPDFLoader("report.pdf").load()
docs_web = WebBaseLoader("https://example.com/docs").load()
docs_word = Docx2txtLoader("manual.docx").load()Transform:清洗 + 切块 + 元信息标注
清洗:原始文档里充满了噪声——PDF 提取出的页眉页脚、反复出现的「如有疑问请联系 xxx」、多余的空白行。不清洗直接切块,噪声会混进向量库,污染后续检索。
python
import re
def clean_text(text):
text = re.sub(r'\n{3,}', '\n\n', text) # 合并多余空行
text = re.sub(r'第 \d+ 页', '', text) # 去掉页码
text = re.sub(r'\s{2,}', ' ', text) # 合并多余空格
return text.strip()切块(Chunking):清洗完的文档通常很长,不能整篇存进向量库,要切成小片段。RecursiveCharacterTextSplitter 是最常用的方案,它会按优先级依次尝试不同的分隔符(段落 → 句子 → 词),尽量在「自然边界」处切,而不是在一个词中间截断。
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 每块最大 token 数
chunk_overlap=64, # 相邻块重叠 64 token,防止把连贯语义从中间切断
separators=["\n\n", "\n", "。", "!", "?", " "]
)
chunks = splitter.split_documents(docs)元信息标注:为每个 chunk 附加 metadata,是非常值得投入的优化。元信息不参与向量相似度计算,但可以在检索时做精确过滤,把检索范围精准缩小。
python
for chunk in chunks:
chunk.metadata.update({
"source": "HR_Policy_2024.pdf",
"department": "HR",
"doc_type": "policy",
"created_date": "2024-01-15",
})Load:向量化 + 入库
向量化选模型时要注意:中文场景用 BGE 系列(BAAI/bge-large-zh-v1.5)效果远好于通用的 OpenAI Embedding;如果是英文或者预算充足,OpenAI 的 text-embedding-3-large 效果也非常好。
python
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Milvus
embed_model = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5")
vectorstore = Milvus.from_documents(
chunks,
embed_model,
collection_name="company_docs",
connection_args={"host": "localhost", "port": 19530}
)入库的同时,元信息也一并存进去,检索时就可以按元信息过滤:
python
results = vectorstore.similarity_search(
query, k=4,
filter={"department": "HR"} # 只在 HR 部门文档中检索,不污染结果
)元信息过滤可以把检索范围缩小 90% 以上,大幅提升精准度和速度。这是很多人搭 RAG 时忽视的优化点,但效果立竿见影。
🎯 面试总结
RAG ETL 三步说不清楚是大忌。E(抽取):不同格式难度不同,PDF 最复杂;T(转换):清洗噪声 → 切块(RecursiveCharacterTextSplitter,chunk_overlap 保持连贯)→ 元信息标注(支持过滤检索);L(加载):Embedding 模型选型 + 存入向量库。元信息标注这个细节如果能主动说出来,面试官会认为你真的搭过生产级 RAG,不只是看过教程。
130. RAG 在数据处理上会遇到哪些挑战?
👔面试官:构建 RAG 知识库时,数据处理环节有哪些难点?
实际做过 RAG 项目的人,一定踩过数据处理的坑。这道题考的是你的工程经验,不是背书。
💡 简要回答
数据处理五大挑战:
- 多格式文档解析:PDF 表格、图片、扫描件,每一种都有不同的坑
- 切块粒度的两难困境:块太大噪声多、块太小上下文不完整
- 数据质量问题:重复文档、过时版本、低质量来源
- 专业术语和多语言:通用 Embedding 对领域术语表示不准
- 结构化数据:数据库和表格无法用向量检索,需要单独处理
📝 详细解析
PDF 解析陷阱:PDF 格式复杂到超乎想象。一份「普通」的 PDF 可能有:扫描图片(没有文字层,要 OCR)、多栏排版(按行读取会把左右两栏的内容交叉混合)、嵌入式表格(提取出来是一堆乱序的数字,行列关系全丢了)、带水印的页面(OCR 识别出来全是噪声)。不同类型需要不同的工具,不能一套方案通吃。
切块粒度困境:chunk_size=128 时,一句话可能被截断,检索到了也看不出语义;chunk_size=2048 时,一块包含多个主题,Embedding 把所有语义「平均」进去,检索时什么都能匹配上但什么都不精确。这个问题没有绝对的最优解,要根据文档类型和查询特点调。解决方法是「小块检索、大块返回」的 Parent Document Retrieval,下文会详细讲。
数据质量问题:知识库里同时存在「HR_Policy_v1.pdf」和「HR_Policy_v2.pdf」,两份文件说的是不同版本的规定,甚至互相矛盾。检索时两个都被召回,LLM 拿到矛盾信息,生成的答案也是矛盾的。需要做文档去重和版本管理,确保知识库里只有最新有效的版本。
专业术语问题:通用 Embedding 模型用通用语料训练,对「HNSW 算法」「CRS 综合征」「EBITDA」这类专业术语的语义表示往往不准。解决方法:用领域微调的 Embedding 模型,或者用混合检索中的 BM25 做关键词精确匹配来补充。
结构化数据问题:数据库和 Excel 表格里存的是数字和关系,不适合语义检索。「2023年Q4华南区的销售额」这个问题需要精确查询,不需要语义理解。这类问题应该走 Text-to-SQL 路由,不走向量检索。
🎯 面试总结
数据处理挑战五点:PDF 解析(多种格式各有坑,扫描件要 OCR,表格要专门提取)、切块粒度(用 Parent Document Retrieval 同时保证精度和完整性)、数据质量(去重 + 版本管理,防止矛盾知识入库)、专业术语(领域 Embedding + 混合检索 BM25 补充)、结构化数据(Text-to-SQL 路由,不走向量检索)。能说出每个挑战的具体对策,说明你真正做过数据处理。
131. 从复杂 PDF 提取数据为什么难?如何解决?
👔面试官:为什么说 PDF 解析是 RAG 工程里的难点?你是怎么处理的?
💡 简要回答
复杂 PDF 解析难在三类问题:
- 表格:提取出的文字丢失了行列结构,一堆数字没有上下文
- 多栏排版:文本读取顺序混乱,两栏 PDF 按行读取会把左右栏交叉
- 扫描件/图片 PDF:根本没有文字层,全是图片,需要 OCR 识别
分级处理策略:有文本层用 PyMuPDF,表格密集用 pdfplumber,扫描件用 PaddleOCR,超复杂文档用 Azure Document Intelligence。
📝 详细解析
为什么 PDF 这么难?
PDF 格式设计的初衷是「打印排版」,不是「方便机器读取」。里面存的不是「这是一段文字,那是一个表格」,而是一堆「在坐标 (x, y) 处画一个字符」的指令。从 PDF 里重新恢复文字的结构,是个逆向工程问题。
python
# 有文本层的 PDF:PyMuPDF 最快
import fitz # PyMuPDF
doc = fitz.open("report.pdf")
for page in doc:
text = page.get_text("text") # 提取文本,但可能顺序混乱
# 表格密集的 PDF:pdfplumber 保留结构
import pdfplumber
with pdfplumber.open("report.pdf") as pdf:
for page in pdf.pages:
tables = page.extract_tables()
for table in tables:
# 转成 Markdown 表格格式存储,保留行列关系
header = "| " + " | ".join(str(c) for c in table[0]) + " |"
separator = "| " + " | ".join(["---"] * len(table[0])) + " |"
rows = ["| " + " | ".join(str(c or "") for c in row) + " |" for row in table[1:]]
md_table = "\n".join([header, separator] + rows)扫描件 OCR 处理
python
# 扫描件:PaddleOCR(中英文都支持)
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=True, lang='ch')
result = ocr.ocr("scanned.pdf", cls=True)
text = "\n".join([line[1][0] for line in result[0]])超复杂文档:云端文档 AI
对于混合了文字、表格、图表、印章的复杂文档(如合同、财报),可以用云端的文档理解服务。Azure Document Intelligence、AWS Textract 这类服务能识别文档结构(标题层级、表格、段落),提取结果的质量远超本地工具,缺点是有 API 调用成本和数据隐私风险。
🎯 面试总结
PDF 解析的坑:格式本身为打印设计,机器读取是逆向工程。三类难题对应三种方案:有文本层 → PyMuPDF(快,但可能顺序乱);表格密集 → pdfplumber(保留行列结构,转 Markdown 存储);扫描件 → PaddleOCR。表格转 Markdown 格式存储这个工程技巧,面试官听了会觉得你踩过坑,有实际经验。
132. 结构化数据查询在 RAG 中为什么更难?如何处理?
👔面试官:如果知识库里有 Excel 表格或者数据库,该怎么处理?为什么向量检索不适合?
💡 简要回答
结构化数据(数据库、Excel)的核心问题:用户问的是精确的数字和关系,向量相似度搜索是找「语义相近」的,两者根本不是同一件事。
「2023年Q4各地区销售额对比」这个问题需要精确查询数据,不需要语义理解。往向量库里塞表格,既丢失了数值精度,又浪费了向量语义能力。
正确解法:Text-to-SQL——让 LLM 把自然语言问题直接翻译成 SQL,去数据库精确查询,拿到结果后再交给 LLM 组织成自然语言答案。
📝 详细解析
python
# Text-to-SQL(LangChain SQLDatabaseChain)
from langchain import SQLDatabase, SQLDatabaseChain
from langchain.chat_models import ChatOpenAI
db = SQLDatabase.from_uri("sqlite:///sales.db")
db_chain = SQLDatabaseChain.from_llm(
ChatOpenAI(temperature=0), # temperature=0,减少 SQL 的随机性
db,
verbose=True
)
result = db_chain.run("2023年Q4各地区的销售额分别是多少?")
# LLM 内部生成:SELECT region, SUM(revenue) FROM sales WHERE quarter='2023Q4' GROUP BY region
# 执行 SQL,把查询结果返回给 LLM 组织答案混合路由:现实系统的标配
现实中的用户问题往往同时涉及两种数据:「这个季度华南区的销售超标了,根据公司的激励政策,销售团队能拿到什么奖励?」——前半句需要查销售数据库,后半句需要检索激励政策文档。
python
def route_query(question):
routing_prompt = f"""
判断以下问题需要哪种数据源:
A. 结构化数据(数字/统计/排名/比较)→ SQL 查询
B. 非结构化文档(政策/知识/解释/背景)→ 向量检索
C. 两者都需要 → 混合查询
问题:{question}
答案(只输出 A/B/C):
"""
route = llm(routing_prompt).strip()
if route == "A":
return text_to_sql(question)
elif route == "B":
return rag_search(question)
else:
sql_result = text_to_sql(question)
rag_result = rag_search(question)
return synthesize(question, sql_result, rag_result)🎯 面试总结
结构化数据不走向量检索,因为数值/关系查询需要精确匹配,向量相似度是近似匹配,两者逻辑不同。解决方案:Text-to-SQL(LLM 把自然语言转 SQL,精确查数据库)。生产系统往往需要智能路由:纯数字问题走 SQL,纯知识问题走 RAG,复合问题两路并行再汇总。能说出这个路由设计,面试官会觉得你做过真实的 NL2Data 系统。
133. 文档切分中如何保持语义完整性(Semantic Chunking)?
👔面试官:切块时如何保证语义完整性?Semantic Chunking 是什么原理?
🙋♂️我:就是按语义切,不按字符数切。
👔面试官:「按语义切」怎么实现的?和固定大小切块的区别在哪?chunk_overlap 有什么用?
切块策略是 RAG 最底层的工程决策之一,选错了后面所有优化都是白费力气。
💡 简要回答
固定大小切块的问题:按字符数死切,不管文本语义边界,可能把同一句话切成两半,也可能把完全不相关的两段内容切进同一个 chunk。
Semantic Chunking:对每个句子计算 Embedding,计算相邻句子的余弦相似度,相似度骤降的地方说明「话题发生了跳转」,在那里切——每个 chunk 内部语义都是连贯的。
📝 详细解析
四种切块策略对比
| 策略 | 方法 | 适合场景 | 缺点 |
|---|---|---|---|
| 固定大小 | 每 512 字符强制切 | 简单测试 | 会截断句子,破坏语义 |
| 递归字符切分 | 按分隔符(\n\n→\n→句号)递归切 | 通用场景的生产首选 | 块大小不均匀 |
| Semantic Chunking | 按语义相似度跳变切割 | 质量要求高的场景 | 需要额外 Embedding 计算,慢 |
| 文档结构切分 | 按标题/章节切 | 有清晰标题结构的文档 | 依赖文档格式 |
Semantic Chunking 实现原理
python
from langchain_experimental.text_splitter import SemanticChunker
from langchain.embeddings import OpenAIEmbeddings
splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95 # 相似度低于第 95 百分位时,判定为语义跳变,切割
)
# 内部工作流程:
# 1. 把文本拆成句子列表
# 2. 对每个句子计算 Embedding
# 3. 计算相邻句子的余弦相似度
# 4. 找出相似度骤降的位置(语义跳变点)
# 5. 在语义跳变点处切割,每块内部语义连贯
chunks = splitter.create_documents([long_text])举个例子:一篇文章前五句讲「深度学习的历史」,第六句突然开始讲「GPU 加速的原理」。固定大小切块可能把第五句和第六句切进同一个 chunk,导致这个 chunk 语义混杂。Semantic Chunking 则会检测到第五和第六句之间的语义距离骤增,精确地在那里切开。
chunk_overlap 不可省略
python
# 为什么需要 chunk_overlap?
#
# 考虑这段文字被切成两块:
# 块1:[...公司 A 于 2020 年完成了 B 轮融资,首席执行官王某表示...]
# 块2:[...将继续深耕企业服务市场,目标是三年内 IPO。]
#
# 如果用户问「王某对公司未来的计划是什么」,
# 「王某」在块1,「未来计划」在块2,单独检索任何一块都回答不完整。
#
# chunk_overlap=64 让两块重叠:
# 块1:[...王某表示将继续深耕企业服务市场...] ← 包含了完整语义
# 块2:[王某表示将继续深耕...目标是三年内 IPO。]建议 chunk_overlap 设为 chunk_size 的 10%~20%,太大会浪费向量库空间,太小起不到保护作用。
🎯 面试总结
切块策略从简到好:固定大小(不推荐)→ RecursiveCharacterTextSplitter(生产通用首选,按分隔符递归切,尊重自然边界)→ Semantic Chunking(质量最高,按语义跳变切,代价是多一次 Embedding 计算)。chunk_overlap 是防止跨块信息丢失的关键参数,建议 10~20% 的 chunk_size。能说出 Semantic Chunking 的工作原理(计算相邻句子相似度,跳变处切割),面试官会认为你对 RAG 细节理解到位。
134. 如何选择合适的 chunk size?有什么最佳实践?
👔面试官:RAG 中 chunk size 怎么设置?有什么依据?
没有「最优的 chunk size」这回事,这是个需要根据多个因素综合判断的工程决策。面试官想听到的是你的判断框架,不是一个固定数字。
💡 简要回答
chunk size 的选择要看三个维度:Embedding 模型的推荐输入长度、LLM 的 context window、查询问题的粒度。不同场景的经验值:
| 文档类型 | 推荐 chunk size | 原因 |
|---|---|---|
| FAQ / 知识库 | 128~256 token | 问答对本身就很短,小块精确 |
| 技术文档 / 代码 | 256~512 token | 细粒度,精确匹配代码逻辑 |
| 新闻 / 文章 | 512~1024 token | 保留段落完整性 |
| 法律 / 合同 | 1024~2048 token | 条款上下文必须完整 |
📝 详细解析
判断框架:三个约束条件
约束一:Embedding 模型的推荐长度
BGE-large-zh 的训练序列长度是 512 token,输入超过这个长度,模型会截断或质量下降。OpenAI text-embedding-3-small 支持更长,但也有推荐最优范围。chunk_size 必须在 Embedding 模型的「舒适区」内,超出之后向量质量线性下降。
约束二:LLM 的 context window 上限
Top-K=4,chunk_size=1024,4 个 chunk 就是 4096 token。再加上 system prompt 和用户问题,很容易撑满一个 8K 的 context window,剩下没有空间了。反过来,如果你用的是 Claude 200K 或者 GPT-4 128K,可以适当放宽 chunk_size。
约束三:查询问题的粒度
「第 3.2.1 条款的违约金是多少?」→ 这是极具体的问题,小 chunk 精确匹配;「请总结这份合同的主要风险点」→ 这需要综合多个条款,大 chunk 保持完整语义。如果你的业务场景两种问题都有,就要用下面这个方案来解决两难困境。
最优工程方案:Small-to-Big Retrieval
小 chunk 精度高但上下文不完整,大 chunk 上下文完整但精度差。解决方法:用小块检索定位,返回对应的大块给 LLM。
python
from langchain.retrievers import ParentDocumentRetriever
retriever = ParentDocumentRetriever(
vectorstore=child_vectorstore, # 小块(128 token)存向量库,用于检索定位
docstore=parent_docstore, # 大块(512 token)存文档库,用于返回给 LLM
child_splitter=RecursiveCharacterTextSplitter(chunk_size=128),
parent_splitter=RecursiveCharacterTextSplitter(chunk_size=512),
)
# 检索时:用 128 token 的小块做精准语义匹配
# 找到之后:返回对应的 512 token 大块给 LLM,保证上下文完整
docs = retriever.get_relevant_documents("第 3.2.1 条款违约金")这个方案的精妙之处在于:检索精度由小块保证,生成质量由大块保证,两者互不干扰。
🎯 面试总结
chunk size 选择不是拍脑袋决定的,要看三个约束:Embedding 模型推荐长度(BGE 系列通常 512 以内)、LLM context window(Top-K × chunk_size 不超过 context limit)、查询粒度(具体问题配小块,宏观问题配大块)。两难困境的最优解是 Small-to-Big Retrieval(小块检索定位,大块返回上下文),能说出这个方案,面试官会认为你真的解决过 RAG 的工程问题。
135. 如何优化 RAG 的检索效果?
👔面试官:RAG 上线之后检索效果不好,有哪些优化手段?
这道题是 RAG 工程里含金量最高的一道,考察你从索引到检索到生成的全链路优化思路。能说出五个方向的人,面试官会觉得你真的在生产环境里调过 RAG。
💡 简要回答
RAG 检索优化的五个方向,优先级从高到低:
- Reranking(精排):投入产出比最高,Cross-Encoder 对粗排结果重新打分
- 混合检索(Hybrid Search):稠密向量 + 稀疏 BM25 并行检索,覆盖关键词和语义两种场景
- 查询端优化:HyDE、多查询扩展、查询重写,提高召回率
- 索引优化:更好的切块策略(Semantic Chunking / Small-to-Big)、元信息标注
- 上下文压缩:去掉检索片段中的无关句子,压缩后再喂给 LLM
📝 详细解析
Reranking:最值得优先做的优化
粗排(向量相似度检索)快但不够精。Bi-Encoder 是独立编码 query 和 doc,两者没有交互;Cross-Encoder 把 query+doc 拼在一起联合编码,query 的每个词都能和 doc 的每个词互相 Attention,精度高很多。
实际做法:向量检索召回 Top-50,Cross-Encoder 精排选 Top-5 喂给 LLM。召回阶段保广度,精排阶段保精度。
python
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-large") # 中文首选
# 精排:对粗检结果打分重排
pairs = [(query, doc.page_content) for doc in retrieved_docs]
scores = reranker.predict(pairs) # 每对 (query, doc) 一个相关性分数
# 按分数重新排序,取 Top-5
ranked = sorted(zip(scores, retrieved_docs), reverse=True)[:5]HyDE:解决「问题向量和答案向量不对齐」的问题
用户问的问题是「量子计算对密码学有什么威胁?」,而知识库里存的文档是「量子计算机利用 Shor 算法可以在多项式时间内分解大整数…」。这两段文字的语义方向不同——一个是疑问句,一个是陈述句——直接用问题向量去检索答案向量,天然有 gap。
HyDE 的解法:先让 LLM 凭空生成一个「假设性答案」,这个答案不需要准确,只要和真实答案在语义空间里大致对齐就行,再用这个假设答案的向量去检索。
python
question = "量子计算对密码学有什么威胁?"
hypothetical_answer = llm(f"请用一段话简要回答:{question}")
# → "量子计算机利用 Shor 算法能在多项式时间内破解 RSA,Grover 算法能把对称加密的安全强度折半..."
# 用假设答案的向量去检索(答案找答案,语义更对齐)
results = vectorstore.similarity_search(hypothetical_answer, k=4)MMR:避免检索结果全是「同一份文档的相邻段落」
普通相似度检索会召回一堆高度相似的结果——可能都是同一篇文档的第 3、4、5 段,覆盖的信息量其实非常有限。MMR(最大边际相关性)在保持相关性的同时,尽量让每个被选中的结果都和已选集合不同,最大化信息多样性。
python
results = vectorstore.max_marginal_relevance_search(
query,
k=4, # 最终返回 4 个结果
fetch_k=20, # 先检索出 20 个候选
lambda_mult=0.7 # 0.7 偏向相关性,0.3 偏向多样性
)🎯 面试总结
RAG 检索优化的优先级:先做 Reranking(Cross-Encoder 精排,效果立竿见影)→ 再做混合检索(Dense+BM25,关键词场景大幅提升)→ 再做查询优化(HyDE/多查询,提高召回率)→ 最后做索引和上下文压缩。能说出这个优先级顺序,说明你知道哪些事情「投入少但效果大」,这是真正的工程思维,不是理论罗列。
136. 什么是混合检索(Hybrid Search)?如何结合稠密检索和稀疏检索?
👔面试官:混合检索是什么?稠密检索和稀疏检索各有什么优缺点?两种结果怎么合并?
有个现象很普遍:搭了向量检索之后,用户一问到产品型号、人名、缩写这类词,召回就变得很差。原因很简单——这正是 Embedding 的弱点。混合检索就是来补这个缺口的。
💡 简要回答
- 稠密检索(Dense):用 Embedding 向量做相似度搜索,擅长「语义相近」的匹配——「汽车」能匹配「车辆」,「手机」能匹配「移动电话」
- 稀疏检索(Sparse):用 BM25 做关键词词频匹配,擅长精确匹配——「iPhone 15 Pro Max」就是「iPhone 15 Pro Max」,不会把「华为 Mate 60」也匹配上来
- 混合检索:两路并行检索,用 RRF(倒数排名融合)合并结果,各取所长
📝 详细解析
为什么单靠 Embedding 不够?
Embedding 的「语义理解」是把双刃剑。它能把近义词拉近,但也会把不该近的东西拉近。问「GPT-4 的 context window 是多少」,Embedding 可能把「GPT-3.5 的 context window 是多少」的答案排到前面,因为语义太相似了。而 BM25 只看词频,「GPT-4」这个精确字符串就是最强的匹配信号。
两者的互补关系:
| 方法 | 擅长 | 不擅长 |
|---|---|---|
| 稠密检索(Embedding) | 语义理解、近义词、跨语言 | 专有名词、型号、代码、精确匹配 |
| 稀疏检索(BM25) | 关键词精确匹配、专有名词 | 语义理解、同义词 |
RRF 合并:不依赖绝对分数,只看排名
合并两路检索结果的难点是:向量相似度分数(0.87)和 BM25 分数(12.3)完全不在同一量纲,没法直接相加。
RRF 的解法非常优雅:不看分数,只看排名。对每个文档,把它在每路检索结果中的排名分别换算成 1/(k+rank) 的贡献分,求和作为最终分数。排名越靠前贡献越大,在多路检索中都靠前的文档得分最高。
python
def reciprocal_rank_fusion(results_list, k=60):
"""
k=60 是经验值,用于平滑排名靠前和靠后的差异
score(doc) = Σ 1/(k + rank_i)
"""
doc_scores = {}
doc_map = {}
for results in results_list:
for rank, doc in enumerate(results, start=1):
doc_id = doc.metadata["id"]
doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1.0 / (k + rank)
doc_map[doc_id] = doc
ranked_ids = sorted(doc_scores, key=doc_scores.get, reverse=True)
return [doc_map[doc_id] for doc_id in ranked_ids]
# 混合检索:两路并行,RRF 合并
dense_results = vectorstore.similarity_search(query, k=20)
sparse_results = bm25_retriever.get_relevant_documents(query)[:20]
final_results = reciprocal_rank_fusion([dense_results, sparse_results])在两路结果中都排名靠前的文档,RRF 会给它最高分,这正是「两种方法都认为它相关」的文档,被选中的置信度最高。
🎯 面试总结
混合检索解决的是 Embedding 单打不了专有名词匹配的问题。实现方式:Dense(Embedding 语义)+ Sparse(BM25 关键词)并行检索,RRF 合并排名(score = Σ 1/(k+rank_i),不依赖分数量纲)。实验数据上,混合检索比纯向量检索在含专有名词的查询上提升 5~15%。能手写 RRF 合并逻辑,面试官会认为你不只是知道概念,真的实现过。
137. 什么是重排(Reranking)?常用的重排模型有哪些?
👔面试官:RAG 中为什么需要 Reranking?Bi-Encoder 和 Cross-Encoder 有什么区别?
这道题要说清楚一件事:Reranking 不是第二次检索,它是对第一次检索结果的重新打分——用更精准但更慢的模型,对粗排召回的候选列表重新排个序。
💡 简要回答
RAG 检索分两阶段:
- 召回(粗排):Bi-Encoder 快速从大量文档中召回 Top-50,每个文档的向量是离线预计算好的,检索时只算 query 向量,然后做相似度排序,速度极快
- 精排(Reranking):Cross-Encoder 对粗排的 Top-50 重新打分,选出最终 Top-5 喂给 LLM,精度高但慢(每次都要对 query+doc 对联合计算)
为什么需要两阶段?因为精度和速度是矛盾的。精排模型太慢,扛不住大规模召回;粗排模型太快,精度不够。粗排保广度,精排保精度。
📝 详细解析
Bi-Encoder vs Cross-Encoder:精度差异从哪来
python
# Bi-Encoder(双塔):query 和 doc 完全独立编码,互相不知道对方
query_emb = embed_model.encode(query) # query 的向量
doc_emb = embed_model.encode(doc) # doc 的向量(可以离线预计算)
score = cosine_similarity(query_emb, doc_emb)
# Cross-Encoder:query+doc 拼在一起,送进同一个 Transformer,互相 Attention
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-large")
score = reranker.predict([(query, doc)]) # 无法预计算,每次都要重算Cross-Encoder 精度更高的根本原因:query 里的每个词都能和 doc 里的每个词做 Attention,能捕捉到细粒度的相关性信号(比如 query 里提到「副作用」,doc 里的「不良反应」能被匹配上),而不只是整体向量的余弦相似度。
代价是无法预计算(query 不固定,query+doc 的组合要临时计算),所以只能用在候选数量少(几十个)的精排阶段。
常用 Reranking 模型
| 模型 | 特点 | 推荐场景 |
|---|---|---|
| BAAI/bge-reranker-large | 中英双语,开源,效果强 | 中文 RAG 生产首选 |
| Cohere Rerank API | 云端服务,集成简单 | 不想自己部署模型 |
| cross-encoder/ms-marco-MiniLM-L6 | 轻量,英文,延迟低 | 英文场景、资源受限 |
完整的两阶段检索流程
用户提问
↓
Bi-Encoder 粗排(从 10 万文档中召回 Top-50,~50ms)
↓
Cross-Encoder 精排(对 50 个候选重打分,选 Top-5,~200ms)
↓
上下文压缩(可选:去掉 Top-5 中与问题无关的句子)
↓
LLM 生成答案(基于精排后的 Top-5)总延迟约 250ms,精度接近理论上限,这是生产 RAG 系统的标准配置。
🎯 面试总结
Reranking 是「召回后再精排」,不是第二次全量检索。核心区别:Bi-Encoder 独立编码(可预计算,快,精度有限)→ Cross-Encoder 联合编码(不能预计算,慢,精度高)。中文首选 bge-reranker-large,能手写完整的两阶段流程,说清楚精度差异的原因(联合 Attention),面试官会认为你真的懂检索。
138-139. 什么是多查询扩展和查询重写?
👔面试官:多查询扩展和查询重写分别是什么?有什么用?
用户原始问的问题,往往不是最适合检索的形式。这两个技术都是在「用户提问」和「进入向量库检索」之间做的预处理,目的是让检索更准、召回率更高。
💡 简要回答
查询重写(Query Rewriting):把用户原始问题改写成更适合检索的形式——去掉代词(「它」→ 具体名词)、补充对话上下文、拆解复杂多跳问题。
多查询扩展(Multi-Query):用 LLM 从不同角度生成 3~5 个相关查询,分别检索后合并去重,提高召回率——一个视角可能召回不到的文档,换个角度问就召回了。
📝 详细解析
查询重写:最常见的场景是多轮对话指代消解
多轮对话中用户往往用代词指代之前提到的东西。「它」「这个」「上面说的那个」直接拿去检索,向量库根本不知道你在问什么。
python
# 多轮对话指代消解示例
history = [
("你有没有 LangChain 的教程?", "有的,LangChain 是一个 LLM 应用开发框架..."),
("它和 LlamaIndex 有什么区别?", "...") # ← 「它」指的是 LangChain,但直接检索「它」没有意义
]
rewrite_prompt = f"""
对话历史:
{format_history(history)}
当前问题:它和 LlamaIndex 有什么区别?
请将当前问题改写为独立的完整问题,不含任何代词(参考对话历史解析指代):
"""
rewritten = llm(rewrite_prompt)
# → "LangChain 和 LlamaIndex 有什么区别?"多查询扩展:用不同视角覆盖更多相关文档
同一个问题可以从多个角度来问,不同角度的查询会命中知识库里不同的文档。多查询扩展通过生成多个等价查询来提高召回的覆盖面。
python
from langchain.retrievers.multi_query import MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=llm
)
# 原始问题:"深度学习为什么比传统机器学习更强大?"
# LLM 生成的多个查询视角:
# 1. "深度学习和传统机器学习的核心区别是什么?"
# 2. "神经网络相比 SVM、随机森林的优势在哪里?"
# 3. "深度学习在哪些任务上超越了传统方法?"
# 三路并行检索,合并去重,最终结果覆盖了更多维度
docs = retriever.get_relevant_documents("深度学习为什么比传统机器学习更强大?")多查询扩展的代价:每个扩展查询需要一次检索,N 个查询就是 N 次检索,延迟线性增加。通常扩展到 3~5 个查询是合理的,太多会增加延迟,且边际效益递减。
🎯 面试总结
查询重写和多查询扩展都是「检索前预处理」,解决的是不同问题。查询重写:原始问题质量差(有代词、依赖上下文、口语化)→ 改写成检索友好的独立完整问题。多查询扩展:单一查询视角窄,召回不全面 → 多角度生成多个查询,并行检索合并,提高召回覆盖。两者配合使用效果最好:先用查询重写消解代词,再用多查询扩展提高召回率。
140. 什么是上下文查询增强?如何处理无关问题?
👔面试官:多轮对话 RAG 里,如何保证每次检索都有正确的上下文?知识库里没有答案怎么处理?
💡 简要回答
上下文查询增强:在多轮对话中,把历史对话的关键信息融入到当前检索 query 里,让每次检索都是基于完整上下文的独立问题,不会因为指代消解问题导致检索失败。
无关问题处理(Relevance Gating):对检索结果做相关性阈值判断,如果最高分文档和问题的相关性低于阈值,宁可拒绝回答,也不能用不相关的文档强行生成一个听起来像样但实际乱说的答案。
📝 详细解析
上下文增强:多轮对话 RAG 的必要处理
python
def contextual_query_enhancement(history, current_query):
if not history:
return current_query # 第一轮直接用
prompt = f"""
对话历史(最近 3 轮):
{format_history(history[-3:])}
当前问题:{current_query}
请将当前问题改写为一个独立的完整问题,解析所有代词和指代,
使它不依赖对话历史就能理解:
"""
return llm(prompt)
# 使用示例
enhanced_query = contextual_query_enhancement(chat_history, "那它的价格怎么样?")
# → "华为 Mate 60 Pro 的价格是多少?"(根据对话历史解析出「它」=华为 Mate 60 Pro)
retrieved_docs = vectorstore.similarity_search(enhanced_query, k=4)无关问题的兜底:相关性门控
RAG 系统里有一个很常见的错误设计:不管检索到什么,都把结果塞进 prompt 让 LLM 生成答案。用户问了一个知识库里根本没有的问题,LLM 拿到一堆毫不相关的文档,仍然能「很自信地」生成一个听起来合理但完全是编造的答案。这比不用 RAG 还糟糕。
正确做法:在生成之前,先判断检索结果和问题的相关性,分数太低直接拒绝。
python
def rag_with_relevance_gate(query, retrieved_docs, threshold=0.4):
relevance_scores = reranker.predict(
[(query, doc.page_content) for doc in retrieved_docs]
)
max_score = max(relevance_scores)
if max_score < threshold:
return "抱歉,我在知识库中没有找到与您问题相关的信息,建议联系人工客服进一步咨询。"
relevant_docs = [
doc for doc, score in zip(retrieved_docs, relevance_scores)
if score >= threshold
]
return generate_answer(query, relevant_docs)阈值设多少取决于业务场景:对准确性要求很高(如法律、医疗)阈值要高(0.5~0.7),宁可多拒绝;通用问答场景可以低一些(0.3~0.4),多回答但降低准确性要求。
🎯 面试总结
两点核心设计:上下文增强(每轮对话都要重写 query,把代词解析成具体名词,让每次检索都是独立完整的问题);相关性门控(检索分数太低就拒绝生成,「宁可不答,也不乱答」是生产 RAG 的基本原则)。能说出相关性门控的阈值设计和取舍逻辑,面试官会认为你考虑过 RAG 的健壮性,不只是能跑通 happy path。
141. 为什么需要 RAG-Fusion?它解决了什么问题?
👔面试官:RAG-Fusion 是什么?它和普通 RAG 有什么区别?
💡 简要回答
RAG-Fusion 是「多查询扩展 + RRF 融合」的组合方案,解决的是单一查询视角狭窄导致召回不全的问题。
流程:LLM 从不同角度生成 N 个查询 → 每个查询独立检索 Top-K → RRF 合并 N 组结果重排 → 最终 Top-K 喂给 LLM 生成。
核心改进:和多查询扩展相比,RAG-Fusion 增加了 RRF 合并这一步,能系统地去掉多路检索中的噪声,而不是简单拼接去重。
📝 详细解析
python
def rag_fusion(original_query, k=4):
# 1. 生成多个查询(包括原始问题)
expanded_queries = generate_queries(original_query, n=3)
all_queries = [original_query] + expanded_queries
# → ["深度学习和机器学习的区别",
# "神经网络相比传统算法有哪些优势",
# "深度学习擅长解决哪类问题",
# "为什么深度学习在图像识别上效果好"]
# 2. 每个查询独立检索
all_results = []
for q in all_queries:
results = vectorstore.similarity_search(q, k=10)
all_results.append(results)
# 3. RRF 合并:在多路检索中都排名靠前的文档,得分最高
fused = reciprocal_rank_fusion(all_results)
return fused[:k]为什么 RRF 比简单合并更好:如果只是把多路结果拼起来去重,每路的噪声都会保留下来,反而增加了干扰。RRF 的设计是:一篇文档只有在多路检索中都排名靠前时,才能得到高分。偶尔出现在某一路结果尾部的噪声文档,RRF 分数会很低,会被自动过滤。这是一种隐式的投票机制。
代价:N 个查询 = N 次 LLM 调用(生成扩展查询)+ N 次向量检索,延迟通常是普通 RAG 的 2~3 倍。适合召回率优先的场景(如知识密集型问答),不适合延迟敏感的实时交互。
🎯 面试总结
RAG-Fusion = 多查询扩展(提高召回覆盖)+ RRF 融合(去噪,让多路结果中的高质量文档脱颖而出)。核心价值:单一查询视角下召回不到的相关文档,换个角度问就找到了;RRF 还能过滤掉只在某一路结果里出现的噪声。代价是延迟增加 2~3 倍,适合召回率优先、对延迟不敏感的场景。能解释 RRF 的「隐式投票机制」,面试官会认为你真的理解了这个方案的设计思路。
142-143. 什么是元信息标注(Metadata Enrichment)?
👔面试官:RAG 里为什么要给文档加元信息?元信息怎么用到检索里?
元信息标注是 RAG 工程里非常容易被忽略但效果立竿见影的优化。用一句话解释:向量相似度决定「哪些文档语义最相关」,元信息过滤决定「在哪些文档里找」。两者结合,才是精准检索。
💡 简要回答
元信息(Metadata)标注:在把文档 chunk 存入向量库时,同时附加一组结构化的标签——来源文件名、文档类型、所属部门、更新日期、版本号等。检索时可以先按元信息精确过滤,把检索范围圈定在正确的文档子集里,再做向量相似度排序,大幅提升精准度。
自动关键词丰富:对于没有明显结构化标签的文档,可以用 LLM 自动从每个 chunk 里提取关键词并存入 metadata,让关键词也能参与过滤。
📝 详细解析
为什么需要元信息过滤?
想象一个企业知识库,里面有 HR 政策文档、产品手册、销售话术、财务报告……各种类型混在一起。用户问「ModelX Pro 如何安装」,向量检索可能把销售话术里「ModelX Pro 是行业最强」这句话的 chunk 也排进来,明明在语义上相关(都提到了产品名),但对回答安装问题毫无用处。
如果有元信息过滤,只在 doc_type = "product_manual" 的文档里搜,这类噪声就完全不会出现。
python
# 存储时附加元信息
for chunk in chunks:
chunk.metadata.update({
"source": "产品手册_v2.3.pdf",
"doc_type": "product_manual",
"product": "ModelX_Pro",
"version": "2.3",
"department": "engineering",
"last_updated": "2024-03-01",
})
vectorstore = Milvus.from_documents(chunks, embed_model, ...)
# 检索时精确过滤:只在产品手册中检索
results = vectorstore.similarity_search(
"ModelX Pro 安装步骤",
k=4,
filter={"doc_type": "product_manual", "product": "ModelX_Pro"}
)自动关键词标注
对于没有结构化来源的文档(比如从网页爬来的文章),可以用 LLM 自动提取关键词作为 metadata:
python
def auto_enrich_keywords(chunk_text):
prompt = f"从以下文本提取 5 个最重要的关键词,用逗号分隔:\n{chunk_text}"
keywords_str = llm(prompt)
return [kw.strip() for kw in keywords_str.split(",")]
for chunk in chunks:
keywords = auto_enrich_keywords(chunk.page_content)
chunk.metadata["keywords"] = keywords
# 之后可以按关键词过滤:filter={"keywords": {"$in": ["RAG", "向量检索"]}}实际效果
元信息过滤能把检索范围缩小 90% 以上。如果一个知识库有 10 万条 chunk,用元信息把范围圈到 department="HR" 之后只剩 3000 条,不仅精准度大幅提升,向量检索的速度也更快(候选集更小)。
这是一个「不做就一直有噪声,做了立刻见效」的优化。
🎯 面试总结
元信息 = 向量检索的「前置过滤器」。向量相似度管语义,元信息过滤管范围,两者配合才能做到真正精准。核心用法:存储时每个 chunk 附加来源/类型/部门/日期等结构化标签,检索时先按标签缩小范围再做向量排序。能主动说出「自动关键词丰富」的技巧,面试官会认为你对 RAG 工程的细节了解得很深入。
144-152. RAG 常见问题诊断
👔面试官:RAG 系统上线之后效果不好,你会怎么排查?
这道题是 RAG 工程经验的照妖镜。能系统说出排查思路的,说明真的在生产环境里踩过坑;只会背优化方案的,一遇到具体问题就抓瞎。
排查 RAG 问题的核心思路:按阶段定位。RAG 管道有三个阶段——召回、排名、生成,每个阶段都可能出问题,要先确定是哪个阶段的问题,再对症下药,不能盲目加优化。
💡 RAG 问题对照排查表
| 现象 | 根本原因 | 所在阶段 | 解决方案 |
|---|---|---|---|
| 文档里有答案但 RAG 没返回 | 文档未被正确解析/索引 | 召回前(ETL) | 检查 ETL 流程,确认文档已入库 |
| Top-1 结果不相关 | Embedding 质量差 / 关键词匹配弱 | 召回(粗排) | 加 Reranking / 混合检索(BM25) |
| 回答和检索内容无关 | LLM 忽略检索结果,用自身知识回答 | 生成 | 强化 System Prompt:「只基于以下资料回答」 |
| 答案在文档中但提取不出来 | chunk 太小,上下文不完整 | 索引 | 增大 chunk_size 或 Small-to-Big 检索 |
| 多条件问题只回答了部分 | 单次检索只命中部分条件 | 召回 | 多查询扩展 / 问题分解(Query Decomposition) |
| 回答过于笼统,缺少细节 | 检索到摘要而非详细内容 | 索引 | 优化索引粒度,确保详细内容被切片索引 |
| 输出格式不对 | Prompt 格式指令不清晰 | 生成 | 明确格式要求 / JSON Schema 约束 |
| 多文档综合问题答不全 | Top-K 片段分散,LLM 难以整合 | 生成 | MapReduce 策略 / 增大 context window |
📝 按阶段系统排查
第一步:检查 ETL——文档是否正确入库?
排查的第一步是最容易被遗漏的:确认相关文档真的在向量库里,而且被正确解析了。
python
def check_document_indexed(doc_id):
result = vectorstore.get(ids=[doc_id])
if not result:
print(f"文档 {doc_id} 未入库!检查 ETL 流程")
else:
print(f"入库内容:{result[0].page_content[:200]}...")
print(f"元信息:{result[0].metadata}")第二步:检查召回——相关文档是否被检索出来?
python
def diagnose_retrieval(query, expected_doc_id):
retrieved = retriever.get_relevant_documents(query)
retrieved_ids = [doc.metadata["id"] for doc in retrieved]
if expected_doc_id not in retrieved_ids:
print("召回失败:相关文档未被检索到")
print("→ 建议:换 Embedding 模型、加混合检索(BM25)、检查 chunk 切分是否合理")
else:
rank = retrieved_ids.index(expected_doc_id) + 1
print(f"召回成功,排名:Top-{rank}")
if rank > 3:
print("→ 排名靠后,建议加 Reranking 改善排名")第三步:检查生成——LLM 是否忠实使用检索内容?
召回正确但答案还是不对,问题就在生成阶段——LLM 没有「使用」检索到的内容,而是用了自己的参数记忆来回答。
检查方法:把检索到的原文和 LLM 的回答放在一起,看答案里的关键信息有没有出现在原文里。
python
def check_faithfulness(answer, retrieved_docs):
context = "\n".join([doc.page_content for doc in retrieved_docs])
prompt = f"""
请判断以下答案中的关键信息是否都来自背景资料(0~1分):
背景资料:{context}
答案:{answer}
忠实度分数(0=完全不忠实,1=完全基于资料):
"""
return float(llm(prompt))RAGAs:自动化评估的标准工具
手工诊断效率低,生产环境要用评估框架自动化检测:
python
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall
scores = evaluate(
dataset=test_dataset,
metrics=[faithfulness, answer_relevancy, context_recall]
)
# 三个核心指标:
# faithfulness(忠实度):答案是否基于检索内容,低 → 生成阶段问题
# answer_relevancy(答案相关性):答案是否回答了问题,低 → 生成阶段问题
# context_recall(上下文召回率):相关文档是否被召回,低 → 召回阶段问题
print(scores)三个指标可以精确定位问题在哪个阶段:context_recall 低 → 改检索;faithfulness 低 → 改 Prompt;answer_relevancy 低 → 两者都要看。
🎯 面试总结
RAG 问题排查要按三个阶段逐一定位,不能一遇到问题就「全加上」——ETL(文档是否正确入库)→ 召回(相关文档是否被检索出来,排名是否靠前)→ 生成(LLM 是否忠实使用检索内容)。工具:RAGAs 框架三个指标——context_recall 低改检索,faithfulness 低改 Prompt,answer_relevancy 低两者都要排查。能说出这个按阶段定位的思路,面试官会认为你有系统性的 RAG 工程排查能力,不是靠感觉乱优化。
153. 什么是向量数据库?它主要解决什么问题?
👔面试官:向量数据库是什么?它和传统数据库有什么区别?
🙋♂️我:向量数据库就是专门存向量的数据库。
👔面试官:传统数据库也能存向量数组,为什么要专门用向量数据库?核心操作是什么?
💡 简要回答
向量数据库解决的核心问题是:如何从海量高维向量中快速找到「最相似」的 Top-K 个。
传统数据库(MySQL)里可以存一个 float 数组,但要找最相似的向量,只能全表扫描逐一计算相似度,百万级数据下耗时秒级,不可用。向量数据库内置了专为相似度搜索设计的索引结构(HNSW、IVF 等),能在毫秒级完成从百万向量中的 Top-K 搜索,这是本质区别。
📝 详细解析
传统数据库 vs 向量数据库
| 维度 | 传统数据库(MySQL) | 向量数据库(Milvus) |
|---|---|---|
| 存储对象 | 结构化数据(行/列) | 高维向量(1536 维 float) |
| 查询方式 | SQL 精确匹配 | ANN 近似最近邻搜索 |
| 相似度计算 | 不支持 | 余弦/L2/内积,毫秒级 |
| 索引结构 | B-Tree、Hash | HNSW、IVF、PQ |
| 十万级查询延迟 | > 1s(全表扫描) | < 10ms(ANN 索引) |
「近似最近邻(ANN)」不是「精确最近邻」——它不保证找到绝对最相似的那一个,但能在毫秒内找到「几乎最相似的」Top-K,精度通常在 95%+ 以上,完全满足 RAG 的实际需求。
各主流向量数据库选型
| 数据库 | 定位 | 优点 | 适合场景 |
|---|---|---|---|
| FAISS | 本地 C++/Python 库 | 极快,无服务器开销 | 开发原型、单机 < 1000 万向量 |
| Milvus | 分布式数据库服务 | 云原生、持久化、支持标量过滤 | 生产环境、大规模部署 |
| Chroma | 轻量 Python 数据库 | 上手极快,零配置 | 快速验证、中小规模 |
| Pinecone | 云端 SaaS | 开箱即用,无需运维 | 不想自己部署运维 |
| Weaviate | 内置混合检索 | 原生支持 BM25 + 向量 | 需要混合检索的场景 |
python
# 向量数据库核心操作示例(Milvus)
query_vector = embed_model.encode("用户的问题") # [1536 维 float]
results = collection.search(
[query_vector],
"embedding",
{"metric_type": "COSINE", "params": {"ef": 64}},
limit=5,
expr='department == "HR"' # 同时支持标量过滤
)🎯 面试总结
向量数据库 ≠ 普通数据库存向量。核心价值是内置 ANN 索引(HNSW/IVF 等),让「从百万向量中找 Top-K 最相似」从秒级变成毫秒级。选型原则:快速原型用 FAISS 或 Chroma,生产大规模用 Milvus,不想自运维用 Pinecone,需要混合检索用 Weaviate。
154. FAISS 和 Milvus 各有什么特点?如何选型?
👔面试官:FAISS 和 Milvus 有什么区别?什么时候用哪个?
这道题本质上是「库 vs 服务」的工程选型题。两者定位根本不同,不是谁比谁好,而是各自解决不同规模和场景下的问题。
💡 简要回答
| 维度 | FAISS | Milvus |
|---|---|---|
| 本质 | 本地 C++/Python 库,进程内运行 | 独立的分布式数据库服务 |
| 持久化 | 需手动 write_index / read_index | 原生持久化,重启不丢数据 |
| 扩展性 | 单机单进程 | 分布式,水平扩展,支持千亿级 |
| 运维成本 | 零(没有服务) | 需要部署和运维服务 |
| 功能 | 纯向量检索 | 向量 + 标量过滤 + 混合检索 + CRUD |
| 适用规模 | < 1000 万向量 | 千万~百亿向量 |
📝 详细解析
FAISS:原型和单机场景的最优选择
FAISS 的优势是零服务器开销——它是一个库,直接嵌在你的 Python 进程里,没有网络调用,没有服务器,速度极快。适合:快速验证想法、单机部署、数据量不大、能接受重启时重新加载索引。
python
import faiss, numpy as np
d = 1536 # OpenAI text-embedding-3-small 维度
index = faiss.IndexHNSWFlat(d, 32) # HNSW,M=32 邻居
vectors = np.array(embeddings, dtype='float32')
index.add(vectors)
query_vec = np.array([query_embedding], dtype='float32')
distances, indices = index.search(query_vec, k=5)
faiss.write_index(index, "my_index.faiss") # 手动持久化Milvus:生产环境的标准选择
Milvus 是一个独立运行的数据库服务,原生支持持久化、水平扩展、实时数据更新,还支持向量+标量的混合过滤。适合:生产环境、需要多进程/多服务访问同一个索引、数据量大、需要实时增删改。
python
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
connections.connect("default", host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=2000),
FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=500),
]
collection = Collection("docs", CollectionSchema(fields))
collection.create_index("embedding", {
"index_type": "HNSW", "metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 200}
})
results = collection.search(
[query_embedding], "embedding",
{"metric_type": "COSINE", "params": {"ef": 64}},
limit=5,
expr='source == "HR_Policy.pdf"' # 向量+标量混合过滤
)🎯 面试总结
核心区别一句话:FAISS 是库(进程内,零开销,手动持久化),Milvus 是服务(独立运行,原生持久化,支持分布式)。工程迁移路径:开发/原型阶段用 FAISS(快速上手),上生产时迁移到 Milvus(稳定、可扩展)。如果数据量 < 100 万且单机够用,FAISS 配合定时保存索引完全 OK;一旦要多服务共享、需要实时更新、超过千万量级,就该上 Milvus。
155. ANN 算法有哪些?各有什么优缺点(HNSW / IVF / PQ)?
👔面试官:向量检索的 ANN 算法有哪些?HNSW 和 IVF 的区别是什么?
💡 简要回答
| 算法 | 核心思路 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| Flat(暴力) | 逐一遍历全部向量 | 精确 100% | O(N),慢 | 向量数 < 10 万 |
| HNSW | 分层图,跳表式搜索 | 高精度、低延迟、支持增删 | 内存大(存图结构) | 通用首选,千万以内 |
| IVF | K-means 聚类 + 倒排 | 内存小、大数据快 | 精度略低、不支持增量 | 内存受限,大数据集 |
| PQ(乘积量化) | 向量压缩存储 | 内存极小 | 精度损失大 | 单独不用,配合 IVF |
| IVF_PQ | IVF + PQ 组合 | 内存小 + 速度快 | 精度最低 | 亿级向量生产 |
📝 详细解析
HNSW:理解原理才能调好参数
HNSW(Hierarchical Navigable Small World)的名字直接说明了它的结构:「分层的、可导航的、小世界图」。
它建了一个多层图,顶层是稀疏的「高速公路」——每个节点只连接距离很远的几个邻居,用于快速跳转到目标区域;底层是稠密的「街道」——精细搜索找到真正的近邻。搜索时从顶层开始,像导航一样逐层收缩到目标区域。
python
# HNSW 关键参数,理解这三个就能调优
M = 16 # 每个节点在底层的邻居数
# 越大:精度更高、内存更大、构建更慢
# 推荐值:8~64,通常用 16 或 32
ef_construction = 200 # 构建索引时的候选集大小
# 越大:索引质量越高、构建越慢
# 推荐值:100~400
ef_search = 64 # 查询时的候选集大小(运行时可动态调整)
# ef=16:~2ms,召回率约 91%
# ef=64:~8ms,召回率约 98%
# ef=128:~16ms,召回率约 99%IVF:用聚类换内存,但有代价
IVF(Inverted File Index)的思路是:先把所有向量用 K-means 聚成 nlist 个簇,把每个簇的向量存在对应的「桶」里。查询时,先找到最近的 nprobe 个簇,只在这几个桶里搜,跳过了大部分无关向量。
python
nlist = 1024 # 聚类数,通常设为 sqrt(N) 或 4*sqrt(N)
nprobe = 16 # 查询时搜索的簇数(nprobe/nlist 越大精度越高,越慢)
# nprobe=1:只搜最近一个簇,极快但精度低
# nprobe=64:搜 64 个簇,精度接近暴力搜索IVF 的问题在于边界效应:查询向量正好在两个簇的交界处时,相关文档可能在相邻簇里,如果 nprobe 太小就会被漏掉。
规模选型指南
| 向量规模 | 推荐索引 | 理由 |
|---|---|---|
| < 100 万 | HNSW | 内存放得下,精度最高 |
| 100 万 ~ 1 亿 | IVF_FLAT 或 HNSW | 视内存情况选择 |
| > 1 亿 | IVF_PQ | 向量压缩,极大减少内存 |
🎯 面试总结
HNSW vs IVF 一句话区别:HNSW 是图结构(精度高、支持在线增量更新、内存大),IVF 是聚类倒排(内存小、适合超大规模、不支持增量更新)。超大规模(亿级)加 PQ 压缩:用少量精度损失换取极大的内存节省。工程实践:默认选 HNSW,内存吃紧或超过千万量级再考虑 IVF_PQ。能说出 ef_search 调大会提升精度降低速度的调参逻辑,面试官会认为你真的调过向量检索。
156. Milvus 的索引类型有哪些?如何选择?
👔面试官:Milvus 里有这么多索引类型,实际工程怎么选?
这道题面试官想考的不是你背出所有索引名称,而是你的选型判断逻辑——给你一个场景,你能不能说出用哪个、为什么。
💡 简要回答
| 索引 | 内存 | 精度 | 查询速度 | 适用场景 |
|---|---|---|---|---|
| FLAT | 最大 | 100% | 最慢 | 向量数 < 100 万,精度必须完美 |
| IVF_FLAT | 中 | 高 | 中 | 100 万 ~ 1000 万,通用 |
| IVF_SQ8 | 小(8 倍压缩) | 中 | 快 | 内存受限,可接受精度略降 |
| IVF_PQ | 极小 | 低 | 最快 | 亿级向量,内存极受限 |
| HNSW | 大 | 极高 | 极快 | 延迟敏感,查询 P99 < 10ms |
| DISKANN | 磁盘存储 | 高 | 中 | 超大规模且内存不足 |
🎯 面试总结
Milvus 索引选型三步走:
- 向量规模 < 1000 万:选 HNSW(精度和速度最佳平衡,Milvus 2.x 推荐默认,且支持在线增量更新)
- 向量规模 > 1 亿:选 IVF_PQ(内存压缩,牺牲部分精度)
- 对查询延迟极度敏感(P99 < 10ms):选 HNSW,调低
ef_search
内存受限但精度要求不高时,IVF_SQ8(8 位量化,内存缩小 8 倍)是 IVF_PQ 的平衡替代。能说出「默认 HNSW,超大规模 IVF_PQ,内存极限 DISKANN」这个选型框架,面试官基本就满意了。
157. 向量检索中如何平衡召回率和查询速度?
👔面试官:向量检索在准确率和速度之间如何权衡?有哪些调优手段?
这道题考的是你对向量检索「精度-速度权衡」的系统性理解,不只是知道「ef_search 调大会慢」,而是能给出完整的工程调优思路。
💡 简要回答
召回率和速度的权衡有三个层次的调优手段:
- 索引参数调节:HNSW 的
ef_search,IVF 的nprobe——小 = 快但精度低,大 = 慢但精度高,这是最直接的旋钮 - 两阶段检索:ANN 粗召回(低 ef,快速)→ Cross-Encoder 精排(高精度)——在相同精度下比单阶段高 ef 快 5~10 倍
- 硬件加速:FAISS-GPU、IVF_PQ 向量压缩减少内存带宽
📝 详细解析
参数调节的实际数据
通过实验建立 ef_search 和精度/延迟的对应关系,在可接受的精度下找速度最快的参数:
python
ef_values = [8, 16, 32, 64, 128]
for ef in ef_values:
recall = evaluate_recall_at_ef(ef)
latency_ms = measure_latency_at_ef(ef)
print(f"ef={ef:4d}: recall@10={recall:.3f}, latency={latency_ms:.1f}ms")
# 典型输出:
# ef= 8: recall@10=0.87, latency=1.2ms
# ef= 16: recall@10=0.91, latency=2.1ms
# ef= 32: recall@10=0.95, latency=4.0ms
# ef= 64: recall@10=0.98, latency=8.3ms
# ef=128: recall@10=0.99, latency=15.6ms根据业务要求的召回率下限,选最小的 ef 值——比如召回率 > 0.95 就够了,选 ef=32(4ms),不需要追求 ef=128(16ms)。
两阶段检索:最实用的工程方案
单纯调高 ef 来提升精度,收益递减很快(从 91% 到 98% 要把 ef 从 16 增到 64,延迟从 2ms 变到 8ms)。更好的方案是两阶段:
第一阶段:ANN 粗召回
ef=16,延迟 ~2ms,从 10 万文档召回 Top-100
(召回率 91%,但有一定误差)
第二阶段:Cross-Encoder 精排
对 Top-100 重打分,选 Top-5,延迟 ~200ms
总延迟:~202ms
最终精度:接近 99%(Cross-Encoder 修正了 ANN 的误差)和单阶段 ef=128(16ms + 精度 99%)相比,两阶段方案的总延迟更高,但精度更好,且 Cross-Encoder 能发现 ANN 单靠向量相似度发现不了的细粒度相关性。这是生产 RAG 系统的标准配置。
生产环境要同时监控两个指标
python
metrics_to_monitor = {
"p50_latency_ms": "中位数延迟 < 50ms",
"p99_latency_ms": "99 分位延迟 < 200ms(SLA 要求)",
"recall_at_10": "Top-10 召回率 > 0.95",
"qps": "每秒查询量,压测确认容量上限"
}只看延迟不看召回率,可能在性能好的时候悄悄降了检索质量;只看召回率不看延迟,可能在精度好的时候超出了 SLA。两个指标都要监控。
🎯 面试总结
召回率 vs 速度的调优旋钮:ef_search(HNSW)或 nprobe(IVF),数值越小越快但精度越低。工程最优方案不是单纯调高 ef,而是两阶段:ANN 用低 ef 快速粗召回(保速度),Cross-Encoder 精排修正误差(保精度),两者结合的性价比远超单阶段调参。生产环境要同时监控 P99 延迟和召回率,两个指标缺一不可。能给出这套完整的工程思路,面试官会认为你在生产环境里真的优化过向量检索系统。
第四分类「四、RAG(检索增强生成)」全部完成,共覆盖题目 125-157(RAG 基础/ETL/检索优化/问题诊断/向量数据库)