Zettel 森林的自我生长

市面上有很多基于 Markdown 的 Zettelkasten 笔记系统最著名的莫过于 Obsidian然而

  • 我不想使用 Markdown

    • 正如我在这篇文章中讨论的那样Typst 是一个加强版 Markdown 和 LaTeX 的混合体具有更强的表达能力和更好的可读性我希望我的 Zettelkasten 笔记系统能够直接使用 Typst 来编写笔记
    • 由于我的博客专题笔记是用 Typst 编写的如果能够保持相同的技术栈整理笔记和撰写博客就可以无缝衔接效率更高
  • 我想要一个基于我个人编辑环境的 Zettelkasten 笔记系统对于我而言就是 Neovim

Neovim 的好处是高度的可扩展性可能的坏处就是需要我们自己动手实现想要的功能好在现在有 AI Agent在这种脚本型自己用的开发环境上效率还是非常高的关键问题在于 Context Engineering 的实践这篇博客想要传达的中心观点就是构建 Zettelkasten 笔记系统所需要的上下文可以通过 Zettelkasten 笔记系统构建

什么是 Zettelkasten

你可以参考这个来获得一个很好的概述我们在这里只做一个非常简短的介绍

  • Zettelkasten 是一个德文单词意思是卡片盒
  • 卡片盒中的对象是一系列卡片这些卡片承载着单一主题的信息例如一个概念一个想法一个问题等被称为原子式的笔记
  • 我们已经提到了对象那自然还会想到有态射 Zettelkasten 中的态射就是链接通过链接来构建笔记之间的关系例如在撰写有关 Flat Connection 和 Local System 之间关系的笔记时我可以写

    
                    
    = Flat/Integrable Connection and Local Systems <2602151850>
    
                    
    #tag.geometry
    
                    
    
    
    
                    
    Let $X$ be a complex-analytic variety.
    
                    
    The following are equivalent:
    
                    
    
    
    
                    
    - Local system $->$ Holomorphic vector bundle + Flat connection
    
                    
      - The complex @2602150951 $V$;
    
                    
      - The @2602151916 $cal(V) = cal(O) times.o V$ endowed with its canonical connection.
    
                    
    = Flat/Integrable Connection and Local Systems <2602151850>
    
                    
    #tag.geometry
    
                    
    
    
    
                    
    Let $X$ be a complex-analytic variety.
    
                    
    The following are equivalent:
    
                    
    
    
    
                    
    - Local system $->$ Holomorphic vector bundle + Flat connection
    
                    
      - The complex @2602150951 $V$;
    
                    
      - The @2602151916 $cal(V) = cal(O) times.o V$ endowed with its canonical connection.
    
                    
    = Flat/Integrable Connection and Local Systems <2602151850>
    
                    
    #tag.geometry
    
                    
    
    
    
                    
    Let $X$ be a complex-analytic variety.
    
                    
    The following are equivalent:
    
                    
    
    
    
                    
    - Local system $->$ Holomorphic vector bundle + Flat connection
    
                    
      - The complex @2602150951 $V$;
    
                    
      - The @2602151916 $cal(V) = cal(O) times.o V$ endowed with its canonical connection.
    
                    
    = Flat/Integrable Connection and Local Systems <2602151850>
    
                    
    #tag.geometry
    
                    
    
    
    
                    
    Let $X$ be a complex-analytic variety.
    
                    
    The following are equivalent:
    
                    
    
    
    
                    
    - Local system $->$ Holomorphic vector bundle + Flat connection
    
                    
      - The complex @2602150951 $V$;
    
                    
      - The @2602151916 $cal(V) = cal(O) times.o V$ endowed with its canonical connection.

    其中@2602151916@2602151916@2602150951@2602150951 就是链接分别指向了Holomorphic vector bundleHolomorphic vector bundleLocal systemLocal system这两个概念的笔记对象这样我们

    • 在撰写时不必在每个笔记中重复定义这些概念节省时间保证一致性
    • 在阅读时如果不清楚这些概念可以通过链接快速跳转到相关的笔记如果清楚则可以一笔带过
image
Zettelkasten 笔记示例我配置了 Neovim Extmark 将链接的笔记标题覆盖在链接上在保证链接唯一性的同时提高了可读性
image
Zettelkasten 笔记示例我配置了 Neovim Extmark 将链接的笔记标题覆盖在链接上在保证链接唯一性的同时提高了可读性

这或许让人感到非常熟悉没错无论是 Wikipedia, nLab 还是 Stack Project他们都是通过这种 Zettelkasten 的方法来构建的

  • 每一个条目原子化地承载单一概念
  • 通过链接来构建不同概念的关联

从某种意义上说构建一个个人的 Zettelkasten 笔记系统就是在构建一个个人的 Wikipedia/nLab/Stack Project

Zettelkasten 森林的生长

正如所有 AI 编码项目一样最大的挑战就是如何设计好 Prompt在我们这个项目中Prompt 的设计主要体现在两个方面

  • 长线作战我们是一个长期维护的项目这意味着每一次单开一次 AI session都将面对一个全新的 AI Agent作为一个 stateless 的对象我们需要手动维护一个巨大的上下文包含但不限于

    • 整体的项目架构包含整个项目的愿景与结构等
    • 当前的开发进度包含已经完成的功能和待完成的功能等
  • 短暂任务迭代笔记系统并不是我们的主要生活AI 参与的敏捷开发时保持笔记常青的唯一途径

我们会注意到构建笔记系统的需求本质上都是原子式可以通过单主题的语言来描述例如加一个快捷键Telescope 插件增加一个功能然而当我们需要将这些原子式的需求投喂给 AI 时就必须要包含其依赖这事实上与 Zettelkasten 的核心理念是非常契合的每一个笔记都是一个独立的原子式对象通过链接来构建它们之间的关系这就是我们使用 Zettelkasten 方法来实现上下文工程的初衷

接下来我们就来开始简单地展示我是如何让 Zettelkasten 方法结合 AI 实现笔记系统的自我生长的

首先我们需要一个 idea在 Zettelkasten 笔记法中就对应这一个笔记当然我们现在还处于鸿蒙初开的时期啥也没有这得要我们自己先构建一个项目基本架构我的想法是~/wiki/~/wiki/ 目录下构建我的知识库并包含一些入口文件和模板文件正文放在 ~/wiki/note/~/wiki/note/ 目录下文件名为时间戳例如 2602151850.typ2602151850.typ把这个想法写成文字这就是我们这个 Zettelkasten 笔记系统中的第一个笔记了 (由于基本是终端阅读导向因此 ASCII 风格的树状结构就足够了)

Remark
我们这里所有的 Tag 为了方便全部写成类似 <1><1> 的样子而不是按照规范写成时间戳主要是为了方便读者阅读
Remark
我们这里所有的 Tag 为了方便全部写成类似 <1><1> 的样子而不是按照规范写成时间戳主要是为了方便读者阅读

            
= Typst ZK Note Taking <1>

            


            
我们想要构建一个基于 `Typst` 的 ZK 笔记系统来管理我们的笔记和知识库

            


            
== 结构

            


            
`~/wiki/` 目录下的结构如下

            
```

            
wiki/

            
├── index.typ

            
├── link.typ

            
├── note/

            
│   ├── 2602072319.typ

            
│   ├── 2602082037.typ

            
│   ├── 2602082106.typ

            
```

            
其中 `index.typ` 是整个 wiki 的入口文件包含了所有的函数调用样式配置与定义`link.typ`包含了所有`note/`下笔记的`#include`y 语句`note/`目录下的每个`.typ`文件都是一个笔记文件名是笔记的时间戳格式为`yyMMddHHmm`

            
- `#include` 语句的格式为 `#include "note/xxxx.typ"`其中 `xxxx` 是笔记文件的时间戳格式为`yyMMddHHmm`

            
- 整体结构为

            
```typ

            
// index.typ

            
// ... 其他内容 ...

            
#include "note/<YYMMDDHHMM>.typ"

            


            
// note/YYMMDDHHMM.typ

            
#import "../include.typ": *

            
#show: zettel

            


            
= Note Title <YYMMDDHHMM> // Note title 用户更新label 则由系统自动生成

            
@YYMMDDHHMM 链接其他笔记

            
```

            
= Typst ZK Note Taking <1>

            


            
我们想要构建一个基于 `Typst` 的 ZK 笔记系统来管理我们的笔记和知识库

            


            
== 结构

            


            
`~/wiki/` 目录下的结构如下

            
```

            
wiki/

            
├── index.typ

            
├── link.typ

            
├── note/

            
│   ├── 2602072319.typ

            
│   ├── 2602082037.typ

            
│   ├── 2602082106.typ

            
```

            
其中 `index.typ` 是整个 wiki 的入口文件包含了所有的函数调用样式配置与定义`link.typ`包含了所有`note/`下笔记的`#include`y 语句`note/`目录下的每个`.typ`文件都是一个笔记文件名是笔记的时间戳格式为`yyMMddHHmm`

            
- `#include` 语句的格式为 `#include "note/xxxx.typ"`其中 `xxxx` 是笔记文件的时间戳格式为`yyMMddHHmm`

            
- 整体结构为

            
```typ

            
// index.typ

            
// ... 其他内容 ...

            
#include "note/<YYMMDDHHMM>.typ"

            


            
// note/YYMMDDHHMM.typ

            
#import "../include.typ": *

            
#show: zettel

            


            
= Note Title <YYMMDDHHMM> // Note title 用户更新label 则由系统自动生成

            
@YYMMDDHHMM 链接其他笔记

            
```

            
= Typst ZK Note Taking <1>

            


            
我们想要构建一个基于 `Typst` 的 ZK 笔记系统来管理我们的笔记和知识库

            


            
== 结构

            


            
`~/wiki/` 目录下的结构如下

            
```

            
wiki/

            
├── index.typ

            
├── link.typ

            
├── note/

            
│   ├── 2602072319.typ

            
│   ├── 2602082037.typ

            
│   ├── 2602082106.typ

            
```

            
其中 `index.typ` 是整个 wiki 的入口文件包含了所有的函数调用样式配置与定义`link.typ`包含了所有`note/`下笔记的`#include`y 语句`note/`目录下的每个`.typ`文件都是一个笔记文件名是笔记的时间戳格式为`yyMMddHHmm`

            
- `#include` 语句的格式为 `#include "note/xxxx.typ"`其中 `xxxx` 是笔记文件的时间戳格式为`yyMMddHHmm`

            
- 整体结构为

            
```typ

            
// index.typ

            
// ... 其他内容 ...

            
#include "note/<YYMMDDHHMM>.typ"

            


            
// note/YYMMDDHHMM.typ

            
#import "../include.typ": *

            
#show: zettel

            


            
= Note Title <YYMMDDHHMM> // Note title 用户更新label 则由系统自动生成

            
@YYMMDDHHMM 链接其他笔记

            
```

            
= Typst ZK Note Taking <1>

            


            
我们想要构建一个基于 `Typst` 的 ZK 笔记系统来管理我们的笔记和知识库

            


            
== 结构

            


            
`~/wiki/` 目录下的结构如下

            
```

            
wiki/

            
├── index.typ

            
├── link.typ

            
├── note/

            
│   ├── 2602072319.typ

            
│   ├── 2602082037.typ

            
│   ├── 2602082106.typ

            
```

            
其中 `index.typ` 是整个 wiki 的入口文件包含了所有的函数调用样式配置与定义`link.typ`包含了所有`note/`下笔记的`#include`y 语句`note/`目录下的每个`.typ`文件都是一个笔记文件名是笔记的时间戳格式为`yyMMddHHmm`

            
- `#include` 语句的格式为 `#include "note/xxxx.typ"`其中 `xxxx` 是笔记文件的时间戳格式为`yyMMddHHmm`

            
- 整体结构为

            
```typ

            
// index.typ

            
// ... 其他内容 ...

            
#include "note/<YYMMDDHHMM>.typ"

            


            
// note/YYMMDDHHMM.typ

            
#import "../include.typ": *

            
#show: zettel

            


            
= Note Title <YYMMDDHHMM> // Note title 用户更新label 则由系统自动生成

            
@YYMMDDHHMM 链接其他笔记

            
```

那么现在我们肯定要在这个主题下继续思考首先我会想到我需要一些脚本来帮助我

  • 自动编译 link.typlink.typ来包含 note/note/ 目录下的所有笔记的 #include#include 语句
  • 在 Neovim 中实现一些快捷键帮助我 CURD 笔记内容以及搜索笔记
  • 实现正向与反向跳转功能来帮助我在依赖关系中快速跳转

把它们全部记录在上面的笔记中就可以开始初步的构建了

前两个任务看起来都很简单为了让 AI 明白我们要干什么比如我们希望写一个lualua 脚本来实现第二个功能作为插件在 Neovim 中使用那么我们就可以写一个新的笔记来描述这个功能 (在我的实践中其实我是分了几个笔记分别追踪管理这些功能在这里我们就先放在一起)


            
= ZK and Neovim Tools <2>

            


            
我们需要构建编辑器工作流目前的规范参考 @1

            


            
自动化这个流程需要

            
-  自动根据当前日期时间生成 id并创建对应的 `.typ` 文件进入这个文件进行编辑

            
-  在创建文件的同时自动在 `link.typ` 中添加 `#include` 语句来引用这个 note

            


            
这将被实现为 Neovim 脚本

            
- [ ] `ze` 结合链接关系从当前笔记出发将所有直接或间接依赖的笔记都导出并复制到剪贴板为 AI 提供上下文

            
- [ ] `zn` 触发创建 note 的流程

            
  - 直接`cd`  `~/wiki/` 目录下执行创建 note 的命令命令包含

            
    - [ ] 生成 id按照模板创建文件打开文件进行编辑光标落在标题处

            
    - [ ]  `link.typ` 中添加 `#include` 语句

            
- [ ] `zs` 绑定 Telescope 插件按照标题等搜索笔记选中后跳转

            
- [ ] `zr` 绑定删除当前插件需要

            
  - [ ] 删除当前笔记文件

            
  - [ ]  `link.typ` 中删除对应的 `#include` 语句

            
= ZK and Neovim Tools <2>

            


            
我们需要构建编辑器工作流目前的规范参考 @1

            


            
自动化这个流程需要

            
-  自动根据当前日期时间生成 id并创建对应的 `.typ` 文件进入这个文件进行编辑

            
-  在创建文件的同时自动在 `link.typ` 中添加 `#include` 语句来引用这个 note

            


            
这将被实现为 Neovim 脚本

            
- [ ] `ze` 结合链接关系从当前笔记出发将所有直接或间接依赖的笔记都导出并复制到剪贴板为 AI 提供上下文

            
- [ ] `zn` 触发创建 note 的流程

            
  - 直接`cd`  `~/wiki/` 目录下执行创建 note 的命令命令包含

            
    - [ ] 生成 id按照模板创建文件打开文件进行编辑光标落在标题处

            
    - [ ]  `link.typ` 中添加 `#include` 语句

            
- [ ] `zs` 绑定 Telescope 插件按照标题等搜索笔记选中后跳转

            
- [ ] `zr` 绑定删除当前插件需要

            
  - [ ] 删除当前笔记文件

            
  - [ ]  `link.typ` 中删除对应的 `#include` 语句

            
= ZK and Neovim Tools <2>

            


            
我们需要构建编辑器工作流目前的规范参考 @1

            


            
自动化这个流程需要

            
-  自动根据当前日期时间生成 id并创建对应的 `.typ` 文件进入这个文件进行编辑

            
-  在创建文件的同时自动在 `link.typ` 中添加 `#include` 语句来引用这个 note

            


            
这将被实现为 Neovim 脚本

            
- [ ] `ze` 结合链接关系从当前笔记出发将所有直接或间接依赖的笔记都导出并复制到剪贴板为 AI 提供上下文

            
- [ ] `zn` 触发创建 note 的流程

            
  - 直接`cd`  `~/wiki/` 目录下执行创建 note 的命令命令包含

            
    - [ ] 生成 id按照模板创建文件打开文件进行编辑光标落在标题处

            
    - [ ]  `link.typ` 中添加 `#include` 语句

            
- [ ] `zs` 绑定 Telescope 插件按照标题等搜索笔记选中后跳转

            
- [ ] `zr` 绑定删除当前插件需要

            
  - [ ] 删除当前笔记文件

            
  - [ ]  `link.typ` 中删除对应的 `#include` 语句

            
= ZK and Neovim Tools <2>

            


            
我们需要构建编辑器工作流目前的规范参考 @1

            


            
自动化这个流程需要

            
-  自动根据当前日期时间生成 id并创建对应的 `.typ` 文件进入这个文件进行编辑

            
-  在创建文件的同时自动在 `link.typ` 中添加 `#include` 语句来引用这个 note

            


            
这将被实现为 Neovim 脚本

            
- [ ] `ze` 结合链接关系从当前笔记出发将所有直接或间接依赖的笔记都导出并复制到剪贴板为 AI 提供上下文

            
- [ ] `zn` 触发创建 note 的流程

            
  - 直接`cd`  `~/wiki/` 目录下执行创建 note 的命令命令包含

            
    - [ ] 生成 id按照模板创建文件打开文件进行编辑光标落在标题处

            
    - [ ]  `link.typ` 中添加 `#include` 语句

            
- [ ] `zs` 绑定 Telescope 插件按照标题等搜索笔记选中后跳转

            
- [ ] `zr` 绑定删除当前插件需要

            
  - [ ] 删除当前笔记文件

            
  - [ ]  `link.typ` 中删除对应的 `#include` 语句

这样就可以开始做了由于目前 zeze 还没有写出来还是得手动将两个笔记的内容复制到剪贴板来提供给 AI不过完事开头难把这个初始的目标做好后面会越来越快

编辑了几个笔记之后我发现写 - [ ]- [ ] 的时候非常辛苦因为要手动输入 [ ][ ]不放过这个想法也把它记录下来


            
= ZK TDL Keymap <3>

            
#tag.done

            


            
@2 中的 Neovim 插件还需要添加

            
- [x] 快速加 `- [ ]` 来标记待办事项 (`ctrl+t`)以及用快捷键来切换 `- [ ]`  `- [x]` 来标记完成状态 (如果已经有 `ctrl+t` 来切换状态)

            
= ZK TDL Keymap <3>

            
#tag.done

            


            
@2 中的 Neovim 插件还需要添加

            
- [x] 快速加 `- [ ]` 来标记待办事项 (`ctrl+t`)以及用快捷键来切换 `- [ ]`  `- [x]` 来标记完成状态 (如果已经有 `ctrl+t` 来切换状态)

            
= ZK TDL Keymap <3>

            
#tag.done

            


            
@2 中的 Neovim 插件还需要添加

            
- [x] 快速加 `- [ ]` 来标记待办事项 (`ctrl+t`)以及用快捷键来切换 `- [ ]`  `- [x]` 来标记完成状态 (如果已经有 `ctrl+t` 来切换状态)

            
= ZK TDL Keymap <3>

            
#tag.done

            


            
@2 中的 Neovim 插件还需要添加

            
- [x] 快速加 `- [ ]` 来标记待办事项 (`ctrl+t`)以及用快捷键来切换 `- [ ]`  `- [x]` 来标记完成状态 (如果已经有 `ctrl+t` 来切换状态)

直接调用 zeze 来导出当前笔记和上下文扔给 AI 写完验收

写着写着我发现@yyMMddHHmm@yyMMddHHmm的格式虽然方便索引但是不方便在笔记中阅读我注意到 Neovim 可以用 Extmark 在 buffer 上绘制一些额外的内容想法呼之欲出用 Extmark 将全文标题覆盖在 @yyMMddHHmm@yyMMddHHmm这样既可以保证文件名的唯一性和索引的便利性又可以让笔记内容更易读不放过这个想法创建一个笔记记录下来


            
= Extmark and ZK Link <4>

            


            
- [ ] 使用 Neovim 的 Extmark 功能在笔记中将 `@yyMMddHHmm` 的格式展示为对应笔记的标题例如 `@2602151850` 展示为 `@Flat/Integrable Connection and Local Systems`来提升笔记的可读性

            
= Extmark and ZK Link <4>

            


            
- [ ] 使用 Neovim 的 Extmark 功能在笔记中将 `@yyMMddHHmm` 的格式展示为对应笔记的标题例如 `@2602151850` 展示为 `@Flat/Integrable Connection and Local Systems`来提升笔记的可读性

            
= Extmark and ZK Link <4>

            


            
- [ ] 使用 Neovim 的 Extmark 功能在笔记中将 `@yyMMddHHmm` 的格式展示为对应笔记的标题例如 `@2602151850` 展示为 `@Flat/Integrable Connection and Local Systems`来提升笔记的可读性

            
= Extmark and ZK Link <4>

            


            
- [ ] 使用 Neovim 的 Extmark 功能在笔记中将 `@yyMMddHHmm` 的格式展示为对应笔记的标题例如 `@2602151850` 展示为 `@Flat/Integrable Connection and Local Systems`来提升笔记的可读性

直接调用 zeze 来导出当前笔记和上下文扔给 AI 写完验收

写着写着发现可能需要一个 Tag 标记笔记的状态类别比如todotodo wipwip neovimneovim这个直接在 Typst 的模板函数中实现就好了当然也需要在 Telescope 中实现按照 Tag 搜索的功能追加一个笔记


            
= ZK Tag Search <5>

            


            
我们需要在 @2 中的 Neovim 插件中添加

            
- [x] 通过 Tag 搜索笔记的功能利用 Telescope 来实现

            
  - `zs` 唤出 Telescope 搜索页面后按照 `ctrl+t` 来切换搜索模式切换到 Tag 搜索模式后输入 Tag 名称来搜索对应的笔记

            
= ZK Tag Search <5>

            


            
我们需要在 @2 中的 Neovim 插件中添加

            
- [x] 通过 Tag 搜索笔记的功能利用 Telescope 来实现

            
  - `zs` 唤出 Telescope 搜索页面后按照 `ctrl+t` 来切换搜索模式切换到 Tag 搜索模式后输入 Tag 名称来搜索对应的笔记

            
= ZK Tag Search <5>

            


            
我们需要在 @2 中的 Neovim 插件中添加

            
- [x] 通过 Tag 搜索笔记的功能利用 Telescope 来实现

            
  - `zs` 唤出 Telescope 搜索页面后按照 `ctrl+t` 来切换搜索模式切换到 Tag 搜索模式后输入 Tag 名称来搜索对应的笔记

            
= ZK Tag Search <5>

            


            
我们需要在 @2 中的 Neovim 插件中添加

            
- [x] 通过 Tag 搜索笔记的功能利用 Telescope 来实现

            
  - `zs` 唤出 Telescope 搜索页面后按照 `ctrl+t` 来切换搜索模式切换到 Tag 搜索模式后输入 Tag 名称来搜索对应的笔记

导出上下文后继续实现

渐渐地我们发现在不断使用这个系统的过程中

  • 由于创建笔记的压力在第一步便消去使得我们可以迅速地将想法记录下来形成一个个原子式的笔记
  • 在使用笔记时涌现出来的改进意见不断地被记录下来形成一个个原子式的功能需求
  • 通过将其在撰写过程中与整理过程中不断与之前的笔记对象进行链接形成一个个链接关系对象构成完整的需求文档
  • 将这个包含上下文与原子式需求的文档投喂给 AI 来实现

最终我们通过

  • 设计好笔记的结构与格式
  • 纪律性的记录规范与链接习惯

几乎没有任何压力非常自然地构建了一个基于 Typst 的 Zettelkasten 笔记系统并且通过 AI Agent 来实现了这个系统的自我生长

由于功能添加在 Zettelkasten 笔记系统中被记录为一个个原子式的笔记对象因此每当我们想要添加一个功能时我们只需要创建一个新的笔记来描述这个功能需求相对于为每一个功能都新建一个 Prompt 与 Context这样的途径显然阻力很小效率更高

当然这个构建的过程只是一个开始还有很多的 idea 值得被实现比如多端同步基于 LSP 的连接正向反向跳转基于标签的智能搜索等等这些都可以通过同样的方式来实现将它们记录为一个个原子式的笔记对象并且通过链接来构建它们之间的关系最终投喂给 AI 来实现验收这些功能我均已实现不过由于涉及到的技术细节比较多或许更适合在后续的文章中进行展示

Back to all articles

Loading comments...