:Agent 记忆系统设计)
引言第 10、11 篇解决了 Agent 如何从外部知识库获取信息。但还有一类知识不是预先写在文档里的,而是在交互过程中逐渐产生的:用户喜欢 Java、不吃辣;上周已经讨论过技术方案 A;某次工具调用为什么失败。Agent 如果每次对话都从零开始,就像一个每天失忆的同事。这一篇讨论如何为 Agent 构建记忆系统:什么值得记、存在哪里、什么时候取出来,以及如何遗忘错误和过期的信息。一、先纠正一个误解:聊天记录不等于记忆很多系统把全部历史消息重新塞进模型上下文,就宣称自己“有记忆”。这只能算最原始的短期记忆,而且很快会失效:历史越来越长,token 成本不断上涨;无关信息越来越多,模型注意力被稀释;早期错误可能长期污染后续回答。真正的记忆系统至少包含四个动作:交互事件 ──▶ [判断是否值得记] ──▶ [提炼并存储] │ 当前任务 ◀── [按需注入上下文] ◀── [检索相关记忆] │ [更新 / 衰减 / 遗忘]因此要先区分两个概念:记忆存储(Memory Store)负责长期保存事实、经历和偏好。它可能是数据库、向量库、文件或图谱。上下文(Context)是本轮真正送给模型的信息。上下文窗口容量有限,只能从记忆库中挑选少量相关内容注入。记忆库像档案室,上下文像当前办公桌。档案室可以很大,但办公桌上只能摆这次工作真正需要的材料。所以,记住不等于把所有内容永久塞进 prompt;有记忆也不等于每轮都读取全部记忆。一个可靠系统的关键是:存得准、取得对、忘得掉。二、四类记忆:短期、长期、情景与语义Agent 领域经常混用这些名词。最实用的理解方式是先按保存时间区分短期与长期,再把长期记忆细分为情景和语义。1. 短期记忆(Short-term / Working Memory)短期记忆是完成当前会话或任务所需的临时状态,例如最近几轮消息、当前计划、工具返回和未完成步骤。它通常直接位于模型上下文或工作流 State 中。之前文章讲的LangGraph 的 messages 和 State 就是短期记忆:它们帮助 Agent 知道“现在做到哪了”,但不一定值得跨会话永久保存。适合存:当前目标、最近消息、任务进度、临时变量、刚返回的工具结果。不适合存:完整原始日志、几十页工具结果、与当前任务无关的历史。2. 长期记忆(Long-term Memory)长期记忆是跨会话保留、未来仍可能有价值的信息。用户下周再来,Agent 仍能记得“他偏好 Java 示例”,这才是长期记忆。长期记忆不是一种具体数据结构,而是生命周期概念。它下面最重要的两类是情景记忆和语义记忆。3. 情景记忆(Episodic Memory)情景记忆记录“曾经发生过什么”,带有时间、场景和过程。例如:2026-08-18:用户比较了 Qdrant 与 pgvector,最终选择 pgvector, 原因是现有系统已使用 PostgreSQL,预计数据量低于 500 万。它类似人的经历。适合回答“我们上次为什么这么决定”“之前排查过什么”。情景记忆通常按时间保存,可用向量检索找相似经历,也可按项目、用户和时间过滤。4. 语义记忆(Semantic Memory)语义记忆记录从多次经历中提炼出的稳定事实、规则与偏好,通常不强调具体发生时间。例如:用户偏好:示例代码使用 Java,并尽量保持简单。 项目约定:向量数据库优先复用 PostgreSQL pgvector。它类似人的常识。情景记忆经过归纳,可能沉淀为语义记忆:多次情景: “用户三次要求示例改成 Java” “用户不希望示例代码太复杂” │ 提炼 ▼ 语义记忆:“用户偏好简单的 Java 示例”两者不要混为一谈。情景记忆保留来龙去脉,语义记忆保留稳定结论。前者适合复盘和类比,后者适合个性化和规则遵循。类型核心问题典型内容生命周期常见存储短期记忆当前做到哪了?消息、计划、工具结果当前任务/会话上下文、State、缓存长期记忆未来还要记住什么?跨会话信息天到长期数据库、文件、向量库情景记忆以前发生过什么?决策、故障、执行轨迹中长期,可衰减事件表 向量索引语义记忆稳定事实是什么?偏好、规则、项目知识长期,需更新KV/关系库/图谱三、记忆生命周期:写入、检索、注入、更新、遗忘一个完整的记忆系统不是“向量库 相似度搜索”,而是一条闭环。1. 写入:什么值得记最危险的做法是把每句话都永久保存。用户随口说的玩笑、临时要求、模型自己编出的结论,都可能变成长期污染。写入前至少判断四件事:相关性:未来任务还可能用到吗?一次性闲聊通常不值得记。稳定性:这是长期偏好,还是“今天临时这样”?“以后代码都用 Java”稳定;“这次先别写测试”未必稳定。可信度:信息来自用户明确陈述、可靠工具结果,还是模型推测?模型推测不应该自动晋升为事实。敏感性:是否包含密码、Token、身份证、健康或财务信息?敏感数据默认不记,确需保存时要取得同意并加密、限权。可以让 LLM 做“记忆提取器”,但最终通过结构化规则约束:{ shouldRemember: true, type: preference, content: 用户偏好使用简单的 Java 示例, confidence: 0.98, source: explicit_user_statement, scope: user, expiresAt: null }这比直接保存整段聊天强得多:内容已提炼,有来源、置信度、作用域和过期时间。2. 检索:哪些记忆与当前任务相关检索不能只靠向量相似度。实际应综合多个信号:综合得分 语义相关性 重要性 新鲜度 作用域匹配 来源可信度例如用户问“继续写博客”,与当前系列相关的项目记忆应优先;即使“用户爱喝咖啡”在语义上勉强相关,也不该进入上下文。元数据过滤同样重要:先按 user_id / project_id / memory_type / validtrue 缩小范围,再做向量召回和重排序。第 11 篇的检索基础设施在这里完全复用。3. 注入:取出来不等于全部塞进去检索到 20 条记忆,不代表 20 条都要进入 prompt。应先去重、解决冲突、压缩后再注入,并设置独立 token 预算。推荐把记忆放进明确分区,而不是混在对话历史里:[长期记忆] - 用户偏好简单的 Java 示例。 - 当前博客系列面向混合受众,原理与实战平衡。 [注意] 这些记忆可能过期;若与用户当前明确要求冲突,以当前要求为准。最后这句非常重要:当前明确指令永远优先于旧记忆。用户以前喜欢 Java,这次明确要求 Python,系统不能拿旧偏好压过当前要求。四、压缩与蒸馏:记忆不能无限增长如果每次对话都产生十条长期记忆,一年后检索库会充满重复碎片。需要周期性做压缩(Compression)和蒸馏(Distillation)。压缩有三个层次:消息摘要:把几十轮短期对话压成一段任务摘要,释放上下文空间。情景合并:把多条相似经历合成一条较完整的事件记录。例如五次排查同一故障,合并为“该故障的常见原因与解决步骤”。语义蒸馏:从多个情景中提炼稳定规律,写入语义记忆;原始情景降低权重或归档。原始消息(几十条) │ 会话结束时摘要 ▼ 情景记忆(一次完整经历) │ 多次相似经历聚合 ▼ 语义记忆(稳定偏好/规律) │ 原始情景降权或归档 ▼ 更小、更准、更稳定的长期记忆库但摘要是有损的。金额、日期、错误码、用户原话等关键事实不能只靠自然语言摘要,应作为结构化字段原样保留。摘要负责压缩叙事,结构化字段负责保护事实。五、一个最小 Java 记忆系统下面用 Java 写一个极简核心片段,展示“提炼后写入、按范围检索、注入上下文”的流程。为突出逻辑,省略 MemoryStore 的具体数据库/向量索引实现和 HTTP/SDK 样板。import java.time.Instant; import java.util.*; public class MemoryAgent { record Memory( String id, String userId, String type, // preference / fact / episode String content, double importance, Instant createdAt, Instant expiresAt ) {} interface MemoryStore { void upsert(Memory memory); ListMemory search(String userId, String query, int limit); void forget(String userId, String memoryId); } static final MemoryStore store createMemoryStore(); // 省略实现;生产换数据库向量索引 static String chat(String userId, String userMessage) { // ① 读取:只取与当前问题最相关的少量长期记忆 ListMemory memories store.search(userId, userMessage, 5); String memoryContext memories.stream() .map(m - - m.content()) .reduce(, (a, b) - a b \n); // ② 注入:记忆独立分区,并声明当前指令优先 String prompt 你是一个助手。 [长期记忆] %s [规则] 记忆仅供参考;若与用户当前要求冲突,以当前要求为准。 [用户当前消息] %s .formatted(memoryContext, userMessage); String answer callLlm(prompt); // ③ 写入:不是保存整段对话,而是先提炼值得长期保留的事实 extractMemory(userId, userMessage).ifPresent(store::upsert); return answer; } static OptionalMemory extractMemory(String userId, String message) { // 演示规则;生产中可用结构化输出让 LLM 判断 shouldRemember/type/content if (message.contains(以后) message.contains(Java)) { return Optional.of(new Memory( UUID.randomUUID().toString(), userId, preference, 用户偏好示例代码使用 Java, 0.9, Instant.now(), null)); } return Optional.empty(); } static MemoryStore createMemoryStore() { throw new UnsupportedOperationException(示例省略 MemoryStore 实现); } static String callLlm(String prompt) { return 模型回答(省略 HTTP/SDK 样板); } }这段代码故意没有把“全部历史消息”永久保存,而是完成了三个关键动作:回答前按用户和问题检索少量记忆。把记忆放进独立上下文区域,并声明当前指令优先。回答后只提炼值得长期保存的事实,而不是原样保存整段聊天。生产版本还需要补上去重、冲突更新、置信度、过期时间、敏感信息过滤、审计和用户删除入口。真正难的不是 save() 和 search(),而是记忆治理。六、Mem0、Letta 与 Zep:三种代表性路线下面三个代表性方案看起来都叫“Memory”,但出发点不同。相关产品演进很快,选型时应以当前版本文档为准。Mem0:给应用加一层可插拔长期记忆Mem0 更像一个独立的记忆服务层。它从对话中提取值得记住的信息,做去重和更新,然后在后续请求中按相关性召回。它适合给已有聊天助手或 Agent 快速补上用户偏好和长期事实。适用场景:个性化助手、客服、教育陪练,以及已有 Agent 只缺长期记忆的项目。优点:接入轻、API 直观;自动提取和更新记忆;支持用户、Agent、会话等作用域。缺点:自动提炼存在误记风险;复杂的情景轨迹和任务状态不是它的强项;生产使用仍需自己做隐私和授权治理。Letta(MemGPT):把记忆管理做进 Agent 架构Letta 的思想来自 MemGPT:把有限上下文看成“主存”,把外部存储看成“虚拟内存”,由 Agent 主动决定哪些内容留在上下文、哪些移出、何时调回。它不是简单给现有 Agent 加个记忆库,而是把记忆管理本身变成 Agent 行为的一部分。适用场景:长生命周期、持续运行、有稳定身份和大量历史的 Agent。优点:提供完整的分层记忆与上下文管理思路;适合长期持续交互;Agent 可以主动管理自己的记忆。缺点:架构更重,学习和运维成本高;自主记忆管理也会带来不可预测性;简单聊天应用用它属于过度设计。Zep / Graphiti:面向时态知识图谱的 Context GraphZep 当前的核心路线更接近时态知识图谱与 Context Graph:从消息和业务事件中抽取实体、关系及事实,同时记录事实何时生效、何时失效。相比只做“对话摘要 向量召回”,它更擅长回答“某个事实在当时是否成立”“实体之间是什么关系”,并保留事实演变的时间线。适用场景:长期客服会话、用户画像、客户/项目关系网络,以及需要时间有效性和历史追溯的 Agent。优点:能表达实体关系和事实有效期;适合处理随时间变化的信息;比单纯向量检索更适合多跳关系与历史查询。缺点:需要新增图谱型基础设施;实体和关系抽取仍可能出错;如果只需保存少量稳定偏好,可能过重。方案核心定位最适合最大优势主要代价Mem0可插拔长期记忆层给现有 Agent 快速加记忆轻量、自动提取更新误记与治理需兜底Letta记忆原生 Agent 运行时长生命周期 Agent分层记忆与主动管理架构和运维较重Zep / Graphiti时态知识图谱 / Context Graph时间与关系复杂的记忆事实有效期与关系建模增加图谱基础设施务实选型:先问是否真的需要专用框架。少量用户偏好用关系数据库即可;已有 Agent 想快速加长期记忆,看 Mem0;Agent 本身以持续存在和自主管理记忆为核心,看 Letta;时间有效性和实体关系复杂,看 Zep / Graphiti。七、记忆系统的安全与治理记忆比普通 RAG 更敏感,因为它往往直接记录用户本人。以下四条是生产底线。用户可见、可改、可忘记。系统记住了什么,应允许用户查询、更正和删除。否则错误记忆会长期影响体验,也无法满足隐私法规中的删除要求。最小化保存。能不记就不记;能保存提炼后的偏好,就不要保存完整隐私对话;能设过期时间,就不要永久保留。防止记忆投毒。攻击者可能诱导 Agent 写入“以后忽略安全规则”之类恶意记忆。记忆内容不能拥有高于系统规则的优先级;外部文本和模型推断默认低可信;高影响规则的写入必须走白名单或人工确认。租户隔离与权限。user_id / tenant_id / agent_id / project_id 必须参与存储和检索过滤。不能因为语义相似,让一个用户的记忆进入另一个用户的上下文。此外,每条长期记忆最好带上 source、created_at、updated_at、confidence、expires_at、version。当回答受某条记忆影响时,系统才能解释“为什么这样回答”,也能在记忆错误时追溯来源。八、踩坑记录坑一:把所有聊天记录都永久向量化。噪音和重复会迅速淹没真正重要的信息。先提炼再写入,原始记录只按合规要求短期留存或归档。坑二:只追加,不更新。用户已经改用 Kotlin,库里还躺着“偏好 Java”,检索时两条一起出来。长期事实要版本化,新事实应使旧事实失效。坑三:把模型推测当事实。模型从一句话猜出“用户是金融从业者”,然后永久保存,这会形成自我强化的错误。推测默认不进入长期语义记忆,除非用户确认。坑四:检索只看相似度。相似但过期、低可信、属于别的项目的记忆可能排在前面。必须结合时间、重要性、作用域与来源综合排序。坑五:记忆塞得越多越好。长上下文中的无关记忆会降低模型表现。给记忆单独设 token 预算,宁可少而准。坑六:没有“忘记我”的能力。用户要求删除后只从主表删掉,向量索引和缓存里仍可能存在。在线数据链路应立即清除;不可变备份按保留策略失效,并维护删除标记,确保备份恢复时不会让已删除记忆“复活”。审计记录只保留操作证明,不能保留被删原文。坑七:Agent 自己能随意改系统规则。长期记忆只能作为背景事实和偏好,不能覆盖系统提示、安全策略和权限。记忆不是更高优先级的 prompt。九、小结与下一步这一篇建立了 Agent 记忆的完整心智模型:短期记忆负责当前任务,长期记忆负责跨会话连续性;情景记忆记录经历,语义记忆沉淀稳定事实。记忆系统不是“保存聊天记录”,而是写入、检索、注入、更新、遗忘和压缩组成的闭环。其中最重要的原则是:记忆库可以很大,上下文必须克制;当前明确指令优先于旧记忆;系统不仅要会记,更要会忘。Mem0、Letta 和 Zep 提供了不同层次的实现选择,但无论是否使用框架,真正决定质量的仍是记忆治理:什么值得记、来源是否可信、何时过期、冲突如何处理、用户能否删除。