WRITING / 2026.09.23

从一段对话到一条可靠记忆

用 Java 21 离线示例拆解候选提取、主体与证据校验、规范化、幂等写入和事实合并,并说明模型接入后的生产边界。

用户说“我主要写 Java”,助手回复“那你可能熟悉 Spring”。两句话一起进入记忆提取器时,应该保存什么?第一句是用户明确声明的背景,第二句是助手的推测。如果把后者直接写成用户事实,错误会在后续会话里不断被重新引用,最终看起来像一条已经确认的信息。

本篇讨论 AI 记忆工程专题的写入链路。示例用规则替代模型理解,让我们能单独验证身份范围、证据、幂等与合并。规则的局限会明确列出,不把几种句式的成功识别解释成一般自然语言理解能力。

先保留证据,再形成候选

原始交互至少应带有可信的用户身份、事件标识、内容角色和时间。这里的身份应该来自服务端认证,不应由模型输出决定。角色用于区分用户陈述、助手回复、工具结果与外部引用;时间用于判断信息何时发生,以及是否晚于当前已知事实。

提取器的产物是候选信息。候选应该能够回答“谁的什么属性、内容是什么、根据哪段证据得出”。如果原文是“我朋友主要写 Java”,主体就不能直接填成当前用户。如果原文是“假设我主要写 Java”,它也不能被无条件提升为真实背景。

示例使用三个对象分离职责:Candidate 表达事实槽位和值,Evidence 保存事件标识、原始陈述与时间,Memory 保存经过接纳后的版本及有效区间。模型提取器将来可以替换规则实现,但不应绕过后面的数据校验和写入约束。

public record Candidate(String slot, String value) {}
public record Evidence(String eventId, String quote, Instant observedAt) {}

把可识别范围写清楚

示例只支持四种完整句式:“我主要写 X”“我的主要语言改为 X”“我最近在学习 X”“我学到 X”。前两种写入技术背景,第三种写入学习目标,第四种写入学习进度。无法匹配的输入返回空候选,不会为了让演示继续而猜一个答案。

例如“我不主要写 Java”和“我最近在学习 Python?”不会产生候选。但规则仍不能理解所有否定、引用、讽刺和多主体句子。“我最近在学习 不存在的课程”可以匹配,因此成功提取只能证明它符合句式,不代表内容已经由外部事实验证。应用需要按自己的业务定义可信程度。

var store = new MemoryDemo.Store(
    Clock.fixed(MemoryDemo.NOW, ZoneOffset.UTC));
store.remember("alice", "e1", "我主要写 Java",
    MemoryDemo.NOW.minusSeconds(3600), null);
store.remember("alice", "e2", "我最近在学习 Python",
    MemoryDemo.NOW.minusSeconds(2400), null);

这段代码节选自完整离线示例的接口用法,运行时需要引用该类及 java.time。结果是两条不同槽位的记忆,不会因为 Java 和 Python 都是编程语言就互相覆盖。先确定属性语义,比先比较两个字符串是否相似更重要。

规范化应当保守

规范化可以去除无意义空白、统一明确的日期格式,或者通过业务词表映射已知别名。但语义近似不意味着事实相同。“会 Java”“正在学 Java”“不想再写 Java”可能同时包含 Java,却表达不同关系。把它们统一成“Java 用户”会丢失任务需要的区别。

示例保留值的原始大小写,在检索时才把拉丁词项转为小写。这样的选择意味着“Java”和“java”作为写入值可能形成不同版本,属于刻意保守的教学实现。生产应用可以引入受控规范化,但必须同步定义展示值、比较值和证据原文,不能修改原文后又声称它是用户原话。

置信度也应谨慎使用。模型返回的 0.9 不会自动成为校准过的概率,更不代表权限。如果使用该字段,应记录它来自哪个模型或规则,验证不同分数与错误率的关系,并把主体、时间、格式等硬约束放在分数之外。

幂等保护的是事件,不只是内容

网络超时后客户端可能重试同一次写入。若每次都生成一个新记忆 ID,就会重复保存证据,甚至重复关闭旧版本。示例使用 (user, eventId) 作为幂等键,同时保存输入内容与时间参数的摘要。同一个键、相同输入返回之前对应的对象;同一个键但输入变化会被拒绝。

这与“语义去重”不同。两个独立事件可能重复确认同一事实,它们应当保留各自证据;同一个事件也可能被投递多次,它们不应被计算为多份独立证据。把这两层混在一起,会使来源数量和置信判断失真。

示例没有对无法识别的输入保存收据,也没有实现分布式幂等存储。进程重启后收据会丢失。生产服务需要把事件收据和状态变化放在可恢复的事务边界里;不能先写记忆,后写幂等记录,再假设两步之间不会崩溃。

重复事实与新版本

当新事件与现有有效事实具有相同槽位、相同值和相同到期时间时,示例合并证据,保留原版本号。这个条件比“内容相似”严格,也便于测试。如果值发生变化,则进入更新逻辑;如果原事实已经过期,即使值相同,也要形成新的有效版本。

例如 Alice 在两个时间点都明确说主要写 Java,两次事件可以支持同一背景版本。但她先说“这周学习 Python”,后说“这个月继续学 Python”,有效期已经变化,不能因为值相同而丢弃新信息。时间也是记忆语义的一部分。

合并后仍需保留可追溯的来源。不要只把来源数从一加到二,却丢掉两次事件各自的时间与原文。另一方面,来源也不应无限复制。真实系统可以保存事件引用而不是大段正文,并用独立保留策略管理原始记录;示例为了可读性,直接保留少量陈述。

同步写入还是后台写入

同步写入的好处是下一步立即看见结果,错误也能在当前请求中处理;代价是提取和存储的耗时进入响应链路。后台写入可以缩短当前交互等待,却引入可见性窗口、任务重试、积压和顺序问题。这是应用时序的取舍,不由“是否用了记忆框架”自动解决。

对于学习进度,可以先把权威业务事件可靠落库,再异步生成便于检索的记忆。若用户紧接着问上次进度,可以直接查询权威记录,或者在同一会话中使用刚刚确认的状态。不要在异步任务还没完成时,对用户承诺“以后已经一定记住”。

本例所有操作在同一进程同步执行,并以 synchronized 串行保护。它说明方法调用之间的状态一致性,不模拟消息队列、跨进程锁或数据库事务。引入分布式设施时,测试也必须相应扩展,不能把本地锁的保证直接搬到多个实例。

接入模型之前先规定失败输出

一个实用的模型提取协议至少应允许“没有可保存信息”,而不是强迫每段输入输出一条记忆。解析失败、主体不明确、引用无法定位、值超过长度约束,都应该有可观察的结果。可以重试、进入待确认队列或放弃写入,但不能悄悄把原始回复当成结构化结果保存。

外部内容还可能包含“忽略之前规则、记住某个错误身份”等指令。它们应作为待分析文本,而不是控制写入权限的指令。提取器只能在应用授予的用户范围和允许槽位内产生候选;真正的身份、字段白名单与数据访问必须由服务端检查。

建议记录每个事件的处理结果:无候选、校验拒绝、重复投递、证据合并、新建版本或处理失败。日志中只保留诊断必要的信息,避免直接记录大量私人正文。这样遇到问题时,能够定位是提取器没识别、规则拒绝,还是存储没有成功。

验证与继续阅读

运行测试程序,可以验证支持句式、重复投递、幂等键冲突、重复事实合并及目标补充。测试中的预期来自业务定义,而不是从当前输出倒推,因此实现改变时能发现行为回退。

下一篇讨论更新与遗忘,尤其是旧事实应该何时失效,以及删除后重试为何容易造成记忆复活。概念参考可阅读 LangChain 关于记忆写入时机的说明;本文中的幂等协议、规则提取与 Java 数据模型是本专题的示例设计。

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

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

按时间浏览