从Obsidian到AI知识库:Markdown清洗、分块与RAG全流程解析
2026/9/24 21:10:01 网站建设 项目流程

很多人第一次听到“把 Obsidian 变成 AI 知识库”这个说法,第一反应是装个插件,点一下同步,然后就能跟自己的笔记对话了。我一开始也这么想,结果折腾一圈发现,事情远没那么简单。真正的核心不在于“对话”,而在于“让 AI 能一次性读懂你整个库”。这背后涉及 Markdown 解析、文本分块、向量化、检索链路一整套事情。这篇内容我就把整套流程从零讲透,包括我怎么写脚本批量读取 Obsidian 全库、怎么把 Markdown 变成干净的纯文本、怎么选择分块策略,以及最后怎么接入本地或云端的 RAG 流水线。如果你手里的笔记已经积累了上千篇,想用 AI 直接基于这堆 Markdown 做问答、写周报或者找关联,那这篇文章就是为你准备的。

现在市面上的知识库方案很多,但大部分是给企业文档用的,拿到个人 Obsidian 库上往往水土不服。原因很简单:Obsidian 里全是 Markdown 文件,而且带有自己的一套语法——[[双向链接]]![[嵌入]]、YAML frontmatter、标签体系,这些玩意儿普通解析器根本不认。如果直接把原始.md文件丢给知识库工具,轻则格式混乱,重则检索结果完全跑偏。所以,把整个 Obsidian 变成 AI 知识库,本质上是一个“格式归一的工程问题”,而不是简单调个 API 的事。

1. 先把整个库读出来:Markdown 批量扫描与链接语法清理

1.1 为什么不能直接拿.md文件当输入

Obsidian 的仓库本质上是一个本地文件夹,里面散落着几百上千个.md文件。表面上看,这些文件都是纯文本,好像随便哪个脚本都能读。但问题出在 Obsidian 自己的语法上。比如我随手翻开一篇笔记,里面可能是这样:

--- title: 分布式系统笔记 tags: [分布式, 架构] date: 2024-06-01 --- # CAP 定理 在分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三者不可兼得。 相关笔记:[[BASE 理论]]、[[Paxos 算法]] ![[系统架构图.png]]

这一段里有三处普通文本解析器会犯迷糊的地方。

第一,文件开头的---包裹区叫 YAML frontmatter。它存储的是元数据,不是正文内容。如果不加处理直接塞给 AI,这些键值对会污染语义。比如date: 2024-06-01可能被 AI 误以为是在讲时间相关的概念。

第二,[[BASE 理论]]这种双链语法,是 Obsidian 用来表示笔记间关联的。但 AI 并不知道[[...]]是什么意思,它只会看到一堆方括号。更麻烦的是,[[Paxos 算法]]可能是一个尚未创建的笔记名,AI 如果去检索这个文件名,可能会返回空结果。

第三,![[系统架构图.png]]是图片嵌入语法。AI 读不到图片内容,但会把这行文本当作有效内容,导致检索时出现一个永远无法匹配的内容碎片。

所以,让 AI 读取 Obsidian 的第一步,不是“读”,而是“翻译”——把 Obsidian 方言翻译成大白话 Markdown。

1.2 遍历全库时这些目录必须跳过

写脚本遍历目录很简单,Python 里一个os.walk()就搞定。但有几个目录必须要过滤掉,否则你的向量库里会混进大量垃圾数据。

首先是.obsidian目录。这是 Obsidian 的配置目录,里面存着你的插件配置、工作区布局、快捷键设置,全是 JSON 文件。这些跟你的知识内容没有任何关系,纯属噪音。

其次是.trash目录。Obsidian 删除笔记时会把它移到这里,如果你启用了“软删除”,这个目录会越来越大,里面全是废弃内容。把它向量化,等于 AI 每天在拿你删掉的草稿当记忆。

然后是.git目录(如果你用了 Obsidian Git 插件做版本管理)。这里记录的是仓库的每次 commit 历史,体积巨大,而且全是重复的文本快照。喂给 AI 不仅浪费 token,还会让检索结果被历史版本淹没。

最后是附件目录。如果你习惯把图片、PDF、音频都放在仓库里的assets附件文件夹,遍历时一定要按扩展名过滤,只处理.md文件。否则遇到 PDF 或者扫描件,普通脚本读出来的全是乱码。

我自己的过滤逻辑是这样写的:

import os VAULT_PATH = "/path/to/your/vault" SKIP_DIRS = {".obsidian", ".trash", ".git", "node_modules", ".smart-env"} def find_markdown_files(vault_path): md_files = [] for root, dirs, files in os.walk(vault_path): # 原地修改 dirs,实现剪枝 dirs[:] = [d for d in dirs if d not in SKIP_DIRS] for f in files: if f.endswith(".md"): md_files.append(os.path.join(root, f)) return md_files

这里有个小坑:Python 的os.walk()在遍历时,dirs列表直接决定了后续要进入哪些子目录。老老实实在循环里写if d in SKIP_DIRS: continue是没用的,必须用dirs[:] = [...]这种切片赋值的方式,否则 itertools 的遍历顺序会把过滤逻辑打乱,还是会走进.git目录。

1.3 处理 frontmatter、双链和嵌入标记

拿到文件列表后,下一步就是逐文件清洗。我整理了一套比较稳妥的清洗规则,实测下来能把 Obsidian 的方言语法降噪到一个很干净的程度。

  • YAML frontmatter:用正则匹配开头的------之间的内容,整体删除。但要注意,正文中如果用了---做分割线,可能被误删。我的做法是只匹配文件开头 200 个字符内的---块,避免误伤。
  • 内部链接[[笔记名]]:转成纯文本。如果是[[笔记名|显示文字]],保留显示文字;如果没有显示文字,保留笔记名本身。这样 AI 仍能理解这里引用了一篇叫“Paxos 算法”的笔记。
  • 嵌入![[xxx.png]]:直接删除整行。因为图片信息 AI 读不到,留一个空洞的引用没有任何检索价值。
  • 外部链接[文字](https://...):保留文字,去掉 URL。这样既能保留语义,又不会让 AI 陷入一堆网址字符。
  • 标签#标签名:如果标签是行内的,保留词本身但去掉#符号。如果是文件底部的标签列表,直接删除。

清洗逻辑写成代码大概是这个感觉:

import re def clean_markdown(text): # 删除 YAML frontmatter if text.startswith("---"): parts = text.split("---", 2) if len(parts) == 3: text = parts[2].lstrip() # 处理嵌入图片 text = re.sub(r"!\[\[.*?\]\]", "", text) # 处理内部链接 def replace_wikilink(match): target = match.group(1) if "|" in target: return target.split("|")[-1].strip() return target.strip() text = re.sub(r"\[\[(.*?)\]\]", replace_wikilink, text) # 处理外部链接 text = re.sub(r"\[(.*?)\]\(https?://.*?\)", r"\1", text) # 处理标签 text = re.sub(r"#([\w\u4e00-\u9fa5]+)", r"\1", text) return text

这一步做完,你的 Markdown 文件就从 Obsidian 方言变成了通用 Markdown,AI 能读懂的通用文本格式。

2. Markdown 怎么切才不傻:分块参数与边界策略

2.1 一次性全塞进去不现实,暴力拼接的代价是什么

在 RAG 方案里,切分文档是最容易翻车的一步。很多人上来就把整篇笔记当成一个 chunk,几百篇笔记直接拼接成一个大文本扔给 embedding 模型。这样做有两个直接后果。

第一,是 token 成本。假设你的库有 2000 篇笔记,平均每篇 2000 字,那总字数就是 400 万字。按一个中国汉字约等于 1.5 个 token 算,一次调用 embedding API 的费用虽然不高,但如果后面每次问答都要全量检索一遍,那响应延迟和费用都会很感人。

第二,是检索精度。RAG 的原理是先把你问的问题做向量化,再在你的知识库里找最相似的片段。如果你把整篇长文当一个片段,那这篇长文只要有一个段落跟问题相关,整篇就会被检索出来。结果就是,AI 收到了一堆不相关的上下文,回答质量反而下降。

所以,正确的做法是:把每篇 Markdown 切分成多个语义完整的片段,每个片段作为一个独立的知识点参与向量检索。

2.2 按标题切分、按段落切分、固定窗口切分的取舍

切分策略大致有三种,实话说没有绝对的最好,只有适不适合。

第一种是按标题切分。也就是识别 Markdown 里的#####标题,把每个小标题下的内容作为一个 chunk。这种方案最适合 Obsidian 里的长文笔记,比如读书笔记、课程笔记。因为这类笔记通常结构清晰,每个小节讲一个独立主题。按标题切能把语义单元保持得比较完整。缺点是如果某篇笔记压根没有二级标题,那整个文件就只有一个 chunk,起不到切分效果。

第二种是按段落切分。用空行作为分隔符,把文本切成自然段,再按长度合并成 chunk。这种方案比较通用,适合大多数场景。但段落长短不一,有的段落只有一句话,有的段落却是一整篇长文的论证过程。合并的时候如果处理不好,容易把两个无关主题拼在一起。

第三种是固定窗口切分。不关心标题和段落,直接按字符数切,比如每 800 个字符一段,前后重叠 100 个字符。这种方案的优点是实现简单、逻辑统一、性能稳定。缺点是容易把一句话从中间切断,强行拆散语义。

我的实践经验是:先用标题切,如果标题下内容还是太长,再用固定窗口兜底。用一个混合策略:

def smart_chunk(md_text, max_len=800, overlap=100): sections = re.split(r"\n(?=#{1,6} )", md_text) chunks = [] for sec in sections: if len(sec) <= max_len: chunks.append(sec) else: # 长段落按固定窗口切,保留 overlap for i in range(0, len(sec), max_len - overlap): chunks.append(sec[i:i+max_len]) return chunks

re.split(r"\n(?=#{1,6} )", ...)这种正则可以做到在标题前断开,保留标题本身作为 chunk 的开头。这样每个 chunk 自带小标题,在向量检索时命中率会高不少。

2.3 chunk 长度到底该设多少:中文场景的特殊考量

关于 chunk 大小,市面上常见的默认值是 512 token 或者 1024 token。这个值主要受两个因素影响:一是 embedding 模型的最大输入长度,二是你之后要接的大模型上下文窗口。

这里有一个纯中文场景的特殊问题要提一嘴。很多 embedding 模型是按西文 token 训练的,中文在分词后 token 数大约是字数的 1.5 倍。假设你设 chunk 大小是 512 token,那实际能容纳的中文字符数大约是 340 个,也就是三四百字的段落。这个长度对很多笔记来说其实是偏小的,一篇 2000 字的深度思考笔记会被切成六块,语义可能被拆散。

所以我的建议是:把 chunk 大小设置在 600 到 800 token 之间,overlap 设置为 100 到 150 token。这个区间在主流向量数据库里都能跑,而且对中文文档的语义完整性相对友好。

另外一个细节是,Obsidian 里常见的双链笔记通常都很短,几百字一篇。这种短笔记就别切了,直接整篇作为一个 chunk,因为短笔记语义高度集中,拆开反而丢失上下文。

3. 向量化与检索链路:从文本到可用的 AI 知识库

3.1 嵌入(Embedding)是什么,以及为什么它决定知识库的上限

聊到知识库,离不开“向量化”这个词。我尽量用最直白的方式讲清楚:嵌入(Embedding)就是把一段文字变成一个一串数字组成的向量,让语义相近的内容在向量空间里靠得近。比如“分布式系统”和“微服务架构”的向量距离会很近,而和技术无关的“番茄炒蛋”就会离得很远。知识库做问答时,其实不是你“问”AI,而是把你问的问题也变成一个向量,然后去向量库里找最接近的片段,再把这些片段拼成上下文喂给大模型。

嵌入模型选得怎么样,直接决定了知识库的上限。这一步选不好,后面所有优化都是白费劲。

现在主流的选择有三类。第一类是云端 API,比如 OpenAI 的text-embedding-3-small、国内的bge-m3系列 API、智谱的 embedding 接口。优点是质量高、不用自己维护模型,缺点是收费、有网络依赖。第二类是本地开源模型,比如bge-large-zh-v1.5text2vec-large-chinese,用Ollama就能跑。优点是免费、离线可用,缺点是 embedding 模型也得吃显存,512 维度的模型大概要 1 到 2GB 内存,这还好,但如果你跑的是更重的模型,老机器会吃力。第三类是用大模型自带的 embedding 能力,比如本地部署的Qwen-7B本身不做 embedding,但你可以在它上面配一个专用的 embedding 微调模型。整体来说,个人知识库阶段没必要上那套,直接用bge-m3或 OpenAI embedding 就够了。

3.2 向量数据库的选型:不要一上来就上 Milvus

跟嵌入模型配套的,是向量数据库。很多文章一上来就推 Milvus,说它能支持十亿级向量。我劝你别被带偏。个人 Obsidian 库撑死几万个 chunk,用 Milvus 属于高射炮打蚊子,运维复杂度反而高。根据我的经验,Chroma 是最适合个人知识库起步的选择。它是纯 Python 实现,直接 pip install 就能用,数据存在本地 SQLite 里。几万个向量在它上面检索延迟是毫秒级,完全够用。

如果你的知识库将来要几个人协作访问,可以换 Qdrant,它提供了 Docker 部署方案,接口也更规范,方便以后迁移到生产环境。说实话,个人场景下 Chroma 已经能优雅地撑很长时间了,不需要一上来就考虑分布式。

3.3 从向量到答案:完整的 RAG 检索链路

分块完、向量化完,最后一步就是 RAG 检索。所谓 RAG,就是“检索增强生成”。这步链路如果搭得自然,你才会觉得 AI 真的是“懂”你的笔记库的。

完整链路是我在代码里封装的一个函数:

def ask_vault(question, k=5): q_vec = embed_model.encode(question) results = chroma_collection.query(query_embeddings=[q_vec], n_results=k) context = "\n\n".join([doc["document"] for doc in results["documents"]]) prompt = f"""基于以下笔记内容回答问题,如果笔记中没有相关信息,请直接说不知道。 笔记内容: {context} 问题:{question} """ response = llm.chat(prompt) return response

关键就在k=5这个参数。它决定了每次问答会从知识库里捞出多少个片段。太小了,上下文不足;太大了,噪音会淹没真实答案。我一般在 5 到 8 之间调。另外,context拼接时用两个换行分隔,是为了避免大模型把一些碎片连读,产生幻觉。

这整个流程其实可以用 Dify 这种成熟的工具一键跑通,自己写一遍代码主要是为了理解原理。后面我会专门讲一下用 Dify 搭流水线的路线,两条路径各有利弊。

4. 选一条适合自己的搭建路线:本地部署 vs 云端工具链

4.1 手搓一套方案:脚本 + Ollama + Chroma

自己写脚本的路线其实非常适合 Obsidian 用户,因为 Obsidian 本身就是一套纯本地工具,你的笔记默认存放在本地文件夹,不需要额外的同步逻辑。

具体流程是:先用前面提到的扫描清洗脚本,把整个库通过smart_chunk()切块,然后调用本地 Ollama 里的 embedding 模型生成向量,写入 Chroma。答案生成可以继续用 Ollama 跑一个对话模型,也可以外接云端 API。

这里分享一个我用得比较顺手的组合:

  • 本地跑ollama pull bge-m3,做 embedding。
  • 本地跑ollama pull qwen2.5:14b,做对话生成。这个模型对中文的支持非常好,生成质量比 7B 强了一个档次,显存占用大概 10GB,如果你只有 8G 显存,可以用qwen2.5:7b代替。
  • Chroma 存向量,直接用chromadb==0.4.x版本,配合langchainChroma封装写代码。

整个链路用 Python 写起来大概 200 行左右。好处是完全离线、隐私无忧,Obsidian 本身就是一个重隐私的工具,这算是精神上的匹配。

4.2 用 Dify 搭知识库流水线:傻瓜式但又不失灵活

如果你不想写代码,还有一个更省力的方案:用 Dify 搭建知识库流水线。Dify 是一个开源的大模型应用开发平台,里面专门做了知识库功能。你只需要把清洗后的 Markdown 文件上传,Dify 会自动完成分段、向量化、索引入库,然后在应用里配置一个“知识库检索 + 大模型回答”的工作流,就能直接对话了。

用 Dify 有几个很明显的优势。第一,它内置了分段和索引策略,你不用自己调参数。第二,它支持混合检索,也就是向量检索加全文检索。全文检索能解决向量检索常见的“关键词完全匹配但向量距离远”的问题,比如你笔记里写的是“Redis”,问的是“缓存数据库”,向量检索能找到近似语义,但全文检索能帮你精确定位到字面匹配的片段。两者结合,召回率会高很多。第三,Dify 提供了可视化的工作流编排界面,你可以在知识库检索后面接一个“重排”(Rerank)步骤,进一步筛掉不相关的片段。

我自己实测下来,Dify 的分段质量已经比很多手搓方案好很多。它有一个“文档分段”配置,支持按标题标记、按段落切分,还支持分段长度和重叠长度设置。上传一批 Markdown 后,你可以直接在界面上预览切分结果,所见即所得,比调试 Python 脚本直观得多。

4.3 Obsidian Git 和插件生态的配合

不管走哪条路线,有一点必须重视:知识库更新之后,向量库必须同步更新,否则 AI 回答的还是旧知识。Obsidian 场景下,我的同步方案是双保险。

第一层保险是 Obsidian Git 插件,我设置了每次文件变更后自动 commit 并 push 到远程仓库。这样知识库本身永远有一个可恢复的历史版本。

第二层保险是我写了一个定时脚本,每天凌晨扫描一次整个库,对比文件的修改时间,只对变动过的文件重新分块和向量化,删除旧的向量记录,写入新的。这样增量更新的成本非常低,不至于每次全量重跑一遍。增量更新大概的核心逻辑是这样:

for file_path in changed_files: delete_vectors_by_file(file_path) chunks = process_single_file(file_path) add_vectors_to_chroma(chunks)

这两层配合下来,基本能做到 Obsidian 里改了笔记,第二天 AI 就能用上新内容。

另外,Obsidian 自身的插件生态里还有个 Dataview,它可以把笔记的 YAML frontmatter 按条件汇总生成表格,相当于一种弱结构化的元数据视图。我在清洗阶段会顺手把每条笔记的标题、标签、创建时间等元数据提取出来,存成一个 JSON 文件,跟着向量一起入库。这样后续可以对知识库做按标签过滤、按时间过滤的精确检索,比纯靠向量相似度靠谱多了。

5. 离线部署踩过的坑:embedding 模型、编码、显存管理

5.1 encoding 与文件读取的隐性坑

Obsidian 默认保存的中文内容有些是 UTF-8 无 BOM,有些老用户可能开了 GBK 而不自知。Python 读文件如果用默认编码,会在中文笔记上直接抛UnicodeDecodeError,或者读出一堆乱码。我的建议是读取文件时统一指定:

with open(file_path, "r", encoding="utf-8", errors="replace") as f: content = f.read()

errors="replace"很关键,它保证即使某篇文章有零星几个非法字符,也不会导致整个文件读取失败,而是用\ufffd替代掉。否则你动不动就会遇到一次任务中断。

5.2 本地 embedding 模型的显存与速度权衡

本地跑 embedding 模型,看着轻巧,实际跑起来还是有一些性能细节。bge-m3的模型文件大概 2.3GB,用 CPU 跑的话,几百个 chunk 的向量化可能要花几分钟。如果你的库有几千个 chunk,整个过程会比较磨人。我的做法是把OLLAMA_NUM_CTX调大一些,让一次性处理的文本更长,减少模型加载次数,然后首次全量向量化时找个空闲时段跑完。后续增量更新每次只处理几个文件,CPU 模式下也就是几秒钟的事。

如果你的机器有 N 卡,记得把 Ollama 的gpu开起来,向量化速度能快十倍以上。但如果你的显存只有 4GB,同时跑 embedding 模型和对话模型会爆显存,只能二选一。这时候可以把 embedding 模型放在 CPU 上跑。ollama pull bge-m3之后,在代码里用ollama.embed时,它会自动选择设备,CPU 模式下也就慢一点,不影响结果质量。

5.3 聊一聊我踩过的最亏的一个坑:没有提前清洗数据

这是我最后想重点强调的一点。最初我把一个两千多篇笔记的库直接向量化,检索的时候发现,AI 经常答非所问。查了很久才发现,我的 Obsidian 库里有大量自动生成的 MOC 索引笔记,这些笔记内容就是一堆[[链接]]的罗列,本身没有实质知识。清洗脚本确实会保留链接中的笔记名,但 MOC 里成百上千个笔记名组合成的文本,在向量检索时会形成“磁铁效应”,把其他笔记的向量吸引过来,导致上下文严重污染。

后来我自己加了一道过滤:如果一篇笔记经过清理后,有效内容低于 200 个字符,就整篇跳过,不进知识库。MOC、索引类笔记如果没有补充额外注释,就会被自动忽略。这个过滤规则很简单,但让检索准确率提升非常明显。所以,如果你的库很大,建议先做一轮“内容质量预筛”,再谈向量化和检索。

6. 实战:用知识库做的事,和想象中不太一样

整个链路搭好之后,我服务了自己的 Obsidian 库一段时间,有些体会还挺反直觉的。

第一,知识库最擅长的事不是“答疑”,而是“找角度”。你问它“我研究过哪些跟缓存有关的内容”,它能给你列出一堆从 Redis 到浏览器缓存再到分布式缓存的一致性笔记。这种能力不是靠大模型的记忆力,而是靠向量检索帮你把散落各处的相关片段捞出来。同样的问题,如果你在 Obsidian 里搜索“缓存”,你可能只会搜到标题里带缓存的笔记,但语义相似的“写入放大”“Cache Aside”这些概念你可能就错过了。

第二,知识库的“导入清洗”远比“模型选型”重要。一个干净的知识库配一个中档模型,效果永远好于一个脏乱库配最好的模型。原因很简单:RAG 的上限不取决于生成模型的聪明程度,而取决于检索到的内容有多相关。如果你的库被无效信息淹没,再强的模型也只能基于噪音内容作答,效果当然差。所以我常跟人说,搭知识库的功夫,70% 花在清洗上,这才是一本万利的事。

第三,Markdown 结构的保留是个双刃剑。我一开始把标题全删了,只留正文,结果 AI 回答时经常“断章取义”。后来我把标题作为 chunk 的前缀强制拼上,比如把## Redis 持久化这个标题跟下面的正文合并成一个 chunk,检索命中率有明显提升。大模型在看到标题和正文的完整组合时,对语义的理解会好很多。

这些经验放在这里,大家搭库的时候可以少走一些弯路。至于要不要追求“一次性读取整个 Obsidian”,我的看法是,技术上完全可行,但前提是你要先想清楚“读取”之后要拿来做什么,然后再决定清洗和切分的颗粒度。方向不对,跑得再快也是白费功夫。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询