news 2026/11/2 13:47:36

RAG面试深度解析:从文档切分到检索优化的全链路实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG面试深度解析:从文档切分到检索优化的全链路实战

1. 面试官到底想听什么:RAG 的本质拆解

面试里被问到 RAG,很多人第一反应是背概念:“检索增强生成,就是把文档切块、向量化、存进向量库,用户提问时检索相关片段拼进 Prompt 让大模型回答。”这段话本身没错,但面试官听完通常不会有任何反应,因为它只说明了“是什么”,没有说明“为什么这样做”“哪里会出问题”“你怎么解决”。

我在带团队和参与面试的过程中发现,能把 RAG 讲清楚的人,往往不是背概念最熟的人,而是真正动手搭过、踩过坑、调过参数的人。RAG 的全称是 Retrieval-Augmented Generation,检索增强生成。它的核心逻辑可以用一个生活场景来类比:你是一个客服,客户问你一个非常冷门的售后政策。你不可能把所有政策全背下来,但你可以先翻手册找到相关那几页,然后照着那几页的内容组织语言回答客户。RAG 里的“翻手册”就是检索,“组织语言回答”就是生成。

这个类比里藏着 RAG 要解决的三个核心问题。第一,大模型的知识是冻结的,训练完成之后它不知道你公司内部的最新文档;第二,大模型的上下文窗口有限,你不可能把几百页手册全塞进去;第三,大模型会“编”,也就是幻觉,当它不知道答案时可能一本正经地胡说。RAG 通过“先检索、再生成”的方式,把外部知识动态注入到生成过程中,让模型基于真实资料回答,从而缓解知识过时、上下文受限和幻觉这三个问题。

面试官真正想考察的,是你对这条链路上每个环节的理解深度。比如:文档怎么切?切多大?Embedding 模型怎么选?向量检索和关键词检索各自适合什么场景?检索回来的内容怎么排序?如果检索结果不相关怎么办?这些问题才是区分“用过”和“懂”的分水岭。下面我会按照一条完整的 RAG 链路,把每个环节的原理、选型逻辑、实操代码和踩坑经验逐一拆开讲。

1.1 RAG 和微调到底该怎么选

面试里高频出现的一个对比题是:RAG 和微调(Fine-tuning)有什么区别,什么时候用哪个。这个问题如果只回答“RAG 是外挂知识,微调是改模型参数”,虽然对,但不够。我一般会从四个维度来对比。

对比维度RAG微调
知识更新改文档库即可,实时生效需要重新训练,周期长
成本低,主要是向量库和推理成本高,需要 GPU 和标注数据
幻觉控制较好,有原文可溯源一般,仍可能编造
适合场景知识频繁更新、需要溯源固定风格、特定格式输出

实际项目中,两者不是互斥的。我做过的一个企业知识库项目,就是用 RAG 做知识检索,同时用少量微调让模型学会公司特定的回答格式和语气。面试时如果你能说出“RAG 管知识,微调管风格,两者可以叠加”,面试官通常会眼前一亮。

还有一个热词叫 Agentic RAG,指的是把 RAG 和 Agent 结合,让模型自己决定什么时候检索、检索几轮、要不要换查询词再检索。传统 RAG 是一次检索就生成,Agentic RAG 是“检索—判断—再检索—再判断”的循环。这个方向在面试里如果被问到,能讲清楚“多轮检索”和“查询改写”的价值,基本就能拉开差距。

1.2 一条完整 RAG 链路的五个阶段

把 RAG 拆开,其实就五个阶段:文档加载、文本切分、向量化与存储、检索、生成。每个阶段都有它的核心决策点。

文档加载阶段,你要处理 PDF、Word、Markdown、网页等各种格式。PDF 里还有扫描件和文字版之分,扫描件需要 OCR。文本切分阶段,你要决定按字符切、按句子切还是按语义切,切多大块,块之间要不要重叠。向量化阶段,你要选 Embedding 模型,是选开源的还是调 API,中文场景和英文场景选型不同。存储阶段,你要选向量库,FAISS、Chroma、Milvus、Qdrant 各有适用场景。检索阶段,你要决定用向量检索、关键词检索还是混合检索,要不要加 Rerank。生成阶段,你要设计 Prompt 模板,控制上下文长度,处理“检索不到”的情况。

这五个阶段里,切分和检索是最容易出问题的两个环节。切分切得不好,检索再准也白搭;检索策略不对,生成质量直接崩。下面我会重点讲这两个环节,同时把其他环节的实操代码也补上。

2. 文档切分:RAG 效果的地基

很多人搭 RAG 时最不重视的就是切分,觉得随便按 500 字切一刀就行。但我可以负责任地说,切分策略直接决定了 RAG 的上限。检索的本质是“用问题去匹配文本块”,如果文本块本身语义不完整,匹配出来的结果自然也是残缺的。

2.1 固定长度切分为什么不够用

固定长度切分是最简单的做法,比如每 500 个字符切一块,块之间重叠 50 个字符。重叠的目的是防止一句话被切断后,前后两块都丢失了完整语义。这种做法实现简单,适合快速验证,但问题也很明显。

假设你的文档里有一句话:“本产品的保修期为自购买之日起 12 个月,但人为损坏不在保修范围内。”如果按 500 字切分,恰好切在“12 个月”和“但人为损坏”之间,那么第一块讲保修期,第二块讲免责,用户问“保修期多久”时,检索到的第一块可能没有提到免责条件,模型就会给出不完整的回答。

更麻烦的是,固定长度切分不考虑段落和标题结构。一份技术文档里,标题“安装步骤”和下面的正文被切到不同块里,检索时标题块可能被匹配到,但正文块没有,回答就缺了操作细节。

2.2 递归字符切分的实操与参数选择

LangChain 里最常用的切分器是RecursiveCharacterTextSplitter,它的思路是“按优先级依次尝试分隔符”。默认的分隔符列表是["\n\n", "\n", " ", ""],也就是先按段落切,段落太大再按行切,行太大再按空格切,最后按字符切。这样能尽量保持语义单元的完整性。

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", " ", ""], length_function=len, ) chunks = splitter.split_text(long_document)

这里有几个参数需要解释。chunk_size=500指的是每块最多 500 个字符,这个值不是拍脑袋定的。太小了,一块里信息不够,检索出来回答不完整;太大了,一块里混入无关信息,检索精度下降,而且占用上下文窗口。我的经验是中文场景 300 到 600 字比较合适,英文场景 500 到 1000 个 token。chunk_overlap=80是块之间的重叠字符数,一般取 chunk_size 的 10% 到 20%,目的是防止边界处的语义丢失。

分隔符列表我加了中文标点“。”“!”“?”,因为中文文档里句号是最自然的语义边界。如果你的文档是代码,分隔符应该改成["\nclass ", "\ndef ", "\n\n", "\n", " ", ""],优先按类和函数切。如果是 Markdown,可以用MarkdownHeaderTextSplitter按标题层级切,这样每个块都带着标题信息,检索时更容易匹配。

注意:chunk_overlap 不要设得太大,否则相邻块内容重复过多,检索时会返回一堆相似结果,浪费上下文窗口。我一般控制在 chunk_size 的 15% 左右。

2.3 语义切分和父子块策略

固定长度和递归切分都是基于字符的,还有一种更高级的做法叫语义切分。它的思路是先用 Embedding 模型把每个句子向量化,然后计算相邻句子的相似度,相似度低的地方就是语义转折点,在那里切一刀。这样切出来的块,内部语义高度一致。

语义切分的效果通常更好,但计算成本高,因为要对每个句子做 Embedding。我的做法是混合使用:先用递归切分做粗切,再对每个块做语义合并,把相似度高的小块合并成一个大块。

另一个实用策略是父子块(Parent-Child Chunk)。具体做法是:把文档切成大块(父块)和小块(子块),子块用于检索,父块用于生成。检索时用子块匹配问题,命中后返回它对应的父块给模型。这样既保证了检索精度(小块语义集中),又保证了生成质量(大块上下文完整)。LangChain 里的ParentDocumentRetriever就是干这个的。

from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain.vectorstores import FAISS from langchain.embeddings import OpenAIEmbeddings parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000) child_splitter = RecursiveCharacterTextSplitter(chunk_size=400) vectorstore = FAISS.from_texts(["placeholder"], OpenAIEmbeddings()) store = InMemoryStore() retriever = ParentDocumentRetriever( vectorstore=vectorstore, docstore=store, child_splitter=child_splitter, parent_splitter=parent_splitter, )

这段代码里,子块 400 字用于检索,父块 2000 字用于生成。实际测试下来,这种策略在长文档问答场景里,回答完整度比单纯用 500 字块提升明显。

3. Embedding 与向量库:检索的发动机

切分完之后,下一步是把文本块变成向量。这一步的核心是 Embedding 模型的选择,它直接决定了检索的语义匹配能力。

3.1 Embedding 模型选型的三个维度

选 Embedding 模型,我一般看三个维度:语言支持、维度大小、是否可本地部署。

语言支持方面,中文场景优先选对中文优化过的模型。比如 BGE 系列(BAAI/bge-large-zh-v1.5)在中文语义匹配上表现很好,MTEB 中文榜单上长期靠前。英文场景可以用 OpenAI 的 text-embedding-3-small 或 text-embedding-3-large,也可以用开源的 all-MiniLM-L6-v2。多语言场景可以选 multilingual-e5 系列。

维度大小影响存储和检索速度。维度越高,表达能力越强,但存储成本也越高。比如 text-embedding-3-small 是 1536 维,bge-large-zh 是 1024 维,all-MiniLM-L6-v2 只有 384 维。384 维的模型检索速度快,适合大规模数据;1024 维以上适合对精度要求高的场景。

是否可本地部署,取决于你的数据敏感度。如果文档涉及内部资料,不能调外部 API,那就必须选开源模型本地跑。Ollama 现在支持直接拉取 Embedding 模型,比如ollama pull nomic-embed-text,然后通过 LangChain 的OllamaEmbeddings调用,零成本搭建本地 RAG。

from langchain_community.embeddings import OllamaEmbeddings embeddings = OllamaEmbeddings(model="nomic-embed-text")

提示:Embedding 模型一旦选定,索引和查询必须用同一个模型。换了模型就要重新建索引,否则向量空间不一致,检索结果会完全乱掉。这是新手最容易犯的错误之一。

3.2 FAISS 上手:从建索引到相似度检索

FAISS 是 Facebook 开源的向量检索库,特点是快、轻量、无需独立服务。它适合中小规模数据(百万级以内),可以直接嵌在 Python 进程里用。LangChain 对 FAISS 的封装很友好。

from langchain_community.vectorstores import FAISS from langchain_community.embeddings import OllamaEmbeddings embeddings = OllamaEmbeddings(model="nomic-embed-text") vectorstore = FAISS.from_texts( texts=chunks, embedding=embeddings, metadatas=[{"source": f"doc_{i}"} for i in range(len(chunks))], ) vectorstore.save_local("faiss_index")

建索引时传入metadatas是个好习惯,可以把来源文件名、页码、章节等信息存进去,检索时一并返回,方便溯源和展示引用。

检索时:

query = "保修期是多久" results = vectorstore.similarity_search_with_score(query, k=5) for doc, score in results: print(f"分数: {score:.4f}") print(f"内容: {doc.page_content[:100]}") print(f"来源: {doc.metadata}")

similarity_search_with_score返回的分数是距离,FAISS 默认用 L2 距离,分数越小越相似。如果你用余弦相似度,需要归一化向量。LangChain 的FAISS.from_texts默认会对向量做归一化,所以分数可以直接比较。

3.3 向量检索的瓶颈在哪里

向量检索不是万能的。它的核心问题是:语义相似不等于关键词匹配。比如用户问“RAG 的 hit rate 怎么提升”,向量检索可能返回一堆讲“检索优化”的段落,但没有一段提到“hit rate”这个具体术语。因为 Embedding 模型把“hit rate”和“检索优化”映射到了相近的向量空间,但具体术语的精确匹配能力弱。

另一个瓶颈是长尾查询。如果用户问的是一个非常具体的产品型号,比如“X200-Pro 的电池容量”,向量检索可能匹配到“X200 系列电池”的段落,但具体型号的精确信息可能被淹没。

解决这两个瓶颈的常见方案是混合检索:向量检索负责语义匹配,关键词检索(BM25)负责精确匹配,两路结果融合后排序。LangChain 里的EnsembleRetriever就是做这个的。

from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_texts(chunks) bm25_retriever.k = 5 faiss_retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, faiss_retriever], weights=[0.4, 0.6], )

权重可以根据场景调。如果查询里术语多,BM25 权重大一些;如果查询偏口语化,向量检索权重大一些。实测下来,混合检索在 hit rate 上通常比单路检索高 10 到 20 个百分点。

4. 检索优化与生成:从能用到好用

检索回来内容之后,还有两步能显著提升效果:Rerank 重排序和 Prompt 设计。

4.1 Rerank 重排序:把最相关的顶上来

向量检索返回的 top-k 结果,排序依据是向量距离,但向量距离不完全等于相关性。Rerank 的做法是:先用向量检索召回较多候选(比如 20 个),再用一个交叉编码器(Cross-Encoder)对每个候选和问题做精细打分,重新排序,取 top-5 给模型。

交叉编码器和 Embedding 模型的区别在于:Embedding 是分别编码问题和文档,然后算距离;交叉编码器是把问题和文档拼在一起输入模型,直接输出相关性分数。后者精度更高,但速度慢,所以只适合对少量候选做重排。

from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compressor = CrossEncoderReranker(model=model, top_n=5) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble_retriever, )

Rerank 的代价是增加一次模型推理,延迟会上升。如果对延迟敏感,可以只在检索结果分数分布比较分散时启用,或者用更小的 reranker 模型。

4.2 Prompt 模板设计:让模型基于资料回答

检索到内容后,最后一步是拼 Prompt。Prompt 设计的目标是让模型严格基于检索内容回答,不知道就说不知道,不要编。

from langchain.prompts import ChatPromptTemplate template = """你是一个严谨的助手。请仅根据以下参考资料回答问题。 如果参考资料中没有相关信息,请直接说“根据现有资料无法回答”,不要编造。 参考资料: {context} 问题:{question} 回答:""" prompt = ChatPromptTemplate.from_template(template)

这个模板里有三个关键点。第一,“仅根据以下参考资料”是约束模型不要用内部知识。第二,“没有相关信息就说无法回答”是防幻觉。第三,把{context}放在问题前面,是因为很多模型对上下文末尾的内容注意力更强,问题放在最后更容易被关注。

实际使用中,我还会在 context 里给每个片段加上来源标记,比如[来源1],这样模型回答时可以引用来源,用户也能核对。

4.3 检索不到怎么办:兜底策略

RAG 系统上线后,一定会遇到检索不到的情况。这时候如果直接把空 context 传给模型,模型要么说“无法回答”,要么开始编。我的做法是加一层判断:如果检索结果的最高分低于某个阈值,就不走生成,直接返回“未找到相关资料,请换个问法或联系人工”。

results = vectorstore.similarity_search_with_score(query, k=5) if not results or results[0][1] > 1.5: return "未找到相关资料,请换个问法。"

阈值需要根据你的 Embedding 模型和距离度量方式来调。L2 距离下,1.5 是一个经验值,实际项目里我会用一批测试问题跑一遍,看正样本和负样本的分数分布,取一个能分开两者的值。

5. 常见问题与排查技巧实录

RAG 搭起来容易,调好难。下面是我在实际项目中遇到的高频问题和排查思路。

5.1 检索结果不相关:从切分和模型两头查

检索不相关是最常见的问题。排查顺序是:先看切分,再看 Embedding 模型,最后看检索策略。

切分方面,检查块大小是否合适,块之间是否有重叠,分隔符是否匹配文档格式。我遇到过一次,PDF 解析出来的文本没有换行符,所有内容挤成一行,递归切分失效,切出来的块全是乱的。后来在解析后加了一步文本清洗,把连续空格和异常换行处理掉,问题就解决了。

Embedding 模型方面,检查模型是否支持你的语言。用英文模型处理中文,效果会差很多。另外检查索引和查询是否用了同一个模型。

检索策略方面,试试混合检索和 Rerank。如果单路向量检索 hit rate 只有 60%,加上 BM25 和 Rerank 后通常能到 80% 以上。

5.2 回答不完整或答非所问:上下文和 Prompt 的问题

检索到了相关内容,但回答不完整,通常是上下文拼接的问题。比如检索了 5 个块,但 Prompt 里只放了 3 个,或者块之间的顺序乱了,模型理解出现偏差。

我的做法是:按相关性分数排序后拼接,每个块之间加分隔符,并在每个块前标注来源。如果上下文太长超过模型窗口,优先保留分数高的块,或者用模型做一次摘要压缩。

答非所问则可能是 Prompt 约束不够。检查模板里是否有“仅根据资料回答”的约束,是否给了模型“不知道就说不知道”的出口。

5.3 常见问题速查表

问题现象可能原因排查方向解决思路
检索结果完全不相关索引和查询模型不一致检查 Embedding 模型统一模型,重建索引
检索结果部分相关切分粒度不合适检查块大小和重叠调整 chunk_size 和 overlap
回答不完整上下文拼接丢失检查 Prompt 里的 context按分数排序,补全块
模型编造答案Prompt 约束不足检查模板加“仅根据资料”和兜底话术
检索速度慢向量库规模大检查索引类型用 IVF 或 HNSW 索引
中文效果差用了英文模型检查模型语言支持换 BGE 等中文模型

5.4 几个容易被忽略的实操心得

第一个心得:建索引时一定要存 metadata。来源、页码、章节这些信息,在调试和展示引用时非常有用。我见过有人只存文本不存来源,出了问题根本不知道是哪篇文档的哪一段。

第二个心得:准备一批测试问题。RAG 调优不能靠感觉,要有一批带标准答案的测试问题,每次调整后跑一遍,看 hit rate 和回答准确率的变化。测试问题要覆盖简单查询、复杂查询、多跳查询和无关查询。

第三个心得:日志要记全。每次查询的原始问题、改写后的问题、检索到的块、分数、最终回答,都要记下来。出问题时可以回溯,也能用来做后续的微调数据。

第四个心得:不要追求一步到位。先搭一个能跑的最小版本,用固定长度切分加 FAISS 加基础 Prompt,跑通之后再逐步优化切分、加混合检索、加 Rerank。每加一个组件,都要用测试集验证是否真的有提升,避免过度工程。

6. 从零搭建一个本地 RAG 知识库的完整流程

把上面的内容串起来,一个可复制的本地 RAG 流程是这样的。

第一步,准备文档。把 PDF、Markdown、TXT 等文件放到一个目录下。用 LangChain 的DirectoryLoader加载。

from langchain_community.document_loaders import DirectoryLoader, TextLoader loader = DirectoryLoader("./docs", glob="**/*.md", loader_cls=TextLoader) documents = loader.load()

第二步,切分。用RecursiveCharacterTextSplitter,中文场景 chunk_size 设 500,overlap 设 80。

第三步,向量化并建索引。用 Ollama 的 nomic-embed-text 或 BGE 模型,存到 FAISS。

第四步,检索。先用向量检索,再加 BM25 做混合,最后加 Rerank。

第五步,生成。用 ChatPromptTemplate 拼上下文,调本地模型或 API 生成回答。

第六步,加兜底。检索分数低于阈值时直接返回未找到。

这套流程我在几个内部知识库项目里用过,文档量在几千到几万块之间,效果稳定。如果文档量超过百万块,FAISS 需要换 IVF 索引,或者迁移到 Milvus、Qdrant 这类支持分布式和持久化的向量库。

最后分享一个我在面试里常用的回答框架:当被问到 RAG 时,先讲它解决什么问题(知识更新、上下文限制、幻觉),再讲一条链路的五个阶段,然后重点讲切分和检索这两个最容易出问题的环节,最后补一个你实际踩过的坑和解决方法。这样讲下来,面试官基本能判断你是真做过还是只背过。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 13:46:28

最大似然序列估计(MLSE)实战拆解:从维特比搜索到工程落地

最大似然序列估计这个词,第一次看到的人多半会被“最大似然”四个字劝退,觉得又是概率论里那套绕来绕去的东西。但如果你做过数字通信接收端的活儿,尤其是信道存在记忆性、当前符号会被前面若干个符号拖累的场景,你就会发现MLSE其…

作者头像 李华
网站建设 2026/11/2 13:45:47

Agent判断器:Laya与Jev实现可控决策闭环

1. 这个“判断器”不是加功能,是给 Agent 装上刹车和后视镜最近在几个技术群和开源项目讨论区里,反复看到有人问:“我的 Agent 总是跑偏,指令理解错、工具调用乱、结果不可控,有没有办法让它‘想一想再动手’&#xff…

作者头像 李华
网站建设 2026/11/2 13:45:42

12款开源OCR引擎横评:按场景选型与落地避坑

1. 测评缘起与整体设计思路先说结论:OCR 这活儿,看着是"图片进、文字出"的黑盒,实际上不同引擎在不同场景下的差距能有天壤之别。同一张发票,A 引擎能给你连表头带税额框完整还原,B 引擎可能把""认…

作者头像 李华
网站建设 2026/11/2 13:45:39

deepseek-super-agent深度解析与思考

deepseek-super-agent深度解析与思考 10 月 31 日晚上,deepseek 发布技术性文章《Super-agent 设计探索:迈向更自主的 Agent 系统》。 熬夜看了一下,为大家解析一下主要内容和我自己的思考。 总的来说呢,感觉很适合改进dots、grok…

作者头像 李华
网站建设 2026/11/2 13:45:18

语音交互技术:从原理到工程实践

1. 语音交互技术概述语音交互(Voice-Based Communication)正在重塑人机交互的范式。从智能音箱到车载系统,从客服机器人到无障碍设备,这项技术已经渗透到我们生活的方方面面。作为从业者,我见证了语音交互从简单的命令识别发展到如今能够理解…

作者头像 李华
网站建设 2026/11/2 13:45:16

NASA数字孪生技术在航天增材制造中的应用与突破

1. 项目背景与核心价值NASA正在推进的这项数字孪生技术研究,本质上是通过构建航天部件的虚拟镜像来预测其增材制造性能。这相当于给每个物理零件配备了一个"数字克隆体",在虚拟空间中实时模拟材料特性、结构强度等关键参数。这种技术组合在航天…

作者头像 李华