资讯动态

LangChain实战:从零搭建RAG知识库问答系统

发布时间:2026/10/23 5:27:02 来源:尧图企业网站定制
1. RAG知识库到底解决什么问题LangChain快速入门这个系列前两篇我写了基础概念和Chain/Model的用法这一篇终于轮到RAG知识库。RAG这个词现在的热度不用我多说它解决的问题也很直接让大模型在面对私有知识、实时信息时不再“一本正经地胡说八道”而是先到你的知识库里检索相关内容再基于检索结果生成回答。说白了就是把搜索引擎的召回能力和大模型的生成能力拼在一起。适合谁看你手上有一批PDF、Word、TXT文档想做一个内部问答系统或者想让聊天机器人学会回答产品手册里的问题那这篇就是为你准备的。整体思路我会按照工程落地的顺序来从选型讲到代码再讲排障。1.1 从“模型不知道”说起大模型不是数据库它背下来的知识是训练时喂进去的那一堆公开语料有自己的截止日期。你问“我们最新的报销流程是什么”如果这个流程是上个月刚更新的模型基本不知道就算它“知道”也可能是从某个相似场景里硬猜出来的答案。更麻烦的是模型在不确定时会流畅地编造这在问答场景里是很伤的。RAG做的事就是给模型配一个外挂资料库。你提问的时候系统先去这个资料库里找出和问题最相关的几个片段然后把这些片段连同问题一起丢给模型让模型“看着资料回答”。我习惯用一个类比让一个刚入职的同事回答公司制度问题你直接问他他只能瞎说但如果你先把员工手册翻到对应页面再让他照着念准确率立马上来了。RAG就是这个“翻手册”的过程。从用户视角看这个方案有三个好处。一是知识可以随时更新文档改完向量库里重新跑一遍模型不需要重训。二是回答可以溯源系统可以告诉你答案来自哪份文档、哪个段落。三是实现门槛比微调低得多不需要训练样本也不需要GPU去跑微调流程。1.2 RAG的完整链路和核心组件一个最朴素的RAG链路可以拆成两条线离线索引和在线问答。离线索引做的是“把知识变成可搜索的结构”。原始文档经过加载、清洗、切分变成一个一个文本块每个文本块通过Embedding模型转成向量向量写入向量数据库同时保留原始文本方便后面引用。在线问答做的是“把问题变成向量、再检索、再生成”用户问题同样转成向量在向量库里找最相似的几个文本块把它们和问题一起组装成Prompt交给大模型生成答案。对应到LangChain里核心组件无非这么几类DocumentLoader负责读文件TextSplitter负责切文本Embeddings负责向量化VectorStore负责存储和检索Retriever负责把向量存储包装成方便问答调用的接口最后LLM负责生成。LangChain的价值在于把这些组件的接口统一了你换了Embedding模型或者从Chroma换成另一个向量库问答代码基本不用大改。搞清楚了这条链路后面写代码其实就是把每个环节接起来。别一上来就追求复杂的RAG流程先把这条最小链路跑通再考虑重排序、多路召回这些优化。2. 搭建前的准备工作环境、模型与资料选型2.1 环境与依赖安装我用的是Python 3.10LangChain目前对3.8以下的老版本已经不太友好了建议直接上3.10或3.11。第一步创建一个虚拟环境免得依赖打架。安装的时候我建议分开装别只装一个langchain因为很多Loader和向量库在单独的包里面。pip install langchain pip install langchain-community pip install langchain-text-splitters pip install langchain-chroma pip install pypdflangchain-chroma是LangChain官方对Chroma的适配包pypdf用来解析PDF文件。如果你要读Word文档后面可能还要装python-docx读Markdown和TXT则不需要额外依赖。版本方面LangChain迭代非常快不同版本之间API可能有变化装完之后最好先跑一下langchain.__version__和chromadb.__version__把版本记住后面排查问题要用。还有一个容易被忽略的点文档解析依赖最好提前装好。我刚开始图省事只装了核心库结果DirectoryLoader加载PDF的时候直接报错找不到pdf解析器后面还得补装。与其报错再补不如一开始就把常用解析器装上。模型服务方面你需要准备好一个大模型API的访问密钥或者本地模型服务地址这个根据你现有的资源来就好LangChain只调用标准接口切换到不同的模型服务只是换一下Client的事。2.2 Embedding模型和向量库怎么选Embedding模型决定了两个文本在语义上像不像。中文场景我优先推荐本地Embedding模型比如bge-small-zh-v1.5或bge-large-zh-v1.5这类开源中文模型。对入门来说bge-small已经够用而且模型体积小CPU也能跑。如果你对英文文档支持要求高可以考虑多语言模型。选Embedding模型有一个原则尽量选和你文档语言一致的模型否则相似度检索会飘。向量数据库的选择就更宽了。我个人会把它们分成三档入门档是Chroma本地文件型零配置直接pip install就能用进阶档是FAISS也是文件型检索速度快适合单机中等规模生产档是Milvus或pgvector这类服务型数据库支持大规模、高并发、权限管理。对小项目和个人知识库来说Chroma足够而且它支持持久化到本地目录不用担心重启丢数据。方案类型适合场景上手难度Chroma本地文件向量库入门、个人知识库、Demo低FAISS本地文件向量库中等规模、性能敏感中Milvus分布式向量数据库生产环境、大规模并发高pgvectorPostgreSQL扩展已有PostgreSQL体系中我给的建议是先选Chroma把流程跑通跑通之后再谈换不换。千万别在第一步就纠结基础设施RAG的核心瓶颈通常不在向量库而在文档处理和检索质量。2.3 准备知识库文档格式与清洗要点在写代码前先把“原材料”准备好。常见文档来源是PDF、Word、Markdown、TXT或许还有网页。为了演示我会把知识库文档放在一个data/目录下里面放几份手册、FAQ、制度说明之类的文本。清洗这一步特别容易被跳过却是RAG效果差异最大的地方。PDF如果是从扫描件生成的直接读取会得到一堆乱码这种必须先做OCR处理再进入后续流程。文档里如果带页眉、页脚、页码Loader读进来后这些噪音也会混入文本影响切分和检索。另外表格类内容在PDF解析后通常会丢失结构可能变成一串没有分隔符的文本检索时很难命中。我的习惯是能导成Markdown或TXT的文档就尽量不用PDF只有不得不给PDF时才交给Loader去解析。还有一个小提醒给知识库文档做版本管理。我吃过一次亏把旧版制度文档和新版混在同一个目录里结果问答系统一会儿答旧规则一会儿答新规则。后来我改成按目录区分版本并且在文档里加上版本号和生效日期检索时也能把这些信息一并返回给模型。数据质量决定了RAG的上限后面调什么都只是补救。3. 核心实现一步步搭建RAG知识库3.1 文档加载从本地文件到LangChain的Loader先加载知识库文档。LangChain里最简单的做法是把data/目录整个喂给DirectoryLoader它会根据扩展名自动选择对应的加载器。不过我不建议完全黑盒操作第一次跑的时候最好先加载一个文件看看内容再批量加载。from langchain_community.document_loaders import PyPDFLoader, TextLoader from langchain_community.document_loaders import DirectoryLoader # 单文件预览 loader PyPDFLoader(data/product_manual.pdf) docs loader.load() print(docs[0].page_content[:500]) print(docs[0].metadata)DirectoryLoader默认会用text加载器处理所有文件所以需要显式指定PDF加载器loader DirectoryLoader( data/, glob**/*.pdf, loader_clsPyPDFLoader, loader_kwargs{mode: single} ) docs loader.load()这里我踩过一个坑loader_kwargs里的参数不是所有版本都支持比如某些版本不接收extract_images会直接报TypeError。你要是遇到这种问题就把loader_kwargs删掉用默认参数试试。加载完成后建议打印一下docs的数量和每个文档的metadata确认metadata里包含了source字段后面用来追溯答案来源非常有用。3.2 文本切分chunk size怎么定加载出来的Document往往很长直接整篇丢进向量库有两个问题一是Embedding模型对输入长度有限制超长会被截断二是检索时返回一个巨大片段Prompt会超长而且不相关内容太多会干扰生成。所以切分很有必要。我常用RecursiveCharacterTextSplitter它按一组分隔符递归切分尽量保留语义完整性。一个典型的配置是from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) split_docs text_splitter.split_documents(docs) print(f原始文档数: {len(docs)}, 切分后文本块数: {len(split_docs)})chunk_size500是按字符数来的。为什么取500而不是1000主要看Embedding模型的最大输入token数。以常见的中文Embedding模型为例它通常支持512个token中文一个token大约对应1.5个汉字换算下来700到800个字符以内比较安全。我留一点余量取500字符这样即使切分算法带有一些冗余也不容易超限。chunk_overlap50是为了避免一刀把一句话从中间劈开重叠部分能让前后块共享一些上下文检索时更不容易漏掉关键信息。如果你要切分的文档是结构化很强的比如每章每节都很清晰可以自己定义分隔符优先按章节切。我见过不少同学不调参数直接用默认的 去切中文结果每个chunk都是断句碎片检索质量惨不忍睹。中文数据请务必将中文标点放进分隔符列表甚至可以把。都列进去。3.3 向量化与存储写入向量库切分完就要把文本块转成向量并写入向量库。我以Chroma为例Embedding用本地模型from langchain_community.embeddings import HuggingFaceEmbeddings embedding HuggingFaceEmbeddings( model_namebge-small-zh-v1.5, encode_kwargs{normalize_embeddings: True}, ) from langchain_chroma import Chroma vectorstore Chroma.from_documents( documentssplit_docs, embeddingembedding, persist_directory./chroma_db, )第一次运行会下载Embedding模型网络慢的话可能要多等一会儿。persist_directory是持久化目录运行完成后会在当前目录生成一个chroma_db文件夹里面就是排好版的向量数据。下次再启动可以直接Chroma(persist_directory./chroma_db, embeddingembedding)加载不用重新embedding全文。这里有两个经验。第一normalize_embeddingsTrue可以让后续相似度计算更稳定尤其用余弦相似度的时候。第二数据的增删改最好通过向量库接口操作不要手动去改本地文件否则索引和文档块可能会不一致。向量化阶段最花时间大批量文档建议分段跑跑完一部分先打印日志确认再继续。3.4 检索与增强构建RetrievalQA链路向量库建好之后就可以做问答了。先把它包装成一个Retriever设置每次检索返回的片段数kretriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4} )然后创建问答案例。以LangChain中的RetrievalQA为例from langchain.chains import RetrievalQA qa RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, verboseFalse, ) result qa.invoke({query: 员工离职需要提前几天申请}) print(result[result]) print(result[source_documents])这里的llm是你已经初始化好的模型对象我在这里用llm做占位是因为不同人的模型服务不一样你自己换成就好。chain_typestuff表示把检索到的片段全部“塞进”Prompt让模型一次生成代码逻辑最简单适合片段少的情况。如果检索片段很多、Prompt超长再考虑map_reduce先把每个片段分别处理再汇总。return_source_documentsTrue会返回命中的源文档这是追溯答案出处的重要开关。如果你不想用RetrievalQA这种高层封装也可以手动构造Prompt把检索结果拼进去再调用模型。高层封装适合快速跑通手动构造适合精细控制。我自己的习惯是先跑通高层封装再替换成自定义Prompt一步步拆开看效果。4. 调优实践让检索结果更靠谱4.1 检索参数与相似度阈值调整很多人把RAG做出来之后发现回答经常跑偏第一反应是换更强的模型。其实很多时候问题出在检索环节召回的片段和问题不相关大模型再强也答不对。所以要学会调Retriever。第一个参数是k。k太小可能漏掉正确答案k太大很多无关片段混进来模型容易被带偏。我一般先设为4然后根据测试结果上下调。知识库很小、文档块很少的时候k设3-5就够了如果文档块很多、知识面很广可以适当调到6-10配合后面的阈值过滤。第二个参数是相似度阈值。Chroma支持similarity_score_threshold检索类型只保留相似度超过阈值的片段。以我常用配置为例retriever vectorstore.as_retriever( search_typesimilarity_score_threshold, search_kwargs{score_threshold: 0.5, k: 4}, )阈值设多少要看你的Embedding模型。不同的模型和归一化方式分数分布差异很大不要照搬网上的数字。我的做法是先用similarity模式查几轮打印出命中的分数再根据分数分布来确定阈值。比如发现正确结果都在0.45以上无关结果在0.3以下那我就把阈值设在0.4左右既能过滤噪音又不漏答案。4.2 提示词模板与重排序的影响检索调完之后生成端的Prompt也很关键。我常用这样一个模板你是知识库问答助手。请根据下面提供的资料回答问题。如果资料中没有足够的信息直接回答“资料中未找到相关内容”不要编造。 资料 {context} 问题{question}在RetrievalQA中使用from langchain.prompts import PromptTemplate prompt PromptTemplate.from_template(template_str) qa RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: prompt}, )注意模板里的{context}和{question}是约定俗成的变量名不要随意改成其他的除非你自定义Chain。提示词的作用是给模型划清边界有资料就答没资料就承认不知道这会大幅减少幻觉。生产环境里还可以加一个重排序Rerank步骤先用向量检索召回20个候选片段再用一个专门的重排序模型对候选做精排取前3-5个。向量检索负责“宽进”重排序负责“精选”这套组合拳效果非常明显。Rerank模型的选型可以先用本地小模型或云服务根据你的部署环境来定。我给的建议是如果你的知识库文档数量不多、答案相对集中暂时不用上Rerank如果你已经发现“检索到的很多但排在前面的不是最想要的”那就可以考虑加。5. 常见问题与排查实录5.1 文档加载乱码、切分碎片化加载PDF后如果page_content里是一堆乱码大概率是扫描件。解决办法是先走OCR流程把图片转成可搜索的文本再交给LangChain。千万别指望PyPDFLoader能直接读懂扫描件它不是OCR工具。另一个问题是切分碎片化。如果打印出的文本块都是一两句话的碎片或者上下文不连贯多半是separators没有针对中文设置或者chunk_size太小。我建议先打印几个样本chunk肉眼看一眼。很多问题不是看代码能发现的真的要去读一遍切分结果。把chunk_size从200调到500后很多碎片问题能明显缓解。5.2 检索结果不相关的排查我自己的排查顺序通常是三连问。第一问向量库里到底有没有数据用vectorstore._collection.count()确认一下别稀里糊涂对着空库检索。第二问Embedding模型选对了吗中文文档配英文Embedding模型检索效果会非常不稳定这一点换模型立竿见影。第三问直接查询相似度结果看了吗我喜欢写一个小函数来做检索自检def debug_search(query, k5): hits vectorstore.similarity_search_with_score(query, kk) for doc, score in hits: print(f相似度: {score:.4f}) print(doc.page_content[:200]) print(---)如果直接查相似度结果就不相关那问题出在Embedding或数据上再调Prompt也没用。如果相似度结果相关但生成答案不对那才需要调Prompt或LLM。这个顺序能帮你省很多时间。5.3 内存与性能优化第一次跑全量embedding时内存占用可能很高尤其是大文档加本地模型。建议分批处理每批比如100个Document处理完及时释放变量。如果频繁重启加载向量库也建议做一次持久化避免每次都重新embedding。Chroma的目录别放系统盘占空间不小。在线问答的延迟大头一般不在检索而在LLM生成。测试的时候可以先关掉return_source_documents减少返回数据量真要上生产再加缓存层把相同问题缓存到本地文件或内存里。还有一个小习惯把向量库的加载和LLM的初始化放在进程启动时完成不要在每次请求时重新初始化这个坑我踩过耗时差好几倍。6. 写在最后数据先行链路第二最后分享一点个人体会。RAG知识库这个功能刚跑通的时候会觉得“就这么点东西”但真正用起来调试的重头戏几乎全在上游数据。文档格式乱、切分不合理、Embedding不匹配随便哪个环节崩了最后都会表现为“模型答得不对”。所以我的建议是别急着堆组件先把一条最小链路跑通然后从文档加载、切分、检索、生成四步依次检查把每一步的输出都打出来看一眼。我的另一个习惯是先设定一个验收基线拿10个典型问题跑通最小链路后逐个记录回答质量后面每做一次调整就重新跑同一组问题对比答案有没有变好。不要凭感觉觉得“这次优化有效”要有记录。这个工程化的习惯比任何花哨的框架都值钱。你先把这篇的代码和排障思路过一遍再回头去调自己的数据会发现RAG知识库其实没那么神秘。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑