Agent 存储:JSONL、SQLite 与数据库

版本说明:本文保留为已发布快照。持续更新、经过当前源码核验和 SQLite 实践验证的版本,见 《Agent 工程实践》第 6 课。后续完整正文只在书籍仓库维护。

“Agent 应该使用 JSONL 还是 SQLite?” 这个问题少了最重要的前半句:准备存什么,谁会读写,崩溃后要恢复到哪里?

Transcript、Checkpoint、长期记忆和 Artifact 的生命周期并不相同。把它们统称为 “Agent Memory”,再比较文件扩展名,很容易得到一张看似整齐、实际无法指导设计的优缺点表。

1. 同一个 Session 里混着八类数据

1.1 Session 元数据与 Transcript

Session 元数据保存会话 ID、模型、创建时间、当前分支和生命周期。Transcript 保存实际发生的 User、Assistant、Tool Call 与 Tool Result。

Transcript 是 “ 发生过什么 “ 的事实来源,却不等于下一轮全部发送给模型。

1.2 Prompt View

Prompt View 是本次真正给模型看的派生列表:

1
2
3
4
5
System Prompt
+ Compaction Summary
+ 最近原文
+ 当前轮次
+ 按需召回的长期记忆

过滤 Prompt View 不应顺手删除 Transcript。它通常只存在于内存,下一轮可以重新构造。

1.3 两种 Checkpoint

Compaction Checkpoint 为上下文窗口服务,记录摘要、保留边界和最近原文。

Workflow Checkpoint 保存工作流某一步的完整状态。LangGraph 用 thread_id 关联一系列 Graph State Checkpoint,让中断恢复、Human-in-the-loop 和 Time Travel 成为可能。LangGraph Checkpoint

二者都叫 Checkpoint,恢复对象却不同:一个恢复模型上下文,一个恢复工作流执行状态。

可以用两个重启问题区分:

1
2
3
4
5
“旧历史摘要和最近原文是什么?”
→ Compaction Checkpoint

“程序停在哪个节点,是否正在等人工批准?”
→ Workflow Checkpoint

它们可以保存在同一个 SQLite 中,但不能因为介质相同就混成同一种状态。

1.4 Tool Execution Ledger

Ledger 记录工具执行事实:

1
2
3
4
5
tool_call_id
execution_id
idempotency_key
approved → running → succeeded / failed / unknown
result 或错误证据

它用于幂等、审计和崩溃恢复,通常不进入 Prompt。Transcript 可以只保留一条最终 Tool Result,Ledger 则要保留多次执行尝试:

1
2
3
4
5
6
Ledger
|- exec_1:unknown
`- exec_2:succeeded

Transcript / Prompt
`- call_7 的最终 Tool Result:succeeded

Ledger 面向恢复与审计;Transcript 面向会话语义。把每次 approved/running/retry 都塞进 Prompt 只会制造噪声。

1.5 长期 Memory / Store

长期记忆跨 Session 保存偏好、规则和项目知识。它需要 namespace、更新、删除、过滤与召回,不等于 “ 保存很久的聊天记录 “。LangGraph 也把单 Thread 的 Checkpointer 与跨 Thread 的 Store 分成两个系统。LangGraph Persistence

1.6 Artifact、索引与缓存

大型日志、文件、图片和二进制输出属于 Artifact。Transcript 只放状态、小预览、路径与 Hash,完整内容留在文件系统或对象存储。

FTS、向量索引和缓存属于可重建数据:

  • FTS 像书后的关键词目录,支持按原词查找。
  • 向量索引保存文本的数字表示,用于查找语义相近内容。
  • 缓存保存已经算过或查过的结果,避免重复工作。

删除这些派生数据后,程序应能从 Transcript 或 Memory 原文重新生成。如果向量索引仍命中旧规则,而 Memory 原文已经更新,应相信原文并重建索引,不能让导航覆盖正文。

2. 访问模式决定 Backend

需求更合适的起点
本地、单 Writer、顺序追加与人工检查JSONL
同机事务、索引、筛选、FTS、有限并发SQLite
多 Worker 共享、低延迟、TTL、协调Redis,另定持久化策略
多主机、多写者、强约束、复杂查询PostgreSQL
只想把对话续接交给 ProviderServer-managed Conversation/Response ID

2.1 JSONL:顺序事实流

假设 session.jsonl 已经写了十万行。程序重启后,你想找出所有最终状态为 unknown 的工具执行。

JSONL 当然能查。但程序得从第一行读到最后一行,再按 execution_id 整理每次状态。偶尔查一次没问题;每次启动都查,代码就会慢慢长出索引、去重和锁。

JSONL 一行一条 Event,适合追加、Tail、导出和人工检查。Pi Coding Agent CLI 默认就是每 Session 一个 JSONL,Entry 通过 id/parentId 组成树。Pi Session Format

代价也很直接:多 Writer 需要自己加锁,跨多行原子提交需要额外协议,任意字段查询通常要扫描或另建索引,损坏行和 Schema 迁移也由应用处理。

若继续为 JSONL 增加状态索引、唯一键检查、文件锁和提交协议,应用就在逐步实现一个小数据库。

JSONL 不是 “ 不可靠 “,只是把数据库替你做的工作留给应用。

2.2 SQLite:同机事务与查询

SQLite 把 Schema、事务、唯一约束、索引和 FTS 放进一个本地数据库。WAL 是 Write-Ahead Log:Writer 先把完整事务顺序追加到 WAL,不立即改 Reader 正在看的主库页面。Reader 在开始查询时确定自己的快照边界,只读取该边界之前已经提交的 WAL;之后新增的事务不会突然混入同一次读取。最后由 Checkpoint 把 WAL 安全合并回主库。SQLite WAL

唯一约束也不是普通的 “ 先查再写 “。两个进程可能同时查询到 “ 这个 idempotency_key 不存在 “,随后都尝试插入;由数据库执行 UNIQUE(idempotency_key),才能在同一个原子写入边界内只接受一个。

1
2
3
Writer:顺序追加 WAL
Reader:主库 + 自己快照边界内的 WAL
Checkpoint:稍后合并回主库

WAL 减少读写互相阻塞,但同一时刻仍只有一个 Writer,并依赖同一主机上的文件锁与共享内存。它不把 SQLite 变成多主机数据库,也不自动提供备份、Migration 或高可用。

SQLite 可以通过 FTS5 虚拟表做全文搜索;具体构建需要启用或加载 FTS5。SQLite FTS5 SQLite 官方也提供可加载的 vec1 向量扩展,支持 L2、Cosine 与近似最近邻;这不表示任意 SQLite 安装都天然具备向量索引,使用前仍要安装扩展并按数据量验证性能。SQLite vec1

它适合桌面 Agent、CLI/UI/后台任务共享状态、Session 搜索和本地 Ledger。它不自动解决多主机共享、高写入争用、备份和 Migration。

2.3 Redis 与 PostgreSQL

Redis 适合共享短期状态、TTL、锁与任务流,但 “ 在 Redis 里 “ 不等于永久可审计。RDB 定期生成内存快照,可能丢失最近一次快照之后的数据;AOF 追加写操作,丢失窗口取决于 fsync 策略。复制和故障切换也需要明确配置、监控与演练。Redis Persistence

PostgreSQL 适合多实例、多租户和多写者,能用事务、唯一约束和关系查询保护 Session、Checkpoint 与幂等键。它是否值得,仍取决于团队是否已经具备迁移、备份、监控和恢复能力。

2.4 服务端续接不是本地状态的替代品

OpenAI 可以用 Conversation ID 或 previous_response_id 续接对话;Agents SDK 也支持 OpenAI Conversations Session。这样能少管理一份客户端历史,但不能取代 Harness 的本地状态。OpenAI Conversation State

1
2
3
4
5
Harness                         Provider
组装 Context ----------------> 运行 Model
校验和执行 Tool <-------------- 返回 Tool Call
保存 Ledger / Artifact
维护审批和业务 Memory

Provider 只知道 Harness 发给它的内容。若 Provider 返回 send_email,Harness 执行后在回传 Tool Result 前崩溃,Provider 只知道模型请求过发送邮件,无法确认邮件是否成功。本地 Ledger、外部服务回执和幂等键仍不能省略。

3. 五个框架为何没有同一个答案

案例当前默认或主要路径说明
PiCoding CLI 默认 JSONL;Agent Core 可选 SQLite产品默认与框架 Backend 可以不同
OpenClaw持久会话每 Agent SQLite;Incognito 在内存旧 JSON/JSONL 主要作为迁移输入
Hermesstate.db + WAL + FTS多进程、搜索和 In-place Compaction 推动 SQLite
LangGraph可替换 Checkpointer 与 StoreSQLite 适合本地;PostgreSQL 面向生产共享状态
OpenAI Agents SDKSession Protocol + 多种 BackendSQLite、Redis、SQLAlchemy、MongoDB 或服务端续接都可替换

Pi 很能说明问题:默认 Coding Agent CLI 继续使用 JSONL,同一仓库却提供独立的 SQLite Session Backend。当前 Backend 明确不导出 Search Service 或 FTS Index,搜索已经属于单独的 S3 Projection。不能用一句 “Pi 使用 JSONL” 概括整个项目,也不能把其他 Projection 的能力算到 SQLite Backend 身上。Pi SQLite Backend

OpenClaw 与 Hermes 的 SQLite 也不是 “ 把 JSONL 换个扩展名 “。OpenClaw 用数据库管理 Session、Transcript、生命周期和迁移;Hermes 还使用 WAL 与 FTS。查询和状态管理变复杂后,数据库才开始回本。OpenClaw SessionOpenClaw Database SchemasHermes Session Storage

4. 当前教学代码为什么仍用 JSONL

本专栏代码的存储需求是逐步长出来的:

1
2
3
4
5
6
7
8
02_rember.py
最新 Checkpoint JSON:summary + turns

03_context.py
append-only Session JSONL:Message + Compaction

04_tool_reliability.py
同一 JSONL 增加 Tool Execution Ledger

代码分别见 02_rember.py03_context.py04_tool_reliability.py

当前仍是本地单进程、单 Writer,主要操作是追加、顺序回放和人工观察。迁移 SQLite 会增加 Schema 与 Migration,却没有解决新的真实问题。

出现下面任一需求时,SQLite 才开始值得:

1
2
3
4
5
多个本地进程需要共享 Session
频繁查询所有 running / unknown Execution
用唯一约束保护 idempotency_key
跨 Session 搜索与筛选
多条状态需要同一事务提交

如果进一步变成多主机、多 Worker,SQLite 又可能不够,此时才考虑 PostgreSQL 或其他服务。

5. 最小决策树

1
2
3
4
5
6
7
8
本地单 Writer,只追加和回放?
├─ 是 → JSONL
└─ 否
├─ 同机,需要事务、索引或 FTS? → SQLite
├─ 多 Worker,需要低延迟或 TTL? → Redis,并单定持久化
├─ 多主机、多写者、强约束? → PostgreSQL
└─ 只想托管对话续接? → Server-managed
本地 Ledger、Artifact、Memory 仍需设计

最后记住两句话:数据库只是 “ 纸条放在哪里 “,不是记忆机制本身;JSONL 不是低级,SQLite 也不是高级,访问模式不同而已。

主动回忆自测

  1. 选择 JSONL 或 SQLite 前,至少要先回答哪三个问题?
  2. Transcript 与 Prompt View 有什么区别?
  3. 同一个 Tool Call 重试两次时,Transcript 与 Ledger 分别保存什么?
  4. Compaction Checkpoint 与 Workflow Checkpoint 分别恢复什么?
  5. FTS、向量索引和缓存为什么通常属于可重建数据?
  6. SQLite WAL 为什么能减少 Reader 与 Writer 的互相阻塞?
  7. RDB 与 AOF 为什么不能让 “ 写入 Redis” 自动等于永久审计?
  8. Provider Conversation 为什么不能替代本地 Harness 状态?
  9. 当前教学 Agent 在什么访问模式下应该从 JSONL 迁到 SQLite?
  10. Redis、PostgreSQL 与 Server-managed Conversation 分别适合什么问题?
展开查看简答
  1. 存什么、谁读写、常见访问与崩溃恢复方式是什么。
  2. Transcript 保存完整会话事实;Prompt View 是本轮实际发送给 Model 的派生列表。
  3. Transcript 保留一条最终 Tool Result;Ledger 保留所有执行尝试及状态变化。
  4. 前者恢复模型历史视图;后者恢复工作流节点、变量和等待状态。
  5. 它们能从 Transcript 或 Memory 原文重新生成,不能反过来成为唯一事实来源。
  6. Writer 先追加 WAL,不立即修改 Reader 使用的主库页面;Reader 使用开始查询时的一致快照。
  7. RDB 有快照间隔,AOF 有 fsync 策略,复制与故障切换也需配置,因此仍可能存在丢失窗口。
  8. Provider 负责模型推理和可选会话续接;Tool 副作用、Artifact、审批、Ledger 与业务 Memory 仍由 Harness 及本地系统负责。
  9. 出现同机多进程共享、频繁状态查询、FTS、唯一约束或多条事务更新时。
  10. Redis 适合 TTL 与快速协调;PostgreSQL 适合多主机、多写者和强约束;Server-managed Conversation 只托管模型对话续接。

参考资料