Memory 记忆设计讨论:Agent Memory 的数据模型可以怎么设计

上一篇讨论了为什么向量数据库不能单独代表 Agent Memory。这篇进一步往下走一步,拆分成了:Event、Evidence、Candidate 与 Memory Fact几步。

如果要真正实现一个 Memory,至少应该保存哪些数据?

这里不讨论某个具体数据库的表结构,而是先讨论几个必须区分的概念。概念没有分清,后面无论用关系库、文档库、向量库还是图数据库,都会不断出现边界问题。

一、不要把所有信息放进同一个抽屉

用户说“继续处理我上次的旅行计划”,系统可能需要用到:

  • 用户之前说过什么
  • 这些话来自哪里
  • 哪些内容只是模型推测
  • 哪些内容已经确认可以长期使用
  • 当前任务做到哪一步

这些内容都和旅行有关,但它们不是同一种数据。

如果全部压成一段摘要,再保存成一个向量,系统会失去来源、状态、版本和更新关系。更合理的方式,是先区分不同对象,再决定哪些对象需要全文、关键词或向量索引。

二、Event:记录发生了什么

Event 是最基础的对象,用来记录系统中发生过的事情。

例如:

  • 用户说预算不超过 8000 元
  • 用户把预算改成 10000 元
  • 用户确认了一个酒店
  • 外部预订系统返回支付成功
  • Agent 生成了三份候选方案

Event 的重点是“发生过”,不一定代表“应该长期记住”。

一个事件通常至少需要这些信息:

event_id: 唯一标识
actor: 谁产生了事件
scope: 属于哪个用户、任务或场景
source: 事件来自哪里
occurred_at: 什么时候发生
payload: 事件内容
idempotency_key: 用于避免重复写入

为什么要记录事件,而不是只保存最终结果?

因为事件可以帮助系统去重、审计和重放。网络重试可能让同一事件到达两次,系统应该只处理一次;如果后续发现治理规则有问题,也可以根据事件重新构建派生结果。

三、Evidence:记录依据在哪里

Event 告诉我们发生了什么,Evidence 则回答:这件事的原始依据是什么?

比如候选记忆“用户这次希望选择可取消的酒店”,依据可能是:

  • 某次对话中的一句话
  • 一份用户填写的旅行偏好表
  • 一个外部系统返回的订单条件

Evidence 可以保存原文引用、文档位置、对话编号、时间、来源版本和访问范围。

来源不是装饰信息。没有来源,用户无法追问“你为什么这么判断”,系统也无法在源数据删除或修订后同步处理派生记忆。

因此,一个长期可用的记忆至少应该能追溯到一条或多条 Evidence。

四、Candidate:模型提出的候选

模型擅长从自然语言中提取信息,但模型提取出来的内容不应该自动成为事实。

例如模型从几次旅行对话中提出:

用户可能偏好直飞航班。

这只是 Candidate。

Candidate 可以包含:

  • 候选内容
  • 候选类型,例如事实、偏好或经验
  • 提取它的模型和规则
  • 支持它的 Evidence
  • 置信度
  • 建议的适用范围和有效时间
  • 当前处理状态

状态可以是:

  • pending:等待处理
  • accepted:已经接受
  • rejected:不采用
  • superseded:被新版本替代
  • needs_confirmation:需要用户确认

Candidate 这个中间层非常重要。它给系统留下了一个“模型可以猜,但系统还要判断”的空间。

五、Memory Fact:正式记忆

Memory Fact 是通过治理后,系统允许后续使用的内容。

例如:

在当前家庭旅行任务中,用户优先选择可取消的酒店,有效至该任务结束。

或者:

用户通常优先选择直飞航班,但这次任务的预算约束优先级更高。

和 Candidate 相比,Memory Fact 至少应该增加:

memory_id: 记忆标识
revision: 当前版本
scope: 适用范围
valid_from: 生效时间
valid_until: 失效时间
status: 当前状态
evidence_refs: 证据引用
supersedes: 被替代的旧版本

这里最重要的不是字段数量,而是几个工程约束:

  1. 记忆必须知道适用于谁、什么任务或什么场景。
  2. 记忆必须知道什么时候生效、什么时候失效。
  3. 记忆必须知道当前版本以及旧版本的关系。
  4. 记忆必须能追溯到证据。

六、Task State:任务当前做到哪一步

Task State 和 Memory Fact 很容易被混淆。

例如:

goal: 完成一次家庭旅行预订
done: 确定目的地,筛选出三家酒店
missing: 选择酒店,确认付款
next: 展示候选酒店并等待选择
blocker: 等待用户确认

这描述的是一个任务的运行状态,不是用户的长期事实。

任务状态需要支持明确的状态转换,例如:

created -> planning -> waiting_confirmation -> executing -> completed

如果外部系统返回不确定结果,还应该允许进入 outcome_unknown,先查询和对账,再决定是否继续,而不是直接假设失败或重复执行。

七、这些对象如何协作

用户说:“继续处理我上次的旅行计划。”

系统可以按以下路径工作:

  1. 通过 Event 找到相关的用户行为和任务事件。
  2. 通过 Evidence 找到预算、偏好和候选方案的原始依据。
  3. 读取已经通过治理的 Memory Fact。
  4. 读取当前 Task State,知道上次停在哪里。
  5. 根据当前权限和时间范围过滤结果。
  6. 组装上下文交给 Agent。
  7. 用户做出新选择后产生新的 Event。
  8. 从新 Event 中提取 Candidate。
  9. 处理 Candidate 和旧 Memory Fact 的冲突。
  10. 接受后生成新 revision,并更新 Task State。

这里可以看到,向量索引只是帮助找到 Event、Evidence 或 Memory Fact 的查询投影。真正的事实关系仍然由规范对象和状态变更决定。

八、为什么要保留版本

用户先说“预算控制在 8000 元以内”,后来改成“预算可以到 10000 元”。

如果系统直接覆盖旧值,就很难回答:什么时候发生了变化,为什么当前判断是 10000 元,旧信息是否仍然适用于另一个任务。

有版本的记忆可以表达:

revision 1: 预算不超过 8000 元
revision 2: 本次旅行预算调整为 10000 元

新版本生效后,旧版本可以标记为被替代,而不是物理上无痕地消失。这样既方便解释,也方便审计和纠错。

结论

一个可用的 Agent Memory,至少要把以下概念分开:

  • Event:发生了什么
  • Evidence:依据来自哪里
  • Candidate:模型认为可能值得记住什么
  • Memory Fact:系统正式允许使用什么
  • Task State:当前任务做到哪里

最小工程契约可以概括为:

每条长期记忆都必须带证据、作用域和版本;每个正在运行的任务都必须有结构化状态。

下一篇继续讨论这些对象的控制边界:谁可以写入,谁可以读取,删除如何传播,外部动作如何确认真的完成。

posted @ 2026-09-10 21:06  杜文龙  阅读(20)  评论(0)    收藏  举报