Skip to content

四、RAG(检索增强生成) ​

覆盖 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 项目的人,一定踩过数据处理的坑。这道题考的是你的工程经验,不是背书。

💡 简要回答 ​

数据处理五大挑战:

  1. 多格式文档解析:PDF 表格、图片、扫描件,每一种都有不同的坑
  2. 切块粒度的两难困境:块太大噪声多、块太小上下文不完整
  3. 数据质量问题:重复文档、过时版本、低质量来源
  4. 专业术语和多语言:通用 Embedding 对领域术语表示不准
  5. 结构化数据:数据库和表格无法用向量检索,需要单独处理

📝 详细解析 ​

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 解析难在三类问题:

  1. 表格:提取出的文字丢失了行列结构,一堆数字没有上下文
  2. 多栏排版:文本读取顺序混乱,两栏 PDF 按行读取会把左右栏交叉
  3. 扫描件/图片 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 检索优化的五个方向,优先级从高到低:

  1. Reranking(精排):投入产出比最高,Cross-Encoder 对粗排结果重新打分
  2. 混合检索(Hybrid Search):稠密向量 + 稀疏 BM25 并行检索,覆盖关键词和语义两种场景
  3. 查询端优化:HyDE、多查询扩展、查询重写,提高召回率
  4. 索引优化:更好的切块策略(Semantic Chunking / Small-to-Big)、元信息标注
  5. 上下文压缩:去掉检索片段中的无关句子,压缩后再喂给 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 检索分两阶段:

  1. 召回(粗排):Bi-Encoder 快速从大量文档中召回 Top-50,每个文档的向量是离线预计算好的,检索时只算 query 向量,然后做相似度排序,速度极快
  2. 精排(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、HashHNSW、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 服务」的工程选型题。两者定位根本不同,不是谁比谁好,而是各自解决不同规模和场景下的问题。

💡 简要回答 ​

维度FAISSMilvus
本质本地 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分层图,跳表式搜索高精度、低延迟、支持增删内存大(存图结构)通用首选,千万以内
IVFK-means 聚类 + 倒排内存小、大数据快精度略低、不支持增量内存受限,大数据集
PQ(乘积量化)向量压缩存储内存极小精度损失大单独不用,配合 IVF
IVF_PQIVF + 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 索引选型三步走:

  1. 向量规模 < 1000 万:选 HNSW(精度和速度最佳平衡,Milvus 2.x 推荐默认,且支持在线增量更新)
  2. 向量规模 > 1 亿:选 IVF_PQ(内存压缩,牺牲部分精度)
  3. 对查询延迟极度敏感(P99 < 10ms):选 HNSW,调低 ef_search

内存受限但精度要求不高时,IVF_SQ8(8 位量化,内存缩小 8 倍)是 IVF_PQ 的平衡替代。能说出「默认 HNSW,超大规模 IVF_PQ,内存极限 DISKANN」这个选型框架,面试官基本就满意了。


157. 向量检索中如何平衡召回率和查询速度? ​

👔面试官:向量检索在准确率和速度之间如何权衡?有哪些调优手段?

这道题考的是你对向量检索「精度-速度权衡」的系统性理解,不只是知道「ef_search 调大会慢」,而是能给出完整的工程调优思路。

💡 简要回答 ​

召回率和速度的权衡有三个层次的调优手段:

  1. 索引参数调节:HNSW 的 ef_search,IVF 的 nprobe——小 = 快但精度低,大 = 慢但精度高,这是最直接的旋钮
  2. 两阶段检索:ANN 粗召回(低 ef,快速)→ Cross-Encoder 精排(高精度)——在相同精度下比单阶段高 ef 快 5~10 倍
  3. 硬件加速: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/检索优化/问题诊断/向量数据库)

最后更新2026-04-26
觉得有帮助?把这个链接转给正在求职的朋友 · 用 Ctrl + K 全站搜索其它题