用 Rust 和 AI 搭建个人知识库:从笔记到可检索的第二大脑方案
用 Rust 和 AI 搭建个人知识库:从笔记到可检索的第二大脑方案
前言
上个月我终于受不了了,决定用 Rust + AI 搭一个属于自己的知识库系统。这篇文章就是整个方案的复盘——从数据收集到向量检索,一整套流程。
一、整体架构设计
1.1 系统的三个层次
整个知识库系统分三个层次:数据采集层(Markdown 笔记、代码片段、网页剪藏、GitHub Issues)、处理管线(文件监听、文本分块、Embedding 生成、向量存储)、检索服务(CLI 查询、全文搜索、混合排序、AI 摘要)。
每个层次的职责很清晰:采集层负责把散落的知识碎片聚合在一起;处理管线负责把原始文本变成可检索的向量;检索服务负责把查询变成有用的结果。
架构设计的核心原则是增量更新——不是每次全量重建索引,而是通过文件监听自动检测变更,只重新处理修改过的文件。这保证了知识库的数据始终最新,不需要手动触发索引。
1.2 技术选型的考量
文件监听用 notify crate——纯 Rust,跨平台,支持增量更新。文本分块用自定义 Markdown parser(基于 pulldown-cmark)——按标题层级智能分块,不截断代码块。Embedding 用 OpenAI text-embedding-3-small——性价比最高(1536 维向量,每次调用约 $0.0001)。向量数据库用 Qdrant——支持混合搜索,Rust 客户端成熟。全文搜索用 tantivy——类似 Lucene,纯 Rust,性能好。
一个重要的备选方案是 fastembed crate——本地 embedding 模型,离线可用,不依赖 API。我的当前方案依赖 OpenAI API,在离线环境下不可用。后续计划切换到 fastembed 实现完全本地化。
二、核心处理管线的设计思路
2.1 文件监听与增量索引
文件监听器用 notify crate 实现,递归监听笔记目录,只关注.md文件变更。每次变更生成一个ChangeEvent(Created/Modified/Deleted),推入队列供后续处理。
增量索引的设计思路是:Created 事件触发完整处理流程(分块 → Embedding → 存储),Modified 事件先删除旧索引再重新处理,Deleted 事件只删除旧索引。这样只处理变更的文件,而不是每次全量重建。
实际使用中有个坑:某些编辑器(如 Obsidian)保存文件时会触发多次 Modify 事件(先写临时文件再重命名),导致同一个文件被重复处理。我加了 500ms 的防抖——同一文件在 500ms 内的多次变更合并为一次处理。
2.2 智能文本分块:按标题层级切分
文本分块是整个管线中最关键的步骤,直接影响检索质量。我选择了按 Markdown 标题层级切分,而不是按固定字符数切分——因为标题是天然的语义边界,切出来的块语义完整性更好。
分块器维护一个标题栈(如["Rust", "所有权", "借用规则"]),每个块都记录自己的标题路径。这样搜索结果可以显示"这个块来自 Rust > 所有权 > 借用规则",用户一眼就知道上下文。
// 分块结果——每个块有标题路径、文件路径、代码块数量等元数据 #[derive(Debug, Clone)] pub struct DocumentChunk { pub id: String, // 唯一标识: 文件路径:行号:序号 pub content: String, // 块文本内容 pub file_path: String, // 所属文件路径 pub heading_path: Vec<String>, // 标题路径: ["Rust", "所有权", "借用规则"] pub code_block_count: usize, // 代码片段数量 pub char_count: usize, // 字符数 }这个结构体展示了分块结果的核心元数据。heading_path是最关键的字段——它让搜索结果不只是"一段文字",而是有明确上下文的知识片段。用户看到heading_path = ["Rust", "所有权", "借用规则"],就知道这段文字是在讲 Rust 所有权中的借用规则,而不是泛泛的"借用"概念。
分块器的参数有三个:min_chunk_size(低于此阈值合并到上一块,避免碎片化)、max_chunk_size(超过此阈值强制分割,避免单块过大)、overlap_size(块之间重叠的字符数,防止语义断裂)。我的经验值是:min=100、max=2000、overlap=100。
2.3 Embedding 生成与批量优化
Embedding 生成用 OpenAI 的 text-embedding-3-small 模型,每次调用最多支持 2048 个文本。我实现了批量生成接口,一次调用处理多个块——比逐个调用快 10 倍以上(API 的网络延迟是主要瓶颈,批量请求只需一次网络往返)。
成本方面,text-embedding-3-small 的定价是 $0.02/1M tokens。我的知识库约 500 个 Markdown 文件,分块后约 2000 个块,全量索引的 embedding 成本不到 $0.5。增量更新每月新增约 50 个块,成本几乎可以忽略。
一个需要注意的细节:OpenAI 的 embedding API 有速率限制(每分钟最多 1500 次请求)。批量生成可以减少请求次数,但单次请求的文本数量也有上限。我设置了每批最多 100 个文本,配合 500ms 的请求间隔,从未触发过速率限制。
三、混合检索系统
3.1 为什么混合搜索优于纯向量搜索
纯向量搜索的问题在于:语义匹配好但精确匹配差。比如搜索Arc::new,向量搜索可能返回所有提到"Arc"的内容(包括 ArcGIS、Archive 等无关内容),而全文搜索(BM25)能精确匹配函数名。
混合搜索把两种搜索的结果融合起来,用 RRF(Reciprocal Rank Fusion)算法排序。RRF 的公式很简单:score = 1/(k + rank)对每个搜索来源求和,k通常设为 60。排名越靠前贡献越大,两个搜索都排名靠前的结果得到最高融合分数。
3.2 RRF 融合的实现逻辑
RRF 融合的实现分三步:先对向量搜索结果计算 RRF 分数(1/(60 + rank + 1)),再对关键词搜索结果计算 RRF 分数,最后对两个来源中相同块的结果累加分数。
这个逻辑的关键是:如果一个块在向量搜索和关键词搜索中都排名靠前,它的融合分数会远高于只在一种搜索中排名靠前的块。这正是我们想要的效果——语义相关且关键词匹配的内容优先级最高。
纯向量搜索会返回大量"语义相关但关键词不匹配"的结果(如搜索Arc::new时返回"Rust 的并发原语"),纯关键词搜索会返回"关键词匹配但语义无关"的结果(如搜索Arc::new时返回 ArcGIS 的文档)。RRF 融合让两种优势叠加,劣势互相抵消。
3.3 搜索结果的 CLI 展示
搜索结果的 CLI 展示用 dialoguer 实现——先显示结果列表(标题路径 + 文件名 + 预览),用户选择后显示完整内容。每个结果还显示融合分数和来源分数(语义: 0.85, 关键词: 0.72),让用户知道结果为什么被排在前面。
CLI 交互的设计原则是"搜索后不离开终端"——所有信息在终端内展示,不需要打开浏览器或编辑器。这和知识库的使用场景匹配——我通常是在写代码时突然想起"之前记过这个概念",快速搜索一下,不需要打断当前的工作流。
四、数据流转与持续使用
4.1 从写笔记到搜索结果的完整链路
整个知识库的数据流转是这样的:写笔记(Obsidian/Markdown)→ notify 监听变更 → 增量索引(分块 + Embedding)→ 向量数据库 + 全文索引 → 搜索查询 → 语义搜索 + 关键词搜索 → RRF 融合 → 搜索结果 → 可选 AI 摘要。
这条链路的每个环节都是自动化的——写完笔记后不需要任何手动操作,知识库自动更新索引。搜索时也不需要指定搜索方式(语义还是关键词),系统自动做混合搜索。
4.2 持续使用是最大的挑战
搭建知识库不难,但持续使用才是真正的挑战。我之前试过 Notion、飞书、Obsidian,每次都是"前两周认真记,第三周开始偷懒,第四周彻底放弃"。原因很简单:手动维护太累——每次写完笔记要手动整理、手动标签、手动同步。
Rust 知识库解决了手动维护的问题:文件监听自动索引,搜索自动混合,不需要任何手动操作。写笔记就是正常写 Markdown,搜索就是敲一行命令,中间的过程全部自动化。
但还有一个挑战没有解决:知识来源的多样性。目前只支持 Markdown 笔记和代码片段,网页剪藏和 GitHub Issues 的采集还没实现。这意味着我在飞书和微信里记的东西还是散落的——知识库只有 70% 的知识碎片,剩下的 30% 仍然找不到。
五、总结
用 Rust 搭建个人知识库这件事,从技术上看并不难——向量数据库、全文索引、文件监听这些都有成熟的库。真正难的地方在于:坚持用。
几个复盘心得:
- 数据来源要多样化:知识库的价值在于把所有知识碎片聚合在一起。笔记、代码、网页、Issues——来源越多,搜索越有价值。目前我只实现了 Markdown 和代码片段,后续要补充网页剪藏和 GitHub Issues
- 增量索引是持续使用的关键:手动触发索引会让人懒得更新,文件监听 + 自动索引才能保证数据始终最新。这个设计让知识库的维护成本降到零
- 混合搜索优于纯向量搜索:向量搜索在语义匹配上好,但精确匹配(如函数名、配置项)时全文搜索更准,两者结合才是王道。RRF 融合的公式虽然简单,但效果出奇地好
- 本地嵌入模型是趋势:fastembed 等 Rust binding 可以让整个系统完全离线运行,适合公司内网等安全需求高的场景。我的当前方案依赖 OpenAI API,离线场景下不可用——这是下一步要解决的问题
作为自学转码者,这个项目对我来说有特殊意义——它不只是一个工具,更是我学习 Rust 一年来的"知识资产"容器。每次搜索到自己以前记的笔记,都有一种"原来我这么认真学过"的满足感。知识库让碎片化的学习变成了可检索、可回顾的系统化积累。
保持学习,保持输出。虽然现在还是个菜鸡,但我相信只要坚持,总能写出越来越好的代码。
