简介:面向大模型应用开发者与RAG技术学习者的实战型PDF资料,以百度百科藜麦数据为私有数据源,完整演示基于LangChain搭建问答系统的可落地流程。内容从环境搭建讲起,明确交代CUDA 11.7、Python 3.10、PyTorch 1.13.1+cu117等版本组合,随后逐步展开本地文档加载、固定长度字符分割、m3e-base模型Embedding向量化以及Chroma向量数据库写入等关键操作,每个环节均配有可直接运行的代码片段和输出示例,适合希望快速上手RAG应用的初中级开发者。包内包含1个PDF文件,压缩包大小仅390KB,轻量易读,已有283人学习下载。该案例还专门提醒了OCR扫描文档时可能出现的个别字识别错误或漏识别问题,需要结合上下文人工修正,这一实战经验对处理非标准文本数据很有参考价值。此外,资料穿插介绍了藜麦的品种特性、原产地、营养成分及国际市场前景等背景信息,帮助读者更好地理解数据语义。通过这一完整项目,读者能够掌握从数据采集、清洗、切分、向量化到问答系统部署的整条技术链路,并学会借助LangChain快速搭建私有知识库问答应用。
1. 被丢来一份《基于langchain RAG问答应用实战》的PDF时,你首先要看懂面试官在问什么
一份标题里同时带着"八股文面试"和"基于langchain RAG问答应用实战"的PDF,说明两件事:面试既考你对RAG原理的背诵,也考你对LangChain落地代码的掌握。这里的RAG,就是当下解决大模型幻觉、知识过时和企业私有知识问答最常用的技术路线;而LangChain,是把它实现出来时被提名频率最高的编排框架。对准备大模型岗位面试的人,以及刚接手"用内部文档做一个问答机器人"这类需求的开发来说,这份资料的价值不是把概念背熟,而是把概念和代码绑在一起。这篇文章就顺着这条线,把RAG必答的八股、最小可运行的完整链路,以及那些不跑一遍根本发现不了的坑,一次性讲透。
2. 先把八股立住:RAG为什么存在,LangChain在链路里到底管哪一段
2.1 从上下文长度和幻觉说起:RAG要解决的三个真问题
面试如果问"为什么需要RAG",别一上来背定义。你先说场景:模型训练完,知识就冻结在某个时间点之前,这是第一个问题,叫知识截止。接着是幻觉,模型在不确定时会一本正经地编答案,尤其当问题超出它训练数据覆盖的范围时。第三个问题更实际:企业内部的制度文档、产品手册、客服话术,这些私有知识根本没进过训练集,你想让模型学会,总不能为改一句话就重新训练一次。
有了这三个问题,RAG的思路就顺理成章:不把答案塞进模型参数,而是在回答时先从外部知识库检索出最相关的片段,把片段和问题一起交给模型。模型不靠记忆回答,靠开卷考试回答。这也是为什么面试里"为什么不用微调解决"这个问题经常紧跟着出现——微调是把知识写进权重,成本高、更新慢、仍然可能幻觉;RAG把知识放在外部,要更新就更新知识库,成本低得多。
提示:如果面试官追问"长上下文模型出来了,是不是RAG就没用了",回答的落点是:长上下文解决的是"能不能容纳更多资料",解决不了"哪些资料和当前问题相关"。把上万token全塞进去,模型照样可能被无关信息干扰,还多花成本。RAG的本质是让模型只看它该看的部分。
2.2 LangChain在RAG链路里的角色:不是框架,是胶水
要说清LangChain,先把RAG链路拆开。它至少有五段:加载文档(Loader)、把长文档切成片(Splitter)、把每片转成向量(Embedding)、把向量存起来支持检索(Vector Store)、最后把检索结果和问题组装成Prompt交给模型(Chain)。你完全可以用原生代码把这五段全部写出来,但每个环节都有选型,LangChain做的事是给这些环节定义一套统一接口,让你自由组合,这就是"胶水"的定位。
这套抽象的价值在代码层面更明显。你可以今天用Chroma当向量库,明天换成Milvus或pgvector,对上层问答逻辑零改动;也可以把Embedding从bge-m3换成别的模型,只改一行。对面试来说,能说出"LangChain本身没有检索能力,检索能力来自它包装的向量库和Embedding模型",比背十个API函数加分得多。
常见做法是,RAG项目也不只LangChain一家,但面试题里提到它频率最高,原因之一是它沉淀了一套稳定术语:Document Loaders、Text Splitters、Embeddings、Vector Stores、Retrievers、Chains。这六个词本身就是RAG八股的骨架。你背熟这六个词,面试官问任何一环,你都能把它放回整条链路里回答。
2.3 RAG知识库与结构化知识库(KG)的区分与应用场景
这一节是最近面试的高频追问。日常说的"知识库问答"其实分两类:一类是向量知识库,也就是RAG默认形态,把非结构化文本切片、向量化、按语义相似度召回;另一类是结构化知识库,往上就是知识图谱(KG),把实体和关系用三元组表示,靠结构化查询或图遍历拿答案。这两者不是替代关系,是适用场景不同。
判断口径可以记两句话:问"哪段文档讲了什么"用RAG向量库;问"A和B是什么关系""有多少个符合条件的产品"用KG。"某产品的退货政策是什么"是前者,"退货政策对两个产品线有什么差异"这种跨文档聚合,纯向量检索容易漏,KG反而擅长。还有一种混合形态被频繁提起,典型做法是:先走RAG召回候选文档,再用KG把候选文档涉及的实体关系补全,最后一起喂给模型。面试里能说出"向量库负责粗召回,图谱负责关系和聚合"这个分工,基本算过关。实际落地时,大多数企业的第一版RAG都先做纯向量库,只有当问题里频繁出现"对比""汇总""关系"字眼且效果不好时,才值得引入KG层。
3. 动手搭一个LangChain RAG问答:从PDF加载到流式回答的最小链路
3.1 环境准备与模型选型:本地Ollama还是在线API
先选模型再写代码。RAG链路里要两个模型:生成用的Chat模型,和做向量化的Embedding模型。Embedding决定检索质量,很多人在这里踩坑,后面单独讲。生成模型有两条主流路线。第一条是本地部署,用Ollama拉模型,推荐qwen2.5:7b作为生成模型、bge-m3做向量化,优点是不花钱、数据不出内网,适合企业私有化部署的场景;缺点是吃内存或显存,7B量化模型在macOS上也能跑,速度尚可,正好适合"怎么在mac上搭建RAG知识库"这类诉求。第二条是在线API,不少国产大模型平台提供OpenAI兼容接口且有免费额度,比如DeepSeek开放平台。代码上只需要把base_url和api_key换成对应值,其余完全一样。对刚起步验证想法的人来说,在线API省去本地环境折腾;生产环境则必须先回答数据出内网是否合规,面试里主动提这一点会加分。
# 本地方案:安装Ollama并拉取两个模型(macOS / Linux 命令行均可执行) ollama pull qwen2.5:7b # 生成模型,约4.7GB,首次自动下载 ollama pull bge-m3 # 中文Embedding模型Ollama拉完模型后默认跑在localhost的11434端口,代码里不需要写任何密钥。如果你在一台没有GPU的Linux机器上跑,7B模型走CPU也能出结果,只是每条回答多等几秒,验证流程完全够用。在线API路线不需要执行这一段,把密钥写进环境变量即可。
Python依赖同样走pip,注意版本之间兼容性,LangChain 0.3之后的集成包是独立维护的。
pip install langchain langchain-community langchain-ollama langchain-chroma chromadb pypdf这里的langchain是核心框架,langchain-ollama和langchain-chroma是两个官方适配包,pypdf用来解析PDF,chromadb做本地向量库。这套组合的好处是全部用开源组件,不需要注册任何在线服务就能把链路跑通。
3.2 文档加载与切分:PyPDFLoader与RecursiveCharacterTextSplitter,chunk_size和overlap怎么设
加载PDF是RAG第一步。常见做法是用LangChain的PyPDFLoader,它能把PDF每一页读成独立的Document对象,保留正文和包含页码的元数据。手上只有一份PDF,直接传路径即可;如果是一整个目录,用DirectoryLoader配合glob参数批量加载。
from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("产品手册.pdf") docs = loader.load() print(len(docs)) # 总页数 print(docs[0].metadata) # {'source': '产品手册.pdf', 'page': 0}load()把整份PDF读成列表,每个元素代表一页。metadata里的page字段后面做引用回答很有用,模型可以说"答案来自第3页",这是面试里体现工程感的细节。
切分是决定检索质量的第一道关口。我一般会用RecursiveCharacterTextSplitter,它按一组分隔符递归切分,优先保住语义完整。参数先说结论:chunk_size设500左右、chunk_overlap设50左右,中文场景足够起步;代码和日志类文档可以降到300,论文类升到800。下面是一个可直接抄的配置。
from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", ",", " ", ""], ) chunks = splitter.split_documents(docs) print(len(chunks)) # 切分后的片段数chunk_size不是严格的字符上限,而是近似目标值,超过就会继续切;chunk_overlap让相邻片段保留50字符重叠,避免一句话被拦腰截断导致语义断裂。separators的排序很关键,它会优先按最长的"\n\n"切,切不动再换"\n",中文场景把"。"和","加进分隔符效果比默认的英文标点好。这不算玄学,你切出来的chunk如果一半是半句话,检索再准也白搭。
3.3 向量化与检索:bge-m3 Embedding + Chroma,top_k怎么定
Embedding模型的选型,中文场景我一般直接用bge-m3,它对中文支持好,最长支持8192 token,语义检索能力在中文语料上是第一梯队。对比用通用英文模型跑中文文档,bge-m3要可靠得多,这是踩过坑之后得出的经验。
from langchain_ollama import OllamaEmbeddings embeddings = OllamaEmbeddings(model="bge-m3")OllamaEmbeddings把bge-m3包装成LangChain的Embeddings接口,调用embed_documents和embed_query时会走本地Ollama服务。向量库选Chroma,因为它零配置、落本地磁盘、支持持久化,个人项目和原型验证最好用;生产环境常见做法是换Milvus或pgvector,代码层面只改一个import。
from langchain_chroma import Chroma vector_store = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db", # 本地持久化目录 ) retriever = vector_store.as_retriever( search_type="similarity", # 还有 mmr 可选 search_kwargs={"k": 4}, # 每次召回4个片段 )Chroma.from_documents会把chunks一次性向量化并写入本地目录,第二次启动直接加载目录即可,不用重新切分。search_kwargs里的k是召回数量,起步设4;如果文档长、知识密,可以提到6到8,但要小心上下文被塞满后回答跑题。
如果面试官追问检索模式,就往MMR上说:纯相似度检索(similarity)容易出现内容高度重复的多个片段,而MMR(maximal marginal relevance)在相似之外还做多样性去重。当chunk里有大量雷同内容时,把search_type换成"mmr",并设lambda_mult(默认0.5,越大越偏向多样性)。能说出这两种模式的区别,属于明确的加分项。
3.4 组装问答链:RetrievalQA与LCEL两种写法,temperature该不该调
组装问答链是整个RAG应用最核心的一步。老写法是LangChain封装好的RetrievalQA,三行代码跑通;新写法是LCEL(LangChain Expression Language),用管道符显式声明数据流。面试和实战都建议重点看LCEL,因为它是官方主推、也更好排查中间结果。
from langchain.prompts import ChatPromptTemplate from langchain_ollama import ChatOllama from langchain.schema import StrOutputParser from langchain_core.runnables import RunnablePassthrough # 生成模型:本地用 qwen2.5,在线API换成对应的兼容客户端 llm = ChatOllama(model="qwen2.5:7b", temperature=0.1) prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个企业内部知识助手。请只依据提供的上下文回答问题," "如果上下文不足以回答,就老实说'资料中没有相关内容',不要编造。"), ("human", "上下文:\n{context}\n\n问题:{question}"), ]) def format_docs(docs): # 给片段编号,方便模型引用来源 return "\n\n".join(f"[片段 {i+1}] {d.page_content}" for i, d in enumerate(docs)) chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) answer = chain.invoke("退货政策是什么?") print(answer)这段代码值得逐行看。整个链路从左到右执行:先由retriever检索,format_docs把多个片段拼成带编号的上下文文本,然后填进prompt模板,交给qwen2.5生成,最后StrOutputParser把模型返回的内容对象转成字符串。片段编号放进prompt有个好处:模型可以回答"片段2提到了……",方便你追踪答案来源,这让RAG的"可解释"落到实处。
参数说明:temperature在RAG问答场景要调低,0.1到0.3之间合适。temperature高会让回答发散,发散意味着更容易离开上下文去编。如果换成在线API,把ChatOllama换成对应兼容客户端,其余结构完全不用改。这一点正好回击面试里"RAG是不是绑死在某个模型上"的问题:不绑死,模型层可以整体替换。
对照写法RetrievalQA,长这样:
from langchain.chains import RetrievalQA qa = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff", # 把所有片段一次性塞进上下文 ) answer = qa.invoke("退货政策是什么?")["result"]RetrievalQA的优势是三行起跑,但chain_type="stuff"会把所有召回片段一次性塞进上下文,片段一多容易爆上下文。面试如果问"RetrievalQA和LCEL的区别",回答落点应该是:前者封装完整、适合快速验证;后者显式可控、每个环节都能插桩排查。实际项目中我基本只用后者,因为排错方便。
4. 面试官会追问的进阶八股:检索质量、记忆与Agent框架
4.1 检索是RAG的天花板:召回率、重排(Rerank)与混合检索
RAG里有一个经常被忽视的事实:生成侧的能力大家差距不大,真正的差距在于"把什么喂给了模型"。面试八股对这块有三个递进追问:原始query检索不到怎么办、召回一堆相似内容怎么办、多路召回后怎么排序。
先解决漏召回。常见做法是query改写,把用户口语问题先交给模型转成一个更适合检索的陈述句。比如"那个退货时限是不是写了30天"改写为"退货时限是30天",向量检索的命中率会明显提升。这是成本最低、见效最快的一招。
再解决召回内容重复的问题。上一章的MMR是第一种去重手段;更常用的方案是引入重排(Rerank),先粗召回50个片段,再用cross-encoder模型逐对精排,取前5个送进生成。Rerank的代价是额外耗时,常见做法是只在粗召回后使用,不要拿它直接替代Embedding。面试被问到"粗召回和精排的架构",能答出这个两级结构就算合格。
第三是混合检索(Hybrid Search),尤其适合内容里充满专有名词、型号、编号的文档。常见组合是BM25稀疏检索加上向量稠密检索,两者都跑一遍再融合。向量检索擅长语义相似,BM25擅长精确命中"iPhone 15"这种词。能说出"单纯向量检索对名词和数字不敏感,混合检索是生产级RAG的标配",这就是老手回答的味道。
4.2 多轮对话与记忆管理:ConversationBufferMemory的正确打开方式
面试官有个经典场景题:用户第一句问"退货政策是什么",第二句问"它有例外吗?",如果RAG只拿第二句去检索,上下文里根本没有"它"的指代对象,检索必然失败。这是多轮对话在RAG里最典型的问题。
常见做法有两套。一套是用LangChain的记忆组件,在链路上维护历史消息;另一套更轻也更常用:每轮把最近N条对话历史压缩成摘要,拼接进检索query再查。压缩式的好处是保留指代信息的同时,不让大段历史把检索带偏。
在LangChain里,传统写法用ConversationBufferMemory配合ConversationChain,但新版里记忆更多由独立的Runnable或LangGraph的State管理。面试需要掌握的点是:记忆不只是把历史消息拼进Prompt,而是分两路——一路进检索前的query改写,一路进生成时的上下文。少一条路,多轮效果都会打折。能分清楚这两路,是一个值得在面试中主动展开的地方。
4.3 LangChain、LangGraph、Dify、CrewAI:框架边界与选型回答
这些框架常被拎出来对比。LangChain做链路编排,是RAG和Agent的主开发框架;LangGraph是同一团队为有状态、带循环的Agent流程出的图编排层,你可以理解为在LangChain基础上专门管理节点、边和状态快照;Dify是开源低代码平台,主打可视化编排、应用发布和知识库管理,非纯代码团队友好;CrewAI是轻量的多Agent协作框架,适合让多个角色分工完成任务。
面试回答这类对比的得体口径是:先按场景分层,再给结论。如果任务是把企业内部文档做成RAG问答,最务实的三条路线是——已有代码资产、需要精细控制检索链路时选LangChain配合LangGraph;需要快速交付且让非技术同事能运营时选Dify,知识库管理和API发布都是现成的;想快速验证角色协作式Agent选CrewAI。这里没有绝对最好,决策主线只有两条:你的团队接下来谁来维护、你的问题是否需要复杂状态流转。
5. RAG落地避坑指南:面试里最容易暴露实战水平的五个翻车场景
5.1 检索不到:chunk切太小还是Embedding模型不匹配
现象:用户问的问题在文档里明明有,但RAG回答"资料中没有相关内容",或者打印出来的召回片段跟问题明显对不上。
原因分三层排查。第一层看查出来的片段本身是否完整,很多情况是chunk_size设太小,把关键句拦腰切断,语义只剩一半。第二层看切分是否合理,比如按固定字符硬切,把"退货政策"的完整说明拆到两个chunk里,没有一个片段包含完整答案。第三层看query和文档的表述差异,文档里写的是"售后条款",用户问"退换货怎么办",纯向量检索对相似但不同词的处理能力有限。
解决:先把chunk_size适当调到600到800并加上overlap;再给query做改写,把口语问题扩写成包含多个近义表述的语句;最粗暴也最有效的排查手段是先把召回片段打印出来看,而不是直接换模型。不少人一上来怪Embedding模型不好,八成其实出在chunk和query上。另外,如果文档里有大量表格、图片,默认PDF解析会丢内容,这也是"检索不到"的隐藏原因,需要换带OCR或版面解析的加载方式。
5.2 检索到了但答非所问:Prompt没约束还是上下文顺序不对
现象:召回片段看起来相关,但模型答出来是错的,甚至答出了片段里没有的信息。这就是检索成功、生成失败的典型场景。
原因通常是Prompt约束太弱。上下文里同时给了多个片段,其中包含相似但矛盾的表述,模型不知道以哪个为准;还有一种是Prompt没有显式声明"只能依据上下文回答,禁止使用固有知识",模型一放松就用记忆补全。
解决要双管齐下。先把Prompt写死:"如果上下文自相矛盾,指出矛盾并列出两个出处;如果上下文不足,直接说无法回答。"再把片段按与问题的相关性排序后拼进上下文,最相关的排最前。实践里还要把temperature压到0.2以下,给模型更少的发挥空间。这一条最容易被忽略:你以为换检索模型能好,其实是生成端没管住。
5.3 文档更新后回答还是旧的:索引没刷新与缓存问题
现象:把新版本的制度文档替换进知识库后,回答仍然引用旧条款,而且非常顽固。
原因一般有两个。一是向量库持久化在磁盘,很多原型代码只在首次启动时执行from_documents,之后启动直接加载,新增文档根本没进索引。二是Chroma这类本地向量库默认保留旧向量,删除源文档时如果没做对应删除操作,旧内容就一直参与检索。
解决:把索引更新做成一整套明确流程,而不是启动时的副作用。常见做法是给文档维护版本号或更新时间字段,每次增量更新先按source批量删除旧chunk再写入新的。另一个隐蔽点来自回答缓存:如果生产环境用Redis缓存了问答结果,更新知识库后必须把知识库版本作为缓存key的一部分,否则会出现"文档换了、答案还是旧的"的诡异现场。
5.4 RAG知识库和结构化数据同时存在:混合路由怎么设计
现象:知识库里既有制度文档,又有产品参数表、组织架构这类结构化数据,RAG对表格类问答表现极差。
原因:向量库擅长非结构化文本语义召回,但对"查某个字段值""统计某类数据"这类精确型问题天生不行。让RAG从一串文本里抽字段值,不如直接查数据库准。这正好对应前面说的RAG知识库与结构化知识库(KG)的区分。
解决:生产级方案普遍加一层路由(Router)。先让LLM判断题的类型,开放式问答走RAG链路,精确查询或关系型问题走SQL或图谱查询链路,最后把结果统一送进生成模型。LangChain里用RunnableBranch可以轻松实现。面试被问到"你的知识库架构怎么设计",能说出这个路由分层,而不是"我们全塞进向量库",就拉开了差距。
5.5 没有评估指标就是玄学:拿一套固定问题集做回归
现象:调完参数觉得效果好了,上线第二天用户反馈又不行了;改了一个chunk参数,之前能答的问题反而答不出来。这是典型的没有评估体系。
原因:RAG链路变量太多,chunk_size、top_k、temperature、prompt、Embedding模型,任何一处变化都可能让一部分问题变好、另一部分变差。没有固定测试集和回归流程,你根本不知道一次改动的影响范围。
解决:至少维护20到50条覆盖典型场景的问答对,每条标注预期答案来源和页码,每次改动参数后跑一遍,记录三个指标:召回命中率(检索出的片段是否包含答案)、生成准确率(人工判断回答是否正确)、引用正确率(回答引用的出处是否真实存在)。这三组数字摆出来,调参才有方向,否则就是靠手感碰运气。面试中主动说"我有固定评估集,指标出来后才调参",比说"我凭感觉调效果好"有说服力得多。
6. 一套能背下来的面试答题框架与自查清单
面试官说"讲讲你的RAG项目"时,建议按四段讲。第一段是场景与选型:项目为什么选RAG而不是微调,数据是什么形态,先分清RAG知识库和结构化知识库的边界;第二段是链路:按索引、检索、生成三步走,每一步带具体参数,比如chunk_size为500、overlap为50、top_k为4、temperature为0.1;第三段是难点与优化:检索不到怎么排查、多轮指代怎么处理、文档更新后怎么刷新索引、有没有加过Rerank或混合检索;第四段是评估与边界:固定测试集的三项指标,以及当前方案的瓶颈在哪里。这套结构下来,面试官基本没有空隙再打断问细节,因为每一段的参数都是他下一步要追问的点。
下面这张自查清单,是我做RAG项目上线前都会过一遍的,面试前照着过也管用。它覆盖了从文档加载到评估的全链路,每条都对应一个真实翻车过的坑。
| 排查点 | 自查内容 | 常见错误 |
|---|---|---|
| 文档加载 | PDF里的表格、图片信息有没有丢失 | 默认解析出乱码,关键数据缺席 |
| 切分参数 | chunk_size是否匹配文档粒度,overlap是否保留语义 | 无脑500字符,不关注标题和段落结构 |
| Embedding | 中文语料是否用了中文模型 | 通用模型跑中文,召回率明显偏低 |
| 检索召回 | top_k是否合理,是否试过MMR或混合检索 | 只会similarity,重复片段占满上下文 |
| Prompt | 是否限定了只依据上下文回答 | 没写约束,模型拿固有知识代答 |
| 温度 | 问答场景是否调低 | 默认高温度,生成发散、幻觉上升 |
| 多轮对话 | query是否做了指代消解和历史压缩 | 第二轮直接检索,指代词导致空召回 |
| 索引更新 | 文档更新后是否增量刷新,删除是否同步 | 启动时建索引,后续不更新不清理 |
| 缓存 | 回答缓存是否和知识库版本绑定 | 文档换了,回答命中旧缓存 |
| 知识库形态 | 是否区分向量库、SQL、KG各自适用场景 | 所有数据都塞向量库 |
| 评估 | 有没有固定测试集和三个回归指标 | 凭感觉调参,改了无法知道影响 |
最后聊个我自己的习惯:每次接到RAG相关需求,我都会先把"如何评估"这件事放到和"如何实现"同等优先级。哪怕只是30条测试问题,也能让调参不变成一场赌运气的游戏。RAG的坑不会消失,但用固定评估集和可观测链路把它们变成一个一个可归因的问题之后,这个方案就值得投入,也不会总在同一个地方翻车。希望这篇笔记能帮你在面试和实战里都少走一段弯路。
本文还有配套的精品资源,点击获取