尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RAG实战全链路:从文档切分到检索生成,面试与工程细节

RAG实战全链路:从文档切分到检索生成,面试与工程细节 1. 面试官问RAG时到底在考察什么面试里被问到RAG很多人第一反应是背定义检索增强生成把外部知识检索出来喂给大模型减少幻觉。这话没错但面试官听完只会点点头然后追问一句那你实际搭过吗检索命中率怎么保证场面就冷了。问题不在于你记不住概念而在于你脑子里没有一条完整的链路讲不出数据从哪来、怎么切、怎么存、怎么召回、怎么拼进prompt、效果差的时候从哪下手。我自己带过几个做RAG项目的同学也面过不少人发现一个规律能把RAG讲清楚的人往往不是背得最熟的而是真正跑通过一遍完整流程、踩过检索召回不准的坑、调过chunk大小和top-k的人。RAG这东西概念五分钟就能讲完但工程细节能讲一整天。面试官真正想确认的是你有没有把检索和生成这两段真正接起来而不是停留在我知道有这么个东西。这篇内容我打算用一个具体的故事线串起来假设你在一家做企业知识库的公司老板让你做一个能回答内部文档问题的助手。我们从零开始把RAG的每个环节拆开讲配上能跑的代码最后聊聊面试里最容易被追问的几个点。关键词里提到的LangChain、FAISS、Embedding、大模型幻觉都会落到具体操作上。不管你是刚入门想搞懂RAG是什么还是已经写过demo但说不清原理这篇都能帮你把链路补完整。先说清楚RAG解决的核心问题。大模型的知识是训练时冻结的它不知道你公司上周发的制度文件也不知道你私有的产品手册。你直接问它它要么说不知道要么一本正经地编——这就是幻觉。RAG的思路很朴素既然模型不知道那我在它回答之前先去我的知识库里把相关内容找出来连同问题一起塞给它让它看着材料回答。类比一下闭卷考试变开卷考试模型从凭记忆答题变成翻书答题准确率自然上来了。但开卷这两个字背后全是工程活。书怎么拆成能快速翻的页码怎么保证翻到的是对的那一页翻多了模型看不过来翻少了又漏关键信息。这些才是RAG的真正难点也是面试拉开差距的地方。下面我按数据准备、向量化、检索、生成、优化这条主线一段段拆。2. 把文档变成可检索的碎片切分策略决定上限2.1 为什么不能整篇文档直接塞给模型很多人第一个想法是我把整份PDF直接丢给模型不就行了。理论上可以实际上有两个硬约束。第一是上下文长度一份产品手册几万字就算模型支持长上下文成本和延迟也扛不住而且长上下文里模型对中间部分的注意力会衰减关键信息容易被忽略。第二是检索精度整篇文档作为一个向量它的语义被平均掉了你问一个很具体的问题整篇文档的向量和问题的向量相似度未必高召回就失败了。所以第一步必须切分把大文档拆成一个个语义相对完整的小块英文叫chunk。每个chunk单独向量化、单独存储、单独被检索。这一步看着简单其实是整个RAG效果的天花板。切得不好后面检索再优化也救不回来。2.2 固定长度切分和语义切分的取舍最省事的做法是按固定字符数切比如每500个字符一块块之间留50个字符重叠。重叠是为了防止一句话正好被切断导致语义断裂。这个方案实现简单适合结构松散的文本比如聊天记录、会议纪要。但固定长度有个明显问题它可能把一个小节的标题和内容切散也可能把两个不相关的段落拼在一起。对于结构清晰的文档比如Markdown、带标题的Word更好的做法是按结构切。LangChain里提供了不少切分器我常用的组合是先用RecursiveCharacterTextSplitter它会按段落、句子、词的优先级递归尝试切分尽量在自然边界断开。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ] ) chunks splitter.split_text(long_document)注意separators里我把中文标点也加进去了因为默认的分隔符是给英文设计的中文文档如果只按空格和换行切很容易切出半句话。这个细节很多人会忽略实测对中文检索命中率影响不小。2.3 chunk大小到底设多少这是面试高频问题也是没有标准答案的问题。我的经验是chunk大小取决于你的问答粒度。如果用户问的是XX功能的参数是什么这种细粒度问题chunk要小200到400字符保证检索出来的就是那一段。如果用户问的是XX模块整体怎么设计这种概括性问题chunk要大800到1500字符否则检索出来的碎片拼不出完整答案。一个折中方案是分层切分大chunk用于概括性检索小chunk用于细节检索检索时两者都召回这就是后面会提到的多向量检索思路。实际项目里我一般从500字符、80重叠起步然后根据bad case调整。如果发现检索出来的内容总是缺上下文就加大chunk和重叠如果发现检索出来的内容里有一半是无关的就减小chunk。提示chunk大小没有万能值别照搬别人的参数。先跑一批真实问题看检索结果再调。调参的依据是bad case不是感觉。2.4 元数据别丢它是过滤和溯源的关键切分的时候除了文本内容一定要把元数据带上来自哪个文件、哪一章、哪一节、第几页。这些信息在检索阶段能用来做过滤比如用户只想在财务制度这个目录下检索元数据就能帮你缩小范围。在生成阶段元数据能用来做引用溯源告诉用户答案来自哪份文档的哪一页这在企业场景里几乎是刚需。LangChain的Document对象就是干这个的切分时把metadata一起传进去from langchain.schema import Document docs [Document(page_contentchunk, metadata{source: 员工手册.pdf, page: i}) for i, chunk in enumerate(chunks)]别小看这一步等你要做权限控制、按部门过滤、答案溯源的时候没有元数据就得推倒重来。3. Embedding和向量库检索的地基怎么打3.1 Embedding模型选型不是越贵越好Embedding模型的作用是把文本转成一串数字向量语义相近的文本向量距离也近。选模型的时候很多人直接上最大的觉得效果一定好。实际上要综合考虑三点效果、速度、成本。中文场景下我一般会对比几个方向开源的BGE系列、M3E系列以及各家API提供的embedding服务。开源模型的好处是数据不出本地适合有隐私要求的场景缺点是得自己部署占显存。API服务省事但每次调用都要花钱量大起来成本不低。选型时有个容易被忽略的点embedding模型和你的检索语言要匹配。如果你的文档是中英混合最好选多语言模型否则中文问题检索英文文档时相似度会偏低。另外模型有最大输入长度限制一般512个token超过会被截断所以chunk不能设得太大否则尾部信息直接丢了。from langchain.embeddings import HuggingFaceEmbeddings embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-base-zh-v1.5, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} )normalize_embeddingsTrue这个参数建议打开它把向量归一化到单位长度这样用内积算相似度就等价于余弦相似度数值更稳定。3.2 FAISS为什么适合入门和中小规模向量存下来之后需要能快速找到和问题最相近的几个。暴力遍历所有向量当然可以但文档一多就慢。FAISS是Facebook开源的向量检索库专门解决这个问题它用近似最近邻算法在精度和速度之间做权衡几百万向量也能毫秒级返回。FAISS的好处是轻量、纯本地、不依赖服务特别适合入门和中小规模知识库。它的索引类型有好几种最常用的是IndexFlatL2精确但慢数据量大时用IndexIVFFlat先聚类再检索快很多但会损失一点精度。from langchain.vectorstores import FAISS vectorstore FAISS.from_documents(docs, embeddings) vectorstore.save_local(faiss_index) # 加载 vectorstore FAISS.load_local(faiss_index, embeddings, allow_dangerous_deserializationTrue)allow_dangerous_deserializationTrue这个参数是因为FAISS用pickle反序列化存在安全风险本地自己生成的索引可以放心打开但别加载来路不明的索引文件。3.3 相似度检索的top-k怎么定检索时返回几个chunk就是top-k。k太小可能漏掉关键信息k太大塞给模型的无关内容变多既增加成本又干扰模型判断。一般从k3或4起步看效果调。这里有个反直觉的点k不是越大越好。我实测过把k从4加到10召回率确实涨了一点但最终答案质量反而下降了因为无关内容稀释了关键信息模型被带偏了。所以调k的时候要看的不是召回率而是最终答案的准确率。3.4 向量库不止FAISS什么时候该换FAISS适合单机、中小规模、不需要频繁增删的场景。如果知识库要频繁更新或者要支持多用户并发、元数据过滤、权限控制就该考虑Chroma、Milvus、Qdrant这类向量数据库。Chroma轻量易用适合快速原型Milvus功能全适合生产环境Qdrant的过滤能力很强元数据过滤场景很顺手。选型逻辑很简单先问自己数据量多大、更新频率多高、要不要过滤、要不要分布式。入门阶段FAISS和Chroma足够别一上来就上重型数据库运维成本会拖垮你。4. 检索到生成把碎片拼成答案的完整链路4.1 一次完整的RAG调用长什么样前面铺垫了这么多现在把链路串起来。用户问一个问题系统做四件事把问题向量化、在向量库里检索最相近的k个chunk、把问题和chunk拼成一个prompt、交给大模型生成答案。from langchain.chat_models import ChatOpenAI from langchain.chains import RetrievalQA llm ChatOpenAI(modelgpt-4o-mini, temperature0) retriever vectorstore.as_retriever(search_kwargs{k: 4}) qa_chain RetrievalQA.from_chain_type( llmllm, retrieverretriever, return_source_documentsTrue ) result qa_chain.invoke({query: 员工年假有多少天}) print(result[result]) print(result[source_documents])temperature0是为了让输出稳定RAG场景不需要模型发挥创造力要的是忠实于检索到的材料。return_source_documentsTrue把检索到的原文一起返回方便做溯源和调试。4.2 prompt怎么写才能压住幻觉RAG能不能压住幻觉一半靠检索质量一半靠prompt。prompt里必须明确告诉模型只根据提供的材料回答材料里没有就说不知道不要自己编。这句话看着简单但少了它模型很容易在检索结果不相关的时候强行编一个答案。我常用的prompt模板是这样的你是一个企业知识库助手。请严格根据下面提供的资料回答问题。 如果资料中没有相关信息直接回答根据现有资料无法回答不要编造。 资料 {context} 问题{question}{context}就是检索到的chunk拼接。这里有个细节多个chunk拼接时最好加上分隔符和来源标记让模型知道每段材料的边界避免它把不同来源的内容混在一起。4.3 检索不到内容时怎么办真实场景里用户的问题经常和知识库完全不相关比如问今天天气怎么样。这时候检索出来的chunk相似度都很低如果直接塞给模型模型可能硬答。解决办法是加一个相似度阈值低于阈值的chunk直接丢弃如果所有chunk都低于阈值就直接回复这个问题不在我的知识范围内不调用大模型。retriever vectorstore.as_retriever( search_typesimilarity_score_threshold, search_kwargs{score_threshold: 0.5, k: 4} )阈值设多少要看你的embedding模型和相似度算法建议先跑一批问题看正例和负例的分数分布再定阈值。这个细节面试里能讲出来说明你真的调过。4.4 多轮对话里RAG怎么处理单轮问答好办多轮就麻烦了。用户第二句问那它呢这个它指代什么检索系统不知道。常见做法是把历史对话一起交给模型让它先把问题改写成独立完整的问题再去检索。这一步叫query改写或query重写。rewrite_prompt 根据对话历史把用户的最新问题改写成不依赖上下文、可以独立检索的问题。 对话历史{history} 最新问题{question} 改写后的问题改写完再走检索流程命中率会明显提升。这个技巧在面试里是加分项因为它体现了你对真实使用场景的思考而不是只跑通了demo。5. 效果不好时从哪几个方向排查5.1 先分清是检索问题还是生成问题RAG效果差第一步是定位问题出在哪一段。方法很简单把检索到的chunk打印出来看。如果检索结果里根本没有正确答案那是检索问题如果检索结果里有正确答案但模型答错了那是生成问题。这两类的优化方向完全不同别一上来就换模型。我见过不少人一遇到效果差就换更大的模型结果发现是chunk切分把答案切散了检索根本没召回换再大的模型也没用。5.2 检索问题的三个常见根因检索不准八成是这三个原因之一。第一是chunk切分不合理答案被切散或者被无关内容淹没。第二是embedding模型不适合你的领域比如用通用模型处理大量专业术语语义匹配会失准。第三是用户问题和文档表述差异太大用户用口语问文档用书面语写向量相似度上不去。针对第三种可以引入查询扩展把用户问题改写成几个不同表述再检索或者用HyDE思路先让模型根据问题生成一个假想答案再用这个答案去检索因为假想答案的表述更接近文档风格。5.3 生成问题的两个典型表现生成出问题通常有两种表现。一种是模型无视检索材料凭自己的知识回答这多半是prompt没约束好。另一种是模型被无关材料带偏答非所问这通常是top-k太大或者检索精度不够塞了太多噪声进去。还有一种隐蔽的情况检索材料里有正确答案但模型只用了其中一部分漏了关键信息。这往往是因为材料太长关键信息在中间被忽略了。解决办法是把最相关的chunk排在前面或者减少chunk数量让模型聚焦。5.4 用评测集把优化变成可量化的过程调RAG最怕凭感觉今天觉得好了明天又觉得差了。正确做法是建一个小评测集准备几十个真实问题和标准答案每次改动后跑一遍看准确率变化。评测指标可以简单点就看答案对不对不用搞太复杂。有了评测集你才能知道chunk从500调到300到底是变好了还是变差了top-k从4调到6有没有用。面试里如果你能说出我建了评测集每次调参都跑一遍对比面试官对你的评价会完全不一样因为这体现了工程思维。问题现象可能原因排查方向检索结果无正确答案chunk切分、embedding模型、表述差异打印chunk、换模型、查询改写有答案但模型答错prompt约束、top-k过大改prompt、减小k、重排答案不完整chunk过大、关键信息被忽略减小chunk、调整排序答非所问检索噪声多、阈值缺失加阈值、加过滤、重排6. 面试里那些容易被追问的RAG细节6.1 RAG和微调到底怎么选这是必问题。我的回答框架是看知识是事实型还是能力型。如果是要让模型知道一些具体事实比如公司制度、产品参数用RAG因为知识更新方便改文档就行不用重新训练。如果是要让模型学会一种风格或能力比如模仿某个专家的语气、掌握某种推理模式那微调更合适。实际项目里两者经常结合用微调让模型学会领域表达方式用RAG给它补充实时事实。面试时能讲清这个边界比单纯说RAG更好要专业得多。6.2 怎么评估RAG系统的检索质量检索质量有两个核心指标命中率和召回率。命中率是检索结果里有没有正确答案召回率是正确答案有没有被完整召回。工程上更实用的是看检索结果里包含正确答案的比例这个指标直接决定生成阶段有没有材料可用。评估方法就是建评测集每个问题标注它对应的正确chunk然后看检索结果里有没有命中。这个工作枯燥但必要没有它优化就是盲人摸象。6.3 大知识库怎么保证检索速度知识库大了之后检索速度会成为瓶颈。优化方向有几个用近似最近邻索引替代精确检索用倒排索引先做粗筛再向量精排用元数据过滤缩小检索范围把向量库做成分布式。这些手段可以叠加使用。面试时如果被问到可以按数据规模分层回答十万级用FAISS足够百万级考虑Milvus或Qdrant千万级以上要考虑分片和分布式。这样回答显得你有规模意识不是只会跑demo。6.4 Agentic RAG是什么和普通RAG差在哪普通RAG是一条直线检索一次生成一次。Agentic RAG把检索变成智能体可以反复调用的工具模型可以自己决定要不要检索、检索几次、换个关键词再检索。它适合复杂问题比如需要多步推理、需要综合多个来源的场景。区别在于控制权普通RAG的检索时机是固定的Agentic RAG让模型自己判断。代价是延迟和成本上升因为可能触发多次检索和模型调用。面试里能讲清这个权衡说明你跟得上技术演进。6.5 本地知识库怎么搭数据不出本地有隐私要求的场景整套链路都要本地化embedding模型本地部署向量库本地跑大模型用本地推理框架加载开源模型。LangChain加FAISS加本地模型这套组合能跑通完整的本地知识库。代价是效果和速度不如云端大模型需要根据场景权衡。如果问题简单、对准确率要求不极端本地方案完全够用。如果问题复杂可能要在本地做检索、云端做生成但这就涉及数据外发得看合规要求。7. 我踩过的几个坑和给你的实操建议第一个坑是chunk重叠设太小。我一开始为了省存储重叠只留了20字符结果很多跨段的答案检索不全。后来加到80命中率明显改善。重叠的本质是给语义边界留缓冲别在这上面省。第二个坑是忽略中文分词和标点。用默认的英文切分器处理中文文档切出来的chunk经常是半句话检索时语义不完整。把中文标点加进分隔符列表是个小改动但收益很大。第三个坑是top-k拍脑袋定。我早期直接设k10觉得召回越多越好结果模型被噪声带偏答案质量反而差。后来建了评测集一个个试发现k4在我的场景下最优。参数一定要用数据说话。第四个坑是没做阈值过滤。用户问知识库外的问题时系统硬答体验很差。加了相似度阈值之后超出范围的问题直接拒答反而显得系统更可靠。最后一个建议别追求一步到位。先把最小链路跑通能回答简单问题然后建评测集针对bad case逐个优化。RAG是个迭代的活没有一次调好的。面试时如果你能讲出这个迭代过程比背一堆概念有说服力得多。这套链路我自己跑过好几遍从切分到检索到生成每一环都有可以深挖的细节。真正吃透RAG不是记住它是什么而是知道它哪里会出问题、怎么定位、怎么修。把这条链路在脑子里过一遍面试再被问到你就有话可说了。
返回列表