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: 被替代的旧版本
这里最重要的不是字段数量,而是几个工程约束:
- 记忆必须知道适用于谁、什么任务或什么场景。
- 记忆必须知道什么时候生效、什么时候失效。
- 记忆必须知道当前版本以及旧版本的关系。
- 记忆必须能追溯到证据。
六、Task State:任务当前做到哪一步
Task State 和 Memory Fact 很容易被混淆。
例如:
goal: 完成一次家庭旅行预订
done: 确定目的地,筛选出三家酒店
missing: 选择酒店,确认付款
next: 展示候选酒店并等待选择
blocker: 等待用户确认
这描述的是一个任务的运行状态,不是用户的长期事实。
任务状态需要支持明确的状态转换,例如:
created -> planning -> waiting_confirmation -> executing -> completed
如果外部系统返回不确定结果,还应该允许进入 outcome_unknown,先查询和对账,再决定是否继续,而不是直接假设失败或重复执行。
七、这些对象如何协作
用户说:“继续处理我上次的旅行计划。”
系统可以按以下路径工作:
- 通过 Event 找到相关的用户行为和任务事件。
- 通过 Evidence 找到预算、偏好和候选方案的原始依据。
- 读取已经通过治理的 Memory Fact。
- 读取当前 Task State,知道上次停在哪里。
- 根据当前权限和时间范围过滤结果。
- 组装上下文交给 Agent。
- 用户做出新选择后产生新的 Event。
- 从新 Event 中提取 Candidate。
- 处理 Candidate 和旧 Memory Fact 的冲突。
- 接受后生成新 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:当前任务做到哪里
最小工程契约可以概括为:
每条长期记忆都必须带证据、作用域和版本;每个正在运行的任务都必须有结构化状态。
下一篇继续讨论这些对象的控制边界:谁可以写入,谁可以读取,删除如何传播,外部动作如何确认真的完成。
浙公网安备 33010602011771号