chDB 为 Agent 记忆提供持久化层
DataHot 速览
chDB 是 ClickHouse 驱动的嵌入式 SQL OLAP 引擎。团队曾用 chDB 在单台笔记本上为 coding agent 保存项目规则、用户偏好、决策、原始转录和召回轨迹,运行数周后却无法把状态带到 CI、可能一小时即消失的沙箱或第二台笔记本。改用 ClickHouse server 后端虽可移植,但会把本地 recall 变成带凭据、连接管理和远程延迟的网络查询。chDB Durable Layer 试图把可恢复的分析状态存入自有对象存储,同时保留本地查询速度。
为什么值得关注:对构建 Data Agent/分析 Agent 的团队有参考价值:它具体讨论 Agent 记忆在本地嵌入式 OLAP、对象存储持久化与远程服务之间的工程权衡,而非停留在概念层。
本文目录 11 节
译文
AI 逐段翻译那个没有跟我们一起回来的状态
以下是我们这事的开端。
我们当时在 chDB 之上构建 agent 记忆,chDB 是一个由 ClickHouse 驱动的嵌入式 SQL OLAP 引擎。一个编码 agent 在一台笔记本电脑上运行了数周,它嵌入的 chDB 悄悄积累了大量状态:项目规则、用户偏好、被修订过两次的决策、这些决策背后的原始对话记录,以及显示哪些记忆在哪些任务中被召回的追踪记录。这套方案运行良好。召回是一次本地查询,无需任何数据离开这台机器。
然后故事就不再只是一台笔记本电脑了。同一份记忆需要出现在 CI 中、出现在一个可能一小时后就会消失的沙箱里,以及出现在第二台笔记本电脑上。这时,本地磁盘这一假设开始造成痛苦:记忆存放在一块磁盘上的 MergeTree 目录中,而那块磁盘在其他任何地方都不存在。
我们的第一个修复方案是添加一个 ClickHouse 服务器后端。这解决了可移植性问题,但破坏了我们最喜欢的那部分:召回是一次本地函数调用,而不是一次网络往返。现在每次工具调用都必须付出凭证、连接管理和远程延迟的代价。过去只是一个目录,现在需要运行或租用一个服务。
这就在本地单机存储和云数据库服务器之间留下了一个空白。对于 agent 记忆,我们想要更轻量的东西:一种将可恢复的分析状态发布到我们自己的存储中的方式,同时不让每次召回都变成远程查询,也不要求用户去运维服务器。
需求变得简单:我们需要一个本地分析数据库,其状态能够比创建它的主机存活得更久。这正是 chDB Durable Layer 所解决的问题:一个围绕 chDB 的小型持久化层,将可恢复状态保存在对象存储中。
如今人们通常如何处理 agent 记忆
在深入了解 chDB Durable Layer 之前,先看看如今 agent 记忆通常是如何存储的会有所帮助。以下选项没有一个是错的。每一个都擅长问题中的不同部分,而其中的空白正定义了 chDB Durable Layer 的空间。
嵌入式键值状态:SQLite 检查点器和简单的记忆存储。这是常见且合理的起点。LangGraph 有 SqliteSaver,OpenAI Agents SDK 有 SQLiteSession。在这种模式下,本地存储跨会话保留运行时状态和小型事实,使 agent 能够从上次中断处继续。这类系统小巧、事务性、可嵌入且成熟。
当记忆变成历史时,不匹配就显现出来了。我们想追问一条记忆来自哪里、哪段对话记录支持了它、它取代了哪个更早的信念、哪些召回从未起过作用,以及一次失败的工具调用是否产生了坏状态。这些问题需要过滤、分组、排序、连接、审计追踪和批量召回。它们是针对仅追加的记忆、追踪记录和对话历史的 OLAP 问题。SQLite 可以存储检查点,但它不是我们想要的分析那段历史的形态,而且它仍然倾向于绑定到某一台机器上的某个文件或目录。
复制的本地 SQLite:SQLite 加 Litestream。Litestream 是一个用于 SQLite 的复制工具:它持续将 WAL 文件复制到对象存储,因此新进程可以从存储桶中恢复数据库。这为 SQLite 解决了单磁盘问题。它仍然让你得到一个行式存储数据库和一个需要运行的复制进程。
服务器数据库或记忆平台:Postgres、pgvector、服务器端 ClickHouse、Letta、mem0 Platform 和 Zep Cloud。对于团队规模的部署、多写入者和共享服务,这通常是正确答案。你能获得共享访问、集中化运维和真正的并发。代价是 agent 现在依赖一个远程服务:网络调用、凭证、连接管理,以及一个必须有人运行的系统。对于主要在一台机器上运行的单用户或单项目记忆,仅仅为了保留一份可恢复副本,这可能就是过多的基础设施。
厂商拥有的持久化状态:Cloudflare Durable Objects。Durable Objects 有一个优雅的形态:每个对象都有身份标识、单线程执行和绑定的持久化存储。这种身份标识加单写入者模型很接近我们想要的。权衡之处在于厂商绑定和存储透明度较低:对象存在于平台边界之后,而且该模型更接近应用状态,而非嵌入式列式分析。
本地嵌入式 OLAP 数据库:DuckDB 或 chDB。本地 OLAP 快速、无服务器,并且使用起来很愉快。这正是我们想要保留的热路径。剩下的问题就是开启这个故事的那个问题:状态仍然只是一块磁盘上的一个目录。
| 选项 | 计算运行位置 | 持久性 | 运维负担 | 写入者 | 分析 |
|---|---|---|---|---|---|
| SQLite agent 检查点器 / 记忆存储 | 进程内 | 本地文件 | 无 | 单个进程 | 无分析性历史扫描 |
| SQLite + Litestream | 进程内 | WAL 复制到对象存储 | Sidecar、PVC / StatefulSet | 单个进程 | 仍是 OLTP;行式存储加 sidecar |
| Postgres / pgvector / 服务器端 ClickHouse / 托管记忆服务 | 远程服务 | 由服务处理 | 运行或租用服务;热路径跨越网络 | 多个 | 需要服务器;热路径跨越网络 |
| Cloudflare Durable Objects | 平台内部 | 由平台处理 | 平台绑定 | 每个对象一个 | 平台绑定;非列式 |
| 本地磁盘上的 DuckDB / chDB | 进程内 | 本地磁盘 | 无 | 单个进程 | 绑定到一块磁盘 |
| chDB Durable Layer | 进程内 | 你自己的对象存储,带有显式 flush() / checkpoint() | 一个存储桶 | 每个对象一个,由租约强制执行 | 完整的列式 OLAP,可恢复状态 |
最后一行就是缺失的形态:保留进程内 OLAP 热路径,将权威副本放入你拥有的存储,并避免数据库服务器、PVC 或 sidecar。
chDB Durable Layer:本地工作副本,权威存储桶副本
chDB Durable Layer 是一个可寻址、单写入者、可恢复的嵌入式分析对象。每个对象都是一个完整的 chDB 数据库。你在命名空间内按名称打开它,在本地查询它,并选择本地状态何时变为持久状态。
安装 chDB 4.4 或更高版本,并带有 durable extra。这包括 S3 后端。GCS 和 Azure Blob 使用单独的 extras。
1pip install "chdb[durable]"下面的序列图展示了 Durable 如何备份本地状态并在另一台机器上恢复它:
端到端示例:写入并恢复
接口很小。它有五个部分。
| 部分 | 角色 |
|---|---|
| ● 本地 MergeTree 工作副本 | 查询针对本地磁盘上的嵌入式 chDB 数据库运行,因此热路径不需要远程往返。chDB 使用与 ClickHouse Local 相同的磁盘格式,因此工作副本是普通的 ClickHouse 数据库目录,而不是专有缓存。参见 chDB 加入 ClickHouse 家族。 |
| ● 对象存储作为权威状态 | 真正重要的副本存放在你拥有的存储桶中。 s3:// 是主要后端。 chdb[durable] 会安装 boto3,而支持条件 PutObject 的 S3 兼容存储可以通过 CHDB_DURABLE_S3_ENDPOINT 使用。 gcs:// 由 chdb[durable-gcs] 提供,带有 GCS 代际前置条件。 azure:// 由 chdb[durable-azure] 提供,带有 Azure ETags。 local: 用于开发和单机使用。 |
● flush() 是持久性边界 | 在应用程序要求发布之前,写入保持在本地。当 flush() 返回时,它所覆盖的写入已经到达对象存储。应用程序选择这个边界,这对于小型连续写入很重要。 |
● checkpoint() 折叠日志 | 在检查点之间,持久状态由基础快照加 WAL 组成。 checkpoint() 写入新的基础快照,因此下一次打开不必重放很长的日志。 |
● head.json 加条件写入提供租约 | 每个对象在存储桶中都有一个 head 记录。针对该记录的比较并交换写入会获取并推进所有权,从而提供单写入者租约和隔离语义。两个进程不应都认为它们拥有同一个嵌入式数据库。 |
这些边界值得明确说明:
- 它是单写入者的。对于每个用户或项目一个“大脑”的场景,这是一个有用的特性。对于共享团队数据库,它是错误的工具。
- V1 WAL 会重放写入语句,因此这些语句必须是确定性的。
INSERT语句如果调用now(),是典型应避免的情况。 - 它不是 OLTP 数据库,也不是 Postgres 的替代品。高频点更新和多写入者事务属于另一个系统。
不仅仅是另一个存储后端
如果唯一需求是“保存这个检查点”,SQLite 已经能做到。添加另一个后端本身并不有趣。
chDB Durable Layer 添加的是一种此前缺失的部署形态:基于可恢复工作副本的嵌入式 OLAP,权威副本位于你拥有的存储中,中间没有服务。
- 本地计算:热查询留在 agent 进程内。
- 分析型布局:MergeTree 适合压缩的仅追加历史、批量召回、过滤、聚合和向量辅助搜索。
- 可移植持久性:同一个对象可以在另一台笔记本电脑、CI 中或短生命周期沙箱内恢复。
- 显式控制:应用程序决定何时调用
flush()以及何时调用checkpoint()。 - 无需运行服务:没有数据库服务器、PVC 或复制 sidecar。SQLite 防止本地状态消失。chDB Durable Layer 使本地分析状态可恢复,同时仍让 agent 在本地分析它。
这在实践中给你带来什么
在构建 chDB Durable Layer 时,有三个特性不断出现,说明它适合 agent 记忆。
让热查询保持本地;只持久化决策。 热分析工作留在 chDB 进程内。Durable Layer 只发布值得跨机器携带的状态:已提交记忆、修订、检查点及其背后的证据。
chDB cookbook 的 部署 chDB 分析师 配方展示了这种运行模式在 Lambda、Lambda MicroVMs、Cloud Run、Azure Container Apps 和 E2B 上的应用:agent 运行时可以被创建、恢复、暂停和销毁,生命周期成本在秒或亚秒级,因此计算实例不必是持久的东西。在我们的 本地与远程基准测试中,本地查询比远程查询快 58 倍,这是问题的另一半:热召回不应变成远程数据库往返。
仅追加记忆压缩效果很好。 Agent 记忆主要是仅追加 JSONL:消息、工具调用、工具结果、token 计数、修订和证据。在一个小型本地实验中,一个 1.45 GB 的 Claude Code 转录样本包含 213,721 行,加载到 MergeTree 后,使用 ZSTD(3) 变为 521 MiB,使用 LZ4 变为 991 MiB。
文本压缩效果很好。昂贵的部分是截图:它们只占行的 0.8%,但在这个样本中约占字节的三分之二。这就是为什么二进制大对象应放在表外,仅将引用保存在类型化列中。
在建模之前查询原始 JSONL。 chDB 可以直接查询 file(..., JSONAsString),因此你可以在决定模式之前检查转录。这在早期很有用:统计事件类型、搜索工具名称、查找昂贵调用,然后将重复字段如 project、timestamp、model、tool、tokens 和 cost 提升为类型化 MergeTree 列。
下面的两张查询卡片展示了这种形态:一个查询对原始事件类型分组,另一个查询在同一个 JSONL 语料上搜索,无需导入步骤。
其他人也注意到了同样的模式。Subara3 的 ccsql 将 Claude JSONL 加载到类型化 MergeTree 表,并提供成本、工具、缓存、会话和热力图报告,而 claude-scope 将 Claude 历史变成本地仪表板。这些项目展示了摄取和查询侧;Durable 为生成的 MergeTree 状态添加了可移植性层。
最佳实践
这些模式来自构建该系统以及以下项目。
将记忆建模为仅追加表,而不是一个文档。 有用的表包括 memories 用于当前信念, memory_history 用于修订行, raw_evidence 用于冷转录和工具输出, recall_traces 用于记录召回了什么以及是否有帮助, conflicts 用于语义接近但相互矛盾的行,以及 tool_events。用 version或时间戳。用标志进行软删除。用ORDER BY version DESC LIMIT 1 BY memory_id推导当前状态。agent 本地数据引擎的帖子展示了当前状态查询、完整历史查询和时点查询。
将原始证据保持冷存储并压缩,同时将二进制大对象排除在外。将对话记录和工具输出放入raw_evidence,并使用CODEC(ZSTD(3))。文本通常可压缩约 4 倍,而热路径不会读取它。截图和其他二进制负载应存放在外部存储桶中,表中只存储键。否则它们会主导每个检查点。用于过滤和分组的列应当有明确类型,在适用处使用LowCardinality,这样热查询就不必触及 JSON。
在有意义的批次之后刷新,而不是每行之后都刷新。Agent 写入通常是一串小事件。让工作副本吸收它们,然后在边界处调用flush(),例如任务结束时、工具循环结束时,或在 N 条已提交记忆之后。刷新之间的间隔就是你愿意接受的数据丢失量。
在压缩点进行检查点。良好的检查点时机包括:批量导入之后、大量修订之后、长时间会话结束时,或在将对象交给另一台主机之前。一个全新的基础快照能让下次打开更快,并保持日志简短。
每个对象名称只使用一个写入者。租约强制实施这一点,应用程序应顺应它。如果两个工作进程必须并发写入,请使用两个对象或使用服务器数据库。
每个用户或项目使用一个命名空间。 Namespace("s3://bucket/agent-memory", owner=...)加上对象名称,例如user-123或org/repo,可以保持故障域较小,使删除成为前缀操作,并为以后列出或查询对象留出空间。
知道何时使用服务器。如果你需要多写入者协作、来自许多客户端的亚毫秒级点更新、比本地磁盘更大的工作集,或集中式合规控制,请使用服务端 ClickHouse 或 Postgres。SQL 和 MergeTree 布局仍然可以沿用,而remote()为你提供迁移路径,无需重写应用程序。
示例:ClickMem
ClickMem是推动这项工作成为焦点的项目,也是 Durable 用例最清晰的示例。
ClickMem 刻意避免将自己变成一个“聊天历史向量数据库”。没有任何东西会自动成为记忆。只有用户或 agent 通过clickmem_remember明确提交某个事实,或者导入诸如AGENTS.md和.cursor/rules/*.mdc这类精选文档时,该事实才会进入存储。原始对话记录作为冷证据保留:可搜索、可审计,但不会作为记忆注入上下文。在此之上是一个信念修订模型,包含五种操作:扩展、修订、收缩、强化和拒绝。当一条新记忆在语义上接近现有记忆但与之不一致时,两行都会被标记为冲突,直到有人解决它。每次召回都可以生成一条追踪,解释某个结果为何匹配。
从数据建模的角度看,这正是上文描述的那组表:已提交记忆、修订历史、冷对话记录、冲突行、召回追踪,以及用于项目、隐私和标签的作用域。ClickMem 仪表盘回答历史问题。这个信念是如何演变的?哪些冲突仍未解决?为什么这次召回匹配?哪条对话记录产生了这条记忆?这就是为什么它运行在 chDB 和 MergeTree 上,而不是键值存储上。
如今 ClickMem 有两种存储模式。单机或局域网共享主机在~/.clickmem/data下使用嵌入式 chDB。多个设备可以通过 ClickHouse 服务器共享记忆。它还具有export / import,包括嵌入向量,以便手动迁移一个“大脑”。
chDB Durable Layer 在这两者之间增加了第三种形态。热路径保持嵌入式,召回仍是本地查询。每个用户或项目的权威副本位于该用户的存储桶中。同一个对象之后可以在另一台笔记本电脑上打开,在需要项目规则的 CI 作业中打开,或在一个小时后就会消失的沙箱中打开。在每批已提交记忆之后刷新,在导入和冲突解决过程之后进行检查点,并为每个项目使用一个对象。这样,记忆存储就不再依赖于它最初构建所在的机器。
同一问题的另外三种形态
Agent 记忆是显而易见的例子,但同样的模式会出现在任何嵌入式分析数据库不再是一次性缓存的地方。
Maple Local:本地优先可观测性。Maple Local 接收追踪、日志和指标,嵌入 chDB,并提供本地查询和仪表盘。一旦数据库包含数天收集的遥测数据,持久性就成为一种产品特性:崩溃恢复、脏存储恢复、检查点和恢复。Durable 将这种恢复模式移入数据库层,同时让应用程序决定何时刷新、何时检查点,以及如何命名每个对象。
ReplayHouse:回放缓冲区和训练状态。ReplayHouse 是一个基于 ClickHouse 的回放缓冲区,在本地工作中使用嵌入式 chDB:它存储轨迹和带评分的 rollout,采样加权训练批次,将训练误差作为优先级写回,并精确查询训练器消费了什么。这是工作流中间隐藏的长期存在状态。回放缓冲区包含长时间运行中收集的昂贵经验。借助 Durable,它可以在批次、epoch 或里程碑边界处刷新,并从笔记本电脑迁移到 CI,再到训练机器。由于引擎仍然是 chDB,采样分布和优先级漂移仍然可以用 SQL 检查。
vcfclick:昂贵导入后可查询的数据。 vcfclick 是一个研究性生物信息学 VCF 数据库,构建于嵌入式 ClickHouse 之上,并带有 DuckDB 注释和 MCP 自然语言层。原始 VCF 文件可能已经安全地存储在别处。昂贵的资产是准备好的状态:已导入、已规范化、已注释、已索引,并且可供查询。如果没有持久化检查点,每台新机器都必须重复该导入过程。有了它,一个准备好的队列数据库可以检查点一次,并在任何地方作为本地查询副本重新打开。
当前状态和即将推出
Durable V1 契约现已在各绑定之间共享。chDB 4.4 随附 Python 实现以及 chdb[durable] 用于 S3 兼容存储,另有 chdb[durable-gcs] 和 chdb[durable-azure] 用于原生 GCS 和 Azure Blob。Node、Go 和 Rust 现在通过 [email protected]、chdb-go/[email protected] 和 [email protected] 公开相同的对象布局和生命周期。
它们全都构建于 chdb-core 26.7.3 中的 Durable V1 组件之上:备份、恢复、语句分类、head.json、WAL、检查点、CAS、租约行为、错误类别以及跨绑定测试夹具。由一个绑定写入的对象,应当能够被另一个遵循相同契约的绑定恢复。
对于本地优先的代理系统,其形态很简单:没有数据库服务器、没有 PVC、没有复制边车,也不需要每次回忆时都发起远程调用。数据库保持嵌入式。状态在机器迁移后依然存在。
代理记忆或许并不那么需要一个更大的上下文窗口,而是需要一个能够经受迁移的本地分析大脑。
接下来试试
- 安装
pip install "chdb[durable]",在你自己拥有的存储桶上打开一个命名空间,并将现有的 chDB 记忆表附加到它上面。 - 如需更清晰的可运行示例,请阅读 durable-agent-memory 食谱配方。
- 有关完整的 Durable 契约和实现细节,请参阅 chdb durable 文档。
- 如果你有使用场景、问题或设计反馈,请在 chdb-io/chdb 中发起讨论。
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏