ARTICLE DETAIL

资讯详情

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

从零搭建个人知识库问答机器人:RAG 分块、检索与 Agent 编排实战

从零搭建个人知识库问答机器人:RAG 分块、检索与 Agent 编排实战 1. 为什么我要自己搭一个个人知识库问答机器人我平时的工作状态大概是这样的浏览器开着三四十个标签页微信收藏夹里躺着几百篇稍后阅读Obsidian 里散落着几千条笔记硬盘里还有一堆 PDF 和截图。每次想找某个具体信息比如上次那个 RAG 分块参数是怎么设的我都要在四五个工具之间来回翻翻到最后往往放弃了直接重新搜一遍。这个痛点我想很多人都有。信息不是不够而是存进去容易取出来难。传统的文件夹分类和关键词搜索本质上要求你记住当时是怎么存的但人的记忆是靠语义关联的不是靠目录结构。这就是为什么 RAG检索增强生成这套东西这两年这么火——它把找信息这件事从精确匹配关键词变成了用自然语言描述你的需求。所以我决定动手做一个个人知识库问答机器人核心目标就三个第一把我散落在各处的文档、笔记、网页剪藏统一收进来第二用自然语言提问就能拿到答案并且答案要标注来源方便我回溯第三整个系统跑在我自己的机器上数据不出本地。这篇文章我会把整个实践过程拆开讲包括架构选型、文档处理、分块策略、检索调优、常见坑以及我踩过的那些血泪教训。适合有一定编程基础、想自己动手搭一套 RAG 系统的朋友也适合已经在用 Dify、Trae 这类工具但想搞懂底层原理的人。我不会只给你一个跑通了的 demo而是把每个参数背后的取舍讲清楚让你能根据自己的场景调整。先说结论这套东西不难但魔鬼全在细节里。分块大小差 200 个字符检索效果可能天差地别embedding 模型选错中文检索直接废掉一半。下面慢慢展开。2. 整体架构设计与技术选型思路2.1 先想清楚个人知识库和团队知识库根本不是一回事很多人一上来就照着企业级 RAG 的方案抄结果发现又重又难维护。我一开始也走了这个弯路后来想明白了个人知识库的数据量级、更新频率、查询模式和团队知识库完全不同。团队知识库可能有几十万份文档需要处理权限、并发、增量更新、多租户隔离。而个人知识库我自己的情况是大概 2000 多份文档总量不到 500MB 纯文本每天新增几篇查询基本就是我一个人用偶尔并发也就两三个请求。这个量级下很多企业级的复杂设计完全是过度工程。举个具体例子企业级方案常用向量数据库集群比如 Milvus 分布式部署来扛并发但我这个量级用本地文件式的向量库比如 Chroma 或者 FAISS 的本地索引完全够用查询延迟在几十毫秒级别还省去了运维成本。这就是AI Agent 怎么扛并发这个问题在个人场景下的答案——你根本不需要扛先跑起来再说。所以我的架构原则是能用单机解决的绝不引入分布式能用现成组件的绝不自己造轮子但核心链路的每个环节我都要能看懂、能调参。2.2 我的技术栈组合与选型理由最终我定下来的技术栈是这样的环节选型理由文档解析Unstructured PyMuPDF覆盖 PDF、Markdown、HTML、Word 等格式PyMuPDF 处理 PDF 速度快文本分块自研递归分块 语义分块混合纯固定长度分块会切断语义纯语义分块又太慢EmbeddingBGE-M3本地部署中文效果好支持多语言可本地跑数据不出门向量存储Chroma本地持久化轻量、API 简单、支持元数据过滤检索向量检索 BM25 混合 重排序单一向量检索对专有名词不友好混合检索更稳生成本地 Qwen2.5-7B 或调用 API本地保证隐私API 保证质量可切换编排自己写的 Python 脚本 简单 Agent 循环不引入 LangChain 全家桶避免黑盒这里我要重点说说为什么不用 LangChain。LangChain 确实方便但它的抽象层太厚出了问题你根本不知道是哪一层挂了。我早期用 LangChain 搭过一版检索结果不对排查了半天发现是它默认的 text splitter 把中文按字符切了导致语义断裂。后来我干脆自己写代码量其实不大但每一行我都清楚在干什么。这不是说 LangChain 不好而是个人项目里可控性比开发速度更重要。关于 embedding 模型我试过好几个。OpenAI 的 text-embedding-3 效果确实好但要联网、要花钱、数据要出去。BGE-M3 是我实测下来中文场景最均衡的本地模型它在 MTEB 中文榜单上表现靠前而且支持 8192 的上下文长度对长文档友好。唯一的代价是需要一张显存 8G 以上的显卡或者用 CPU 慢慢跑CPU 跑 2000 份文档大概要几个小时可以接受。2.3 数据流转的完整链路整个系统的数据流是这样的我用文字描述一遍你脑子里应该能形成一张图入库阶段原始文档PDF/MD/HTML→ 解析成纯文本 → 清洗去页眉页脚、去乱码→ 分块 → 每块生成 embedding → 存入向量库同时保留原文和元数据来源文件、页码、时间。查询阶段用户提问 → 问题改写可选把口语化问题转成检索友好的查询→ 向量检索 Top-K BM25 检索 Top-K → 合并去重 → 重排序模型精排 → 取 Top-N 作为上下文 → 拼进 Prompt → 大模型生成答案 → 附上引用来源。这个链路里最容易被低估的是问题改写和重排序这两个环节。很多人搭完 RAG 发现效果不好第一反应是换 embedding 模型但其实问题往往出在检索后的排序上。向量检索召回的东西里真正相关的可能排在第 8 位而大模型只看到了前 5 位自然答不好。加一个重排序模型比如 BGE-Reranker效果提升立竿见影。3. 文档处理与分块RAG 效果的地基3.1 文档解析别小看这一步坑最多文档解析听起来简单实际上是我整个项目里花时间最多的环节。不同格式的文档解析出来的质量天差地别。PDF 是重灾区。同样是 PDF有的是原生文本可以直接提取有的是扫描件需要 OCR有的是双栏排版提取顺序会乱还有的带复杂表格和公式。我一开始用 PyPDF2结果双栏 PDF 提取出来是乱的左右栏文字交错在一起。后来换成 PyMuPDF它对版面分析做得好一些但双栏问题还是存在。最终的方案是先用 PyMuPDF 提取如果检测到文本块位置有明显左右分栏特征就按坐标排序重新拼接。Markdown 和纯文本最好处理直接读就行。HTML 网页剪藏要注意去掉导航栏、广告、评论区这些噪音我用的是 readability 算法Python 的readability-lxml库它能自动识别正文区域。注意解析后的文本一定要做清洗。我遇到过 PDF 提取出来每行末尾都有软换行符导致分块时把一句话切成两半。清洗规则包括合并被软换行切断的句子、去掉连续空行、去掉页码和页眉页脚、统一全角半角标点。这里有个经验不要追求 100% 完美的解析。有些 PDF 就是提取不干净与其死磕不如接受 90% 的质量把精力放在后面的检索调优上。我现在的做法是解析完随机抽 20 份文档人工检查如果明显有问题就针对性处理没问题就过。3.2 分块策略200 字符的差距能决定成败分块是 RAG 里最玄学的环节也是最影响效果的环节。我先说结论没有万能的分块大小但有一个合理的起点然后根据你的文档类型微调。我试过的分块方案对比方案块大小优点缺点固定长度512 字符实现简单切断语义句子不完整固定长度重叠512128 重叠缓解切断问题冗余多检索重复按段落不定语义完整段落长短不一长段落超限递归分块512按分隔符优先级兼顾语义和长度需要调分隔符语义分块按语义相似度切语义最完整慢且不稳定我最终用的是递归分块为主语义分块为辅的混合方案。具体来说优先按 Markdown 标题、段落、句子这些自然边界切如果单个段落超过 800 字符再用语义相似度把它切开。块大小我定在500 字符左右重叠 100 字符。为什么是 500这是我实测出来的。太小比如 200一个完整概念被切碎检索时召回的是碎片大模型拼不出完整答案太大比如 1500一个块里混了好几个主题向量表示被平均了检索精度下降。500 左右是个平衡点既能容纳一个相对完整的知识点又不会太泛。重叠 100 字符是为了防止边界处的信息丢失。比如一个关键句子正好跨在两个块之间没有重叠的话两个块都只包含半句检索时可能都匹配不上。重叠让边界信息在两个块里都出现提高召回率。代价是存储和计算量增加约 20%可以接受。实操心得分块时一定要保留元数据。每个块要记录它来自哪个文件、第几页、在原文中的位置。这样生成答案时才能给出精确引用用户点一下就能跳回原文。我早期没做这个答案给出来了但不知道出处等于白搭。3.3 中文分块的额外注意事项中文分块和英文有个本质区别英文按空格分词中文没有天然分隔符。如果你直接用英文的分块逻辑处理中文很容易在词语中间切断。我的处理方式是分块时优先在标点符号处切。其次在语义边界切。另外中文的一个字符承载的信息量比英文单词大所以同样500 字符中文块包含的语义其实比英文多。如果你的知识库以中文为主块大小可以适当调小到 400 左右。还有一个坑中英文混排。技术文档里经常有使用 RAG 技术进行 retrieval这种句子。分块时如果按字符数硬切可能把英文单词切断。我的做法是先按标点切再检查每个块的首尾是否是完整词语不是的话做微调。4. 检索环节从能搜到到搜得准4.1 为什么单一向量检索不够用我最初只用向量检索效果时好时坏。后来分析发现向量检索对语义相似的问题很擅长但对专有名词、代码、缩写很无力。举个例子我问BGE-M3 的维度是多少向量检索可能召回一堆讲 embedding 模型的段落但就是漏掉那个明确写着BGE-M3 输出 1024 维的块。因为维度这个词在向量空间里和大小参数很接近模型分不清我到底要哪个。解决办法是混合检索向量检索负责语义匹配BM25一种经典的关键词检索算法负责精确匹配。两路各取 Top-20合并去重后再重排序。这样既能抓住语义相关的内容又不会漏掉关键词精确命中的块。BM25 的实现我用的是rank_bm25这个库它对中文需要先分词我用 jieba。这里有个细节BM25 的索引要和向量库同步更新新增文档时两边都要加否则会出现向量库有但 BM25 没有的不一致。4.2 重排序花小钱办大事的关键一步重排序Rerank是我认为性价比最高的优化。它的原理是先用便宜的向量检索快速召回一批候选比如 50 个再用一个更精细的模型Cross-Encoder对这 50 个逐一打分选出真正最相关的 5 个。为什么有效因为向量检索是双塔结构问题和文档分别编码成向量再算相似度速度快但精度有限。而 Cross-Encoder 把问题和文档拼在一起输入模型能捕捉到更细粒度的交互信息精度高但速度慢。所以用粗排精排的组合兼顾速度和精度。我用的是 BGE-Reranker-v2本地部署对中文支持好。实测下来加了重排序之后答案准确率大概从 65% 提升到 85%。这个提升幅度比换任何 embedding 模型都明显。注意重排序会增加延迟。50 个候选过一遍 Cross-Encoder大概增加 200-500ms。对个人使用完全可接受但如果你要做实时对话需要权衡候选数量。4.3 检索参数怎么调一份实测记录我把关键参数的调优过程记录下来了供你参考参数初始值调整后效果变化向量召回 Top-K520召回率提升但噪音增多BM25 召回 Top-K无20专有名词查询明显改善重排序后保留数无5上下文精简答案更聚焦相似度阈值无0.3过滤掉明显不相关的块分块大小1000500检索精度提升分块重叠0100边界信息不再丢失这张表是我调了两周的结果。核心逻辑是召回阶段要宽精排阶段要严。召回少了会漏召回多了靠重排序筛。相似度阈值是个保险防止在知识库完全没有相关内容时硬凑几个不相关的块给大模型导致它胡编。5. 生成环节与 Agent 编排5.1 Prompt 设计让大模型老实回答检索做好了生成环节的 Prompt 设计同样关键。我踩过的最大坑是大模型倾向于编答案。你给它几个不相关的上下文它也能煞有介事地编出一段话。我的 Prompt 核心约束有三条第一只允许基于提供的上下文回答上下文里没有的信息明确说知识库中没有找到相关信息第二必须标注引用来源每个结论后面跟上来源编号第三不确定时要说不知道不要猜测。具体模板大概是这样你是一个知识库问答助手。请严格基于以下上下文回答问题。 规则 1. 只使用上下文中出现的信息不要引入外部知识 2. 如果上下文不足以回答直接说根据现有知识库我无法回答这个问题 3. 回答时用 [1][2] 标注引用的来源编号 4. 保持简洁直接回答问题不要复述问题 上下文 [1] {chunk_1} [2] {chunk_2} ... 问题{question}这个模板看起来简单但只使用上下文这条约束能极大减少幻觉。我实测下来加了这条约束后胡编的情况从大概 20% 降到 5% 以下。5.2 Agent 循环让机器人会追问和多步检索基础的 RAG 是一问一答但真实场景里问题往往需要多步才能解决。比如我问我上次那个项目的分块参数和 embedding 模型分别是什么这需要检索两个不同的信息点。这时候就需要 Agent 编排。我的做法是加一个简单的查询规划步骤先让大模型判断这个问题需要几次检索、每次检索什么。如果是复合问题就拆成子问题分别检索最后汇总。再进一步可以加反思机制生成答案后让模型自己检查这个答案是否真的被上下文支持如果发现某句话没有来源支撑就重新检索或删掉这句话。这个机制能进一步提升可靠性代价是多一次模型调用。关于agent 记忆我的处理比较简单维护一个对话历史但只保留最近 5 轮更早的做摘要压缩。因为个人知识库问答大多是独立问题不需要长期记忆。如果你的场景需要跨会话记忆可以引入一个专门的记忆存储。5.3 本地模型 vs API怎么选这是个绕不开的问题。本地模型比如 Qwen2.5-7B的优势是隐私、免费、离线可用劣势是质量不如顶级 API 模型且需要硬件。我的方案是可切换默认用本地模型遇到复杂问题手动切到 API。代码上就是抽象一个 LLM 接口配置里改个参数就行。这样日常简单问答用本地省钱又隐私偶尔需要高质量输出时用 API。实测下来Qwen2.5-7B 在基于上下文回答这个任务上表现已经不错因为 RAG 的生成任务相对简单不需要模型有很强的推理能力只需要它忠实转述上下文。真正拉开差距的是复杂推理和多跳问题那种场景 API 模型确实更强。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因排查方向答案答非所问检索召回不准检查分块、embedding、是否加重排序答案说找不到但明明有召回失败检查 BM25 是否同步、相似度阈值是否过高答案胡编Prompt 约束不够加强只用上下文约束加引用要求检索结果重复分块重叠过多降低重叠比例或去重中文检索效果差embedding 模型不适配换中文优化的模型如 BGE新增文档搜不到索引未更新检查向量库和 BM25 是否都更新了响应太慢重排序候选太多减少候选数或用更小的重排序模型6.2 几个我踩过的深坑坑一embedding 模型和检索模型不匹配。我一开始用某个模型做 embedding又用另一个模型做重排序结果两者对相似的定义不一致重排序反而把好结果排下去了。教训是embedding 和 reranker 最好来自同一系列比如都用 BGE 系列。坑二忽略了文档的时间维度。我的知识库里有新旧两版文档讲的是同一个东西但参数不同。检索时两版都被召回大模型不知道该信哪个。后来我在元数据里加了时间戳检索时优先返回新版本或者在 Prompt 里明确标注每块的日期。坑三图片和表格信息丢失。纯文本 RAG 处理不了图片里的信息。我有些笔记是截图里面的文字提取不出来。目前的处理是对图片做 OCR 提取文字表格则转成 Markdown 格式再入库。这也是rag 知识库能存储图片嘛这个问题的现实答案——能存但要额外处理且效果有限。坑四过度依赖单一检索。有段时间我发现某些查询总是失败后来发现是这些查询里的关键词在 BM25 里权重太高把向量检索的好结果挤掉了。解决办法是调整两路检索的融合权重或者用 RRF倒数排名融合这种更鲁棒的合并算法。6.3 关于知识库类型的补充热词里提到kg 知识库、rag 知识库和结构知识库区分。简单说RAG 知识库是文本块向量适合非结构化文档结构化知识库是表格字段适合精确查询知识图谱KG是实体关系适合推理和关联查询。个人知识库大多用 RAG 就够了如果你的笔记里有大量人物-事件-时间这类关系可以考虑叠加一个轻量知识图谱。7. 我个人的一些实践体会这套系统我断断续续搭了一个多月现在日常在用。最大的感受是RAG 的效果80% 取决于数据质量和检索环节只有 20% 取决于生成模型。很多人把精力花在换更大的模型上其实方向错了。把文档解析干净、分块合理、检索调准用 7B 的小模型也能给出很好的答案。另一个体会是不要追求一步到位。我一开始想做个全能系统支持所有格式、所有查询类型结果卡在文档解析上两周没进展。后来改成先支持 Markdown 和纯文本跑通链路再逐步加格式进度一下就快了。个人项目最怕的就是完美主义先让它能用再让它好用。最后分享一个我最近在试的扩展方向把知识库问答和日常笔记流程打通。比如我在 Obsidian 里写完一篇笔记自动触发入库提问时可以直接在编辑器里唤起问答。这样知识库就不是一个额外要维护的东西而是融入日常工作流的一部分。工具只有融入习惯才不会被闲置。如果你也在搭类似的东西我的建议是从最小可用版本开始一个文件夹的 Markdown一个本地 embedding 模型一个 Chroma一个简单的检索生成脚本。跑通了再往上加东西。别一上来就上分布式向量库和复杂 Agent 框架那些是规模上来之后才需要考虑的事。
返回列表