
上周我帮团队做模拟面试连续几个候选人被问到“讲讲 RAG”时回答都高度一致先背一遍“检索增强生成就是检索加生成”然后提到向量数据库、Embedding、Prompt 拼接最后在“为什么能减少幻觉”上含糊其辞。不是他们不懂而是知识是零散的没有形成一条能贯穿始终的直觉链路。这篇文章我想换一种方式——用“学生查资料写报告”的故事做主线把 RAG检索增强生成的每个环节翻译成真实可运行的代码。故事帮你建立直觉代码帮你建立手感两条线走完下次面试再被问到 RAG你不需要背直接讲就行。1. 面试被问倒的根源只记住了定义没建立全链路直觉1.1 标准定义为什么救不了你RAG 的学术定义很简洁给定外部知识库先检索与问题相关的文本片段再把片段拼进 Prompt交给大语言模型生成答案。这句话背下来不难但面试官几乎肯定会在后面跟一句“那你说说为什么向量检索比关键词检索更适合 RAG”或者“你的 Chunk 大小怎么定的Overlap 设多少”这些问题的共同点是它们考察的不是概念而是你在设计一个完整系统时的取舍能力。RAG 看起来是“检索 生成”两步实际展开却是一条完整的链路文档加载、文本切分、向量化、索引构建、召回、重排、Prompt 组装、生成、评测。任何一个环节的决策都会影响最终答案的质量。面试官问的“为什么”本质上是在逼你把这条链路在脑子里跑一遍。我见过不少候选人能背出完整的 RAG 流程图但被问到“Chunk 设 500 还是 200”时第一反应是“看经验”却说不清经验背后的代价。这说明他缺少的不是知识而是把问题映射到具体环节的能力。1.2 RAG 真正要解决的三类问题在动手写代码之前得先想清楚 RAG 存在的必要性。大语言模型本身有两个天然缺陷一是知识有截止日期新发生的事情它不知道二是它靠概率生成遇到不确定的内容时可能一本正经地胡说八道。RAG 是针对这两个缺陷的工程化方案。具体展开RAG 解决的是三个问题知识时效性模型训练完成之后知识就冻结了。RAG 允许你随时往知识库里塞新文档不需要重新训练模型。私有知识隔离企业内部文档、个人笔记、产品手册这些内容不可能写进模型训练集。RAG 把资料放在外部存储里只有被检索到时才会参与生成。可溯源与可干预回答的每一句话都能追溯到是哪一篇文档里的哪一段。出问题时可以查、可以修这种可控性是纯靠模型“回忆”做不到的。面试时如果能主动用这三类问题来解释“为什么需要 RAG”会比单纯讲定义显得更有工程判断力。1.3 一条故事线让学生替你把 RAG 讲明白我试过很多种类比最适合面试场景的是“学生查资料写课程报告”。想象一个学生要写一篇《中国茶文化》的期末报告。他不会凭空瞎写而是先去图书馆在索引柜里查“茶”的卡片位置找到对应的书架再从书架上抽出几本相关的书翻到讲茶的章节快速扫一遍把重要的段落折角、做标记最后带着这几本书回到书桌前翻到折角的那几页开始动笔写。这个场景和 RAG 的对应关系非常清晰学生写报告RAG 组件作用图书馆索引柜向量索引根据语义找到可能相关的资料书架上的书文档库知识来源扫读并折角做标记召回与重排从候选片段中挑出最相关的几段回到书桌翻到折角页Context 组装把检索结果拼进 Prompt动笔写报告LLM 生成基于资料组织语言这条故事线的妙处在于它可以延展。面试官追问“RAG 有哪些变体”你完全可以在同样框架下说学生发现书架上的书不够于是给管理员留言要新书——这就是知识库更新学生写报告中途发现某段资料引用不对再次跑回图书馆重新查——这就是迭代检索学生不只看单本书还对比两本书的交叉观点——这就是多路召回与融合。有了主线之后每个技术名词都挂到了故事上面试回答自然就会结构化。2. 把故事翻译成技术链路你需要的每一个组件2.1 从“学生拿到题目”到“图书馆索引柜”文档加载与切分故事里学生写报告的第一步是确认题目范围对应到 RAG 工程里第一步是把你手头的资料整理成适合检索的形式。原始资料可能是 PDF、Markdown、网页或者数据库记录第一步要把它们统一转成纯文本。这个环节叫文档加载看起来简单但坑不少——PDF 里的表格会被拆乱、扫描件需要 OCR、网页要清洗导航栏。加载完之后的切分是第一个真正影响结果的技术决策。一本书或一份长文档直接扔进模型不现实因为检索的最小单元不应该是整篇文档而应该是“语义相对完整的一段话”。切分的过程就是决定“这段文字从哪里断开”。我常用的切分策略是用递归字符分割器按段落、句子、标点逐级切。关键参数是 Chunk Size 和 Overlap。Chunk Size 指每块文本的长度Overlap 指相邻两块之间重叠的部分。重叠的目的是避免一个完整的意思恰好被切分线劈成两半。比如讲白茶的段落如果前半段落在上一个 Chunk 末尾、后半段落在下一个 Chunk 开头检索时就可能只召回一半的上下文。2.2 向量化把书页变成图书馆索引卡学生去图书馆不需要把整本书背下来只需要记书名、分类号、位置。对应到 RAG 里我们不能把每段文本完整地存进索引而是要把每段文本变成一个“语义指纹”这个指纹就是向量。向量化的工作由 Embedding 模型完成。你输入一段文本模型输出一串几百维的浮点数。设计上语义相近的两段文字它们的向量在空间里距离也更近。这样“白茶有什么特点”这个问题的向量就能和知识库中讲白茶的段落向量靠得很近检索就变成了“在高维空间里找最近邻”。这里有一个需要反复跟面试官强调的点Embedding 模型选型直接影响检索质量。通用领域用 OpenAI 的 embedding 接口很方便但对中文垂直领域直接用开源的 BGE、M3E 这类模型微调过的效果往往更好。原因很简单——Embedding 是语义映射通用模型懂的“语义”和你的行业术语可能完全不是一回事。2.3 召回向量检索为什么不是万能的检索环节是 RAG 的核心。最朴素的实现是把问题向量化然后去向量库里查最相似的 K 个片段。向量检索的优势是它懂语义——“怎么泡茶不出苦味”和“泡茶水温太高导致苦涩”在字面上重合度很低但语义上高度相关向量检索能命中。但向量检索也有短板。关键词精确匹配在处理专有名词、编号、代码符号时更可靠。比如你搜“BERT 的 [CLS] token”向量检索很可能把语义相近但完全不是同一个东西的低级错误排到前面。所以工业界的通行做法是混合检索向量检索跑一路关键词检索BM25跑一路两路结果合并去重之后再统一排序。2.4 重排与生成别把 Top-K 直接丢给模型学生从书架上抽出十本书但他不会十本全摊开看会先看目录和摘要筛出最相关的两三本精读。重排器做的就是这件事召回阶段为了不漏掉候选往往会把范围放宽比如召回 50 段重排阶段再用一个更精细的模型把这 50 段重新打分挑出最相关的 5 段。为什么需要多此一举因为向量检索的“粗排”追求召回率用的是快速但相对粗糙的相似度计算重排则可以用更重的模型比如 Cross-Encoder做精细匹配把问题和知识片段拼在一起过一遍模型判断相关性。面试时能主动提到“召回和重排分离”这个设计面试官会认为你真的做过 RAG 项目而不是只会跑 Demo。最后一步才是生成。把问题、检索到的若干片段、一个写清楚任务指令的 Prompt 一起交给大模型。这里的技巧很多比如要告诉模型“只依据提供的资料回答不要臆测”“如果资料中找不到答案就直接说不知道”。这些指令会让输出质量明显不一样。3. 代码落地一套最简单但完整的 RAG 流水线3.1 环境准备与选型思路写代码之前先把选型说清楚。我选择的是langchainchromadbollama原因有三一是全链路可以在本地跑不需要 API Key适合面试前自己动手练二是各组件之间替换容易今天用 OpenAI、明天换开源模型都方便三是这几个库的社区活跃度最高搜报错容易。整套代码在 8GB 显存的机器上就能跑起来。安装依赖pip install langchain langchain-community langchain-chroma chromadb sentence-transformers模型方面我选了nomic-embed-text作为 Embedding 模型qwen2.5:7b作为生成模型用 ollama 管理。先拉模型ollama pull nomic-embed-text ollama pull qwen2.5:7b如果你本机没有 ollama 或者显存不够替换也很容易Embedding 换成HuggingFaceEmbeddings加载BAAI/bge-small-zh-v1.5生成模型换成调用 OpenAI 兼容接口代码其余部分不用动。3.2 文档加载与切分这一步决定了检索的上限我准备了三段关于茶文化的文本作为示例知识库。注意这里我故意把内容写得信息密度高一些方便后面演示检索效果。from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.documents import Document raw_docs [ 白茶属于微发酵茶制作工艺只有萎凋和干燥两道工序。 白茶的萎凋过程让茶叶中的多酚类物质发生缓慢氧化 形成了白茶特有的清甜口感和毫香。白茶的保存方式以密封避光为主 存放得当的老白茶会转化出枣香和药香。, 冲泡白茶的水温建议在90℃到100℃之间。新茶白毫银针水温可以略低 85℃左右出汤较快避免烫出苦涩味。老白茶则建议用沸水冲泡 甚至可以用来煮饮煮出来的茶汤更加醇厚。, 红茶的发酵程度较高茶多酚在发酵过程中氧化聚合 茶汤呈现红色滋味醇和带甜。常见的红茶有小种红茶、工夫红茶和 红碎茶。冲泡红茶的水温一般控制在90℃到95℃出汤要快。 ] splitter RecursiveCharacterTextSplitter( chunk_size80, chunk_overlap20, separators[。, , \n\n, \n, , ], ) chunks splitter.split_text(\n.join([doc.replace(, ) for doc in raw_docs])) print(f切分后共 {len(chunks)} 个片段)我故意把chunk_size设得很小80 字是为了能在示例里观察到切分效果。实际生产环境中中文场景我一般先试 300 到 500 字观察切出来的每段是否有完整语义再根据你的文档类型微调。chunk_overlap通常取chunk_size的 10% 到 20%。切分切得好不好直接决定后面引用到模型面前的文本是不是“人话”这一步值得花时间反复调。3.3 构建向量库从文本到可检索的索引把切好的片段交给 Embedding 模型算好向量存入 chromadbfrom langchain_community.embeddings import OllamaEmbeddings from langchain_chroma import Chroma embeddings OllamaEmbeddings(modelnomic-embed-text) vectorstore Chroma.from_documents( documents[Document(page_contentc) for c in chunks], embeddingembeddings, persist_directory./tea_kb, ) print(向量库构建完成索引片段数:, vectorstore._chroma_collection.count())Chroma.from_documents会做三件事调用 Embedding 模型把每个片段转成向量、把向量存进本地磁盘、为向量构建近似最近邻索引。你在面试时可以补充一句向量检索用的是近似最近邻ANN不是精确最近邻因为高维空间暴力搜索太慢系统用 HNSW 这类索引结构在“搜索速度”和“召回精度”之间做取舍。3.4 检索与重排看看实际召回了什么现在模拟一个用户问题“白茶为什么会有一股甜味”这个 query 会先被向量化然后到向量库中检索最相似的片段。question 白茶为什么会有一股甜味 retrieved vectorstore.similarity_search_with_score(question, k2) for i, (doc, score) in enumerate(retrieved): print(f[Top {i1}] 距离: {score:.4f}) print(doc.page_content) print(---)要注意的是similarity_search_with_score返回的距离是余弦距离值越小代表越相似。如果召回的前两段都不够理想我一般会先看是不是知识库里根本没有相关内容其次再怀疑 Embedding 模型对领域知识的理解力。最常见的问题是把k调得太大导致不相关内容混进来反而稀释了生成的上下文。你可以在自己的项目里打印出召回的片段肉眼看一遍就能发现很多问题。3.5 构造 Prompt 并调用大模型生成召回之后把片段拼进 Prompt调用 qwen2.5:from langchain_community.llms import Ollama from langchain_core.prompts import ChatPromptTemplate llm Ollama(modelqwen2.5:7b, temperature0.3) prompt_template ChatPromptTemplate.from_messages([ (system, 你是茶文化知识助手。只依据提供的资料回答问题 如果资料中找不到答案请直接回答资料中未提及。 不要自行补充资料之外的信息。), (human, 相关资料如下\n{context}\n\n问题{question}), ]) context \n\n.join([doc.page_content for doc, _ in retrieved]) chain prompt_template | llm answer chain.invoke({context: context, question: question}) print(answer)temperature0.3是有意设置的生成答案时我们希望模型更多依赖提供的资料而不是自由发挥因此温度别调太高。如果资料本身足够完整甚至可以调到 0让输出更稳定。实际跑这段代码qwen2.5 会从检索到的白茶工艺片段里找线索回答“白茶的萎凋过程中多酚类物质氧化形成清甜口感”。到这里一条完整的最小 RAG 流水线就跑通了。但这只是及格线——面试官大概率还会继续追问“你跑完之后怎么证明它好用”。这就进入评测环节。4. 从“能跑”到“好用”RAG 评测才是面试的分水岭4.1 Hit Rate 是什么别只会背缩写搜索引擎里“在第十条结果里找到了答案”算是成功还是失败答案取决于你的标准。RAG 评测中最基础的指标是 Hit Rate定义很朴素在召回的 Top-K 片段里是否包含能回答问题的正确片段。如果包含了就算一次命中。用代码表示def hit_rate(retrieved_chunks, golden_chunk): return 1.0 if golden_chunk in retrieved_chunks else 0.0注意 Hit Rate 只看“正确答案在不在召回列表里”完全不关心上下文里混了多少无关片段。所以它是一个“召回率”导向的指标适合先用来验证知识库和检索链路的基本盘。你的知识库切片有严重问题、或者 Embedding 选错的时候Hit Rate 会立刻暴露问题。Hit Rate 太低的系统后面生成质量一定好不了因为连原料都没找对。4.2 仅看 Hit Rate 的陷阱检索好不等于生成好这是我实际做项目踩过最深的坑。有一段时间我负责的问答系统 Hit Rate 看起来不错——需要的那段资料基本都能进 Top 5——但用户反馈的答案质量依然不稳定。后来逐条看日志才发现检索 Top 5 里经常混着 3 段废话这几段废话和正确答案拼在一起送给模型之后模型被干扰了反而抓不住重点。这时候需要引入第二个指标忠实度。它衡量的是“生成的答案是否严格来源于提供的资料”。比如资料里说“白茶有枣香和药香”模型回答“老白茶有枣香”这是忠实模型回答“老白茶有枣香和桂花香”后半句就是不忠实。忠实度评测适合用更强的大模型来判断比如用一个“裁判模型”读一遍资料和答案打一个相关性/矛盾分数。所以真实的评测体系应该是分层的先用 Hit Rate 验证检索环节有没有把原料找回来再检查上下文里的噪声比例最后才对“最终答案质量”做整体评估。每一层都在为下一层兜底。4.3 离线评测与线上效果之间的最后一公里离线评测只能告诉你“系统在测试集上表现得怎么样”但真实用户的问题千奇百怪很难全部覆盖。我建议的做法是准备一份 50 到 100 条真实问题的评测集先把 Hit Rate 跑到 80% 以上再逐条手动看生成的答案把“答非所问”和“胡说八道”分别归类统计。如果答非所问多问题多半出在检索如果胡说八道多问题多半出在 Prompt 设计或知识库内容与问题不匹配。面试时能把这一套评测思路讲出来并且坦率地说“Hit Rate 高只是必要条件不是充分条件”就已经能和绝大多数只跑过 Demo 的候选人拉开差距。5. 面试加分时刻RAG 的瓶颈与进阶方向5.1 切分导致的语境割裂用父子块和滑动窗口解决RAG 最经典的翻车场景是知识库有完整的一段描述但切分之后命中片段缺少了必要的上下文。比如资料里写“这种方法在A场景不适用但在B场景表现突出”切分时把转折关系切散了模型只看到后半句就会得出错误结论。要解决这个问题工程上有两个常用方案。第一个是父子块切分小块用于检索命中之后用映射关系把整个大块一起喂给模型。血缘关系可以简单用文本哈希或者行号区间记录实现也不复杂。第二个是滑动窗口重叠切分时让相邻块保持足够多的重叠文本保证语义的连续性。我在实际项目中通常两个方案同时用小块控制检索精度父块兜底生成上下文。5.2 Agentic RAG从“查一次”到“按需反复查”基础 RAG 是单轮问答问题进来检索一次生成一次结束。但真实问题往往需要分解。比如“对比白茶和红茶在工艺和冲泡上的区别”一次检索可能拿到白茶的资料但红茶的资料在另一篇文章里。Agentic RAG 的思路是让模型变成“策略调度员”它自己决定先搜什么、看结果之后决定要不要再搜、搜回来之后把多次结果汇总成答案。实现层面你不需要自己写多复杂的 Agent 框架。langchain 里提供了create_retriever_tool把检索器封装成一个工具再交给具备工具调用能力的模型。模型会在需要时自行调用检索工具。代码示意from langchain.tools import create_retriever_tool from langchain.agents import create_tool_calling_agent retriever_tool create_retriever_tool( vectorstore.as_retriever(search_kwargs{k: 3}), search_tea_knowledge, 搜索茶文化知识库获取与茶类、工艺、冲泡相关的资料片段, ) tools [retriever_tool]面试时提到 Agentic RAG重点不是代码而是讲清楚这个演变背后的动因基础 RAG 只能做单跳检索而问题本身是多跳的。Agentic RAG 通过让模型自主规划检索步骤把“问题拆分、逐步检索、汇总生成”串起来。但代价也很明显——延迟变高、token 消耗变大、模型决策不稳定时需要人工兜底。5.3 GraphRAG 与本体 RAG兼顾全局结构GraphRAG 是另一个进阶话题。传统向量检索擅长找“语义相似的局部片段”但不擅长回答“整个知识库中谁和谁之间存在多跳关联”这类全局问题。GraphRAG 的思路是把实体和关系抽出来构建知识图谱在图上做遍历和推理。它比向量检索更擅长回答“某类事件影响了哪些实体”这类结构依赖型问题。面试时不需要把 GraphRAG 讲得多深你只要能说清楚“向量检索擅长局部语义匹配、图结构能在全局关系上提供更强的推理路径、两者互补”就已经足够了。实际上大部分生产级 RAG 系统也不会只靠一种检索方式。5.4 轻量本地部署的实践为什么建议你亲手搭一遍热搜里看到“ollama 简易本地 RAG 知识库”这个组合我非常认同这套学习路线。本地部署最直接的好处是不依赖外部 API跑一次全流程的成本几乎是零。你可以在不影响线上服务的前提下反复调整切分参数、换 Embedding 模型、调 Prompt每一次改动都能立刻在检索结果上看到反馈。在本地搭 RAG 的另一个隐性好处是让你对“数据从哪来、被存在哪、如何被检索”有完整的掌控感。工作中你大概率会用云服务或公司内部平台但理解“底下发生了什么”能帮你在系统出问题时快速定位环节——是切分出了问题Embedding 模型对该领域理解不足还是召回排序不对这些都是生产环境中真实要面对的问题。6. 下次面试被问到 RAG这样组织你的回答6.1 三分钟回答骨架如果面试官让你“讲讲 RAG”我的建议是不要从定义开始而是从故事直接切入然后快速转入工程链路。你可以这样组织第一句话用类比——RAG 相当于给大模型配上图书馆管理员和摘抄本让它在答题时先查资料再落笔。第二句话指出核心价值——把知识从模型的隐性参数记忆变成外部显式资料让答案可控、可溯源、可更新。第三句话开始拆链路——从文档切分、向量化、召回、重排到生成每一环都在解决具体问题。最后一句话用你自己的实践收尾——你在什么场景用过、遇到了什么问题、怎么解决的。这套结构的核心是“每个技术点都有对应的业务问题”。这比任何华丽的术语都更打动人。6.2 被连续追问时的防御策略面试官追问往往是压力测试问的未必是你不会的而是看你会不会慌。这里分享一个很实用的策略当被问到没准备过的细节时不要硬编而是坦诚地说“这个问题我在当前项目里没有直接遇到但根据 RAG 的整个链路逻辑我会这样判断……”。因为 RAG 是一条链路任何一环的选择都会影响下一环所以你可以从链路关系出发做推理。比如被问到“切分时如何判断语义完整性”你可以回答“我会看切分后的片段是否包含一句话的主干和完整论断如果一个片段里只有半句那它即使被检索到也无法生成好答案所以我会同时设定 overlap 和父子块策略来兜底。”这比闷头想正确答案有效得多。6.3 我自己实操后的几点体会最后说点个人经验。RAG 看起来容易做出来难难的不是跑通流程而是调出一个真正能用的系统。切分参数、Embedding 模型、召回数量、Prompt 模板每个环节都有调整空间但它们的相互作用并不直观。我建议新手阶段不要同时调整多个参数一次只动一个对比输出结果这样你才能理解每个键的作用。另一个建议是不要只依赖标准评测集回答面试问题。面试官问“你怎么评测 RAG 系统”你需要的是你自己真实跑过的评测方式和评测结果哪怕评测集只有 30 条问题只要你能讲清楚为什么这么设计、发现了什么问题、怎么改进的就比背十个指标名称有力得多。技术面试到最后永远是在评估你有没有真正做过事情而不是评估你装进脑子里的名词数量。