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 速度快 |
| 文本分块 | 自研递归分块 + 语义分块混合 | 纯固定长度分块会切断语义,纯语义分块又太慢 |
| Embedding | BGE-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 字符 | 实现简单 | 切断语义,句子不完整 |
| 固定长度+重叠 | 512+128 重叠 | 缓解切断问题 | 冗余多,检索重复 |
| 按段落 | 不定 | 语义完整 | 段落长短不一,长段落超限 |
| 递归分块 | 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-K | 5 | 20 | 召回率提升,但噪音增多 |
| BM25 召回 Top-K | 无 | 20 | 专有名词查询明显改善 |
| 重排序后保留数 | 无 | 5 | 上下文精简,答案更聚焦 |
| 相似度阈值 | 无 | 0.3 | 过滤掉明显不相关的块 |
| 分块大小 | 1000 | 500 | 检索精度提升 |
| 分块重叠 | 0 | 100 | 边界信息不再丢失 |
这张表是我调了两周的结果。核心逻辑是:召回阶段要"宽",精排阶段要"严"。召回少了会漏,召回多了靠重排序筛。相似度阈值是个保险,防止在知识库完全没有相关内容时,硬凑几个不相关的块给大模型,导致它胡编。
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 框架,那些是规模上来之后才需要考虑的事。