PostgreSQL · FastAPI · React · Event Sourcing

Memory Ledger

用关系账本给 Agent 做记忆,而不是向量嵌入

GitHub 仓库 ↗许可证 Apache 2.0

一句话

需要记进记忆的东西——陈述一个事实、修改某个字段、加一条注释、对某项存疑——都各自记成账本表里的一行 intent,从不覆盖旧行。需要「此刻的真相」时,SQL 函数按指定时间戳把同一实体的相关行确定性重放、合成。跨会话记忆依赖的是 SQL 与时间戳,而不是 embedding 与相似度。

记忆即账本行而非向量嵌入:向量雾 → 人工闸门 → 有序账本

弥散的向量近似(左)经一道人工闸门(中),收敛为同库可查、可重放的有序账本真相(右)。

为什么是关系库,而不是 embedding

在这套方案里,记忆就是关系表中的账本行;读取时直接用 SQL 查询,结果确定、可复现,全程不经过向量索引。主流的 vector-first 恰好相反——把记忆塞进一套独立的 embedding 索引,召回是近似的、会随模型漂移、难精确查询、也读不出内容。

差异不在用不用数据库(Letta、Mem0 同样跑 Postgres,存的却是向量),而在关系库为主体、SQL 为召回、读路径上没有 embedding

左 SQL 精确命中一行,右 向量近似召回一片

左:SQL 精确命中某一行;右:向量索引只能给出近似的一片——确定 vs 漂移。

机制上,这是对既有业务表做事件溯源(event sourcing):每条 intent 必属四类之一,由 Postgres 的 CHECK 约束在写入时强制校验;账本与业务表同处一库、同一事务,无需维护两份数据。

和主流方案摆在一起看

维度 Vector Memory 时序知识图谱 Memory Ledger
存储 自有向量库 自有图库 业务表同库 + 单账本表
召回 embedding 近似 向量 + 图 + 重排 纯 SQL,无 embedding / LLM
确定性 ✓ 逐字节可复现
时间旅行 部分(事实区间) ✓ 任意时点整体重放
改口撤回 覆盖 失效非删 新行接替旧行成链,可回滚
人工闸门 ✓ 默认改字段须确认
出处 几乎无 片段级 逐字原话 + 来源分级 + 冲突仲裁

各项单独均非首创(双时态 DB、Zep / Graphiti、Letta 各擅其一)。真正的差异在于组合:同库账本 × 确定性 as-of × 默认人工闸门 × DB 强制四类强出处,四者合一。

核心抽象:四类 intent,一次重放

intent 落账,再由重放函数沿时间戳确定性合成任意时刻的真相,供下一轮 LLM 取用。

四类 intent → 账本表 → 重放棱镜 → 当前真相 → LLM

四类 intent 汇入单张账本表 → 沿时间戳确定性重放 → 截至此刻的真相 → 喂给下一轮 LLM。

账本行字段 取值 / 含义
kind PATCH / ASSERT / ANNOTATE / FLAG——DB CHECK 强制四类
status PROPOSED → APPLIED → SUPERSEDED / REJECTED / EXPIRED(五态)
source_layer USER_DIRECT > L2_FORM > L2_CHAT > L2_VOICE > AGENT_INFERENCE(权威递减)
source_quote 逐字截取的原话证据
confidence 0–1 把握度

作为 Agent Memory 基础设施

账本本身就是一层 Agent Memory 基础设施:它是一份可被精确查询的结构化记录,而不是一团读不出内容的向量。这一点和我在做 chatbot 时的取向是一致的——工具做薄、底座做厚,让智能从一次查询里涌现

薄工具层悬于结构化账本基底,一次查询拉起完整链条

薄工具悬于厚账本基底;一次查询即拉起完整链条——工具变薄,智能涌现。

  • 不预载记忆 — 无需预先把整段记忆塞进上下文;agent 需要时调一次工具,就能取回某实体的完整画像:当前真相、逐字出处、存疑标记、变更链、任意历史时点。
  • 工具变薄 — 工具只是账本读取能力的一层薄封装;真相合成与时点重放都交给底层,新增一个工具几乎不增成本。
  • 智能涌现 — 即便冷启动、毫无上下文,一次查询也能取回足够丰富的历史,让 agent 对该实体了然于胸。

确定性,与人的判断

近来的 agent 工程越来越看重两件事:让执行结果确定、可复现,以及让人留在决策回路里。Memory Ledger 从设计之初就是这个取向。

数据沿确定性轨道前行,改字段须经唯一人工闸门方生效,被拒者留痕

数据沿确定性轨道前行,改字段须经唯一的人工闸门方才生效;被拒者留痕。

  • 确定性 — 读取走纯 SQL,同样的输入永远得到同样的结果,逐字节可复现,也便于审计。
  • 人机协同 — 修改既有字段必须过人工闸门:agent 只负责提议(PROPOSED),经人确认后才生效(APPLIED),每条改动都附逐字出处供人复核。

演示:一个会记事的 Personal-CRM

账本只是数据库里的若干行,本身不可见。为便于理解,我套了一个对话式 Personal-CRM 外壳来演示,由真实的 LLM 驱动,示例联系人的职位从产品经理一路变到 CTO、公司从晨星换到蓝湖再到 Globex。外壳只为演示,真正交付的是上面那套设计。

跨会话记忆

在一个全新的空白对话里问「她现在的职位」,没有任何上文,系统照样调用 get_contact 并答出 CTO——记忆按实体保存,跨会话共享。

全新会话仍准确回忆 CTO

左为新建空白线程,右为 Globex / CTO 人卡,无需上文即命中。

流式工具调用

回答之前先流式显示工具调用(get_contact running → done)再作答,说明答案来自后端实时查库,不是模型凭记忆编的。

流式工具条 get_contact

先检索、后作答,数据来自既往会话。

确认闸门

「她升任 CTO 了」要改既有字段,底部弹出确认闸门,确认后方才写入生效真相。

确认闸门:职位 → CTO

「职位 → CTO ‹暂不 / 确认›」,未确认不进入生效真相(停在 PROPOSED)。

账本与逐字溯源

每次改动都留痕:左为 supersede 变更链,右为逐字溯源。

变更链 逐字溯源
变更链:Globex ← 蓝湖 ← 晨星 逐字溯源:原话 + 把握度 + 来源
Globex ← 蓝湖 ← 晨星(supersede 成链) 原话 + 把握 100% + 来源层级

历史回溯(as-of)

拖动时间轴,人卡重建该时刻的真相:晨星 → 蓝湖 → Globex,各由当时的 1 / 2 / 4 条记录经重放函数合成。

拖动时间轴,同一卡随时间由 1/2/4 条记录确定性重建

同一张卡,由当时的 1 / 2 / 4 条记录确定性重建。

拒绝与存疑同样留痕

不止记成功的改动——被拒(REJECTED)与待核实(FLAG)也全程可审计。

未采纳 + 待核实

「所在地 → 柏林」未采纳、「搬迁待核实」,五态状态机全程留痕。

什么时候该用它

判断规则很简单:当「记得准、能追溯、能问责」比「记得多、能联想」更重要时,就用账本。 典型场景是计划 / 项目管理、CRM 联系人档案、工单与合规记录,以及 agent 反复纠正的同一份结构化记录。反过来,任意开放长尾内容、只需近似召回的场合,还是交给 vector memory 更合适——两者互补,账本专司「可审计、可回滚、可时间旅行的结构化改动」。


仅需 Docker 一键起 db + api + web;不配 LLM_API_KEY 也能启动,seed / 历史回溯 / 溯源 / 账本都可看,仅对话走 mock。Apache 2.0 开源。

查看 GitHub 源码 →