LangChain+RAG实战:搭建大模型知识库问答应用

发布时间:2026/10/20 2:45:55
LangChain+RAG实战:搭建大模型知识库问答应用
大模型问答应用如果只靠模型本身的“记忆”很快你就会遇到一个尴尬场面问一个训练数据之外的问题模型会一本正经地给你编一个答案。RAG检索增强生成就是来解决这个问题的——它相当于给大模型外挂一个知识库让模型在回答之前先到资料里“查一遍”再基于查到的内容组织语言。这篇文章我会基于LangChain从原理到代码完整走一遍RAG问答应用的搭建过程全程能跑、能改、能直接搬到项目里用。内容覆盖链路设计、参数调优、避坑经验适合正在做知识库问答、私有文档查询、客服辅助这类场景的开发者参考。1. RAG到底在解决什么问题大模型的幻觉和“开卷考试”1.1 大模型为什么会一本正经地胡说八道先从一个很常见的场景说起。假设你在做一个企业内部制度问答机器人员工问“年假折算到小时数怎么算”模型如果没在训练语料里见过你们公司的制度它会怎么做它会基于通用常识去“猜”一个答案——比如告诉你“年假按日历日折算”而这个回答听起来非常像回事却跟你公司真正执行的规则完全对不上。这不是模型“笨”而是它的本质决定的。现在的LLM本质上是一个超大的条件概率模型它学习的是语言规律和语料中反复出现的知识模式而不是一个可以随时精确查询的外部数据库。模型的参数在训练完成那一刻就“冻结”了它没法凭空知道训练截止之后发生的事情也没法知道只存在于你公司内网里的文档。遇到不知道的问题时它只能依据概率分布生成“最像正确答案”的文本——这就是幻觉的来源。我一度用“一个读了很多书但记性不太好的远房亲戚”来跟朋友打比方你问他你家小区物业的电话他大概能告诉你“电话通常是八位数”甚至还能编一个头头是道的号码但那个号码大概率是错的。要让这个亲戚给出准确答案你最好把通讯录递到他手上。RAG干的就是递通讯录这件事。1.2 RAG的核心思路让模型先查资料再动笔所谓RAG全称是Retrieval-Augmented Generation检索增强生成。它把“问答”这件事拆成两个阶段先到你的知识库里检索出与问题最相关的若干文本片段再把这些片段连同用户的问题一起交给大模型让模型基于这些片段进行回答。你可以把它理解为“开卷考试”。模型不再依靠闭卷记忆硬答而是先翻资料再动笔。这种做法带来几个立竿见影的好处知识可控回答基于你提供的文档来源明确可以追溯到具体片段。更新成本低文档变化时只需要更新知识库不需要重新训练模型。降低幻觉模型的回答被限制在给定资料的范围内编造空间大幅缩小。适配私域数据企业内部文档、个人笔记、产品说明书只要转成文本并入库模型就可以“看到”。很多人会把RAG和Fine-tuning微调放在一起比较。我个人的理解是微调是用来改变模型的“能力与风格”的比如让模型学会某种缩写习惯、特定的判别规则而RAG是用来给模型“补充具体事实”的。如果你要回答的内容是产品参数、内部流程、最新政策这类强事实、高频变动的信息走RAG性价比高得多——不用显卡训练改文档即改答案天然适合落地。2. 整体链路设计RAG问答应用从文档到答案的五个关键环节2.1 一条完整链路加载、切分、向量化、检索、生成RAG不是一个单点技术而是一条流水线。数据从原始文档到最终答案至少要经过五个环节加载Loading从PDF、Word、txt、Markdown、网页中抽取文本内容。切分Splitting把长文本切成一个个有语义边界的片段。这个片段的长度直接影响后续检索精度。向量化Embedding把每个文本片段用嵌入模型转换为向量也就是一组浮点数。只有变成向量才能计算不同文本之间的语义相似度。存储与检索Storage Retrieval把向量存进向量数据库。用户提问时先把问题也转换为向量然后在库里找出与问题向量最接近的若干片段。生成Generation把检索到的片段作为“参考资料”拼进提示词连同问题一起交给大模型生成最终回答。这里有一个关键设计用户的问题和知识库中的文本片段必须经过同一个Embedding模型转换才能放到同一向量空间里计算相似度。换一个模型向量分布就不同检索结果也就不可比了。我在实际项目里最深的感受是很多人把精力全放在最后的“生成”环节但 RAG 的效果上限很大程度上由前面的“切分”和“检索”决定。如果检索出来的片段根本不相关后面的模型再强也是白搭——它没有可以依凭的内容只能开始“创作”了。2.2 为什么选LangChain来做这个事LangChain是这个领域里目前最常用的开发框架。它做的事情其实很朴素把大模型应用里那些高频重复的环节——加载文档、切分文本、调Embedding接口、接向量库、拼Prompt、调LLM——都封装成了可组合的组件。你不需要自己写一套“文档转文本再接向量数据库”的胶水代码LangChain把这些接口统一了。举个例子如果不用LangChain你要写RAG大概得这么干自己写文档解析、自己调Embedding接口并管理大批量请求、自己封装向量库SDK、自己写Prompt拼接逻辑、自己处理重试和错误……当然这些都能写但每换一个向量库或换一个模型都要动一层代码。用LangChain核心链路可以做到与具体工具解耦今天用Chroma明天换FAISS今天调OpenAI接口明天换本地模型改动都被限制在很小的配置范围内。当然LangChain也不是银弹。它的抽象层设计比较重新手有时候会陷入“找类名”而不是“想逻辑”的境地里。如果你的业务非常简单比如就一个固定文档、调用一次模型那直接手写反而更轻。但如果你要做的是一个需要持续迭代、需要支持多种格式和模型的问答应用LangChain确实能帮你省掉大量重复工作。这也是我推荐大多数项目选它的原因。2.3 工具选型思路Embedding、向量库和LLM怎么配RAG链路里的每一个环节都有多种选择我当年第一次搭的时候也犹豫了很久。这里直接给出我总结的选型思路Embedding模型分为在线API和本地开源两大类。在线API使用简单、效果稳定适合快速验证本地开源模型需要自己下载部署但数据不出内网、长期用不花token钱适合有隐私要求或高频调用的场景。向量库常用的有Chroma、FAISS、Milvus、pgvector等它们的定位差异很大。向量库适合场景特点Chroma学习、原型验证轻量数据持久化简单适合单机小规模检索FAISS本地小规模到中等规模计算高效支持大规模索引但本身不自带服务端Milvus生产环境、海量数据分布式部署功能全运维成本也更高pgvector已有PostgreSQL业务复用现有数据库减少额外组件适合中低并发LLM方面同样可以选API或本地开源模型主要取决于你的部署环境、数据敏感程度和预算。做原型验证时先用API把链路跑通再按需替换成私有化部署模型这也是比较稳妥的推进路径。我个人的建议是第一版别追求“最全最优”先选一条最简单、最容易跑通的组合——比如FAISS加一个在线Embedding接口加一个在线LLM把整条链路跑顺再逐步替换组件。这个思路可以帮助你把精力集中在“RAG效果怎么调”这个真正重要的问题上而不是被工具配置拖住。3. 实操用LangChain从零搭建RAG问答应用3.1 环境准备与依赖安装先准备好运行环境。我用的是Python 3.10项目里建议你用虚拟环境避免把依赖装乱。然后安装下面的包pip install langchain langchain-openai langchain-community faiss-cpu openai pypdf这些包分别负责LangChain核心框架、OpenAI的聊天和Embedding接口封装、社区加载器、FAISS向量库、OpenAI SDK以及PDF解析。如果你只需要加载txt文件pypdf也可以不装但留着之后扩充知识库会方便很多。接着设置你的OpenAI API Key。我一般建议直接写成环境变量export OPENAI_API_KEY你的keyWindows用户就对应设置系统环境变量或者在代码里临时配置也行只是注意别把key硬编码到会提交到仓库的文件里。3.2 文档加载与文本切分决定检索效果的第一道关卡先准备一份测试文档我拿一个模拟项目X的产品说明书来演示。文档是txt格式放在llm_rag_hub下面from langchain_community.document_loaders import TextLoader loader TextLoader(llm_rag_hub/模拟项目X产品说明.txt, encodingutf-8) documents loader.load()如果你有多个文件可以用DirectoryLoader加载整个目录格式上也可以支持PDF、Markdown、Word等。加载得到的documents是一个包含Document对象的列表每个Document有page_content和metadata两个主要属性。接下来是关键的一步文本切分。这一步的目的不是简单地把文档变短而是让每个片段保持相对完整的语义。我用的是RecursiveCharacterTextSplitter它支持按一系列分隔符从段落符到标点符号递归切分尽量让片段停在不切断语义的位置from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks text_splitter.split_documents(documents)你会问chunk_size500的“500”是什么概念它指每个片段的最大字符数。对于中文内容500个字符大概是一小段介绍或者两三个短段落语义还比较完整。chunk_overlap50的意思是相邻两个片段之间有50个字符的重叠这是为了处理“切分点恰好切断一句话”的情况——有了重叠即使一句话被分到两个片段里检索时也能在相邻片段中找到它的上下文。3.3 向量化与向量库存储把文本变成可检索的“坐标”现在把切好的片段向量化并存进向量库。这里用OpenAI的Embedding模型做向量化from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore FAISS.from_documents(chunks, embeddings) vectorstore.save_local(faiss_index)这行代码干了好几件事把每个片段交给Embedding接口转换成向量把向量和原始文本一起存进FAISS索引最后把索引保存到本地目录faiss_index方便之后直接加载。Embedding模型本身其实相当“廉价”——它是用来生成文本向量的相比于对话模型它的调用成本通常可以忽略不计。但在选型时有几个细节值得注意中文效果、向量维度、上下文长度。OpenAI的text-embedding-3-small在通用语义相似度上表现不错维度适中适合做中文知识库的原型验证如果你完全不想用外部API社区里也有几款优秀的中文Embedding模型但需要本地部署后面章节再展开。3.4 构建检索器怎么把最相关的片段捞出来向量库建好后下一步是把它封装成检索器retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4} )这里的含义是用户问一个问题向量库会把所有片段按“与问题的向量相似度”从高到低排序然后返回前4个最相关的片段。k4是我在多数项目里比较常用的起始值——少了信息不够多了会带进噪声。到这里检索链路已经通了。你可以先做一次小测试看看检索器能不能抓到相关内容retrieved_docs retriever.invoke(报销流程怎么走) for doc in retrieved_docs: print(doc.page_content)打印出来的内容就是之后要喂给大模型的“参考资料”。这一步一定不要跳过——它能帮你直观判断前面的切分和向量化有没有做好是整个项目排查问题时最核心的调试手段。3.5 组装问答链让大模型基于资料说话最后一步是把检索到大模型串起来。这里我用LangChain的LCEL表达式来定义链路这也是目前推荐的做法结构清楚逻辑直观from langchain_openai import ChatOpenAI from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough prompt_template 你是一个严谨的客服助手。请只根据下面提供的资料回答问题。 如果资料里没有提到相关信息请直接说“资料中未找到相关内容”不要自己编造。 资料 {context} 问题 {question} 回答 prompt PromptTemplate(templateprompt_template, input_variables[context, question]) def format_docs(docs): return \n\n.join(doc.page_content for doc in docs) llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) rag_chain ( { context: retriever | format_docs, question: RunnablePassthrough() } | prompt | llm ) answer rag_chain.invoke(年休假怎么计算) print(answer.content)看到这里你会发现整条链路其实只有三次转换用户问题 → 检索出相关片段 → 拼成Prompt → 调LLM → 得到回答。LCEL表达式用|把这些步骤串起来每一步的输出作为下一步的输入读起来非常直观。关于temperature0这是做知识库问答时我很坚持的一个设定。问答类任务要的是“稳定和忠实于资料”不是“发散和创造”将temperature设为0可以显著减少模型自由发挥的倾向。注意不同版本的LangChain API略有差异如果你安装的是较新版本个别导入路径可能变了。碰到报错时先看报错信息里提示的模块路径再按实际版本调整import语句——这是每个LangChain使用者都会经历的过程不用慌。4. 进阶调优chunk、检索、Embedding和Prompt的关键参数4.1 chunk_size和chunk_overlap到底怎么选很多新手搭完第一版RAG效果经常是“感觉有点相关但又不够准”——十有八九是chunk切分没调好。chunk_size的影响比你想的大得多它的取舍可以这样理解chunk_size优势劣势适合场景200左右定位精准噪声小命中粒度细语义容易被切碎有时信息不完整问答明确、文档条理清晰的FAQ类500左右语义完整度与定位精度较均衡偶尔会带进冗余内容大部分通用知识库我的首选起始值1000以上上下文完整信息量大噪声多、token开销高、检索容易模糊长章节、整体性说明类文档chunk_overlap我个人建议设置在chunk的10%到20%之间也就是500的chunk配50到100的overlap。它的作用是兜住切分点两侧的上下文避免“话说到一半被切开”的情况。overlap太小切分点附近的语义容易被截断overlap太大又会导致大量重复内容入库白白增加存储和token消耗。调chunk时不要凭感觉反复试。一个高效的做法是先定几个候选值比如300、500、800每个都跑一批测试问题把检索到的片段打出来对比看哪个切法更稳定地命中关键内容。这比漫无目的地调参快得多。4.2 检索不准怎么办混合检索、重排序和Query改写当纯向量检索的效果不够时有几种成熟的优化手段按实施难度从低到高排列第一是混合检索。向量检索擅长语义相关性但对精确关键词不敏感而传统的BM25关键词检索可以精准命中专有名词、编号、型号等。两者结合能同时兼顾“意思相近”和“字面相同”。LangChain里可以通过EnsembleRetriever把向量检索器和BM25检索器组合起来再用权重控制两者的贡献比例。第二是重排序Rerank。第一步用向量检索先召回100个候选片段然后用一个专门的Rerank模型对候选片段进行精排取前5个。这样做的好处是粗召回阶段可以大胆扩大候选范围精排阶段再做精细取舍最终效果往往比单纯调整k值提升明显。第三是Query改写。有时候用户的问题本身是模糊的比如只问“怎么申请”很难直接命中知识库里“物资申请流程”这一章节。可以先让LLM把用户的问题改写成更完整、更适合检索的形式比如“如何申请办公物资需要哪些步骤”然后再用改写后的问题去检索。这三种手段不是互斥的线上系统经常一起用。不过我的建议是循序渐进——先确认检索结果本身有没有问题再考虑上重排和改写不要一开始就全堆上以免排查问题时变量太多。4.3 中文场景的Embedding选型建议如果你处理的是中文知识库Embedding选型的优先级甚至高于LLM选型。为什么这么说因为检索结果的质量直接决定回答的上限而Embedding模型对中文语义的理解能力差异非常大——一些面向英文优化的模型在中文上会表现出明显的“语义盲区”。目前中文场景下比较常用的方案有两类一类是继续用在线API模型比如OpenAI的Embedding接口它对中文的支持已经相当不错胜在省事、稳定适合快速上线和中小规模知识库。另一类是本地开源Embedding模型目前社区里比较成熟的有成体系的几个系列它们针对中文语料进行了专门训练在中文句子相似度、检索召回等任务上表现不输在线接口。本地部署之后每次向量化的成本近乎为零适合大规模文档入库和隐私要求较高的场景。我给一个比较实在的建议先拿你的真实文档分别用在线API和本地模型各跑一版挑几十个有代表性的问题对比检索命中效果。不要只看榜单分数实际业务里的问题往往更口语化、更碎片化只有拿自己的数据跑过才有发言权。4.4 Prompt设计让模型学会“不知道就说不知道”RAG里Prompt的作用常常被低估。很多人图省事直接把检索到的文本拼到系统提示词后面却没有明确告诉模型“你要做什么、不能做什么”结果模型依然按照自己的习惯放飞自我。一个合格的RAG系统Prompt至少要包含三层信息角色定义你是谁遵循什么原则。约束条件只依据资料回答、不得编造、资料中未提及的内容要明确说明。输出格式必要时规定回答结构比如先结论、再依据、最后补充说明。实践里我常用的中文模板大概是这样的你是企业内部的智能问答助手。 严格依据给定的资料内容回答用户问题。 如果资料中没有直接答案明确回答“资料未覆盖该问题”不要尝试推测。 回答时尽量引用资料中的关键信息但不要逐字复制整段。这个Prompt在多数项目里都能直接套用。需要注意的是{context}本身也要在“资料”之后明确给出让模型知道哪部分是“可信输入”哪部分是“用户问题”。两者的边界如果不清晰模型可能会把用户问题也当成资料引用导致回答混乱。5. 常见问题与排查技巧我从实操里踩过的坑5.1 答非所问检索结果压根不对怎么排查先说明一个很容易被忽视的事实RAG问答里模型答错绝大多数情况下不是模型的错而是检索环节已经把内容找偏了。遇到答非所问第一件事永远是打开调试开关把检索到的片段打印出来人工看一眼。如果你发现检索到的片段和问题毫无关系按这三个方向排查是不是Embedding模型不适合当前语言或领域换一个专门针对中文或该领域优化的Embedding模型再试。是不是chunk切得太碎导致一个完整的知识点被拆散了适当调大chunk_size。是不是文档本身质量太差比如扫描件、排版混乱、大量表格这类文档要先做文本清洗否则下游做什么都白搭。这里分享一个排障技巧开发阶段在问答界面旁边加一个“引用来源”展示区把模型引用的文本片段原样展示出来。这不仅是排障工具也是让用户信任问答系统的关键设计。用户看到“这个回答出自文档第几节”对结果的接受度会高很多。5.2 模型还是胡编乱造上下文没有真正约束住如果你确认检索到的片段已经很相关了模型还是给出资料里没有的内容问题大概率出在Prompt约束上。最常见的原因是Prompt没有说清楚“只能依据资料回答”。默认情况下模型会把资料当作参考但它仍然倾向于结合自己的预训练知识来“补充润色”。解决办法就是把约束条件写进Prompt并且把这句话放在资料之前让模型优先读取到规则。另外temperature0也是一个必要的防线。还有一种可能是k值设得太大。你以为“多给几段资料更保险”实际上资料越多模型越容易从中挑出一些边缘内容展开联想。我一般控制在3到5个片段多了就进入“信息过载”状态。5.3 Token消耗太大、响应太慢怎么办知识库问答的token开销主要来自三块检索到的片段、Prompt模板和历史对话记录如果做了多轮会话。先算一笔账如果每个chunk是500字符k4那么每次问答大约要消耗2000字符的上下文折合约500到800个token。按GPT-4的价格可能不算便宜但换成便宜一些的模型或者用小参数模型成本能降一个数量级。如果已经是便宜模型了还是觉得贵那就从三个方向下刀调小k值比如从5降到3。调小chunk_size比如从800降到400让单次上下文变短。精简Prompt模板把不影响效果的修饰语删掉。响应速度方面最大的瓶颈往往是向LLM提交的长上下文。减小chunk、减小k同样能加速。另外如果昂贵模型只是用来做“答案润色”不如试试两步走先用便宜模型做检索和草稿再决定是否调用更贵的模型做最终生成——不过这个策略会让系统复杂度上升适合在量真的大起来之后考虑。5.4 向量库构建慢、占用大持久化与规模化管理用FAISS跑本地小规模知识库时几千个文本片段构建索引大概只要几秒完全不用焦虑。但文档数量到了几十万级别时内存占用和构建时间就会变成一个问题。我的建议是把“构建”和“查询”分开管理。构建侧可以做成离线任务——知识库文档更新时后台增量构建索引查询侧只加载已构建好的索引文件。FAISS本身支持save_local和load_local正好适合这种模式# 构建阶段 vectorstore FAISS.from_documents(chunks, embeddings) vectorstore.save_local(faiss_index) # 查询阶段每次启动时 vectorstore FAISS.load_local(faiss_index, embeddings, allow_dangerous_deserializationTrue)注意新版本的FAISS加载本地索引时需要显式传入allow_dangerous_deserializationTrue。这个参数的意思是“信任这个索引文件的来源”——只加载自己构建的索引文件别下载不明来源的索引来加载。数据量更大、需要多人同时查询时就该考虑换成带服务端的向量库了。这时候选型的逻辑会从“怎么存得下”变成“怎么并发查得动”生产环境常见的方案包括Milvus、Elasticsearch内置的向量检索能力或者直接用云数据库自带的向量字段。从单机FAISS升级到这类组件LangChain的优势就体现出来了——你的检索代码基本不用大改只是换一个VectorStore类。6. 写在最后几个我个人的经验之谈RAG这套方案我前前后后搭过好几次最大的体会是它的门槛不高但要做好很考验细节。链路里每一个环节都有自己独立的坑chunk怎么切、Embedding怎么选、Prompt怎么写、k值设多少任何一步没到位最后呈现的效果都会差一口气。如果只让我留三条最重要的建议我会说第一先建好调试链路务必把“检索到什么片段”可视化这是所有优化工作的起点第二第一版全部用最简单、最稳定的组合先跑通再谈混合检索和重排序第三写Prompt时一定要把“不知道就说不知道”写死这是挡住幻觉最有效的一道防线。RAG的下一个阶段我最近在尝试的是把它和多轮会话、长期记忆结合起来——用户连续问几个问题时系统需要判断哪些问题可以复用之前检索到的上下文哪些需要重新走一遍检索。这个方向坑更深但做出来后体验会有本质提升。如果你已经把手上的RAG应用跑通建议也往这个方向试试。