实战 · 打造会记忆的AI 写作搭档(二):数据库篇(从 JSON 到单库,再到关系表)
如果你已经读过《实战 · 打造会记忆的AI 写作搭档(一):多 Agent 架构进化》,大概率对“多 Agent 如何协作、记忆如何串起来”有个整体印象。但真正让系统长期可用的,不只是一张好看的架构图,还得有一套能扛住增长的数据底座:能查、能改、能回溯。
这篇文章专注聊“事实层”(数据库)的演进:JSON 文件 → SQLite 单库(KV)→ SQLite 单库(关系表)。至于语义检索、混合检索、全图谱索引与云化迁移,我单独写在下一篇《实战 · 打造会记忆的AI 写作搭档(坤):检索系统篇(向量检索、混合检索与云化)》里。
长篇小说写作系统的本质,不是“写一段文本”,而是长期维护一个不断生长的世界:角色状态、势力关系、物品流转、地点层级、伏笔链条……随着字数增长,这些信息会指数级膨胀。
当数据只是“文本堆”,你会遇到三类必然问题:
- 查询不动:想找一段“类似氛围/类似冲突”的描写,或者想精确列出“某宗门现役成员”,都很难
- 一致性变差:删不干净、改了 A 忘了改 B、同一实体在不同地方重复定义
- 跨设备维护崩溃:多端同步、合并冲突、回滚备份变成体力活
目标一直很明确:
让数据变成“实体关系系统”,再叠加“检索索引层”,最终让 AI 不只是会写,还要会查、会记、不会乱。
0. 阶段零:JSON 文件(最省事,但很快遇到上限)
0.1 当时的选择
最早为了快速起步,我用文件系统存储:角色库、地图、世界观设定等以 JSON(或类 JSON)文件落盘。
它的好处非常直接:
- 零依赖:不需要数据库,不需要迁移脚本
- 可读可 diff:用 Git 看差异很舒服
- 适配 LLM:大模型提取出来就是 JSON,落盘几乎没有摩擦
0.2 很快遇到的问题
当数据量和功能开始增长,JSON 文件会暴露几个硬伤:
- 缺乏“全局唯一 ID”:一切都靠名字当键,重名、改名、别名会让数据不可控
- 关系难表达:角色↔宗门经历、角色↔功法熟练度、角色↔法宝持有这些都要手写嵌套结构,越来越难维护
- 跨端同步痛苦:两个设备同时修改同一个 JSON,合并冲突很难可靠解决
- 查询很弱:没有索引,最后会变成“加载 JSON → Python 遍历过滤 → 自己维护缓存”
升级的意义并不是“换个更复杂的东西”,而是:把“存档文件”变成“可运行的数据系统”。
1. 阶段一:SQLite 单库(KV 为主)——先把“数据聚合与备份”稳定住
1.1 解决的核心问题
我把早期的 JSON 内容迁移进 SQLite 的 kv_store(key/value)里:例如 character_db、map_db、世界观、未来规划等。
这一步的价值是把写作系统从“多文件散落”升级到“单文件事实来源”的雏形(注意:这并不等同于解决多端并发合并):
- 部署与备份简单:一个
novel.db文件就能跑(备份/回滚更可控) - 读写路径统一:不再到处散落读写逻辑
- 仍保留 JSON 优势:KV 里存的依然是人类可读的 JSON
边界也要说清楚:SQLite 把“事实来源”收敛成单文件,但如果用网盘去同步整个 db 文件,多端同时改写仍然会产生“冲突副本”,无法像文本那样可靠 merge。真正的跨设备同步要靠“中心化仲裁(上云)”或“基于操作日志(op-log)的可合并同步”(后面会在云化迁移里继续展开)。
(实现上会在应用初始化阶段创建 kv_store / chapters / drafts 等基础表,让数据读写从“多文件”收敛到“单库”。)
1.2 留下的问题
KV 的上限也很清晰:
- 查询的上限:所有复杂查询都要“拿出 JSON 再遍历”
- 关系表达的上限:关系被迫写成嵌套 JSON,删除/更新很难保证一致性
- 一致性边界模糊:同一个实体可能在多段 JSON 里被重复描述,冲突难以裁决
这一步适合“系统早期快速迭代”,但不适合“长期维护实体关系图谱”。
2. 阶段二:SQLite 单库(正文表 + KV)——明确“单一事实来源”
2.1 我做了什么
在同一个 data/novel.db 里,除了 kv_store,我也维护结构清晰的正文表:
chapters:章节元数据(title/ulid/时间戳/索引字段;章节正文拆到data/blob_store/)drafts:草稿
它的意义是把“写作正文”从文件读写升级为数据库记录,形成更稳定的版本与同步路径。
2.2 单一事实来源(Source of Truth)
从这里开始,我明确一条底层原则:
事实来源(Source of Truth)=
data/novel.db(结构化数据/元数据/KV/FTS) +data/blob_store/(章节正文对象)。 任何索引、缓存、衍生结构都必须可以从事实来源重建。
这条原则后面会直接决定“检索层”怎么设计:无论是全文检索还是向量检索,都只能是索引层,不能变成第二套事实来源。
3. 阶段三:SQLite 单库 + 关系表——让“记忆库”从文本堆变成实体关系系统
这一阶段的核心决策是:
直接以事实来源(
data/novel.db+data/blob_store/)作为底座:在同一个 SQLite 文件内新增关系型结构表来承载结构化知识。
3.1 为什么要做关系表?
因为写作资料库本质上是一个“实体-关系系统”。当你开始想做这些查询时,KV 模式就会变成维护地狱:
- “南海鳄神拥有哪些法宝/功法?熟练度分别是多少?”
- “蛮鳞古族有哪些成员?哪些是现役?职位是什么?”
- “某功法被哪些人修炼?按熟练度排序”
- “某条未填坑涉及哪些角色/地点/法宝?最早在哪章出现?”
3.2 关系表的两个最关键约束:实体表 + 唯一 ID
更具体一点,“唯一 ID”这件事要刻意做对,因为它决定了后续所有 join、索引、迁移、合并冲突的成本:
- 不要用 name 当主键:名字会改、会重名、会有别名/称号;name 只是可变字段
- 区分“内部行号”和“全局唯一 ID”:
- 本地单机:可以用自增整数主键(性能好、join 轻量)作为内部事实锚点
- 多端/云化:对外引用最好用 ULID/UUIDv7 这类全局唯一 ID,避免离线编辑后合并时发生 ID 冲突
- 用唯一约束表达“业务唯一性”:可以给
name加 UNIQUE(按项目接受程度决定),但仍不把它当主键 - 别名/称号单独表:可以引入
entity_aliases(entity_type, entity_id, alias)解决“同名/外号/称号”与查照问题
在当前实现里,关系表仍以 id INTEGER PRIMARY KEY 为主;同时我已在 chapters 表增加 ulid,用于索引对齐与未来多端同步预留。下一步会把实体表也补齐 ulid/public_id。
3.3 关系表的查询优势:从“遍历 JSON”变成“几行 SQL”
多对多关系抽出来之后,很多功能会突然变得简单、可靠、可优化:
| |
甚至“推测/未填坑”这种看似散文的内容,只要补上 subject_type + subject_id,就会立刻变成强可检索的结构化知识点:
| |
3.4 工程落地:不从“表设计”开始,而从“读写路径”开始
迁移最容易翻车的点,不是 schema 漂亮不漂亮,而是读写路径太激进。
我的策略是“先让系统跑起来,再逐步让关系表成为主路径”:
- 迁移脚本:提供 KV → 关系表的导入脚本,让历史数据可以逐步搬进新结构
- 存储层兜底:读取优先走关系表,同时把 JSON 仍写回
kv_store(过渡期备份/回滚)- 这能在不打断现有功能的前提下,把主读路径慢慢切到关系表
同时,这一阶段一定要补齐“删除语义”,否则 UI 会出现典型问题:“看起来删了,刷新又回来了”。
3.5 一个现实的折中:mentioned_character_ids(反范式字段)
严格来讲,“本章提及角色”可以通过结构化实体引用表(或通过 FTS/NER 解析)在查询时动态计算;但为了让章节库 UI 做“角色筛选”和“本章提及角色展示”更直观,我新增了 chapters.mentioned_character_ids,用 JSON 字符串保存角色表 id 数组。
与此同时,chapters.primary_character_id 对应的“主视角” UI 与检索过滤已移除:多视角写作里,用单一字段表达视角往往会制造更多误导;字段暂时保留,仅用于兼容与未来可能的重新设计。
4. 小结
这篇文章把“事实层”的演进路线讲清楚了:
- 从 JSON 文件起步,快速跑通
- 迁到 SQLite KV,把备份与读写路径先统一
- 再引入关系表,把世界设定从“文本堆”推进到“实体关系系统”
下一篇会把“索引层”讲透:向量检索如何落地、FTS5 与向量如何做混合检索、如何把索引扩展到全图谱,以及为什么云化优先尝试 Cloudflare: