实现一个简单的长期记忆DEMO

实现一个简单的长期记忆DEMO

要理解智能体的长期记忆,可以先看两个问题:一轮对话结束后,系统怎样判断哪些信息值得留下;之后遇到相关问题时,又怎样把这些信息找回来。本文通过一个 Java Demo 展开这两个环节:回答完成后,模型提出记忆操作候选,由 Java 程序校验并保存;下一轮提问时,程序检索相关记忆,连同近期对话一起提供给模型。

代码位于 Gitee 仓库的 Page8 目录:https://gitee.com/toxplore/ai/tree/master/Page8

一、长期记忆在 Demo 中保存什么

Demo 把长期记忆按用户归属,使用 userId 区分不同用户;同一用户的记忆可以在不同 threadId 会话中召回。短期对话消息仍按 userId + threadId 隔离。

long_term_memories
├─ id                记忆 ID
├─ user_id           记忆所属用户
├─ memory_type       semantic / episodic / procedural
├─ content           记忆正文
├─ source_thread_id  来源会话
└─ updated_at        最近更新时间

三类记忆分别用于保存事实、经历、偏好或规则。这里的重点是长期记忆的管理流程;数据库保存完整记录,而本轮只把检索到的少量候选交给模型。

二、从问答到长期记忆变更:整体流程

长期记忆 Demo 流程检索记忆并用于回答,模型提取候选操作,Java 校验后新增、更新、删除或忽略。 用户提出问题main() 按 userId 检索相关长期记忆findRelevantMemories(…, Top K=5)关键词重合计数 + 更新时间排序 组装本轮模型上下文system 提示中的相关记忆 + 近期对话 + 当前问题buildSystemPrompt() / callModel() 回答模型生成答复应用保存本轮短期对话并展示回答store.saveTurn(…) 提取模型提出记忆操作候选读取近期 user 消息与已有记忆快照extractMemoryOperations(…) Java 应用校验候选置信度、类别、长度、敏感内容、目标 IDapplyMemoryOperations(…) 操作类型?且校验通过 ADD UPDATE DELETE IGNORE / 校验失败 新增或去重upsertMemory()写入当前 userId 更新已有记录updateMemory()匹配已有 target_id 删除指定记录deleteMemory()要求明确遗忘意图 不修改记忆表闲聊、临时信息等仍在对话历史中 SQLite 长期记忆表(按 userId 归属)

关键边界:提取模型只返回候选 JSON,不直接连接数据库。Java 应用负责检查候选并执行存储操作;提取调用失败不会撤销已经生成并保存的本轮回答。

三、模型提出的操作 JSON

extractMemoryOperations() 把提取规则、近期用户发言和最多 100 条已有记忆交给模型,请它返回以下结构。Assistant 的回复不会被当作用户事实。

{
  "operations": [
    {
      "action": "ADD",
      "type": "procedural",
      "content": "用户偏好简洁的中文回答,少用术语。",
      "target_id": null,
      "confidence": 0.96
    }
  ]
}
字段含义示例或规则
action模型建议执行的动作ADD、UPDATE、DELETE、IGNORE
type记忆类别semantic 事实、episodic 经历、procedural 偏好或规则
content精炼后的记忆正文新增或更新后要保存的内容
target_id待更新或删除的已有记录 IDADD 通常为 null;UPDATE、DELETE 指向已有记录
confidence模型按提示给出的自评数值当前程序忽略低于 0.85 的候选;不是校准概率

四、applyMemoryOperations():校验后再执行

该函数位于 LongTermMemoryDemo.java。它接收提取模型的 JSON、当前用户和线程信息、最新用户消息以及提取前的记忆快照,返回成功变更的条数。

  1. 读取 operations:如果缺少操作数组或结构不正确,则不修改记忆。
  2. 过滤:置信度低于 0.85 或动作为 IGNORE 时直接跳过。
  3. 处理 DELETE:最新用户消息必须包含明确的遗忘意图;目标 ID 还必须存在于当前用户的记忆快照中。
  4. 验证内容:ADD 和 UPDATE 的记忆类别必须有效;正文长度为 4–400 个字符,且不能命中敏感信息检查。
  5. 执行 ADD:调用 upsertMemory() 写入;数据库用用户、类别和正文的唯一约束避免重复。
  6. 执行 UPDATE:目标记录必须属于当前用户,且类别匹配,然后调用 updateMemory()。
  7. 执行 DELETE:调用 deleteMemory(userId, id);SQL 同时限定用户 ID 和记忆 ID。
DELETE 的额外保护:即使模型返回了 DELETE 候选,代码仍会检查最新用户消息是否明确要求忘记,避免模型单方面删除记忆。

五、长期记忆的查询与手动管理

自动召回

每次普通提问时,findRelevantMemories(userId, question, 5) 先从当前用户的记录中取候选,按关键词重合数量由高到低排序;同分时优先较新记录,最多取 5 条。命中的记忆加入 system 提示,近期对话则以各自的 role 加入请求消息。

这版是轻量关键词检索:中文用相邻双字作为检索词,英文按单词匹配。它不是向量检索,面对同义改写时可能漏召回;数据库中的完整记忆仍保留。

用户命令

命令行为相关存储函数
/memories列出当前 userId 的全部长期记忆及来源线程。listMemories(userId)
/forget <记忆ID>删除当前用户指定的记忆;不属于当前用户的 ID 不会删除。deleteMemory(userId, id)
普通问题自动检索相关记忆并将少量结果提供给模型。findRelevantMemories(...)

六、Demo 和实际项目的差距

这个 Demo 把长期记忆的核心闭环放在一个 Java 程序里,便于看清每一步由谁负责。真正上线后,数据量、用户规模和可靠性要求都会提高,因此下面这些地方通常还需要进一步设计。

1. 记忆提取会增加一次模型调用

Demo 在每次成功问答后同步调用提取模型。这样流程直观,但用户需要多等一次模型响应,也会增加 API 成本;若提取接口故障,记忆本轮就没有更新。实际系统可以把提取工作放到后台队列异步执行,配置重试和失败告警,同时确保主问答不被拖慢。

2. 模型给出的 confidence 不能当作真实概率

这里的 confidence 是模型根据提示自行生成的分数,0.9 并不代表经过统计校准后有 90% 的正确率。Demo 用阈值做一层简单筛选;生产系统还应结合明确的规则校验、来源证据、冲突检测,必要时对高风险记忆要求用户确认,并通过评估数据检查误写和漏写。

3. 关键词检索的语义能力有限

当前实现把问题和记忆拆成词,再按重合数量排序,优点是逻辑简单、结果容易解释,也不需要额外的 Embedding 调用。但“别太啰嗦”和“回答简洁”可能用词不同却意思接近,关键词检索未必能匹配。记忆数量或问法变化增多时,可考虑全文检索与向量检索混合召回,再重排候选,并用检索评估集验证效果。

4. 自动写入需要完整的记忆生命周期管理

只把内容写进去还不够:重复记忆要合并,用户更正要处理旧值与新值的冲突,过时信息要能更新或过期,用户要求遗忘时也要确保真正删除。Demo 只用有限规则演示这些动作;实际项目通常还要记录来源和更新时间,制定保留策略,并提供查看、纠正、删除和审计能力。

5. 存储与权限要适应真实部署

SQLite 适合单机学习 Demo,数据量和并发上升后,生产服务一般会考虑集中式数据库、备份恢复、迁移和并发控制。每次读取、更新或删除都必须校验用户身份和数据归属;敏感信息还需要明确的保存范围、访问控制和删除策略,不能只依赖模型提示词来保护。

总结

通过这个 Demo,可以看到长期记忆并不是把所有旧对话一股脑交给模型。每次提问时,程序先从属于当前用户的记忆中找出少量相关内容,和近期对话一起提供给回答模型;回答完成后,再由提取模型判断用户这轮有没有表达值得以后保留的信息。

提取模型给出的只是操作建议。是否新增、更新或删除,最终由 Java 程序依据规则和用户归属进行检查,再写入 SQLite。这样,模型负责理解内容,应用负责把关并管理数据。

这版实现用关键词排序和简单校验展示了基本思路。真实项目还需要随着用户规模和使用要求,逐步完善记忆检索、异步提取、数据治理、权限与评估。理解这个基本闭环后,我们就有了继续改进长期记忆系统的起点。

本页对应 Page8 Java Demo 当前实现。记忆提取规则、置信度和关键词检索均为教学演示设计。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注