上周帮朋友做模拟面试,让他讲讲 RAG(检索增强生成),他张嘴就是一套标准定义:“从知识库检索相关内容,拼进 prompt,让大模型基于这些内容生成答案。”听起来没毛病,但面试官接着追问了三个问题,他直接卡壳:“你的文档切多大块?为什么?”“Embedding 用的什么模型?怎么选的?”“检索效果不好你怎么排查?”这种场景太典型了。
我见过太多人背熟了概念,却答不出实操细节。RAG 看着简单——检索、拼接、生成三步,但每一步都藏着无数坑。面试官问 RAG,从来不是考你能不能复述定义,而是想确认你有没有真正做过、踩过坑、解决过问题。
所以我今天换个讲法:先用一个故事把 RAG 的机制讲透,再配一套能从零跑通的代码,最后把面试官最爱追问的考点和实战中的坑全部摊开。不管你是准备面试的候选人,还是想自己搭一个知识库问答系统的开发者,这篇文章的目标只有一个:让你不仅能讲清楚 RAG 是什么,更能说清楚每一步为什么这么做。
1. 先讲个故事:RAG 本质上是个“开卷考试”
很多人学 RAG 一上来就怼概念,什么“双组件架构”“检索器生成器”,越听越糊涂。其实 RAG 的机制用一个场景就能说明白:开卷考试。
1.1 从开卷考试到 RAG 的三个核心动作
你想想开卷考试的流程:拿到一道题,先翻目录找相关章节,找到后把内容摘抄到答题区,再结合自己的理解组织答案。RAG 做的事一模一样。
大模型本身像一个“闭卷考试”的考生,知识全存在训练时的参数里。你问它“2024 年某产品的用户协议是什么”,它如果没学过就只能编。RAG 相当于允许它开卷:先从外部知识库里检索相关段落,把段落塞进 prompt,再让它基于这些材料回答。检索对应“翻目录找章节”,拼装对应“摘抄到答题区”,生成对应“组织答案”。
这个类比能帮你回答一个面试必问题:“为什么需要 RAG 而不是直接微调模型?”因为开卷永远比闭卷可靠。模型参数里的知识是训练时刻的快照,更新一次要花几十万成本;而外部知识库可以随时增删改,成本低得多。微调是让模型“记住”新知识,RAG 是让模型“查阅”新知识,前者有遗忘和幻觉风险,后者只需要保证检索质量。
1.2 一次完整的 RAG 调用,数据流到底怎么走
把故事落到工程层面,一次完整调用包含五个环节:
- 文档加载:从 PDF、网页、数据库里把原始文本读出来。
- 文本切分(Chunking):把长文档切成固定大小的小块,比如每块 500 token,相邻块之间重叠 50 token。
- 向量化(Embedding):把每个文本块用 embedding 模型转成向量,比如 768 维或 1024 维的浮点数数组。
- 向量检索:用户提问时,把问题也转成向量,在向量库里做相似度搜索,找到最相关的 3~5 个文本块。
- 生成增强:把检索到的文本块和原始问题一起拼进 prompt,交给大模型生成答案。
面试时你把这条链路背下来,只能得 60 分。要拿 80 分以上,必须能回答“每一环节的关键参数怎么定、出问题了怎么排查”。
1.3 RAG 的三个核心问题和一句话回答
我把 RAG 的考点压缩成三个问题,面试官所有的追问几乎都围绕这三个点展开:
- 查什么:你的知识库粒度多大?切块大小直接决定能不能查到精细信息。
- 怎么查:向量检索够不够?要不要关键词检索补充?要不要重排?
- 怎么用:检索到的内容全部塞给模型?塞不下怎么办?内容相互冲突怎么办?
这三个问题在后面的章节里都会展开。先记住一个结论:RAG 的瓶颈不在生成,在检索。检索质量决定了答案质量的上限,LLM 只是把检索结果做二次加工。
2. 从零实现一个最小 RAG:代码逐行拆解
理论说再多,不如跑通一遍代码。这一节我提供一个最小可运行的 RAG 实现,完全本地方案,用 Ollama 跑 embedding 和 LLM,不花一分钱 API 费用。你跟着敲一遍,就具备了实战的基础认知。
2.1 环境准备:安装依赖和模型
先装依赖,我推荐用 conda 建一个干净环境,避免依赖冲突:
conda create -n rag_demo python=3.10 -y conda activate rag_demo pip install langchain langchain-community chromadb ollama然后安装 Ollama,拉取两个模型,一个是 embedding 模型,一个是生成模型:
ollama pull nomic-embed-text ollama pull qwen2.5:7b选 nomic-embed-text 的原因是它体积小、中文效果够用;生成模型选 qwen2.5:7b 则是因为中文能力扎实,本地推理也不至于太慢。你如果没有 GPU,可以把 7b 换成 qwen2.5:3b,效果略降但 CPU 也能跑。
注意:Ollama 拉取模型需要网络通畅,模型文件比较大(7b 大概 4~5GB),提前确认磁盘空间充足。
2.2 文档加载与切分:不是简单按字数劈开
假设我们有几百条公司内部的 FAQ,存在一个knowledge.txt里,每行一条问答。加载和切分的代码如下:
from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader = TextLoader("knowledge.txt", encoding="utf-8") documents = loader.load() text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"原始文档切分为 {len(chunks)} 个块")为什么用RecursiveCharacterTextSplitter而不是简单split("\n")?因为它的设计很巧妙:优先按段落切,段落太长再按句子切,句子还太长再按标点切,层层递归。这样能最大程度保证每个 chunk 在语义上是完整的。
我实测过一个教训:直接按固定字符数切,很容易把一句话拦腰截断,比如把“本产品保修期为一年,逾期不予受理”切成“本产品保修期为一年,逾期不”和“予受理”,检索时这两块谁都答不完整,召回效果很差。
chunk_size=500和chunk_overlap=50这两个参数后面细讲,你先感受一下这个默认配置。
2.3 Embedding 与向量库存储
切好块之后,把它们向量化并存入 Chroma 向量库:
from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma embeddings = OllamaEmbeddings(model="nomic-embed-text") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db" ) print(f"已存储 {vectorstore._collection.count()} 条向量")这步代码很简单,但背后发生的事你得能说清楚:embedding 模型把一段文本映射成一个向量,语义相近的文本在向量空间里距离也近。比如“产品怎么保修”和“保修政策是什么”虽然字面不同,但向量距离很近;而“今天天气不错”和“保修政策”距离很远。这就是向量检索能理解语义、而传统关键词检索做不到的原因。
我建议你把persist_directory加上,这样向量库会落到磁盘,下次代码直接加载,不用每次重复 embedding。
2.4 检索与生成:核心链路跑通
检索阶段我故意先写一个最朴素的版本,不做任何优化:
question = "产品保修期是多久?" retrieved_docs = vectorstore.similarity_search(question, k=3) for i, doc in enumerate(retrieved_docs): print(f"--- 检索结果 {i+1} ---") print(doc.page_content[:200])similarity_search干了两件事:把问题向量化,然后和库里的每条向量算余弦相似度,返回最相似的 3 条。k=3的意思是我们只取前三条文本块。为什么是 3 不是 10?因为 context 越长,LLM 处理的 token 越多,成本越高、响应越慢,而且无关信息太多反而会干扰生成。
生成阶段,用 LangChain 的 prompt 模板把检索结果和问题拼装起来:
from langchain_community.llms import Ollama from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser llm = Ollama(model="qwen2.5:7b", temperature=0) prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个严谨的客服助手,只能根据给定的资料回答问题。如果资料中没有相关信息,直接回答‘根据现有资料无法回答’,禁止编造。\n\n资料如下:\n{context}"), ("human", "问题:{question}") ]) def rag_answer(question: str) -> str: docs = vectorstore.similarity_search(question, k=3) context = "\n\n".join([doc.page_content for doc in docs]) chain = prompt | llm | StrOutputParser() return chain.invoke({"context": context, "question": question}) print(rag_answer(question))这段代码里 prompt 的设计非常关键。注意 system 提示词里的约束:“只能根据给定的资料回答”“没有相关信息就直说”。这直接决定了 RAG 能不能抑制幻觉。
我见过很多人搭 RAG,prompt 写得极其简单:“根据资料回答问题”,结果模型经常自作主张补充资料里没有的内容。加了“禁止编造”约束之后,效果立刻改善,这是成本最低的防幻觉手段。
2.5 完整代码跑通后的效果验证
跑通之后,你可以测试几类问题来验证系统是不是真的“有检索能力”:
- 原文直接有答案的问题,比如“保修期多久”这类。
- 需要跨 chunk 总结的问题,比如“保修政策和退换货政策分别是什么”。
- 知识库里没有答案的问题,比如“产品支持哪些颜色”。
第三类问题特别值得测。如果系统能回答“根据现有资料无法回答”,说明 prompt 约束起了作用;如果它硬编一个颜色出来,说明要么检索到了无关内容,要么 prompt 约束不够强。这两种情况对应的排查方向完全不同。
我还建议你做个坏测试:把k从 3 改成 10,再问同一个问题,观察回答质量变化。你会发现答案并不一定更好,有时反而更差,因为无关的检索结果混进来了。这个体验能帮你真正理解为什么检索质量比检索数量更重要。
3. 面试官到底想听什么:RAG 高频考点精讲
跑通最小实现之后,你有了动手基础,接下来才是面试分水岭。这一节我把面试官最常追问的五组问题全拆开,每一组都给出深度答案。
3.1 切块参数:chunk_size 和 chunk_overlap 怎么定
面试官问切块,想听的不是“设成 500”,而是“根据什么逻辑设 500”。
先理解两个参数的本质:chunk_size决定每个文本块的大小,chunk_overlap决定相邻块之间的重叠量。两者共同决定了检索的“粒度”。
chunk 越小,语义越单一、检索越精准,但单块信息量少,可能截断关键上下文。比如一个答案需要两段话才能说清,你切得太小,检索到的块可能只有前半段,回答不完整。chunk 越大,上下文越完整,但语义可能混杂,比如一个 chunk 里同时包含保修政策和退换货政策,用户问其中一项,向量匹配时会被另一项干扰,相似度被拉低。
实际项目里我一般从chunk_size=500、chunk_overlap=50起步,然后根据效果调整。调整的依据是:检索到的内容是否包含回答问题的关键句,以及回答是否有信息缺失。
chat_overlap 的作用很多人忽略:它用来缓解“切断点问题”。如果某段关键信息刚好被切在块尾,下一个块又从头开始,两块都没包含完整的上下文。重叠区让切断点附近的信息在相邻块中重复出现,增加召回概率。工程上常见设置是 chunk_size 的 10%~20%。
经验值:中文场景下,chunk_size 300~800 是一个比较通用的区间。FAQ 类的短文本适合偏小,长文档、报告类适合偏大。没有绝对正确,只有针对你数据的最优。
3.2 Embedding 模型选型:为什么不能用 BGE 跑全部场景
Embedding 模型是 RAG 的地基,选错了后面怎么优化都费劲。
先说结论:中文场景优先考虑BGE(BAAI/bge-large-zh)、m3e、text2vec这类中文表现好的模型;如果预算充足、追求最强效果,直接用 OpenAI 的text-embedding-3-large或者 Cohere 的embed-v3。本地离线部署可以用nomic-embed-text或者bge-m3,前者体积小,后者多语言能力强。
面试中问到这个点,你要能说出选型依据:一看语言支持,英文语料用英文模型没问题,中文语料用英文模型效果会大打折扣;二看向量维度,维度越高通常表达越精细,但内存占用也越大,10 万条文本用 1024 维向量,光存储就要近 800MB;三看模型本身的训练数据,垂直领域(比如法律、医疗)最好选在该领域语料上训练或微调过的模型。
我踩过一个记忆很深的坑:项目里直接用 OpenAI 的 embedding 处理中文法律文书,效果一直不理想。后来换成 BGE 中文模型,同样的检索流程,hit rate 提升了将近 15 个点。原因很简单,英文 embedding 模型对中文语义的理解天然弱一截,这种差距不是调参能弥补的。
3.3 检索质量怎么评估:hit rate、MRR 到底怎么算
面试官问“你怎么评估 RAG 效果”,一半人会说“看起来还行”。这个回答直接扣分。评估有定量指标,最常用的是 hit rate 和 MRR。
hit rate(命中率)的含义是:对每个测试问题,检索返回的前 k 个结果中是否包含正确答案。包含就是 1,不包含就是 0,最后对所有问题取平均。比如有 100 个测试问题、k=3,其中 80 个问题在返回的 3 个块里能找到答案,hit rate 就是 80%。这个指标回答的问题是:系统到底能不能检索到正确材料。
MRR(Mean Reciprocal Rank,平均倒数排名)比 hit rate 更进一步。它考虑正确答案在结果中的排名:排第 1 位得 1 分,排第 2 位得 0.5,排第 3 位得 0.33,然后取平均。MRR 高意味着正确答案在检索结果中排得靠前,这直接决定了 LLM 能不能优先看到正确信息。
实际评估时,你需要先构建一个“问题-答案-来源块”三元组的测试集。做法是从知识库里挑出若干文本块,为每个块人工设计 2~3 个问题,记录该块的索引作为标准答案。然后跑检索,看返回结果里是否包含标准块。这套流程能让你在大改切块参数或换 embedding 模型时,有数据支撑而不是拍脑袋。
3.4 朴素 RAG 到高级 RAG:重排、混合检索到底解决什么问题
面试官特别喜欢问:“你的方案是最朴素的 RAG,如果效果不好,你会怎么优化?”这个问题考察的是你是否知道 RAG 的演进路线。
先明确概念:刚才代码里实现的方案叫 Naive RAG(朴素 RAG),检索一次、生成一次,不做任何后处理。它的瓶颈在于:向量检索只考虑了语义相似度,忽略了关键词精确匹配;而且返回结果中可能前两条都不相关,第三条才相关。
针对这两个问题,业界给出了两个方向的优化:
混合检索(Hybrid Search):把向量检索和 BM25 关键词检索的结果合并,再做去重和排序。语义检索擅长找“意思相近但字面不同”的内容,关键词检索擅长找“包含特定术语”的内容。比如搜“RAG 和 GraphRAG 区别”,BM25 能精确命中含“GraphRAG”这种词的文本,向量检索可能只找到语义相近但漏掉关键术语的文本。两者互补,召回效果明显提升。
重排(Rerank):检索阶段先用轻量方法召回 10~20 条候选,再用重排模型精排,取前 3~5 条进入 LLM。重排模型通常基于交叉编码器,把问题和文档同时输入模型,算出一个相关分数,比单纯的向量相似度更准。业界常用
bge-reranker-large或 Cohere Rerank。代价是多一次模型推理,延迟会增加几百毫秒。
面试时如果能说出这两个方案,并且讲清楚“混合检索管召回、重排管精度”,基本就能证明你不是只会调 API。更进一步,你还可以提一句:重排模型的分数还可以当 filter 用,比如低于 0.3 的直接丢弃,避免把无关内容硬塞给 LLM。
3.5 从 RAG 到 Agentic RAG:新一代架构在解决什么
今年(2025 年)面试越来越常问 Agentic RAG。这不是两个词拼在一起,而是完全不同的设计思路。
朴素 RAG 是“一次性”的:用户提问,系统检索一次,生成一次,流程固化。它的问题是:如果第一次检索就不够好,后续生成必然是错的,没有纠错机会。而且遇到复杂问题,比如“对比 A 和 B 两款产品的售后政策”,需要多次查询不同的知识片段,一次性检索根本做不到。
Agentic RAG 把 RAG 变成了一个由大模型驱动的智能体循环:模型先判断问题需要什么信息,决定调用哪个检索工具、用什么样的查询改写方式;拿到检索结果后评估是否足够,不够就再检索;直到信息充分,才生成最终答案。它还能把一个大问题拆成多个子问题,分别检索再汇总。
这个架构对应两个核心能力:
- 查询改写:把用户问题改写成更易检索的形式。比如用户问“保修多久”,改写为“产品保修期限是多少天”,检索效果往往更好。
- 多轮检索和工具调用:根据中间结果决定下一步动作,类似 ReAct 模式。
面试官如果问到 Agentic RAG,你能说出“它把 RAG 从管道变成循环、从单次检索变成多次推理”这个关键差异,就足够拿分了。再补一句它的代价:延迟更高、token 成本更大、失败模式更复杂,说明你真的理解工程取舍。
4. 实战避坑指南:我踩过的坑,你别再踩一遍
代码跑通和面试通过之间,隔着一堆真实项目里才会遇到的坑。这一节我把高频问题全部列出来,每个都给你排查路径。
4.1 检索效果好,生成答案却是错的
出现这种情况,问题大概率出在 prompt 上一一检索结果正确,但模型没有严格遵守“只根据资料回答”的指令。
我遇到过具体案例:知识库里明确写了“保修期为一年”,系统回答却含含糊糊说“保修期大概是一年左右”。原因就是 prompt 里没有强调“引用原文数字,不要自行推测”。加一句“回答时优先使用资料中的原文表达”就能解决。
另一个常见原因是检索结果太多,模型被无关信息干扰。检查的方法是:打印一次实际送给 LLM 的完整 prompt,看看 context 部分是不是混入了答非所问的内容。是就降低 k 值或加重排。
4.2 检索结果为空或有大量不相关内容
先确认问题,再动手改。第-个要查的是:嵌入模型生成的向量质量怎么样。测试方法很简单,找一条知识库里的原文,把它作为 query 去检索,看它自己能不能被找回来。这叫做“自检索测试”。如果连原文都检不回来,说明 embedding 模型和文本不匹配。
自检索测试通过了,再查切块逻辑。我之前排查过一个客户案例:他们的文本是 2000 字一段的合同,我切了 500 字一块,重叠 50,结果用户总检不到完整条款。后来看数据发现,关键条款被切成了两块,两块单独检索都不完整。把 chunk_size 调到 800、overlap 调到 100 后,命中率从 40% 提到 73%。
4.3 上下文太长,模型处理不过来
很多 RAG 项目改着改着,检索结果一大,prompt 就超长了。两个方向:
- 检索侧:严格控制 k 值,加上重排模型后大幅筛掉不相关内容,把进入 LLM 的有效信息浓缩。
- 生成侧:把 LLM 的
max_tokens和上下文窗口配到合理值,比如 qwen2.5:7b 上下文窗口是 32K,可以同时塞 3-5 个 chunk。
还有一个思路:对检索结果做摘要再传给 LLM,而不是原文硬塞。业界叫“压缩式 RAG”,用一个轻量模型把多个 chunk 的关键信息浓缩成几百字,token 成本大幅下降,生成质量反而可能更高。代价是多一步推理,延迟变长。
4.4 知识库更新了,回答还是旧内容
知识库更新后,向量库里旧版本的向量还留在里面,新版本的向量也加进来了。检索时可能同时命中新旧版本,模型不知道哪个是最新。
解决方案是给每个 chunk 加元数据,比如文档版本号、更新时间;检索后先按元数据过滤掉旧版本,再进入 LLM。或者干脆更新时重跑整个索引,删旧加新。这点在面试里也可以提:RAG 不只是“存进去就能用”,增量更新和版本管理是生产环境必须考虑的事。
4.5 面试高频问题速查表
我把面试中最常被追问的问题和答案要点整理成一张表,面试前用这张表自测:
| 面试官问题 | 回答要点 |
|---|---|
| RAG 和微调什么区别? | RAG 是外部知识索引,可实时更新、成本低、可解释;微调是参数更新,训练贵、更新慢、易遗忘。 |
| Chunk 大小怎么定? | 先 500/50 起步,看检索命中率和回答完整性,结合数据长度调整,中文一般 300~800 合理。 |
| Embedding 模型怎么选? | 看语言支持、向量维度、垂直领域训练情况,中文场景优先 BGE、m3e 等。 |
| 检索效果怎么评估? | 构建三元组测试集,算 hit rate 和 MRR,两个指标结合看。 |
| 检索结果不好怎么办? | 排查链条:自检索测试、切块调整、混合检索、重排、query 改写,逐层优化。 |
| 如何避免幻觉? | 强约束 prompt、控制检索质量、加“无法回答”选项、回答要求引用原文。 |
| 生产环境有哪些坑? | 知识库版本管理、token 成本、延迟控制、多路召回与重排的取舍。 |
4.6 工具选型和扩展:LangChain 之外的思路
最后聊几句工具链。我用 LangChain 演示是因为它上手最快、生态全,适合学习和快速验证。但生产环境里,LangChain 的抽象层级较多,出了问题调试链路长;很多团队会直接用 LlamaIndex,或者干脆手写检索逻辑,只把 LangChain 当胶水层。
还有一个值得关注的方向:GraphRAG。它在知识库基础上额外构建实体关系图谱,擅长回答“A 和 B 之间什么关系”“某个事件的影响范围”这类多跳问题。去年微软开源 GraphRAG 项目之后,这个方向热度很高。如果面试官问到更前沿的方案,你提一句“朴素 RAG 适合单跳问答,GraphRAG 适合多跳推理,但它有自己的成本问题”就能展示视野。
本地部署选型上,Ollama 是最省事的,但如果想深入控制量化精度、并发调度、显存管理,vLLM 和 llama.cpp 更合适。我的习惯是:原型验证用 Ollama,上生产换 vLLM,性能和稳定性差距明显。
5. 写在最后:RAG 面试的底层逻辑
我自己面过不少候选人,也帮人模拟过很多次。说句实话,判断一个人是不是真懂 RAG,我最看重的不是他能不能背出概念,而是面对“效果不好你怎么排查”这类开放性问题时的反应。
真正做过 RAG 的人,会先把问题分解:是检索没召回,还是召回了不相关,还是模型没遵循 prompt?然后给出对应的排查手段。没做过的人,通常只会说“调大 k 值”或者“换更好的模型”,这种回答暴露的不仅是经验缺失,更是系统化思维能力的不足。
如果你正准备面试,我建议你把这篇文章里的代码亲手跑一遍,再按第 4 节的坑逐一踩一遍。用一周时间,把一个最小 RAG 从零搭起来,再经历几次“效果烂到想放弃”的时刻,你对 RAG 的理解会远超那些只刷理论的人。
我个人还有个习惯:做 RAG 项目时坚持写测试集。每改一个参数,先跑一遍测试集,记下 hit rate 变化,而不是靠肉眼感受。长期积累下来,你会对自己这套系统的“手感”越来越准,改参数时不再靠猜。
RAG 这个方向不会凉,因为它的核心价值太实在了:让大模型在不重新训练的情况下用上私有知识。只要这个需求存在,RAG 就会一直有位置。理解它的深度,决定你在这个领域能走多远。希望这篇文章能帮你把地基打牢,不管是应付面试还是做真实项目,都能少走点弯路。