一次性读取整个 Obsidian:自动把我的 Markdown 变成 AI 知识库

一次性读取整个 Obsidian:自动把我的 Markdown 变成 AI 知识库

系列:《从 0 打造我的本地 AI 知识库:Obsidian + Ollama + Milvus + RAG + MCP + Agent》

上一篇:《第一次完整 RAG:让本地大模型真正"读懂"我的知识库》

本文关键词:Obsidian Markdown Chunk Embedding Milvus RAG 自动索引 Python


前几篇,我已经成功完成了一个非常小的实验:

text 复制代码
Markdown
   ↓
Embedding
   ↓
Milvus
   ↓
向量搜索
   ↓
Qwen3.5
   ↓
AI回答

第一次跑通的时候,我确实有一种:

"我的本地 AI 终于活了。"

的感觉。

但是冷静下来一看,我马上发现了一个问题。

我的知识库只有:

text 复制代码
obsidian/
└── test.md

这根本不能叫知识库。

这最多只能叫:

一个 RAG Demo。

真正的知识库应该是:

text 复制代码
obsidian/
├── Java/
├── JVM/
├── MySQL/
├── Redis/
├── Spring/
├── AI/
└── Interview/

里面可能有:

text 复制代码
100
1000
10000

篇 Markdown。

所以这一篇,我要解决一个非常实际的问题:

能不能让程序自动读取整个 Obsidian?

答案是:

可以。

而且这一步之后,我的项目才真正开始从:

"实验代码"

变成:

"知识库系统"。


一、先把最终目标画出来

这一篇最终要实现:

text 复制代码
                    Obsidian
                       │
                       │
                整个 Vault
                       │
                       ▼
              扫描所有 Markdown
                       │
                       ▼
                  读取文件
                       │
                       ▼
                    Chunk
                       │
                       ▼
                  Embedding
                       │
                       ▼
                    Milvus
                       │
                       ▼
                  Vector DB

也就是说,以后我只需要:

bash 复制代码
python indexer.py

程序就可以自动发现:

text 复制代码
Java/HashMap.md
Java/ThreadPool.md
JVM/G1.md
MySQL/MVCC.md
Redis/缓存.md
AI/RAG.md

而不用再手动指定:

python 复制代码
FILE_PATH = "./obsidian/test.md"

二、以前的方式有多麻烦?

之前我的代码是:

python 复制代码
FILE_PATH = "./obsidian/test.md"

程序只能处理:

text 复制代码
test.md

如果我又创建:

text 复制代码
HashMap.md
ThreadLocal.md
G1.md
MVCC.md
Redis.md
RAG.md

那我是不是得修改代码:

python 复制代码
FILE_PATH = "./obsidian/HashMap.md"

再运行一次?

然后:

python 复制代码
FILE_PATH = "./obsidian/G1.md"

再运行一次?

显然不可能。

所以我们真正需要的是:

让程序自己寻找 Markdown 文件。


三、Python 怎么寻找整个目录?

Python 有一个非常好用的工具:

python 复制代码
pathlib

它是 Python 标准库,不需要额外安装。

例如:

python 复制代码
from pathlib import Path

然后:

python 复制代码
vault_path = Path("./obsidian")

现在:

text 复制代码
vault_path

就代表:

text 复制代码
obsidian/

接下来可以让 Python 找到所有:

text 复制代码
*.md

文件。


四、第一次自动扫描 Markdown

创建:

text 复制代码
indexer.py

先写最简单的版本:

python 复制代码
from pathlib import Path


VAULT_PATH = Path("./obsidian")


markdown_files = list(
    VAULT_PATH.rglob("*.md")
)


print("发现 Markdown 文件:")
print()


for file in markdown_files:
    print(file)

运行:

bash 复制代码
python indexer.py

如果目录是:

text 复制代码
obsidian/
├── test.md
├── Java/
│   └── HashMap.md
├── JVM/
│   └── G1.md
└── MySQL/
    └── MVCC.md

程序就可以自动发现:

text 复制代码
obsidian/test.md
obsidian/Java/HashMap.md
obsidian/JVM/G1.md
obsidian/MySQL/MVCC.md

这一步看起来很简单。

但它非常重要。

因为:

我的程序第一次真正认识了整个 Obsidian。


五、为什么使用 rglob

这里有一个容易忽略的地方:

python 复制代码
VAULT_PATH.rglob("*.md")

不是简单的:

python 复制代码
glob("*.md")

rglob 中的:

text 复制代码
r

可以理解成:

recursive【递归】

也就是:

text 复制代码
obsidian/
│
├── test.md
│
├── Java/
│   ├── HashMap.md
│   └── ThreadPool.md
│
└── JVM/
    └── G1/
        └── RememberedSet.md

不管 Markdown 藏在多少层目录:

text 复制代码
rglob("*.md")

都可以继续往下面寻找。

所以非常适合 Obsidian。


六、找到文件以后,下一步是什么?

当然是:

读取 Markdown。

代码非常简单:

python 复制代码
from pathlib import Path


file_path = Path("./obsidian/test.md")


text = file_path.read_text(
    encoding="utf-8"
)


print(text)

现在:

text 复制代码
Markdown 文件
      ↓
Python
      ↓
字符串

例如:

markdown 复制代码
# MySQL MVCC

MVCC 是多版本并发控制。

InnoDB 通过 Undo Log 和 Read View 实现 MVCC。

会变成 Python 中的:

python 复制代码
text

七、但是新的问题出现了

假设我有一篇:

text 复制代码
JVM.md

内容:

text 复制代码
10000 字

现在如果直接:

text 复制代码
10000字
 ↓
Embedding
 ↓
一个向量

就不太理想。

因为用户问:

G1 的 Remembered Set 是什么?

真正相关的可能只有:

text 复制代码
500 字

而不是整篇 10000 字。

所以:

我需要 Chunk。


八、Chunk 到底是什么?

Chunk 可以简单理解为:

把一篇长文切成多个可以独立检索的知识片段。

例如:

text 复制代码
JVM.md
   │
   ├── Chunk 1
   │   JVM 内存结构
   │
   ├── Chunk 2
   │   对象创建
   │
   ├── Chunk 3
   │   GC Roots
   │
   ├── Chunk 4
   │   G1
   │
   ├── Chunk 5
   │   Remembered Set
   │
   └── Chunk 6
       Write Barrier

然后:

text 复制代码
每一个 Chunk
      ↓
Embedding
      ↓
一个 Vector
      ↓
Milvus

最终:

text 复制代码
JVM.md
  │
  ├── Chunk 1 → Vector 1
  ├── Chunk 2 → Vector 2
  ├── Chunk 3 → Vector 3
  ├── Chunk 4 → Vector 4
  ├── Chunk 5 → Vector 5
  └── Chunk 6 → Vector 6

这才是比较标准的 RAG 数据处理方式。


九、第一版 Chunk 不需要搞得太复杂

很多 RAG 教程一上来就讲:

text 复制代码
Recursive Character Text Splitter
Semantic Chunking
Parent-Child Retrieval
Late Chunking
Hierarchical Retrieval

第一次学习的时候很容易直接懵掉。

我现在的目标不是:

一步做到企业级 RAG。

而是:

先把完整链路跑通。

所以第一版可以采用非常简单的:

按字符长度切分

例如:

text 复制代码
每 1000 个字符
切成一个 Chunk

再设置:

text 复制代码
100 字符 overlap

也就是:

text 复制代码
Chunk 1
[--------------------]
                ↑
              100字符

Chunk 2
        [--------------------]

这样可以减少因为硬切导致的上下文断裂。


十、为什么需要 Overlap?

假设原文:

text 复制代码
Undo Log 用于保存历史版本。

Read View 用于判断当前事务能够看到哪些版本。

两者共同参与 MVCC 的实现。

如果刚好在:

text 复制代码
Undo Log...

这里切断:

text 复制代码
Chunk 1
Undo Log 用于保存历史版本。

Chunk 2
Read View 用于判断...

可能会让知识上下文被拆开。

所以可以让 Chunk 之间:

text 复制代码
重复一小部分内容。

这就是:

Overlap【重叠区域】

例如:

text 复制代码
Chunk 1
A B C D E F

Chunk 2
        E F G H I J
        ↑
      overlap

十一、写一个最简单的 Chunker

创建:

text 复制代码
chunker.py

代码:

python 复制代码
def split_text(
    text,
    chunk_size=1000,
    overlap=150
):
    chunks = []

    start = 0

    while start < len(text):

        end = start + chunk_size

        chunk = text[start:end]

        chunks.append(chunk)

        start = end - overlap

    return chunks

然后测试:

python 复制代码
text = """
这里是一篇非常长的技术文章。
"""

chunks = split_text(text)

for i, chunk in enumerate(chunks):

    print("Chunk:", i)

    print(chunk)

    print("----------------")

现在:

text 复制代码
Markdown
   ↓
10000字
   ↓
Chunker
   ↓
1000字
1000字
1000字
...

十二、但这里有一个很重要的现实问题

机械按照字符切割:

python 复制代码
text[start:end]

当然不是最优方案。

例如:

text 复制代码
### G1 的 Remembered Set

Remembered Set 是...

如果刚好切在:

text 复制代码
Remembered Set 是

中间:

text 复制代码
Chunk 1
Remembered Set 是

Chunk 2
一种...

语义就被拆开了。

所以真正生产级 RAG 一般会更加重视:

text 复制代码
Markdown 标题
段落
代码块
列表
语义边界

十三、为什么我的知识库特别适合 Markdown Chunking?

因为我的知识本身就是:

text 复制代码
Markdown

而 Markdown 已经帮我提供了大量结构:

markdown 复制代码
# JVM

## JVM 内存结构

### 堆

### 栈

## 垃圾回收

### GC Roots

### G1

所以未来可以设计成:

text 复制代码
Markdown
   ↓
识别标题
   ↓
识别段落
   ↓
保留标题上下文
   ↓
生成 Chunk

例如:

text 复制代码
Chunk:

主题:JVM → 垃圾回收 → G1

G1 是一种面向服务端应用的垃圾收集器......

相比单纯:

text 复制代码
1000字符

这种方式更适合我的技术知识库。


十四、接下来要做一个非常重要的事情:Metadata

现在我们保存:

text 复制代码
vector
text
source

但是以后我希望知道更多信息。

例如:

text 复制代码
这段知识来自:

MySQL/MVCC.md

属于:

MySQL

主题:

MVCC

第几个 Chunk:

3

所以可以保存:

text 复制代码
metadata

例如:

json 复制代码
{
  "source": "MySQL/MVCC.md",
  "category": "MySQL",
  "title": "MVCC",
  "chunk_index": 3
}

这样以后检索的时候就可以做:

text 复制代码
只搜索 MySQL

或者:

text 复制代码
只搜索 JVM

甚至:

text 复制代码
只搜索面试知识

这会成为以后 RAG 优化的重要基础。


十五、现在重新设计我的 Milvus 数据

之前:

text 复制代码
id
vector
text
source

现在我希望:

text 复制代码
id
vector
text
source
title
category
chunk_index

也就是说:

text 复制代码
┌───────────────────────────┐
│ Chunk                      │
├───────────────────────────┤
│ id                         │
│ vector                     │
│ text                       │
│ source                     │
│ title                      │
│ category                   │
│ chunk_index                │
└───────────────────────────┘

这样以后知识库才会越来越完整。


十六、现在开始把整个流程连接起来

终于到了这一篇最核心的部分。

我们现在拥有:

text 复制代码
① Obsidian 扫描
② Markdown 读取
③ Chunk 分块
④ Embedding
⑤ Milvus

把它们连接:

text 复制代码
                    Obsidian
                       │
                       ▼
                扫描所有 .md
                       │
                       ▼
                 读取 Markdown
                       │
                       ▼
                    Chunk
                       │
                       ▼
              Qwen3-Embedding
                       │
                       ▼
                  Vector
                       │
                       ▼
                    Milvus

这就是:

Indexing Pipeline【索引构建流程】


十七、什么叫 Indexing?

这个词以后会经常出现。

Index:

索引

Indexing:

建立索引。

我们做的事情就是:

text 复制代码
我的 Markdown
       ↓
处理
       ↓
Embedding
       ↓
Vector
       ↓
Milvus

其实就是:

把原始知识转换成方便未来检索的索引数据。

所以:

text 复制代码
Indexing

解决的是:

"知识怎么进入知识库?"

而:

text 复制代码
Retrieval

解决的是:

"用户提问时,怎么找到知识?"

这是两个不同的阶段。


十八、整个 RAG 可以分成两条流水线

到这里,一个非常重要的架构开始出现:

text 复制代码
                 RAG
                  │
        ┌─────────┴─────────┐
        │                   │
        ▼                   ▼
   Indexing Pipeline   Retrieval Pipeline
      建立索引              检索
        │                   │
        ▼                   ▼
   Markdown             用户问题
        │                   │
        ▼                   ▼
      Chunk              Embedding
        │                   │
        ▼                   ▼
    Embedding             Milvus
        │                   │
        ▼                   ▼
     Milvus              Top-K
                            │
                            ▼
                         Context
                            │
                            ▼
                         LLM

这张图非常值得记住。

因为以后面试官问:

"你们 RAG 的整体流程是什么?"

就可以从这两条 Pipeline 来回答。


十九、把所有 Markdown 自动处理

现在可以写一个简化版:

python 复制代码
from pathlib import Path
import requests


VAULT_PATH = Path("./obsidian")

EMBEDDING_URL = "http://localhost:11434/api/embed"

EMBEDDING_MODEL = "qwen3-embedding:0.6b"


def split_text(
    text,
    chunk_size=1000,
    overlap=150
):
    chunks = []

    start = 0

    while start < len(text):

        end = start + chunk_size

        chunk = text[start:end]

        chunks.append(chunk)

        start = end - overlap

    return chunks


def embedding(text):

    response = requests.post(
        EMBEDDING_URL,
        json={
            "model": EMBEDDING_MODEL,
            "input": text
        }
    )

    response.raise_for_status()

    return response.json()["embeddings"][0]


markdown_files = VAULT_PATH.rglob("*.md")


for file_path in markdown_files:

    print()
    print("处理文件:", file_path)

    text = file_path.read_text(
        encoding="utf-8"
    )

    chunks = split_text(text)

    print(
        "Chunk 数量:",
        len(chunks)
    )

    for index, chunk in enumerate(chunks):

        vector = embedding(chunk)

        print(
            f"Chunk {index}:",
            len(vector)
        )

现在运行:

bash 复制代码
python indexer.py

程序就会:

text 复制代码
找到文件
 ↓
读取
 ↓
切 Chunk
 ↓
Embedding

二十、不过这里有一个巨大的性能问题

假设:

text 复制代码
1000 篇 Markdown

每篇:

text 复制代码
10 个 Chunk

那么:

text 复制代码
1000 × 10
=
10000 个 Chunk

意味着:

text 复制代码
10000 次 Embedding

如果每次都:

text 复制代码
HTTP 请求
 ↓
Ollama
 ↓
Embedding

速度就会非常慢。

所以真正的知识库还需要进一步优化:

批量 Embedding

也就是:

text 复制代码
Chunk 1
Chunk 2
Chunk 3
Chunk 4
      ↓
一次请求
      ↓
Embedding
      ↓
Vector 1
Vector 2
Vector 3
Vector 4

而不是:

text 复制代码
Chunk 1 → 请求
Chunk 2 → 请求
Chunk 3 → 请求
Chunk 4 → 请求

后面优化索引器的时候,我们会处理这个问题。


二十一、还需要解决"重复索引"

现在如果我运行:

bash 复制代码
python indexer.py

一次。

数据库:

text 复制代码
100 个 Chunk

然后第二天又运行:

bash 复制代码
python indexer.py

如果程序没有判断:

这个文件以前是不是已经处理过?

那么就会变成:

text 复制代码
第一次
100 chunks

第二次
200 chunks

第三次
300 chunks

出现大量重复数据。

所以真正的知识库还需要:

增量索引

也就是:

text 复制代码
文件没变化
   ↓
跳过

文件发生变化
   ↓
重新 Embedding

新增文件
   ↓
建立索引

删除文件
   ↓
删除对应向量

这才是真正可以长期运行的知识库。


二十二、我开始意识到:RAG 不是一个简单的 Demo

到这里,我原来以为:

text 复制代码
RAG
=
Embedding + Vector DB + LLM

现在发现远远不止。

一个真正可用的 RAG 至少涉及:

text 复制代码
数据源
 ↓
文档加载
 ↓
文本清洗
 ↓
Chunk
 ↓
Embedding
 ↓
Vector DB
 ↓
Retrieval
 ↓
Rerank
 ↓
Context
 ↓
Prompt
 ↓
LLM
 ↓
Answer

甚至还包括:

text 复制代码
Metadata
权限
增量索引
删除同步
缓存
评估
引用
监控

所以:

RAG 的难点其实并不只是"调用大模型"。

真正困难的是:

怎么把知识可靠地送到大模型面前。


二十三、这一篇之后,我的项目发生了一个变化

以前:

text 复制代码
test.md
   ↓
Embedding
   ↓
Milvus

现在:

text 复制代码
Obsidian Vault
      ↓
自动扫描
      ↓
Markdown
      ↓
Chunk
      ↓
Embedding
      ↓
Milvus

这意味着以后我只需要把知识写进:

text 复制代码
Obsidian

而不需要再考虑:

text 复制代码
怎么导入?
怎么 Embedding?
怎么保存?

这些工作逐渐可以交给程序。


二十四、我的本地 AI 知识库开始成型

现在整个项目可以理解成:

text 复制代码
                 我的知识
                    │
                    ▼
                Obsidian
                    │
                    ▼
              Markdown Vault
                    │
                    ▼
              自动索引器
                    │
          ┌─────────┴─────────┐
          │                   │
          ▼                   ▼
        Chunk             Metadata
          │                   │
          └─────────┬─────────┘
                    ▼
               Embedding
                    │
                    ▼
                 Milvus
                    │
                    ▼
                Vector DB
                    │
                    ▼
                  RAG
                    │
                    ▼
                Qwen3.5
                    │
                    ▼
                 AI回答

这已经越来越像一个真正的:

Personal AI Knowledge Base【个人 AI 知识库】

了。


二十五、下一步最值得做什么?

现在不要急着上 Agent,也不要急着上 MCP。

因为目前还有一个非常明显的问题:

我的 Chunk 还很粗糙。

所以接下来我要解决:

text 复制代码
Markdown
   ↓
智能 Chunk

而不是简单:

text 复制代码
每 1000 字硬切一次

下一篇就进入:

《RAG 为什么总是"搜不到"?从 Chunk 到语义分块,我终于搞懂检索效果的关键》

我们会开始研究:

text 复制代码
Chunk Size
Overlap
Markdown 标题
代码块
段落
Metadata
Top-K
相似度阈值

并且会做一个非常有意思的实验:

text 复制代码
同一个问题
       ↓
不同 Chunk 策略
       ↓
分别检索
       ↓
比较结果

这样我就能真正理解:

为什么有时候 Embedding 明明做对了,RAG 还是回答不好。

而这一步,也会开始进入真正的:

RAG 检索优化。

相关推荐
程序员柒叔1 小时前
把可观测性数据送进 LLM 追踪平台
人工智能·llm·github·agent·可观测性·langfuse
孙启超2 小时前
【AI开发之Rust】第 5 课:引用与生命周期 —— 借用能活多久?
人工智能·后端·rust·llm·ai应用开发
ZGi.ai17 小时前
知识库权限变了,怎样避免答错?
agent·权限管理·知识库·工作流·zgi·客服运营
掰头战士18 小时前
MCP、Skill、Plugin,都是给agent拓展能力,到底有何区别?
typescript·llm·agent
DigitalOcean20 小时前
Kimi K3 + Claude:AI Agent 多模型路由实战
llm·agent
武子康21 小时前
SGLang 一加并发就出问题,先查显存还是队列
人工智能·llm·agent
挖掘狂人21 小时前
从 LLM 到 Agent Skill:9 个底层概念,一次理清整个 AI 技术栈
llm·agent·ai编程
dong_junshuai1 天前
每天一个开源项目#97 LLM Wiki:把RAG结果变成可维护知识资产
开源·llm·github
ProbeX1 天前
一文搞懂 AI 里的 Harness:它和 Agent、Skill、LLM 到底啥关系?
llm