1. 项目缘起:为什么我决定把数据库从架构图里踢出去
1.1 AI Agent 的数据需求,跟传统数据场景完全不是一回事
先交代背景。我最近在做一个偏个人向的 AI Agent 项目,目标是让一个本地运行的智能体帮我管理日常任务、沉淀知识碎片、追踪一些周期性的调研工作——你可以理解成给自己的“第二大脑”写一套操作系统。项目做到第三周,我遇到了一个绕不过去的设计决策:智能体的“记忆”和“工作状态”到底放哪里?
第一反应当然是上数据库。PostgreSQL 太重,SQLite 轻量又靠谱,先用来存任务、存记忆、存对话记录,看起来天经地义。但真正动手之后,我发现 AI Agent 的数据画像跟传统应用有本质区别:
- 结构极不规则。Agent 产生的内容以自然语言文本、Markdown、JSON、图片、中间推导过程为主,你很难在写代码的第一天就定义好它未来三个月会长成什么样。
- 读写是“人类向”的。我自己需要直接查看、修改、补充,不是只有程序在消费数据。我打开一个任务目录要能看到完整上下文,而不是去数据库里连表查询。
- 状态变化频繁且不可穷举。Agent 执行任务的过程会产生很多中间态,你永远不知道一个新任务会带来哪些字段。数据库表加列要迁移,这个代价在快速迭代期非常昂贵。
换句话说,AI Agent 的核心数据不是“记录”,而是“沉淀物”。记录适合放进表结构,沉淀物更适合放在一个你能伸手摸到的文件系统里。
1.2 文件系统凭什么敢叫自己“数据库”
我承认“文件系统就是新的数据库”这个说法有点标题党,但它背后的逻辑是扎实的。把文件系统当成数据库来用,并不是真要拿它跑高并发 OLTP,而是因为文件系统天然具备一整套数据库最重要的能力:
- 目录层级天然就是 schema。数据库设计要先建表、定义字段、确定关系。文件系统不用,你按业务域建目录,目录里放文件,文件里放内容,这本身就是一种结构。而且重构很便宜——
mv一个目录,比ALTER TABLE加一次迁移要轻松太多。 - 路径就是主键。每条数据的主键不用是自增数字,而是人类可读的路径。
~/workspace/memory/2025/03/product-design-principles.md这一串字符,既表达了内容主题,又编码了时间信息,还天然具备唯一性。 - 文件名就是一种索引。数据库里你为了查询快,要设计复合索引。文件系统里,你把
2025-03-18-review-pdf写进文件名,ls和find就是你的索引扫描器,铺开目录一秒定位。 - 扩展名和 front matter 就是字段。你完全可以用 Markdown 文件的 YAML 头来承载结构化字段,正文承载非结构化内容。这一招兼顾了数据库的字段查询能力和文档本身的阅读体验。
- 版本管理是白送的。任何数据库要做得像 Git 一样留痕、回滚、分支,成本都非常高。但文件系统配合 Git,天然就是一套完整的审计日志系统,每次提交都是时间旅行。
我把这个思路想清楚之后,整个项目的架构方向就变了:与其在数据库之上模拟文件系统的灵活性,不如直接在文件系统之上搭建 Agent 的操作层。
1.3 适用边界:这套方案不适合谁
先泼一盆冷水。“文件系统做数据库”有它的适用边界,我自己的项目里也用了一套混合策略,后面会讲到。
- 不适合高频、高并发的写入场景。文件系统没有事务,并发写同一个文件必然出问题。
- 不适合复杂的跨实体关联查询。你要统计“所有本周到期且优先级高的任务”,在文件系统里做要遍历全部文件再过滤,远不如 SQL 一条语句来得干净。
- 不适合超大规模数据。十万个文件以上,文件系统的性能下降很厉害,但个人层面的量级根本不会触到这个天花板。
- 不适合多用户协作。并发控制和权限管理你需要完全自己实现。
我的建议是:如果你做的是个人工具、自用 Agent、小团队内部系统,文件系统方案会是体验最好的选择。一旦涉及多人高频并发写同一份数据,还是老老实实混合使用——文件系统放正文,SQLite 放高频查询索引,这个组合反而是最优解。
2. 个人操作系统的整体设计与架构拆解
2.1 核心设计思路:让 Agent 直接操作“真实世界”
整个系统的设计目标,不是做一个聊天机器人或者任务队列,而是做一个“个人操作系统”——所有东西都可以被 Agent 感知、被用户看见、被工具链处理。我在架构上分了四层:
| 层级 | 职责 | 技术选型 |
|---|---|---|
| 存储层 | 所有数据落在文件系统 | 一个本地工作根目录 |
| 对象层 | 数据以“对象”为单位组织 | 每个对象是一个目录,内含元数据、正文、产出物 |
| 代理层 | 巡检、索引、执行任务流转 | Python 脚本 + Agent 主程序 |
| 接口层 | 提供读写、检索、状态转换 API | pathlib + 少量标准库 |
最关键的设计决策是:Agent 对“自己的工作”没有任何私有的数据库,它工作的时候就是在读写真实文件。这样做的收益立竿见影:
- 你能用任意一个文本编辑器去看 Agent 到底想了什么、做了什么。
- 任何一步 Agent 卡住或出错,你直接手动改文件就能纠正状态,不用去操作数据库。
- Agent 每一次运行都会留下可追溯的文件痕迹,时间和路径天然记录在文件名和 front matter 里。
这个设计也复用了 Unix 哲学里“一切皆文件”的核心思想。Agent 的记忆、知识、任务、产出、日志,全部都是目录和文件。对我自己来说,这套模式的体验比“一个聊天窗口带走一切”的黑盒方案强太多——因为它把内部状态完全暴露在你眼前。
2.2 目录结构:每个业务域都是一棵子树
工作根目录我放在~/agent-workspace/,下面是完整的目录规划:
~/agent-workspace/ ├── inbox/ # 临时输入区,Agent 从这里领取新内容 ├── tasks/ # 任务对象区 │ └── T-20250720-001/ │ ├── meta.yaml # 状态、优先级、截止时间、标签 │ ├── context.md # 任务背景与输入上下文 │ ├── progress.md # 执行日志与当前进度 │ └── output/ # 最终产出物 ├── memory/ # 长期记忆区 │ └── 2025/07/ │ └── conversation-summary.md ├── knowledge/ # 知识库区,按主题分目录 │ └── ai-tools/ │ ├── index.md # 主题索引 │ └── 2025-07-20-comparison.md ├── notes/ # 随手笔记,inbox 的分流目标 ├── archive/ # 已完成、已归档的区域 └── .agent/ ├── templates/ # 目录模板 ├── scripts/ # 辅助脚本 └── state.yaml # Agent 自身运行状态下面对几个关键目录做一点解释:
- inbox 是所有输入的入口。不管是我的随手想法、剪藏的文章、还是 Agent 自动抓取的信息,都先进 inbox。Agent 做第一轮分类后,把它们分发到 tasks、notes 或 knowledge。这个模式很像 GTD,也让 Agent 永远有一个确定的“待处理队列”。
- task 是最高频使用的对象。每个任务一个目录,任务目录内固定四个元素:meta.yaml 描述结构性信息,context.md 描述背景,progress.md 记录过程,output 承载结果。任务创建时用模板自动生成,Agent 每次更新都追加 progress.md 而不是覆盖,这就形成了一条天然的工作时间线。
- memory 和 knowledge 的差别。memory 存取“关于你的事”,比如对话风格偏好、习惯、最近在忙什么;knowledge 存“关于世界的事”,比如你调研过的主题、整理出来的方法。区分开之后,Agent 读取上下文时可以有针对性,不会把所有东西都灌进一次请求。
2.3 元数据约定与命名规范:把规矩定在前面
文件系统做数据库的最大风险是“乱”。没有表结构约束,写废了就是一片混沌。所以我在项目一开始就给整个系统立了三层规矩:
第一层:任务 ID 用“语义前缀 + 时间戳 + 序号”。T-20250720-001一眼就能看出是 2025 年 7 月 20 号的第 1 个任务。文件排序天然按时间走,不需要额外的时间字段。
第二层:所有 Markdown 正文必须有 YAML front matter。哪怕只有一个字段,也要写。这个习惯保证了任何文件单独拎出来,Agent 都能通过解析头部拿到关键信息,不需要全文扫描才能判断它是什么。
--- type: task-logs task_id: T-20250720-001 status: in-progress priority: high tags: [research, ai-tools] created_at: 2025-07-20T09:30:00+08:00 updated_at: 2025-07-20T10:15:00+08:00 ---第三层:文件命名要“自描述”。我的命名公式是日期-主题-类型,比如2025-07-20-ai-tools-comparison-report.md,一个文件名里同时编码时间和内容和类型。很少需要打开文件才能判断它是什么。
这三层规矩花了我一个下午的时间去设计,但在后续一个月的开发中帮我省了无数力气。Agent 的代码里写数据的时候永远按规范生成,读数据的时候永远按规范解析,两边都轻松。
2.4 为什么不用现成的“知识库工具”
有人可能会问:Notion、Obsidian、各种本地知识库管理工具都现成的,为什么要自己造一套?我试过,也确实从中借鉴了不少思路。但它们都缺了最关键的一环:一个能自由编程的自动化层。
- 知识库工具的重心是“人写笔记、人读笔记”,Agent 不是一等公民,脚本接口很弱。
- 很多工具的数据存储在私有格式或隐藏目录里,你无法直接用 Python 脚本安全地读写。
- 自动化的核心是“状态机”,而状态机需要程序驱动。用纯文本文件和目录结构,任何语言都能操作,任何工具都能介入。
所以我宁可自己维护二十多行 Python 脚本,也不要被某个工具的生态绑住手脚。文件系统本身就是一个最持久、最通用、最不锁定的平台。
3. 核心实现:从零搭建这套个人操作系统
3.1 最小可运行版本:先让 Agent 能“读文件、写文件、改状态”
我不会一上来就给你展示一个几百行的完整项目,而是先搭一个最小可运行版本。整个系统的第一版只需要三个脚本逻辑:
读取目录结构——Agent 通过pathlib遍历工作目录,把任务对象加载成结构化数据:
from pathlib import Path import yaml WORKSPACE = Path.home() / "agent-workspace" def load_task(task_path: Path) -> dict: meta = yaml.safe_load((task_path / "meta.yaml").read_text(encoding="utf-8")) context = (task_path / "context.md").read_text(encoding="utf-8") progress = (task_path / "progress.md").read_text(encoding="utf-8") return { "meta": meta, "task_id": meta.get("task_id"), "status": meta.get("status"), "context": context, "progress": progress, } def list_tasks(status=None): tasks = [] for task_dir in (WORKSPACE / "tasks").iterdir(): if not task_dir.is_dir(): continue task = load_task(task_dir) if status is None or task["status"] == status: tasks.append(task) return tasks这是整个系统最核心的“查询引擎”——遍历目录 + 解析 front matter + 返回 Python 对象。没有 SQL,没有连接池,没有 ORM,但足够满足个人 Agent 所有业务需求。
写入与状态变更——Agent 执行任务时更新 progress.md,状态变更则通过修改 meta.yaml 的 status 字段完成。为了保证修改不破坏已有内容,我用的是“追加式”日志模型:
import datetime def append_progress(task_id: str, content: str): task_dir = WORKSPACE / "tasks" / task_id progress_file = task_dir / "progress.md" with progress_file.open("a", encoding="utf-8") as f: f.write(f"\n## {datetime.datetime.now().isoformat()}\n\n") f.write(content) f.write("\n")这个函数并不复杂,但它定义了一个重要的行为规范:progress.md 只追加不覆盖。这样任务的时间线天然保存在文件本身里,回看的时候就是完整的工作日志。
状态迁移——任务状态从todo到in-progress到done,用update_task_status实现:
def update_task_status(task_id: str, new_status: str): task_dir = WORKSPACE / "tasks" / task_id meta_file = task_dir / "meta.yaml" meta = yaml.safe_load(meta_file.read_text(encoding="utf-8")) meta["status"] = new_status meta["updated_at"] = datetime.datetime.now().isoformat() meta_file.write_text(yaml.dump(meta, allow_unicode=True), encoding="utf-8")看到这里你应该已经明白了:数据库的表、索引、事务、视图,在文件系统方案里分别被目录、命名、追加、front matter 替代了。每一条规则都很朴素,组合起来就成了一整套可工作的存储方案。
3.2 更进一步:状态机与巡检逻辑
脚本只是工具,真正让它变成“操作系统”的是 Agent 主程序里的巡检逻辑。Agent 的默认工作循环如下:
- 扫描
inbox/目录,把每个新文件按类型分发。 - 扫描
tasks/目录,找到所有status: todo且已到开始时间的任务。 - 对每个任务,加载 context.md 和 progress.md,连同相关的 knowledge 和 memory 拼成上下文,交给大模型执行。
- 执行结果追加到 progress.md,必要时把产出物写入
output/子目录。 - 再次读 progress.md,判断是否满足完成条件。满足就更新 meta.yaml 的 status 为
done,并把整个任务目录移到archive/。 - 定期把新完成任务的摘要写入 knowledge 对应主题目录。
这套巡检逻辑相当于数据库里的“定时任务 + 触发器 + 物化视图”。在文件系统上实现并不复杂,因为它本质上就是几个 Python 循环加文件操作。关键的好处是:系统在任何一步中断,都不会丢数据。最坏情况是某个文件写到一半,重来一次就行。数据库方案里你反而要处理事务回滚、锁状态这些问题。
3.3 一个完整示例:Agent 执行一次“工具调研”任务
我拿项目里真实发生过的一个任务来完整走一遍流程,你就能直观感受这套系统的运转方式。
第一步:我往inbox/扔了一条消息:“调研一下现在最值得关注的三款 AI 编码辅助工具,对比优缺点,输出报告。”
第二步:巡检逻辑发现 inbox 有新文件,提取内容后调用任务创建函数生成tasks/T-20250720-001/目录,写入模板文件和任务背景,状态设为todo:
--- type: task task_id: T-20250720-001 status: todo priority: medium tags: [research, ai-tools] created_at: 2025-07-20T09:30:00+08:00 ---第三步:Agent 主程序发现todo任务,开始执行。它先去knowledge/ai-tools/目录读取已有笔记作参考,再向大模型发起调用。模型生成的内容被追加进progress.md。
第四步:Agent 判断报告还缺“对比表”,于是再次调用模型补充,最后把完整报告写到output/2025-07-20-ai-tools-comparison.md。
第五步:Agent 重新读取 progress.md 和 output 文件,确认输出完整,于是把meta.yaml的 status 改为done,把整个目录移入archive/,并在knowledge/ai-tools/index.md里追加一条摘要。
整个流程结束之后,我打开archive/T-20250720-001/,里面有一个任务从创建到完成的全过程。不用 Agent 跟我解释它做了什么,我自己打开文件看一遍就全明白了。
3.4 检索能力:你只需要一个 ripgrep
有人会担忧:没有数据库,怎么快速搜索?我的答案很直接:用rg(ripgrep)。在个人文件规模下,ripgrep 全文搜索的速度基本是秒回的。
搜索所有“包含某个关键词且状态为 done 的任务”:
rg -l "关键词" ~/agent-workspace/tasks ~/agent-workspace/archive --glob "!**/output/**"再结合文件名、front matter 标签做精确过滤:
rg -l '^status: done$' ~/agent-workspace/archive -g "meta.yaml" | head -20rg还可以输出 JSON 格式,方便程序解析:
rg -l "关键词" --json ~/agent-workspace/knowledge对个人 Agent 场景来说,检索速度根本不是瓶颈,真正的瓶颈是“能不能想清楚要检索什么”。文件系统方案让我不得不先把目录、命名、标签规划清楚——这反而是个好事,因为规划和检索本来就是同一件事。
4. 实战中踩过的坑:问题排查与方案优化
4.1 并发写入之争:AI Agent 自己跟自己抢文件
我最早遇到的问题是并发写入。Agent 在巡检任务的同时,我自己用编辑器打开了同一个 meta.yaml 进行修改,两边同时写入,结果就是互相覆盖。排查半天发现是经典的 read-modify-write 竞态问题。
后来我引入了两个约定:
- 所有写入都走“临时文件 + 原子替换”。先写
.tmp文件,再os.replace()覆盖目标文件,避免写一半被读到。 - Agent 和用户不要同时改同一个文件。分工是:Agent 只追加 progress.md 和更新 output 目录,用户只改 meta.yaml 的优先级或截止时间。两个类别的冲突概率降到非常低。
Python 的原子写入实现非常简单:
import os, tempfile def atomic_write(path: Path, content: str): tmp_path = path.with_suffix(".tmp") tmp_path.write_text(content, encoding="utf-8") os.replace(tmp_path, path)4.2 文件数量膨胀:定期归档不能懒
项目跑到第四周,工作目录有超过一千个文件,ls 变慢了,Agent 巡检一次任务目录要花两三秒。我意识到问题出在“文件系统不是无限扩张的数据库”。
解决方案是建立强制性的归档机制:
- 任务完成时自动移动到
archive/目录,核心目录只保留活跃状态。 - 每月自动生成上月工作的汇总索引文件,然后允许把原始分散文件按月份目录打包。
- knowledge 目录按主题维护
index.md,不让碎片无限堆积。
这套机制本质上就是数据库里的“冷热数据分离”。重要数据留在热存储区保证访问效率,历史数据进冷区但依然可查。
4.3 命名里的坑:文件名别用中文
一开始我没注意,任务目录名直接用了中文——因为描述性的内容直观。结果遇到了两个麻烦:一是有些命令行工具在某些环境下对中文文件名的排序和通配符处理有幺蛾子;二是文件名里的中文让 URL 编码和日志打印变得丑陋。
我最终的命名规范是:文件名一律用英文 slug,中文只出现在 front matter 的 title 字段里。这样文件系统层保持干净,而展示层想要什么语言都可以通过解析 front matter 解决。
4.4 问题排查速查表
下面整理了我实际踩过的一些典型问题,附上直接能用的排查命令:
| 问题现象 | 可能原因 | 排查命令 / 方法 |
|---|---|---|
| Agent 找不到某个任务 | 目录路径拼写错误 | ls ~/agent-workspace/tasks先看实际目录名 |
| 状态一直不更新 | meta.yaml 格式损坏 | python -c "import yaml; yaml.safe_load(open('meta.yaml'))" |
| 检索结果不符合预期 | 忽略了文件名过滤 | 用rg -g "*.md"明确限定文件类型 |
| 任务目录混乱 | 没有按模板生成 | 检查.agent/templates/并手工重建 |
| 多个进程同时写文件 | 并发竞态 | 统一用 atomic_write,不要直接写目标文件 |
| Git 提交记录过多 | 每个小改动都提交 | 改为“任务完成时提交一次” |
| Agent 上下文太长 | 把整个 knowledge 目录都读进去了 | 改用 index.md 摘要 + 按需读取 |
这列表你留着,遇到问题先对照一遍,大概率能省掉一小时的排查时间。
4.5 为什么不建议做成 SQLite + 文件系统的二合一(以及我最终的选择)
我在 1.3 节提到过用 SQLite 保存高频索引。实际实验中,我也真的试过用 SQLite 存任务的 front matter 元数据,文件系统存正文,做一个混合方案。测试下来发现:两边的状态同步是一个非常隐蔽的麻烦源。
- Agent 更新了 meta.yaml,SQLite 里的索引不会自动跟着更新,你必须维护同步逻辑。
- 一旦同步逻辑出 bug,你无法确定文件系统和数据库到底谁是对的。
- 为了保持一致性,你得引入事件钩子或定时重建索引,复杂度像滚雪球一样越来越大。
最后我的结论是:个人 Agent 的数据规模根本不需要 SQLite 这一层。一个设计良好的文件系统方案,配合 ripgrep 的搜索速度,已经覆盖了 95% 的查询需求。单机场景就不要自己给自己制造分布式一致性难题。只有当你开始写复杂统计报表、需要按多维度聚合查询时,再把对应数据导出到 SQLite 做离线分析,而不是把它写进核心读写链路。
5. 后续扩展:这套系统还能往哪些方向长
文件系统方案还有一个隐藏优势:扩展方向非常多,因为所有底层都是通用文件格式。
- 我把 memory 目录接入了一个向量化脚本,定期把 Markdown 文件转成向量导入到本地向量库做语义检索。但原始文件仍然是唯一可信源,向量库只是衍生索引,丢了可以随时重建。
- 我在 knowledge 目录上做了一个简单的 MOC(Map of Content)机制,每个主题目录的 index.md 就是导航入口,Agent 可以沿着索引进行多跳阅读。
- 以后如果要支持多设备同步,直接对工作目录做
rsync或者同步盘同步即可,因为服务端看到的依然是普通文件,没有任何私有格式障碍。 - 我甚至有一个想法是把这套系统变成一个“可编程的个人数据工作台”,让不同的 Agent 子模块各管一个目录,互不干扰,又可以通过文件互相协作。文件系统的权限隔离天然支持这种多租户模式。
这些扩展成本都很低,根源就在于“文件是通用接口”。如果你一上来把数据全部锁进某私有数据库,每一次新需求都在跟存量数据格式搏斗。而文件系统方案把底层的苦都吃掉了,上面要长什么花,随你。
我在实际使用中最深的体会是:技术的选择往往不是能力够不够的问题,而是心智负担的问题。数据库给不了你直接打开任务目录就能改状态的痛快感,文件系统给了。当一个系统让你在操作时感觉“所有东西都摊开在桌面上”,你会更愿意持续使用和维护它。这也是我最终把整套个人操作系统的存储层坚定压在文件系统上的原因。