Agent 记忆的时态冲突:有效区间、替代关系与当前事实投影
用户偏好、项目决策和角色都会变化。本文用有效时间与记录时间拆开事实,显式处理替代、迟到和并发冲突,避免最后写入者制造错误记忆。
本文要点
- 主题聚焦:大模型、AI Agent、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
只有 key/value/updated_at 的记忆会被迟到事件复活。多轮对话状态管理 负责会话内状态,本篇处理跨会话时间与冲突。
分开事实何时成立与系统何时知道
版本分开业务有效区间与首次 recorded_at,时间保存 RFC 3339 UTC。服务端把可信资源 ID 规范成 (scope_kind, scope_key),不用模型给的名称;事实身份为 (tenant,subject,scope_kind,scope_key,fact_key)。
CREATE TABLE memory_fact_version (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL,
subject_id uuid NOT NULL,
scope_kind text NOT NULL,
scope_key text NOT NULL,
fact_key text NOT NULL,
value_json jsonb NOT NULL,
valid_during tstzrange NOT NULL,
recorded_at timestamptz NOT NULL DEFAULT now(),
lifecycle text NOT NULL CHECK (lifecycle IN
('PROPOSED', 'ACTIVE', 'SUPERSEDED', 'REVOKED')),
source_id text NOT NULL,
source_hash text NOT NULL,
supersedes_id uuid
);
CREATE TABLE memory_fact_conflict (
left_version_id uuid NOT NULL,
right_version_id uuid NOT NULL,
relation text NOT NULL CHECK (relation IN ('OPEN', 'RESOLVED')),
PRIMARY KEY (left_version_id, right_version_id)
);
区间用 [from,to)。value/valid/source 载荷不可变;version 表的 lifecycle 列与 conflict 表只是可丢弃的当前投影。每次变化追加 memory_fact_event(event_seq,recorded_at,event_type,version_ids,canonical_payload,payload_hash),同键 sequence 单调,可按 cutoff 重放 system-time 历史。
tstzrange 不会阻止重叠,权威表要保留冲突,故不排斥。已裁决投影可启用 btree_gist,再加 EXCLUDE USING gist (tenant_id WITH =, subject_id WITH =, scope_kind WITH =, scope_key WITH =, fact_key WITH =, valid_during WITH &&)。PostgreSQL 18 文档访问于 2026-07-18,换版本须重测。
替代必须是显式关系,不是覆盖
“从 7 月 1 日改用 pnpm”让新版本指向旧版本;“这次用 pnpm”只能绑定任务或短期限,不能关闭全局偏好。记忆写入门禁 保证范围来自证据。
同一身份区间重叠、value 不同且无替代关系时,版本仍为 ACTIVE,另加 OPEN conflict。冲突不是 lifecycle,否则活动查询会漏掉它;范围不同则修正 scope,否则请求澄清。RAG 证据冲突 处理一次回答,这里处理持久事实。
function resolveAt(events: FactEvent[], validAt: Date, systemAsOf: Date) {
const snapshot = replayBySequence(events.filter(e => e.recordedAt <= systemAsOf));
const eligible = snapshot.versions.filter(v =>
snapshot.isEligibleAt(v.id, validAt) && contains(v.validDuring, validAt));
const open = snapshot.conflicts.filter(c => c.relation === "OPEN" &&
eligible.some(v => v.id === c.leftId) && eligible.some(v => v.id === c.rightId));
if (open.length > 0) return { kind: "conflict", edges: open };
if (eligible.length === 0) return { kind: "unknown" };
if (distinctValues(eligible).length > 1) {
return { kind: "conflict", ids: eligible.map(v => v.id) };
}
return { kind: "value", row: eligible[0] };
}
isEligibleAt 应用截至 systemAsOf 已知的 SUPERSEDE 生效时间与 REVOKE 事件;不能拿今天的 ACTIVE 投影回答历史。当前投影只优化 validAt=now, systemAsOf=now。
当前值只是可重建投影
memory_current 只是缓存投影。版本事务写 outbox,单键 worker 重算投影并保存 sequence;落后时回退事件重放或返回 stale。事件驱动架构 的幂等消费适用于此处。
每天用独立 SQL 从版本表重算快照,并比较 current 的 value hash、版本 ID、冲突数和开放区间数。对账不能只比总行数;一个用户的偏好错位不会改变总量。
用乱序、边界与回拨测试验收
准备序列:先收 7 月新值再收 6 月旧值、事件重复、并发冲突、相邻区间、撤销与替代。固定同一 validAt,改变 systemAsOf 前后查询,断言能复现当时所知结果;任意投递顺序得到同一 current hash,未解决冲突绝不进入 Prompt。
灰度前影子计算新投影,与旧表比较七天。已解释差异按“纠正旧错误、发现冲突、范围变化”分类,未知差异必须为零。发布按租户切读,旧投影和规则版本保留一个回滚窗口。
回滚只把读别名切回旧投影并停止新规则消费,不删除新版本或倒改时间。修复规则后从事件序号零重建;若来源本身错误,将对应版本标为 REVOKED,再重算受影响键。可靠时态记忆不是永远给出一个值,而是在事实不确定时拒绝伪造“当前”。
延伸阅读
Related