ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手搓RAG本地知识库问答系统:分块、向量检索与LangChain实战

手搓RAG本地知识库问答系统:分块、向量检索与LangChain实战 不知不觉就到了学习日记第54天。前阵子一直有人私信问我说最近AI应用开发这么火RAG到底是什么、难不难学我的回答一直是一个字别急先把底子打好。到了今天我总算敢拍着胸脯说一句——RAG这趟水我已经蹚到膝盖了。今天这篇学习日记不聊虚的只聊一个核心主题从零手搓一个本地知识库问答系统把RAG检索增强生成的关键链路彻底跑通。如果你也在学RAG、被分块策略和向量检索折磨过或者正准备把LangChain和向量数据库引入自己的项目那这篇应该能帮你少走不少弯路。1. 前53天的铺垫到底值不值为什么我不建议你上来就啃RAG1.1 我的学习路线先广后深先窄后宽很多新手容易犯一个错误——一听说RAG能做知识库问答立刻打开LangChain文档复制一段代码跑通一个例子然后就觉得自己会了。结果遇到一丁点实际问题就抓瞎文档格式稍微复杂一点就切不进去检索出来的东西驴唇不对马嘴生成答案翻来覆去就那几句车轱辘话。我之所以敢在今天动手做RAG是因为前面这53天我把地基打得还算扎实。简单捋一下我的学习路径Python基础与工程化能力前20天把Python语法、装饰器、生成器、类型注解这些过了一遍顺带理解了FastAPI的基本写法因为RAG项目最终要落到一个可交互的服务上不是跑个脚本就完事。大模型API调用与提示词工程第21到30天集中研究OpenAI兼容接口的调用方式搞清楚了system prompt、temperature、max_tokens这些参数的实际作用。向量化与Embedding入门第31天到40天开始研究文本向量化搞明白Embedding模型到底是怎么把一段话变成一串浮点数的也理解了余弦相似度的几何意义。数据库与存储基础第41天到45天补了关系型数据库的知识顺手对比了各类向量数据库的定位差异。LangChain与Agent概念扫盲第46天到53天过了一遍LangChain的核心组件把Chain、Memory、Tool这些概念搞清楚。说实话如果让我重新来一次我会把第31到40天这段向量化的学习节奏再放慢一点。当时我只是理解了余弦相似度的计算公式却没真正理解向量空间里的距离到底代表什么语义导致后面调检索参数时走了不少弯路。这个坑后面细说。1.2 今天的任务目标不只跑通还要明白今天的学习任务很明确搭一个本地知识库问答系统能把一份PDF文档切碎、向量化、存进向量数据库然后用户问问题系统从库里捞出一段相关内容交给大模型组织成自然语言答案。但我的目标不只是让代码跑起来。我给自己定了三条验收标准能说清楚每个环节在解决什么问题如果去掉这个环节会发生什么。能根据文档类型和问题类型主动调整分块大小和检索Top-K。能对检索结果质量差的问题做系统化排查而不是靠瞎猜参数碰运气。这三条标准本质上对应了RAG的三个核心痛点知识切分、相关性检索、上下文融合生成。一条一条来。2. RAG的三段式工作流从文档到答案中间发生了什么2.1 没有RAG的时候大模型有多尴尬先聊一个反常识的现象很多大模型本身的知识截止日期是固定的你问它今年新出的产品发布会内容它只能靠瞎编来圆场。这不是模型笨而是它压根没见过这些信息。大模型说白了就是一个极度擅长接着往下写的文本生成机器它的所有知识都浓缩在训练时的参数里。一旦你问它训练数据之外的东西它就只有两个选择一本正经地胡编或者老老实实说不知道。而RAG的思路很朴素你问的东西我不知道那我现查资料总行吧——查完资料把相关片段塞进提示词里让它基于这些片段来回答。这就是RAG这个名字的来历检索Retrieval 生成Generation中间用增强Augmented的方式连接——把检索到的外部知识作为额外上下文增强大模型的回答能力。它解决的核心问题有三个知识时效性不用重新训练模型就能让它知道最新的信息。私有知识接入公司内部文档、个人笔记这些不可能出现在公开训练集里的内容可以通过RAG让模型读到。幻觉抑制把答案限定在检索到的证据里模型就不能凭空编造了。2.2 索引阶段Indexing把文档变成机器能搜的卡片抽屉要让机器能搜文档第一步是把文档转化成一种方便搜索的形态。这个过程叫索引我习惯把它拆成四步文档加载用Loader把PDF、Word、Markdown等格式的原始文件读出来变成纯文本。这一步看起来简单实际上坑最多——PDF里的表格、多栏排版、扫描图片都会让Loader输出的文本质量急剧下降。文本分块把长文本拆成一个个小块。为什么要拆因为大模型的上下文窗口有限而且你让模型读一万字的文档它也记不住里面的细节检索系统更是在长文本里找相关内容时效率极低、精度极差。把文本切成几百字的块每一块就是一个最小检索单元。向量化用Embedding模型把每一小块文本转成一个高维向量。这个向量就是文本的语义指纹。入库存储把向量连同原文一起存进向量数据库建立索引。2.3 检索阶段Retrieval拿问题去库里面捞相关内容当用户输入一个问题时系统先用同一个Embedding模型把问题转成向量然后到向量数据库里去搜索最相似的向量。相似度用什么衡量最常用的就是余弦相似度——两个向量夹角的余弦值越接近1说明方向越一致语义越接近。系统会返回Top-K个最相似的文本块这个K就是我们常说的召回数量。K太小可能漏掉关键信息K太大又可能塞进去一堆噪音。我今天实测下来在私有知识库场景下K值设为3到5是比较合理的区间具体分析放在后面。2.4 生成阶段Generation把检索结果喂给大模型最后一步把所有检索到的文本块拼装成一份上下文放进Prompt模板里再加上用户原始问题一起发给大模型。模型的任务不是自由发挥而是根据上面提供的资料结合资料内容回答用户问题。这里有一个很多人忽略的细节检索得来的文本块可能相互矛盾也可能包含噪音。所以Prompt里除了告诉模型只依据资料回答通常还要加上一句如果资料中没有相关信息直接说明不知道不要编造。这一句能拦下相当数量的幻觉。3. 动手搭建时的四个关键选型为什么我这么选、不这么选3.1 框架选择LangChain是首选但不是唯一答案RAG的核心流程其实不复杂自己写也就几百行代码。但为什么不从头造轮子因为工程化的RAG项目马上会遇到一堆辅助问题怎么处理不同格式的文档、怎么做文本分割策略切换、怎么优雅地把检索结果格式化、怎么处理多种模型的兼容调用。这些LangChain都已经封装好了直接拿来用能节省大量时间。那要不要用LlamaIndex我的观点是如果你主要做的是知识库问答这一类检索密集型应用LlamaIndex的抽象更贴切文档解析和数据索引的工具也更丰富。但如果你还想做Agent、工具调用、复杂链式流程LangChain的生态更完整。我今天的项目需要后续扩展Agent能力所以选了LangChain。另外提一句现在还有一种思路是彻底不依赖框架直接用Embedding模型向量数据库原生API手写RAG管道甚至直接用pandas做暴力检索。这种方式适合追求极致性能和精细控制的场景但对工程效率和可维护性不友好不建议初学者一上来就走这条路。3.2 向量数据库选型Chroma、FAISS和Milvus的三选一向量数据库是整个检索环节的存储底座选型直接决定了后面的开发体验。我这次做了个简单对比数据库定位优点缺点适用场景Chroma轻量级嵌入式部署简单API友好和LangChain集成度高数据量大时性能一般个人项目、原型验证FAISS向量检索算法库性能极强支持多种索引方式不是真正意义上的数据库没有服务化能力单机大规模检索Milvus分布式向量数据库支持海量数据、高并发、丰富的索引类型运维成本较高需要专门部署生产环境、企业级应用我的选择是Chroma原因很简单今天做的是本地知识库数据量大概是几百个文本块不需要分布式能力Chroma的默认持久化方案是把数据存到本地磁盘目录用起来就像操作一个普通数据库一样轻量。如果后续数据量真的涨到百万级到时候再迁移到Milvus也不迟——因为LangChain把向量存储抽象成了统一的接口切换底层实现只需要替换一行代码。3.3 Embedding模型选型中文知识库为什么更推荐BGE系列Embedding模型是整个RAG系统里最关键的组件没有之一。它的质量直接决定了检索效果的上限后面的重排、压缩都只是在下限上做修补。我用过的几个典型模型有text-embedding-ada-002OpenAI出品的经典模型英文能力强但中文效果一般而且维度是1536存储和计算成本稍高。m3e-base早期国产开源模型里的口碑之选中文效果不错但更新已经放缓。bge-large-zh-v1.5智源推出的中文向量模型在中文语义匹配任务上的表现长期领先支持的最大序列长度是512个token。为了做中文知识库我最终选了bge-large-zh-v1.5。理由有三点一是中文长文本的语义编码能力强对比实测中同义句的向量相似度更高二是开源模型可以完全本地部署文档向量化不依赖外部API安全性和成本都可控三是对中文歧义词、口语化表达的鲁棒性更好这一点在后面的翻车排查里有直接体现。还有一个小细节bge系列的官方建议是在向量化之前给文本加上一个为了搜索相关文本的前缀指令。LangChain里有专门的配置方式我测试过加和不加的差别在中文场景下相关性平均提高了2到3个百分点值得做。3.4 大模型接入本地模型还是云端API生成环节的大模型我在本地跑了一个Qwen系列的量化模型同时保留了OpenAI兼容API的切换能力。LangChain里通过更换BaseURL和模型名就可以无缝切换。这里有个容易被忽略的点生成模型的能力要跟你的检索质量匹配。如果你的检索结果本身不够精准但生成模型特别能说会道它就会把模糊的上下文编成一本正经的答案表面上看起来很好实际上全是幻觉。反倒是能力一般的模型因为组织语言的能力有限会更多依赖资料原文来回答准确率更高。4. 分块策略和检索参数一个下午调参我列了一张效果对照表4.1 分块大小Chunk Size200还是500没有标准答案分块是RAG里最像玄学的参数但背后其实有规律可循。我这次选了一份约15页的产品用户手册做测试文档文本结构是分段式的说明文字夹杂一些表格。我测试了三组配置参数分块200分块400分块800文本块数量约370块约190块约100块单块信息完整度容易截断句子较完整保留一个主题段落可能混入多个主题检索精准度命中更细碎易漏上下文最均衡容易召回无关部分生成效果答案太干瘪能引用到完整背景上下文混乱答非所问最终我选了400这个档位重叠大小设为50个字符。核心逻辑是分块大小应该跟文档的逻辑结构匹配。产品手册的一个段落通常就是300到500字切成400字附近能比较完整地保住一个段落的语义。关于重叠Overlap参数它解决的是切块时把上一句回答问题的关键信息切丢了的问题。50个字符的重叠意味着每个块和下一个块之间有一小段重复的边界内容这样即使答案的关键句落在两块的接缝处也至少能被一个块完整包含住。4.2 Top-K和相似度阈值召回数量和精度的杠杆Top-K决定了一次检索捞回多少个文本块。我做了个简单测试同一个问题把K从1调到8观察输出质量。K1经常漏掉上下文答案非常单薄甚至相关性最高的块只包含了问题答案的一半。K3效果最好既保证了关键信息覆盖又没有引入太多噪音。K5答案明显变啰嗦会出现把较早版本的参数和最新参数混在一起回答的情况。K8上下文冲突明显模型不知所措输出质量直线下降。另一个重要参数是相似度阈值一般建议设置为0.3到0.5之间。低于阈值的检索结果直接丢弃宁可没搜到也不要把瞎搜的硬塞给模型。阈值设太高会漏答设太低会什么怪东西都召回来。我的经验是先设0.3跑一遍看那些明显不相关的结果分数是多少再往上调。4.3 一个容易踩的坑相似度分数高不代表内容对得上这是今天踩的最深的一个坑。我在测试打印预览在哪里设置这个问题时系统召回的文本块分数很高但答案完全不对——它返回的是页面布局相关的内容。后来我才发现bge模型在编码打印预览和页面布局这两个词时因为它们在大量的文档上下文里经常共同出现向量方向非常接近所以余弦相似度被拉高了。问题本身有认知偏差导致检索环节被表面相关性骗了。这个问题警告我向量相似度只是语义接近程度的近似不是精确匹配。要缓解得靠后续的**重排Rerank**环节用专门的排序模型对召回结果再做一次精排。这个我打算明天专门补上。5. 跑通之后的连环翻车我的四条排错路径按顺序走不会慌5.1 PDF加载后表格乱码先检查的是解析器不是代码程序第一次跑通的时候我兴奋地拿了一份带表格的PDF文档去测。问这款产品的电池容量是多少系统给我回了一大段乱码然后模型还在乱码里努力地找答案最后告诉我未找到相关信息。排查链路是这样的先确认不是分块或者检索的问题——单独把加载后的文本打印出来看发现表格行被解析成了乱七八糟的换行符数字和单位被拆得四分五裂。确认是文档加载环节的问题。LangChain的PDF加载器有好几个PyPDFLoader、PDFMinerLoader、PyMuPDFLoader默认的PyPDFLoader遇到复杂布局容易把表格拆散。换成PyMuPDFLoader表格结构保留度大幅提升。但这还不算完最好用UnstructuredLoader它支持按表格结构做后处理。这个问题的教训是调试RAG要从源头开始查。文档加载就出了问题后面再怎么调向量化、调大模型都是白费劲。5.2 检索命中但答案错误把检索到的块拿出来先看一眼第二次翻车更典型。我抛出一个明确的问题设备怎么恢复出厂设置检索到的文本块CM含量很高一看就是相关段落但模型的回答还是驴唇不对马嘴。我排查了一下抓取这个文本块看一下它的开头和结尾。果然文本块开头是一句恢复出厂设置将清除所有本地数据然后紧接着讲备份与恢复整个块跨越了两个主题。问题出在分块边界不理想——块把恢复出厂设置的操作步骤切出去了只把一句警告提示留在了块里。解决办法把分块大小从400降到300观察边界变化同时对文档里的标题行做特殊处理让标题和正文尽量落在同一个块里。5.3 答案重复或引用混乱先在Prompt和上下文压缩上下功夫有个问题是模型回答时喜欢把检索到的所有内容都点评一遍导致答案冗长而且重点不突出。尤其是在K5时两个块的语义有重叠模型就复读了一遍。排查后发现这不是检索的问题而是Prompt的设计不够严格。我后来在Prompt里明确了回答的约束基于资料内容先直接给出答案再进行简洁解释不要逐条罗列所有参考资料资料中的冗余信息不要体现在答案中。效果立竿见影。如果需要更精细的控制可以引入**上下文压缩Contextual Compression**机制检索到文本块后先用一个轻量模型过滤和压缩只保留和问题相关的句子再喂给大模型。这个机制在LangChain里已经内置了。5.4 系统评估不能靠感觉我搭了一个极简验证集调参调了一下午最怕的就是感觉差不多了实际上换个问题就崩。我做了个笨但有效的办法从文档里抽出20个问题-答案来源对覆盖了产品功能、参数设置、故障排查三类问题每次调整参数后都跑一遍这20个问题记录命中率和答案完整度。这个方法看起来土但非常值。它让我能量化地看到分块调到300时故障类问题的命中率从60%上涨到80%但功能类问题的命中率下降了10%因为故障描述往往在单个段落内比较集中而产品功能介绍经常跨段落展开——不同的问题类型对分块大小的偏好真的不一样。RAG的调参最终调的是这个平衡关系。6. 这54天学下来我对RAG最深的几个体会说来也巧今天跑通项目的时候外面正好在下雨。我看着终端里那串流畅的回答突然有点感慨三个月前我还是个对着大模型输出幻觉干瞪眼的人现在至少知道往哪个方向去查、去调、去解决了。我最深的三个体会第一向量数据库不会帮你理解文档。它只能帮你在海量文本里快速定位可能相关的片段真正的理解仍然发生在生成模型那里。所以别指望换个更好的数据库就能让回答变聪明关键还是Embedding模型和分块策略。第二RAG项目的调试顺序比努力更重要。永远先看文档加载是否有问题再看分块是否破坏了语义再看检索分数是否合理最后才去调Prompt。颠倒顺序会让你在错误的方向上浪费时间——我就是那个先调了半小时Prompt才发现问题出在PDF解析器上的人。第三评估数据要提前建。你没法优化一个无法量化的事情。哪怕只是20个问题的小集子也足够让你看清每次调整的收益和代价。接下来几天我打算继续补两个方向一是加上重排Rerank环节把检索质量的最后一块短板补齐二是用今天搭好的验证集把分块策略从固定大小换成递归结构感知看看对那段跨段落手册语料友好不友好。最后分享一个小技巧给打算入坑RAG的朋友不要一上来就拿分布式数据库和大模型API硬怼先从一份你自己特别熟悉的文档开始比如自己的笔记、产品的FAQ一步步走通全流程。你越熟悉那份文档的内容就越容易判断系统给出的答案是真是假也就越容易发现瓶颈到底在哪。这一步没有人能替你走但它也是最值得走的一步。
返回列表