WRITING / 2026.09.23

用户改变主意后,AI 如何更新记忆?

以技术背景变更和学习目标删除为例,说明补充、替换、有效时间、版本历史、幂等收据与删除传播,并用 Java 验证关键边界。

Alice 先说“我主要写 Java”,随后说“我最近在学习 Python”,最后说“我的主要语言改为 Kotlin”。如果系统按最后一句覆盖所有旧信息,就会丢失学习目标;如果所有信息永远并存,回答时又可能同时称她主要写 Java 和 Kotlin。更新的核心是确定哪些事实发生了什么变化。

本文延续记忆写入篇,给出一个可运行的版本与时间模型。它不是完整的生产记忆平台,但足够暴露补充、替换、到期和删除之间的差别,并让这些差别进入测试。

先确定变化发生在哪个属性

示例把主要语言、当前学习目标和学习进度分成不同槽位。学习 Python 是对背景信息的补充,修改主要语言才替换同一槽位。槽位是应用的业务建模结果,不等同于向量相似度。两条内容再相似,也可能属于不同主体、任务或时间范围。

输入 槽位 预期动作
我主要写 Java background.primary 创建背景版本
我最近在学习 Python goal.current 创建目标,保留背景
我的主要语言改为 Kotlin background.primary 关闭旧背景,创建新版本
删除当前学习目标 goal.current 删除该用户该槽位的所有版本

自然语言中的模糊变更不应强行套用这个表。例如“以后也许转 Kotlin”没有明确声明当前主要语言已经改变。生产系统可以暂存意向、请求澄清或维持原值,但需要明确策略。本例仅接受约定句式,不承担模糊语言裁决。

用半开区间表达有效性

每个版本保存 validFrom 和可空的 validUntil,有效区间定义为 [validFrom, validUntil)。开始时刻包含在内,到期时刻排除在外。这样在替换时刻,旧版本退出,新版本进入,不会因为两边都包含端点而同时有效。

public boolean activeAt(Instant at) {
    return !at.isBefore(validFrom)
        && (validUntil == null || at.isBefore(validUntil));
}

这段逻辑与下载示例一致。假设 Java 背景从 07:00 生效,07:50 被 Kotlin 替换,则 07:49:59 仍可对应 Java,07:50 的当前查询只返回 Kotlin。具体值由时间语义决定,不应该依赖数据库行恰好按什么顺序返回。

示例中的输入 observedAt 被直接用作有效开始时间,是为了缩小模型。真实世界可能同时需要事件发生时间与系统得知时间:用户今天补充“上周已经转岗”,这两条时间线不同。不能只增加两个字段就宣称实现了双时间模型,还要规定按哪个时间查询、如何回填和如何解释历史答案。

保留历史与当前查询分开

替换事实时,示例先关闭旧版本的有效区间,再创建版本号递增的新对象。当前查询只返回固定时钟下有效的版本;历史查询用于解释变化。历史里存在一条事实,不代表它可以进入当前回答。检索前的有效性过滤不可省略。

一个重要测试是:更新主要语言后,查询 Java 不再返回旧背景,同时历史中仍能看见旧版本及其结束时间。只验证“新值存在”是不够的,因为错误通常来自新旧两份都被装配到上下文。对需要解释过去行为的场景,可以提供明确的历史查询入口,而不要把历史记录混入默认召回。

版本号解决的是变化标识,不自动解决并发。示例通过同一实例的同步方法串行执行;生产系统通常还要在事务中检查当前版本,防止两个写入同时基于同一个旧状态修改。失败的一方重新读取后再判断补充、合并还是冲突,不应简单重复覆盖。

乱序与同时冲突如何处理

离线示例拒绝早于该槽位最新版本开始时间的事件,也拒绝同一时刻产生的冲突值。这样的规则比较严格,但容易解释:示例不尝试自动重写已经形成的历史。拒绝时应保持原状态不变,调用方能够明确知道需要进一步处理。

这不是所有产品都适合的最终策略。真实事件可能因为离线设备或队列延迟而乱序到达。如果业务必须接纳,应设计历史回填、区间拆分和索引重建,并考虑已经生成的回答是否需要纠正。不要为了减少错误日志,直接把所有迟到事件的时间改成当前时间,那会破坏事实来源。

还要注意示例的删除边界:删除槽位后,它只保留版本计数和已接收事件的幂等收据,不保留完整时间历史。因此相同事件的重试被阻断,但带全新事件 ID 的旧内容仍可能被再次写入。生产环境若需要阻止延迟任务重新写回,还应加入删除代次、撤回水位或任务取消机制。

到期、衰减与删除不是一回事

到期表示信息在某个时刻之后不再参与当前使用;衰减表示降低召回权重;删除则要求按约定范围移除数据。降低分数无法兑现删除承诺。把记录保留在历史里也不等于已经物理清理。产品文案、接口名称和实际行为必须一致。

示例中的 forget(user, slot) 移除该槽位全部版本及其证据原文,搜索自然无法再次返回这些对象。但它保留事件收据的摘要、用户标识和版本计数,用于阻断旧事件重试。这是“忘记某项记忆”,不是完整账户数据清除;摘要也不应被当成匿名化证明。

int removed = store.forget("alice", "goal.current");
var remaining = store.search("alice", "Python", 10);

在完整案例中,removed 为 1,remaining 仍有一项学习进度。这个结果符合槽位范围:删除学习目标不会自动删除进度里的 Python。接口如果提供“删除与 Python 有关的一切”,就需要另外定义匹配范围、关联对象和用户确认方式。

删除后的旧事件重试

假设事件 e2 曾成功写入目标,随后用户删除目标,但消息队列又投递了一次 e2。如果删除时连幂等信息一起清空,系统可能把它当成新事件再次执行。示例保留对应收据;重放相同请求时发现原对象已经不存在,返回空结果,不创建新对象。

如果用户之后明确表达新的学习目标,应该使用新的事件 ID,系统允许重新建立记忆。这里同时满足两个需求:拒绝旧投递复活已撤回的信息,允许新的主动输入形成新的意图。只有一个“永不再记住该文本”的黑名单,会让后者变得困难。

调用者曾经取得的旧对象引用、已经输出的回答、外部日志或缓存,不会因为内存 Map 删除而自动消失。示例的保证只覆盖 Store 内部的后续读取。服务化时应列出所有副本,让删除事件传播到数据库、检索索引、应用缓存和派生画像,并核对完成状态。

可观察的生产治理建议

建议为一次删除生成任务标识,记录请求范围、各存储处理状态和失败重试情况。前台可以区分“已停止使用”与“历史副本按保留策略清理中”,前提是产品确实实现了这种区分。没有完成的环节不能被一个总开关遮蔽。

对于更新,记录旧版本、新版本、来源事件以及变更原因,有助于解释“为什么现在这样回答”。对敏感内容应控制日志中的正文暴露。审计需要证明动作发生过,通常不需要再复制一份完整私人记忆;审计与遗忘之间的保留边界应单独制定。

先用故障注入验证链路:关闭一个索引节点后执行删除,恢复节点再搜索;暂停后台写入后删除目标,恢复队列看旧任务是否复活。这些生产测试尚未包含在本文离线程序中,不能用本地的通过数替代它们。

验证与参考

测试源码覆盖版本替换、区间起止、到期、全部版本删除、旧事件重试、新事件重建以及乱序拒绝。把每条测试对应到产品承诺,才能判断未来更换数据库或框架后是否仍满足要求。

Graphiti 项目提供了时间事实和来源关联的另一种实现视角;具体机制与版本边界见框架比较篇。下一篇进入检索与上下文装配,讨论为什么“数据库里有正确答案”仍然不足以让助手回答正确。

AI 记忆工程:从概念到可验证的系统

  1. AI 记忆工程指南:从概念到可验证的系统
  2. AI 记忆、RAG 与上下文管理是什么关系?
  3. 从一段对话到一条可靠记忆
  4. 用户改变主意后,AI 如何更新记忆? · 当前文章
  5. 检索到了,为什么还是答错?
  6. 用同一个案例拆解 Mem0、Graphiti 与 LangGraph 记忆机制
  7. 加了记忆,Agent 到底变好了吗?

按时间浏览