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

弥散的向量近似(左)经一道人工闸门(中),收敛为同库可查、可重放的有序账本真相(右)。
为什么是关系库,而不是 embedding
在这套方案里,记忆就是关系表中的账本行;读取时直接用 SQL 查询,结果确定、可复现,全程不经过向量索引。主流的 vector-first 恰好相反——把记忆塞进一套独立的 embedding 索引,召回是近似的、会随模型漂移、难精确查询、也读不出内容。
差异不在用不用数据库(Letta、Mem0 同样跑 Postgres,存的却是向量),而在关系库为主体、SQL 为召回、读路径上没有 embedding。

左: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。
| 账本行字段 | 取值 / 含义 |
|---|---|
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——记忆按实体保存,跨会话共享。

左为新建空白线程,右为 Globex / CTO 人卡,无需上文即命中。
流式工具调用
回答之前先流式显示工具调用(get_contact running → done)再作答,说明答案来自后端实时查库,不是模型凭记忆编的。

先检索、后作答,数据来自既往会话。
确认闸门
「她升任 CTO 了」要改既有字段,底部弹出确认闸门,确认后方才写入生效真相。

「职位 → CTO ‹暂不 / 确认›」,未确认不进入生效真相(停在 PROPOSED)。
账本与逐字溯源
每次改动都留痕:左为 supersede 变更链,右为逐字溯源。
| 变更链 | 逐字溯源 |
|---|---|
![]() |
![]() |
| Globex ← 蓝湖 ← 晨星(supersede 成链) | 原话 + 把握 100% + 来源层级 |
历史回溯(as-of)
拖动时间轴,人卡重建该时刻的真相:晨星 → 蓝湖 → Globex,各由当时的 1 / 2 / 4 条记录经重放函数合成。

同一张卡,由当时的 1 / 2 / 4 条记录确定性重建。
拒绝与存疑同样留痕
不止记成功的改动——被拒(REJECTED)与待核实(FLAG)也全程可审计。

「所在地 → 柏林」未采纳、「搬迁待核实」,五态状态机全程留痕。
什么时候该用它
判断规则很简单:当「记得准、能追溯、能问责」比「记得多、能联想」更重要时,就用账本。 典型场景是计划 / 项目管理、CRM 联系人档案、工单与合规记录,以及 agent 反复纠正的同一份结构化记录。反过来,任意开放长尾内容、只需近似召回的场合,还是交给 vector memory 更合适——两者互补,账本专司「可审计、可回滚、可时间旅行的结构化改动」。
仅需 Docker 一键起 db + api + web;不配 LLM_API_KEY 也能启动,seed / 历史回溯 / 溯源 / 账本都可看,仅对话走 mock。Apache 2.0 开源。

