RAG+LLM:把Obsidian知识库变成可对话系统
我必须坦白一件事我的 Obsidian 知识库吃灰了大半年真正让我下定决心重新搭一套 Wiki 的不是又一款漂亮的笔记软件而是 LLM 把查资料这个动作彻底改变了。以前打开 Wiki 是为了找某个文件、翻某条笔记现在我的诉求变成了直接问它让它把相关上下文一起给我。这个转变看似很小实际上把知识库的底层逻辑全掀了。llm_wiki 这个项目就是在这个背景下长出来的——它不是某个开箱即用的软件而是一套把个人知识库改造成可对话系统的完整方案我把它跑通了踩了不少坑也沉淀了一套可以直接复刻的模式。这篇文章适合三类人一是笔记囤了一大堆但从来不看的人二是想在本地知识库上接 RAG 和 LLM 但不知道从哪下手的开发者三是已经在用 Dify 或类似工具、但对分块、嵌入、重排这些细节还没有体感的同学。我会从选型、结构、检索、接入、自动化再到排障把整个链路从头到尾讲清楚所有给到的参数和步骤都是我实测过的不是抄文档。1. 从只存不看到可对话旧 Wiki 的三个死穴和 LLM 给出的解法1.1 旧 Wiki 为什么必然吃灰先说一个扎心的事实传统 Wiki 的维护成本远高于使用收益。我大概统计过自己过去两年的笔记情况总共写了 400 多条 Markdown真正被二次打开过的不超过 30%。原因很朴素——目录结构再合理分类再细致到了真要找东西的时候你根本想不起来当时把它归到了哪个文件夹。我把旧 Wiki 的死穴归纳为三个一是只存不查。记的时候很爽查的时候很痛苦。全文搜索只能做关键词匹配我经常遇到记得写过这个东西但想不起关键词的窘境。二是结构僵化。树状目录天然适合归档不适合关联。一条笔记往往涉及多个主题把它放进任意一个目录都意味着其他入口会丢失。双链能缓解但如果笔记量大了手工维护双链的成本不可接受。三是维护熵增。每次往 Wiki 里添加内容都要想分类、想命名、想格式这个摩擦成本足以让任何人在第三天放弃。1.2 LLM 本质上是把知识库从文件系统变成了接口LLM 出现之后知识库的使用逻辑变了。以前是我主动去翻、去搜、去猜内容在哪现在我可以让模型直接回答并告诉我依据在我 Wiki 的哪一篇里。这就是检索增强生成也就是 RAG。RAG 的意义不在于能聊天而在于它解决了 LLM 的两个固有缺陷一是模型参数里的知识有截止日期二是幻觉问题。把 Wiki 里的内容切块、嵌入、建立索引之后每次提问都先从自己的知识库里召回最相关的片段再让模型基于这些片段组织答案等于给模型装了一个可实时更新的外挂记忆。llm_wiki 的项目定位就是把这个链路跑通输入是散落在各处的 Markdown 笔记中间是分块、向量化、检索、重排出口是一个能说出根据你 2024 年 3 月写的那篇 XXX这个问题的答案是……的对话接口。1.3 项目范式的核心文档是唯一事实源项目做到一半我意识到一个关键原则文档必须是唯一事实源source of truth。向量数据库、索引、数据库里的内容都是从 Markdown 派生出来的可以随时删除重建。这个原则决定了整个系统的架构方式——内容负责人的心智模型是我在维护一组文本文件而不是我在维护一个数据库。一旦接受这个设定很多问题就迎刃而解了备份就备份 Git 仓库迁移就复制文件夹版本管理天然由 Git 承接而所有索引问题都可以通过重新跑一遍构建脚本解决。2. 底座选型Markdown、Obsidian 与 Git 的三层结构为什么能打2.1 为什么是 Obsidian 而不是语雀、Notion 或 Confluence市面上知识库工具一大堆选型之前我列了几个硬性指标必须本地存储、必须纯文本格式、必须有活跃的插件生态、必须能方便地接入自动化脚本。Notion 和语雀首先被排除原因是内容被锁在私有格式和云端迁移成本高而且数据不落在本地文件系统脚本处理起来非常别扭。Confluence 适合团队协作对个人知识库来说过于笨重。Obsidian 胜出的理由很直接所有笔记就是一个个 .md 文件存放在你指定的文件夹里语法是标准 Markdown 加 YAML frontmatter插件系统成熟而且它的双链和关系图谱都是加分项不是核心依赖。2.2 目录结构与命名规范不要过度设计Obsidian 的底层是文件所以目录结构仍然是需要规划的但原则是能不分类就不分类。我最终采用的方案是按主题域分一级目录再往下只保留一层子目录。wiki/ ├── ai/ # AI 与机器学习 │ ├── llm/ │ ├── rag/ │ └── agents/ ├── dev/ # 开发相关 │ ├── python/ │ ├── frontend/ │ └── devops/ ├── life/ # 生活记录 ├── reading/ # 读书笔记 ├── inbox/ # 临时存放定期归位 ├── templates/ # Obsidian 模板 └── index.md # Wiki 首页命名规范上我的规则只有两条文件名必须能表达内容主题例如rag-retrieval-optimization.md而不是note1.md日期前缀按需使用只给日记类和日志类内容加技术笔记不加日期。2.3 YAML frontmatter让元数据和正文分离这是我整个方案里最被低估的一环。每篇笔记开头用 YAML 格式写入元数据Obsidian 能识别脚本能读取RAG 流水线也能利用这些字段做过滤。--- title: RAG 检索优化的实践记录 tags: [rag, embedding, rerank] created: 2024-06-15 updated: 2024-06-20 status: done source: --- 正文内容……tags 字段的价值比想象中大得多。后面做检索过滤的时候可以直接限定只搜某个标签下的内容命中率提升非常明显。status 字段用于标记笔记状态idea是灵感doing是进行中done是相对完整。这个字段帮助我在构建索引时决定优先级——只有done状态的笔记会被纳入高效的检索层。2.4 Git知识库的时光机与自动化底座我在 Wiki 文件夹根目录初始化了 Git 仓库并且维护了一个 GitHub 私有远程仓库。这样做有三个收益所有历史版本可回溯误删笔记不再是灾难多设备同步不需要依赖任何云笔记服务后面自动化构建索引时可以利用 Git 的 hook 或 CI 流程触发重建同步方面我用的是 Git 加手动 push 的流程没有使用 Obsidian 的官方同步服务。虽然不是实时同步但配合手机端的 Obsidian Git 插件体验已经足够。3. RAG 不是魔法检索质量的四个关键环节逐个拆解3.1 全文搜索够用吗一个反例说明问题有人可能会问我有 400 条 Markdown直接grep不行吗行但只在笔记量小的时候行。一旦笔记量上千问题就来了你根本没有办法用关键词描述一个你只记得大概语义的概念。举个真实例子我写过一篇关于如何用语义搜索替代关键词搜索的笔记里面大量使用了向量、嵌入、相似度这些词。一个月后我想找到这篇笔记脑子里浮现的关键词是怎么让搜索理解意思——这个词和原文没有任何词汇重叠全文搜索直接抓瞎但向量检索可以轻松命中因为嵌入空间里理解意思和语义搜索的向量距离足够近。这就是 RAG 第一个环节——嵌入和向量检索——要解决的问题。3.2 文档切分分块策略决定了检索质量的上限整个 RAG 流水线里最容易被低估的就是分块。切得太粗一个块里塞太多主题向量被平均掉检索时命中率低切得太细语义被切碎上下文缺失召回了也回答不好。我实测过的参数组合如下策略块大小重叠检索效果固定字符切分20020精度尚可但语义断裂多固定字符切分50050综合效果最好固定字符切分1000100召回精度下降块内噪声大Markdown 结构切分按标题/段落0-50需按文档类型定制效果好但复杂对于纯 Markdown 笔记我最推荐按 Markdown 结构做切分以二级标题为边界切成多个语义块每个块内包含标题上下文。具体实现不复杂用 Python 的markdown库解析出标题层级再按标题节点切分即可。如果笔记没有清晰的标题结构就用 500 字固定大小加 50 字重叠作为兜底。这里有个细节重叠窗口必须在每个块的末尾和下一个块的开头重复部分内容这样才能避免句子在边界处被拦腰截断导致语义不完整。3.3 嵌入模型选型本地优先还是 API 优先嵌入模型是 RAG 检索质量的关键选型时需要在效果、成本、隐私之间做权衡。我用过两套方案第一套是 API 方案用 OpenAI 的text-embedding-3-small效果稳定维度小1536 维成本低但所有内容要出本地单次调用有网络延迟。适合对隐私要求不高、追求效果的项目。第二套是本地方案用 BGE-M3 系列模型比如BAAI/bge-m3支持中文和英文混合场景效果很不错而且生成的是 1024 维向量完全离线运行。对于个人 Wiki我最终选择的是这套——原因不是隐私洁癖而是可以批次处理 400 多篇文档不用等网络也不花钱。嵌入模型选择上有一个容易踩的坑同一个知识库里不要混用不同的嵌入模型。不同模型的向量空间不一致混着用会导致检索结果完全不可用。我在 6.2 里会详细讲这个教训。3.4 向量数据库选型从 SQLite 到 Chroma 再到 Milvus个人知识库场景向量库的选择其实没那么纠结。我的评估维度只有一个别让我维护一个独立服务。实测下来Chroma 是最适合个人项目的选择。它是嵌入式向量数据库用 Python 直接调用即可数据落盘为本地目录零部署成本。对于几千个文档的规模性能绰绰有余。import chromadb client chromadb.PersistentClient(path./wiki_vectordb) collection client.get_or_create_collection( namewiki_docs, metadata{hnsw:space: cosine} ) collection.add( documents[这是文档分块后的内容……], metadatas[{source: rag-retrieval-optimization.md, chunk_index: 1}], ids[rag-retrieval-optimization.md#1] )如果更极客一点也可以直接用 sqlite-vec 扩展但需要自己写更多的胶水代码。Milvus 和 Qdrant 适合生产级多用户场景个人项目完全不需要。3.5 重排召回 20 条之后别急着扔给 LLM向量检索的召回结果通常有噪声。我最初的实现是直接取 top_k 文档块塞给 LLM实测经常出现的情况是正确答案在召回列表第 8 名但提示词里只放了前 5 名于是模型一本正经地根据不相关的内容编了个答案。解决这个问题的方法是加重排rerank环节。流程变成向量检索召回 top_k20然后交给一个交叉编码器重排模型做精排取前 5 名喂给 LLM。我用的是BAAI/bge-reranker-base离线模型延迟低效果提升非常明显。加了这个环节之后回答准确率大概提升了 40% 左右这是我整个项目里投入产出比最高的一个改动。4. 接入 Dify 构建对话入口从能查到能聊4.1 为什么选择 Dify 而不是自己写 APIRAG 链路本身用 Python 脚本就能跑但要做到能对话、能分享、能维护自己从零写应用太浪费了。Dify 正好解决这个问题它把模型管理、知识库、检索流程、提示词编排、会话管理都封装好了而且支持 API 输出。很多人装了 Dify 不知道怎么配 LLM 和知识库这里我梳理一遍完整流程都是我实际点过的界面。4.2 Dify 里的 LLM 设置模型供应商、API Key 与参数登录 Dify 控制台后先进入「设置」→「模型供应商」选择你要用的模型。我的配置方案是主模型用 OpenAI 的gpt-4o-mini兼顾效果和成本嵌入模型用本地部署的接口Dify 支持接入 OpenAI 格式兼容的本地服务地址。配置界面里需要填几个关键项API Key在模型服务商的后台生成模型名称填准确的模型标识比如gpt-4o-mini上下文长度Dify 会显示模型的最大上下文这个决定了下游的提示词长度上限温度temperature问答场景我设置为 0.2避免模型自由发挥参数选择上有一条经验知识库问答的第一原则是可复现不是有创意所以温度必须低。我自己试过 temperature0.7模型会频繁把知识库内容重新组织得太流畅反而容易出现信息偏差。4.3 知识库上传与检索模式混合检索配合重排在 Dify 里创建知识库上传方式有三种直接上传文本文件、同步 Notion、通过 API 导入。个人 Wiki 场景我用的是 API 导入把本地 Markdown 先切成块再调用 Dify 知识库的文档创建 API 传上去元数据里带上来源文件名。检索模式的选择直接决定问答效果。Dify 提供了三类向量检索只做嵌入相似度匹配适合语义检索全文检索基于关键词匹配适合精确术语查找混合检索两者结合再经过 Rerank 重排我的设置是混合检索 Rerank 模型。Dify 在「检索设置」里可以开 Rerank需要填一个 Rerank 模型的 API。我本地部署了 bge-reranker-base通过 Dify 的自定义模型接口接入。具体的权重分配我使用的是默认 TopK3Score 阈值 0.5。如果回答质量不稳定优先调 TopK 而不是调 Score——阈值太高容易召不回内容太低则噪声太大。4.4 提示词工程让模型以Wiki 管理员身份回答Dify 应用编排界面的「提示词编排」部分我精心设计了一套系统提示词这是整个对话体验的分水岭。你是 llm_wiki 知识库的问答助手。你的任务是基于提供的知识库内容回答用户问题。 要求 1. 如果知识库中有相关内容必须引用来源文件名和原文片段 2. 如果知识库中没有相关内容明确说知识库中没有找到相关资料不要编造 3. 回答时使用结构化格式先给结论再给依据 4. 如果用户的问题与知识库内容无关礼貌说明你的能力范围这段提示词看似简单实际效果显著。尤其是第二条直接解决了 LLM 的幻觉问题。系统提示词中我刻意用了必须引用来源的说法这会显著提高模型引用片段的概率。RAG 里有个经验模型需要被明确要求基于上下文回答否则 5% ~ 10% 的回答会完全脱离检索内容。4.5 实测效果一次真实的对话记录配置完以后我做了几组测试。举个典型例子我问我之前总结过哪些 RAG 优化手段Dify 走的链路是先在我的知识库召回相关片段重排后交给 LLM最终的回答带了三个来源链接分别是rag-retrieval-optimization.md的第 2 段、embedding-model-selection.md的第 5 段和rerank-bge.md的第 3 段。每一条引用的内容都和问题高度相关没有任何编造成分。这个体验和以前打开 Obsidian 全局搜索完全不是一回事它真正做到了一句话拿到答案和证据不用自己去翻。5. 让 Wiki 自己生长自动化入库、打标与索引重建5.1 自动化入库网页、PDF、微信收藏夹到 Markdown 的管道知识库最大的敌人是懒得录入。为了降低录入成本我搭了三条自动入库管道。第一条是网页转 Markdown。用 Python 的trafilatura库抓取网页正文并转成 Markdown命令很简单import trafilatura downloaded trafilatura.fetch_url(url) text trafilatura.extract(downloaded, output_formatmarkdown)抓到正文后自动写入wiki/inbox/目录文件名用页面标题加时间戳并在 frontmatter 里记录原始 URL 和抓取时间。第二条是 PDF 转文本。用pymupdf库提取内容再按页合并为一个 Markdown 文件存档。第三条是微信收藏夹目前是用第三方工具导出为 HTML再走网页转 Markdown 流程。这三条管道都是半自动的——触发靠脚本确认靠人。我会定期运行一次抓取脚本检查 inbox 目录里的内容确认后移到正式目录。5.2 自动摘要与自动打标签笔记进入 Wiki 之后还有一个增殖动作自动生成摘要和标签。这步我用 LLM 的 API 来批量处理思路是读取每篇笔记前 N 个字符调用 LLM 生成摘要并提取 2 到 5 个标签。from openai import OpenAI client OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) def summarize_and_tag(text): prompt f请为以下文档生成摘要和标签。 要求 - 摘要不超过 100 字 - 标签 2~5 个用逗号分隔 - 输出格式摘要内容 || 标签1,标签2 文档内容 {text[:2000]} resp client.chat.completions.create( modelqwen2.5:7b, messages[{role: user, content: prompt}], temperature0.3 ) return resp.choices[0].message.content我用的模型是 Ollama 本地部署的 qwen2.5:7b完全离线。摘要生成后回写到 YAML frontmatter 的summary字段标签合并到tags字段。400 篇文档跑完大概需要 30 分钟因为加了限速避免 API 报错。自动打标的效果一般准确率在七成左右但就算打错了也够用——因为索引重建后标签本身就是辅助过滤。关键是它帮我建立了先入库后打标的高效流程人只需要在后期纠正部分标签。5.3 批量分块、嵌入与索引进 Dify当 Wiki 内容更新后需要重建向量索引。这一步我做了个一键脚本流程是扫描wiki/目录下所有 Markdown 文件解析 YAML frontmatter过滤掉 status 不是 done 的笔记对每篇笔记做结构切分产出多个文本块调用 BGE-M3 模型生成向量写入 Chroma 向量库同时调用 Dify 知识库 API 增量同步整条链路脚本耗时取决于笔记量。我的 Wiki 规模大约 400 篇文档、1600 个文本块在本地的 4070 显卡上跑完嵌入大约需要 3 分 40 秒。5.4 定时重建用 cron 或 GitHub Actions 保持新鲜索引重建可以手动跑也可以设置定时任务。我的方案是本地用 cron 每 3 小时检查一次 Git 仓库变化如果有新 commit就自动执行重建脚本。如果知识库放在 GitHub 仓库里也可以用 GitHub Actions 在 push 事件后自动运行但要注意 Actions 的 runner 没有 GPU嵌入需要在云端或 CPU 模式完成。个人使用建议本地 cron 就够了。# crontab 示例每 3 小时检查并重建索引 0 */3 * * * cd /path/to/llm_wiki ./scripts/rebuild_index.sh logs/rebuild.log 21脚本里我加了增量判断对比 Git 最近 commit 的哈希值和上次构建时的哈希值如果相同就直接跳过避免白白消耗资源。6. 实测中的踩坑记录分块、嵌入与引用的三个深坑6.1 分块太大导致的语义稀释是怎么发生的第一次跑通全流程后我随便问了几个问题发现回答质量很不稳定。查了检索日志才知道问题出在分块参数上。当时我用的是 2000 字的固定分块想着上下文越多模型越懂结果恰恰相反。2000 字的块里经常包含多个主题嵌入向量是所有主题的平均表达任何一个主题的语义都表达不充分。比如一篇笔记里同时写了 RAG 的架构、分块策略和重排模型选择2000 字的块向量在检索重排模型时相关度并不高于另一个只写重排的简洁文档。后来我把分块改成 500 字加 50 重叠并优先按 Markdown 标题结构切分问题立刻缓解。这个教训可以浓缩为一句话RAG 检索的上限由分块粒度决定LLM 只是在做最后的内容组织。6.2 混用嵌入模型检索效果断崖式下降的元凶这是一个让我查了一个晚上的 bug。早期构建索引时部分文档用了text-embedding-3-small做嵌入后来切换到本地 BGE-M3 之后我只是把新文档用新模型向量化、追加到同一个集合里没有对旧向量做任何处理。结果就是相似的内容因为嵌入模型不同向量距离反而很远。检索时某些旧文档总是召不回即使它和问题的语义高度相关。原因在于不同嵌入模型的向量空间不是对齐的——text-embedding-3-small的 1536 维向量和 BGE-M3 的 1024 维向量在度量上完全没有可比性。修这个坑只有一条路混用之后必须清空整个向量库全量重新生成索引。这正好印证了我在 1.3 里说的原则——索引是派生物随时可以重建。如果当初把所有内容都存在数据库里不保留原始 Markdown修复成本会高得多。6.3 中文文本切分不能简单按段落切处理中文文本时我发现了一个细节不能简单用英文的分词逻辑去做切分。英文按空格分词不会有问题但中文分词涉及到字与字之间的语义关系。所以我的切分策略在底层做了调整优先按 Markdown 结构标题切分没有标题时按 500 字固定长度切分单位是字符而不是 token在块的边界处做标点符号感知尽量在句号、问号、感叹号处断开固定长度切分加上标点感知之后中文内容被拦腰切断的情况显著减少检索质量也随之提升。如果是英文文档500 字符大概相当于 100 token 左右中文则更密但在这个量级上对嵌入模型的影响不大。6.4 引用溯源如何防止 LLM 一本正经地编造出处接入 Dify 之后我面临一个新问题模型生成的回答很流畅但有时候引用的来源并不存在。比如问我去年记录的部署难点有哪些它可能引用一个deployment-notes.md但我 Wiki 里根本没有这个文件。这是 RAG 里面的经典「幻觉引用」问题。我的解法是双保险第一层是元数据约束。在知识库的分块元数据里记录source和chunk_id并在系统提示词中要求只可引用提供的知识库内容且必须使用给出的引用格式。第二层是检索日志核查。Dify 的会话界面可以看到每次回答召回的具体片段如果回答引用了不在召回列表里的内容说明模型在编造。这种情况我会在应用设置里加重系统提示词把约束写死。实测下来的效果是幻觉引用从偶发问题降到几乎不可见。但要注意的一点是重排模型并不保证引用正确性它只做相关度排序最终的引用准确率仍然靠提示词和核查机制兜底。6.5 首次全量索引的耗时数据与硬件参考分享一个具体的性能参考。我的环境是 i7-12700 32GB 内存 RTX 4070 12GBOllama 跑 BGE-M3。400 篇文档、约 1600 个切分块第一次全量嵌入耗时 3 分 40 秒向量库占用大约 200MB。如果是纯 CPU 环境比如 MacBook Air M1耗时会在 15 到 20 分钟但要区分是嵌入模型的推理还是查询。查询延迟方面向量检索 20 条候选只需要几十毫秒重排 20 条候选大约需要 200 到 300 毫秒。所以 RAG 链路的整体延迟基本是由 LLM 生成回答的时间决定的本地跑 7B 模型大约 2 到 5 秒云端 API 则取决于网络和服务端排队。如果笔记量大到一万篇这个方案仍然可用只是嵌入时间会线性增长。到时候可以考虑增量嵌入——只在 Git diff 中处理变化的文件这个我在 5.4 里已经做了基础实现。6.6 一个关键提醒不要用 Wiki 内容去微调模型最后这个提醒很重要。很多人跑通 RAG 之后会想既然我有这么多高质量笔记能不能直接拿它微调一个模型我的答案是别除非你有非常特殊的需求。微调和 RAG 是两种完全不同的技术路线RAG 是检索后生成适合知识频繁更新、需要引用溯源的场景微调是把知识固化到模型参数里适合让模型学习某种特定写作风格或领域术语但它的知识更新成本高且不可溯源。对一个个人知识库来说绝大多数需求查我写过的东西、总结我的观点都应该走 RAG而不是微调。微调之后模型可能会把笔记内容背得很流利但你无法知道它哪句话是据实的哪句话是顺着语言习惯编的。llm_wiki 这套方案的价值恰恰在于每个回答都有据可查。最后再分享一个我在反复使用中的心得这套系统的维护成本比想象中低得多真正的门槛只在一次性的搭建和调优。先把链路用十篇笔记完整跑通再把历史内容批量灌进去中间会遇到各种参数问题但只要你坚持Markdown 是唯一事实源、索引是可丢弃的派生物、引用必须可溯源这三个原则这套知识库就会越用越顺手而不是越用越乱。