首页 / 资讯中心 / 文章详情

基于LangChain与RAG构建本地知识库问答系统:从零到实战

基于LangChain与RAG构建本地知识库问答系统:从零到实战 ★ FEATURED ARTICLE
1. 内容整体设计与思路拆解1.1 这个实战项目究竟在解决什么问题作为在AI应用落地层面折腾过不少项目的人我接触过很多想给大模型“注入知识”的场景公司内部的规章制度查询、产品文档答疑、某个垂直领域的专业问答。用了几次纯靠提示词的方式后发现大模型一本正经地胡说八道才是常态尤其当你问的是训练数据里根本没有的内容它会把不确定的东西编得头头是道。后来接触到RAGRetrieval-Augmented Generation检索增强生成发现思路一下就通了既然大模型不知道你关心的专业内容那就先从一个“外挂知识库”里把相关资料搜出来再把资料连同用户问题一起丢给模型让它基于这些材料来作答。这样既不需要重新训练模型又能让模型回答当前需要的专业领域内容。LangChain在这套链路里扮演的是“总调度”的角色它把文档加载、文本切分、向量化、检索、提示词组装、大模型调用这几个环节串成一条流水线。整个项目说白了就是自己动手搭建一套完整的问答系统让大模型能够基于你指定的本地文档进行回答。这个探索过程的价值在于你会亲手走过RAG的每一步对索引构建、召回策略、上下文组装、答案生成这个完整链条建立起非常具体的感知。1.2 为什么值得动手实操而不是直接坐等现成方案市面上确实有不少封装好的RAG框架拖进来就能用。但直接拿来主义很难应对真实场景因为知识库的形态千差万别可能是PDF、Word、Markdown、网页也可能是数据库表。文档内容有的很长有的很短有的表格多有的夹着大量代码。这些差异直接决定切分策略、索引结构、召回方式该怎么设计。通过手动实践跑通一遍LangChain与RAG的组合你能理解每一个环节为什么这么做、数据在中间经过了什么样的形态变化以后无论换什么框架、什么向量库都能自己判断哪里出了问题。我的建议是先用最小可行系统把链路跑通再迭代优化各个环节这比一上来就追求大而全的方案务实得多。1.3 项目适合谁来上手这个项目对有Python基础、想入门大模型应用开发的人比较友好。你不需要对机器学习原理特别精通也不需要懂微调模型但要会基本的Python语法能读懂并修改代码熟悉pip安装依赖。如果你已经在用API调用大模型想进一步提升到“让模型基于私有知识回答问题”的程度那这套东西正是你要的。我对这个项目的定位是一次完整的入门级大模型应用实战从零搭建一套基于本地文档的问答系统。所有环节都通过实际代码落地过程中还会涉及很多择参数、调策略的细节这些细节才是真正拉开工程水平的地方。2. RAG的核心链路深度拆解2.1 从无到有理解RAG外挂知识库如何补全大模型的短板要理解RAG用开卷考试的类比最直观。大模型本身像是一个记忆力很好的考生但它的知识范围在训练时就确定了超出范围的题目就只能靠编。RAG给他额外发了一份参考资料考生遇到问题时先从资料里翻找相关段落再结合自己已有的语言能力组织出答案。这个逻辑看着简单拆开却有很多值得琢磨的地方。比如“翻找”这个动作不可能真拿关键词去硬搜因为用户提问表达方式和文档原文可能是完全不同的措辞这种时候关键词匹配会失灵。RAG的做法是先把文档切分成块把每一块文本用Embedding模型转化为高维向量用户提问时同样转为向量再通过相似度计算找到最相关的若干文本块。这种语义检索的方式能用语义相近而非字面一致的表述来找内容覆盖能力要强很多。还有一个关键设计在于检索出来的文本块只是“参考资料”最终的回答仍然由大模型生成。模型拿到相关文档片段和你提出的问题后会被要求在检索材料基础上作答如果材料里没有明确信息就承认不知道而不是强行编造。这样一来回答的准确性、可信度都大幅提升而且知识库更新时只需要重新构建索引不需要重新训练模型。2.2 RAG与微调的选择什么时候该走哪条路在实际项目中经常有人纠结RAG和微调Fine-tune该怎么选。我以一个知识型客服系统为例假设公司有上百份操作规程文档希望员工提问能迅速得到准确回答且回答能引用原文出处。这种情况用RAG就非常合适因为需求核心是“查询已有内容并准确呈现”知识更新频繁错误答案允许低成本修正。但如果你有一个固定的写作风格要求比如希望模型始终用特定口吻输出或者你的任务结构很稳定、格式模板统一那微调的价值就体现在“让模型学会某种行为模式”上。RAG负责解决“知识”问题微调负责解决“风格与能力”问题两者并不对立。很多成熟系统会在微调基础上叠加RAG先用RAG提供事实材料再由经过微调的模型按规范输出。注意RAG的实现复杂度主要集中在索引与检索部分微调的实现复杂度集中在训练数据准备与训练过程。对入门者来说RAG的调试链路更透明每一步都能肉眼观察结果这也是我推荐先走RAG的原因。2.3 RAG管线的四个核心层从文档清洗到答案合成我把RAG的完整链路归纳成四个核心层理解它们之间的衔接关系是搭建整个系统的关键。第一层是文档处理层。原始文档五花八门PDF可能有复杂排版网页里有大量导航噪声Word里有图片表格。这一层要做的是把文档解析成干净的纯文本必要时还要做OCR识别扫描件、表格结构还原等工作。很多人忽略文档解析对最终效果的影响拿着乱糟糟的PDF文本直接切分后面检索质量高不了这个坑我见过很多次。第二层是索引构建层。清洗后的长文本需要切成合适的大小再逐一用Embedding模型编码为向量最后写入向量数据库。切分得太粗检索片段里混着大量无关信息模型回答容易跑偏切分得太细语义上下文又被切断了。这里的平衡需要根据实际文档类型反复调整。第三层是检索层。用户提问向量化后在向量库里找到Top K个最相关的文本块。进阶方案会在这层做混合检索和重排比如同时用关键词匹配和向量检索再通过重排模型精排结果。检索质量直接决定于后续回答的质量这一层是投入产出比较高的优化点。第四层是生成层。把检索到的文本块和用户问题组合成提示词交给大模型生成最终回答。如何组织提示词、上下文放多少内容、是否需要引用来源都是这里要细化的问题。我实际调过的Prompt细节还挺多比如“仅依据上下文回答不要使用先验知识”就能明显减少模型”多嘴“的情况。这四个层贯穿始终的是数据流的串联每一层的结果都是下一层的输入。LangChain的价值就在于把这些步骤统一封装让你能把主要精力放在调优各环节策略上而不是反复写胶水代码。3. 基于LangChain搭建RAG问答系统实操详解3.1 环境准备与依赖安装我建议在Python 3.10以上的虚拟环境里操作。先建一个干净的虚拟环境避免依赖冲突python -m venv rag_demo source rag_demo/bin/activate接着安装核心依赖pip install langchain langchain-community langchain-openai chromadb pypdf我用的版本大致是LangChain 0.3.x系列如果你安装的版本比较新有些API命名可能有调整遇到报错先查一下官方迁移文档。这里逐个解释一下这几个依赖的作用langchain是核心框架负责编排整个检索生成链路langchain-community包含大量第三方集成组件langchain-openai提供对OpenAI风格接口的封装包括Embedding和LLM调用chromadb是向量数据库负责保存Embedding向量并提供相似度检索pypdf用来读取PDF文档。还需要准备一个大模型API的访问密钥。如果你有OpenAI类型的API接口在环境变量里配置export OPENAI_API_KEY你的密钥如果用的是国内兼容接口可以通过自定义base_url的方式接入比如配置成某个兼容OpenAI协议的网关地址。这一点对国内开发者来说很实用因为不少可用接口都是兼容格式的。3.2 文档加载与切分别小看这一步先看一下文档加载的代码from langchain_community.document_loaders import PyPDFLoader loader PyPDFLoader(./docs/产品手册.pdf) documents loader.load() print(f共加载 {len(documents)} 页文档)这里的输出结果通常是一个文档对象列表每个对象包含页面内容和元数据。PDF加载相对简单如果是扫描版还需要OCR这里先不展开。加载完成后是最关键的切分环节。LangChain里最常用的是递归字符文本拆分器from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ] ) chunks text_splitter.split_documents(documents) print(f切分后得到 {len(chunks)} 个文本块)chunk_size表示每个文本块的最大字符数chunk_overlap表示相邻文本块之间的重叠字符数。这两个参数是最影响检索效果的基础参数后面我会专门说怎么调。为什么要有重叠试想一篇文档中某个段落被拦腰切断第一块末尾是段落的开头第二块开头是段落的结尾如果完全没有重叠且切分点刚好切散了语义检索时就可能匹配到只有半句话的碎片。重叠能让相邻块共享一些上下文减少这种问题。3.3 构建向量索引从文本块到高维空间切分好的文本块需要先转化为向量。这里使用OpenAI的Embedding模型from langchain_openai import OpenAIEmbeddings embeddings OpenAIEmbeddings(modeltext-embedding-3-small)text-embedding-3-small是性价比很高的Embedding模型效果对大多数场景来说够用了。如果想进一步提升效果可以换用text-embedding-3-large代价是向量维度更高、存储和检索开销更大。接着把文档块写入向量数据库from langchain_community.vectorstores import Chroma vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db )persist_directory是持久化目录向量索引会保存在本地下次运行时可以直接加载不用重新Embedding所有文档。这一步是建立整个知识索引的过程只有建立好索引后续才能进行快速检索。Embedding向量是什么概念通俗地说它把一个文本片段映射成几百维的数值数组语义相近的文本在向量空间里的距离更近。检索时把用户问题也转成同一个空间里的向量然后找距离最近的文本块这就是向量检索的基本原理。3.4 构建检索问答链核心代码的组合编排有了索引就可以组装完整链路了。第一步是构造一个检索器设置返回结果数量retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 5} )k表示每次检索返回多少个最相关文本块。我刚开始做时习惯把k设大一点比如10个觉得信息多更保险后面发现这样反而会让上下文过长、干扰信息太多回答质量下降。对大多数场景3到6个是相对合理的区间。为了让问答支持多轮对话还需要一个将历史对话转化为独立检索问题的环节。这可以用LangChain的create_history_aware_retriever实现from langchain.chains import create_history_aware_retriever, create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0.2) contextualize_q_system_prompt ( 给定聊天历史记录和最新的用户问题 如果根据历史记录就能理解问题可以重新表述成独立问题。 否则就直接返回原始问题。不要回答问题只输出重新表述后的问题。 ) contextualize_q_prompt ChatPromptTemplate.from_messages([ (system, contextualize_q_system_prompt), MessagesPlaceholder(chat_history), (human, {input}) ]) history_aware_retriever create_history_aware_retriever( llm, retriever, contextualize_q_prompt )这里为什么需要“重新表述问题”因为RAG检索只能基于当前一次提问的向量进行如果用户说“那第二个方案呢”没有上下文的话检索系统根本不知道“那”指什么。将原始问题结合历史对话重新生成一个完整独立的问题才能确保检索到正确的内容。接着定义回答提示词并组装完整问答链system_prompt ( 你是内部知识库的智能助手。请仅根据提供的上下文内容回答用户问题。 如果上下文中没有相关信息明确说明不知道不要自行编造。 回答时尽量引用原文并附上来源页码或文档名称。 ) qa_prompt ChatPromptTemplate.from_messages([ (system, system_prompt), MessagesPlaceholder(chat_history), (human, {context}\n\n问题{input}) ]) document_chain create_stuff_documents_chain(llm, qa_prompt) retrieval_chain create_retrieval_chain(history_aware_retriever, document_chain)create_stuff_documents_chain会把检索到的文档都“塞进”Prompt里再交给LLM。这种方式英文叫Stuff好处是实现简单、上下文信息完整坏处是如果检索结果太多会超出模型上下文窗口或者让模型注意力分散。后续可以考虑换成MapReduce或Refine模式但入门阶段先Stuff足够。3.5 多轮对话与结果输出完整跑通一个问答流程维护对话历史并调用链from langchain_core.messages import AIMessage, HumanMessage chat_history [] def ask(question): global chat_history result retrieval_chain.invoke({input: question, chat_history: chat_history}) chat_history.append(HumanMessage(contentquestion)) chat_history.append(AIMessage(contentresult[answer])) return result[answer] print(ask(产品A的保修政策是什么)) print(ask(那退换货需要哪些条件))这里的关键点是每一轮对话结束后都把用户问题和AI回答追加到chat_history里。第二轮提问“那退换货需要哪些条件”就会被重新表述成类似“产品A的退换货需要哪些条件”这样检索器才能准确命中对应的文档片段。我实际测试时发现多轮对话的体验比我想象中重要。很多人做Demo只测单轮问答实际使用时用户会连续追问如果对话历史处理不好第二个问题基本就答偏了。create_history_aware_retriever这个环节看着不起眼但对整体体验影响很大。4. 参数调优与效果优化把系统从能用拉到好用4.1 切分参数的实战经验chunk_size和chunk_overlap怎么搭切分粒度是RAG系统里最值得反复实验的参数。我做过一个项目案例文档是产品使用手册每页基本围绕一个功能点展开用chunk_size500, chunk_overlap50效果尚可。后来换成一份年度报告长段落非常多同样参数下经常出现一个文本块横跨多个主题检索返回的块里混杂着不相干的信息。我的经验是如果文档有清晰的结构化分节比如每个章节讨论一个独立主题切分可以跟着标题走把同一小节的内容合并成一个块。LangChain里可以通过自定义分隔符或者按文档原有结构组织来实现。如果文档结构不明确就退回到纯文本切分。推荐尝试几个经验组合chunk_size300, chunk_overlap50轻量文本新闻、留言、短说明够用chunk_size500, chunk_overlap80结构化说明文档、操作手册的基础起点chunk_size800, chunk_overlap120长文背景叙述较多的内容可以试调参时建议用一组覆盖常见问题的评测样例来对比回答质量而不是靠感觉。我一般会准备二十个问题每次调参后逐个测用一个简单的评分表记录回答是否准确、是否引用了正确来源、是否自然流畅。没有评测样的调参基本是盲调。4.2 检索环节升级混合检索与重排的必要性纯向量检索并非万能。商标名、型号代码、生僻术语这类内容向量检索偶尔会匹配到语义相近但不是用户想要的块。比如查“SDK-2000型设备故障码E04”如果文档里写的是“设备型号SDK2000”Embedding可能学到它们含义接近但精确标识符匹配就无能为力了。成熟的RAG系统通常采用混合检索向量检索负责语义召回BM25等关键词检索负责精确匹配最后把两者结果融合。LangChain里可以通过ensemble检索器来组合多个检索器from langchain.retrievers import BM25Retriever, EnsembleRetriever bm25_retriever BM25Retriever.from_documents(chunks) ensemble_retriever EnsembleRetriever( retrievers[bm25_retriever, retriever], weights[0.3, 0.7] )weights控制关键词召回和语义召回的比重。对术语密度高的知识库可以提高BM25的权重对口语化查询较多的场景向量检索占主要比例更合适。检索完还有个被很多人低估的环节——重排。第一次召回结果的排序不一定最能满足需求因为向量相似度和真正的“有用程度”并不完全等价。可以接一个重排模型比如bge-reranker这类先归回一个较大的候选集再对每个候选重新打分取前几个最优的。重排能明显提高结果相关性但会增加延迟也需要根据场景权衡。4.3 Prompt里藏着的关键约束不要小看生成Prompt的措辞。同一个检索结果用不同的Prompt组织回答的差别可能非常大。我遇到过一个隐患没有在Prompt里强调“仅依据上下文回答”结果模型经常自己补充知识把文档里没有的推测当成事实输出。在Prompt里建议明确以下几点角色界定告诉模型它是知识库助手而不是一个通用问答模型事实边界只依据给定上下文回答不随意补充未知处理如果答案不在上下文中直接说“未在提供的资料中找到”不要硬编格式要求比如要求编号列出、引用来源标注等temperature参数也要注意事实问答场景建议调低一些比如0到0.3之间。过高的temperature会让回答过于发散文案创作也许需要温度高一点但事实问答不是创作稳定、直接、忠于原文才是目标。4.4 持续性维护知识库的节奏问题索引不是建一次就一劳永逸的。文档更新、增删、版本迭代都会影响回答质量。实际操作中要注意索引更新策略如果知识库可以定期完全重建直接重新跑一遍Embedding是最省事的做法。如果文档频繁增量更新则要考虑增量添加或删除。Chroma本身支持按ID更新和删除文档LangChain里也有对应的接口。还有一个容易踩的坑不同时间加载的文档如果Embedding模型版本不同生成的向量空间可能不一致混在一起检索效果会受影响。所以尽量固定Embedding模型的版本不要频繁更换。5. 常见问题与排查技巧实录5.1 检索结果与问题完全无关先检查哪里最常见的问题是检索回来的文本块和用户问题“八竿子打不着”。遇到这种情况我有一套排查思路第一步先单独测试Embedding效果。直接拿用户问题去查询向量库看看返回的结果本身相关不相关。如果返回的就不相关说明问题出在索引、切分、Embedding模型或文档预处理上。如果返回的相关但最终答案不相关问题大概率出在生成Prompt或回答模型上。排查检索结果时可以直接看返回内容和相似度分数results vectorstore.similarity_search_with_relevance_scores(保修政策是什么?, k5) for doc, score in results: print(f相似度: {score:.4f}, 内容: {doc.page_content[:100]})如果所有相似度分数都低于0.6甚至低于0.4说明要么文档内容本身没有相关信息要么文档切分方式让有效信息被切碎了。这时候先换其它搜索词测试排除文档内容确实没有答案的情况然后调整切分粒度。5.2 回答了但明显在编造信息幻觉问题怎么压制即使有RAG模型依然可能“编答案”。最常见的场景是检索到的资料太模糊或太短模型觉得素材不够就自行发挥了。解决办法从两个方向入手确保检索质量k值适当调大、结合重排让最相关的内容尽可能出现在上下文中强化Prompt边界在系统Prompt里加更严格的表述比如“如果上下文中没有明确信息一句都不能多编。如果信息不完整就说明缺少哪些信息”还有一个真的有效的技巧要求模型在回答时附上引用的文本块编号或来源。一方面让用户能核对出处另一方面也倒逼模型只从给定内容里组织答案。当模型明确知道自己被要求引经据典时编造概率明显下降。5.3 上下文越来越长、调用越来越慢效率和成本怎么平衡多轮对话场景下每次调用都会把检索到的文本块拼进Prompt上下文会逐渐变长耗时和费用同步上升。我的做法是控制k值不过度召回限制chat_history长度比如只保留最近五轮对话而不是无限累积对长文档块做总结压缩把核心信息抽出来作为上下文补充另外一个细节是如果对话历史很长重新表述问题时会把这些历史全部发给模型成本也不低。可以对历史做截断或摘要减少不必要的重复处理。5.4 中英文混合场景下的Embedding适配中文场景下直接使用针对英文优化的Embedding模型效果会打折扣。我当时测试过几个模型即使是同一个问题不同Embedding模型召回的文档块差异也很大。对于以中文为主的文档优先选用中文语料优化过的Embedding模型或者同时兼容中英文的多语模型。判断标准很简单准备一组中文测试问题分别用不同Embedding模型跑一遍检索看召回结果的直观相关度。5.5 切分场景里的特殊格式表格与代码块怎么处理文档中包含表格和代码块时纯文本切分往往会破坏结构。表格被切散后检索到的可能只是表格的某一行模型无从理解列的含义。代码块则可能被从中间切断导致语法残缺。有条件的情况下尽量使用结构化解析工具先还原表格结构或对代码块单独标注类型并作为独立块保存。不要硬把表格转成一大段连续文本再切分效果一般不好。这一步处理的好坏直接影响后续检索命中率值得多费些功夫。6. 后续还能怎么扩展从RAG基础到智能体应用跑通这个项目后你其实已经拥有了一套基础设施文档解析、向量索引、语义检索、大模型生成。这个设施是一切上层应用的地基。在此基础上做扩展方向其实不少。一个方向是引入Agent的能力。目前你的系统是“用户问一句系统答一句”缺少主动调工具、自主规划的能力。接入LangChain的工具调用机制后系统可以做到当用户问“帮我查一下设备状态并生成巡检报告”它自主判断先调用查询接口获取数据再按模板生成报告再通知后续流程。RAG负责提供领域知识Agent负责调度行动两者结合起来才算完整意义上的智能体。另一个方向是接入非结构化知识之外的结构化数据源。比如让系统查询数据库、调用内部API、读取实时监控数据。这时候检索来源从“文档块”扩展成“工具调用结果”回答不仅基于静态知识还结合动态数据效果会再上一个台阶。注意进入Agent搭建阶段后建议先从单工具、少量场景开始不要一上来就堆一堆工具。每个工具都给Agent带来了新的选择空间工具越多行为就越不可控对提示词的要求也会急剧提高。我个人在实际项目中的体会是RAG系统的瓶颈往往不在模型能力而在工程细节文档预处理做得好不好、切分策略是否符合内容结构、检索结果是否足够精准、Prompt约束是否清晰。大模型确实强但能不能用好取决于背后这些环节是否扎实。如果你跟着这个流程把系统跑通了我强烈建议你再做一次实验换一套不同类型的文档比如把产品手册换成学术论文、规章制度或者聊天记录你会明显感受到参数调整的方向完全不一样。这类经验积累起来的直觉才是做RAG应用最值钱的东西。
阅读完成 · 觉得有帮助?
咨询建站