记忆系统决定了一个 Agent 能否跨会话保持任务连贯、记住用户偏好,而不是每次都从零开始。本文从短期记忆与长期记忆的分工讲起,覆盖写入、检索、遗忘的完整生命周期,并厘清记忆与 RAG 的边界以及一致性、隐私这两个最容易被低估的工程问题。

它解决什么问题

大语言模型本身是无状态的:一次请求能利用的信息,只有当前上下文窗口里装下的那部分。由此产生三个结构性矛盾:

  1. 窗口有限:长对话跑到几十轮后,早期信息必然被挤出窗口,Agent 表现为「聊着聊着忘了前面的约定」。
  2. 会话即焚:进程结束,本轮交互中的用户偏好、任务结论默认全部丢失,下一个 Session 只能重新认识用户。
  3. 塞满不等于用好:把全部历史无差别塞进上下文,token 成本线性上涨,还会触发注意力稀释——关键信息埋在长文中间时更容易被模型忽略。

记忆系统的定位就是在这三个约束下做取舍:短期记忆维护当前任务的完整状态,长期记忆把跨会话仍有价值的信息(偏好、事实、决策、教训)沉淀到外部存储,需要时按相关性取回,而不是把整段聊天记录原样搬回来。

核心原理

短期记忆:上下文窗口内的暂存

短期记忆是当前会话中直接参与推理的内容:用户输入、模型回复、工具返回的中间结果。它的载体就是上下文窗口,因此管理手段全部围绕「控制膨胀」展开,常见三种:

  • 缩减:超过 token 阈值后丢弃最早的 N 轮(滑动窗口),或用轻量模型把历史压缩成滚动摘要,用信息损耗换空间。
  • 卸载:工具返回的巨型结果(整页 HTML、大 CSV)放到外部存储,上下文里只留一个引用句柄,需要细节时再通过工具按范围读取。
  • 隔离:主 Agent 只把子任务说明和必要片段交给子 Agent,避免完整历史随任务广播带来的重复消耗。

长期记忆:外部存储的沉淀

长期记忆由写入(Record)和读取(Retrieve)两条链路构成。写入通常发生在会话结束后:异步任务调用 LLM 对本轮对话做语义提纯,抽取出高价值的结构化事实,例如「用户技术栈是 Python + FastAPI」,再写入向量库、KV 或图数据库。读取发生在新会话开始时:把当前 query 向量化后做相似性检索,命中的条目注入系统提示或独立槽位。

从功能上看,长期记忆还可以细分:情景记忆回答「发生过什么」(上周用户反馈过订单超时),语义记忆回答「这意味着什么」(该用户对性能问题比功能更敏感),程序记忆则沉淀可复用的操作流程(审查该用户的代码时优先查 OOM 风险)。这个分类的价值在于:不同类型记忆的写入时机、更新频率和检索方式都应该不同。

深入机制

写入策略:克制是第一原则

把每轮对话都送去抽取,寒暄、试探性猜测、重复描述都会进库,记忆库很快变成流水账。工程上应设置写入门槛:只持久化用户显式确认过的信息、工具返回的结构化结论、以及经摘要压缩后的高价值事实。

两个容易踩的坑:其一,LLM 可能把「我可能会考虑用 Rust」这样的假设性陈述固化成稳定偏好,写入前可用 JSON Schema 约束字段并复核低置信度条目;其二,重试会产生重复记忆,幂等键应基于源消息 ID + 抽取批次 ID,而不是对抽取结果做哈希——温度采样会让语义相同的内容字面不同。

检索策略:防污染比提召回更重要

检索侧的核心不是「捞得全」,而是「别让无关记忆带偏当前任务」。推荐的最小组合:

def recall(user_id: str, query: str, store, reranker, threshold: float = 0.72):
# 硬过滤由数据层统一注入,不依赖模型自觉
candidates = store.search(query, filter={"user_id": user_id}, top_k=20)
ranked = reranker.rerank(query, candidates) # 交叉编码器精排 + 时间衰减
fresh = [m for m in ranked if not m.expired and m.score >= threshold]
return fresh[:5] # 低分宁缺毋滥

要点有三个:多租户过滤条件(user_id、时间范围)必须由数据访问层强制注入,否则一个用户的偏好会泄漏进另一个用户的上下文;向量召回之后用重排模型精排,并在精排阶段叠加时间衰减,而不是每次对全库算衰减分;设相关性阈值,低于阈值宁可不注入——一条错误的「记忆」对输出的破坏远大于没有记忆。

遗忘与演化:记忆需要新陈代谢

只写不清理的库,几个月后 Top-K 里就全是过时噪音。常见机制包括:

  • 重要性打分:score = relevance × importance × decay(t),衰减通常取指数形式,让久未被使用的内容自然沉底。
  • 冲突解决:新事实与旧事实矛盾时(用户从 Java 8 升级到 Java 21),旧条目标记废弃并打时间戳,既保证检索只命中新事实,也保留审计线索。
  • 合并巩固:同一事实的多个碎片说法由离线任务归并为带来源的实体条目,避免检索结果被重复内容占满。
  • 主动遗忘:支持按用户、按时间范围的删除,这同时是合规要求。

工程实践

  • 长任务定期滚动摘要:不要等窗口爆了再压缩,按固定轮次或 token 比例触发,摘要本身作为短期记忆的一部分参与续写。
  • 写入前脱敏:手机号、密钥、内部链接在进入长期记忆前过滤;高重要性条目保留源对话指针以便追溯。
  • 排障先看检索 Trace:Agent「没用上已有记忆」时,先查 query 改写、过滤条件、候选集、重排打分四步,确认候选库确实缺信息后,再回头改抽取规则。
  • 轻量场景可以用文件:项目约定、少量偏好这类可枚举的内容,写进版本可控的 Markdown 比维护向量链路更划算,还天然可审计;大量非结构化记录才需要检索层。

对比与权衡

维度 短期记忆 长期记忆(向量库) RAG 知识库 全历史直塞
存储位置 上下文窗口内 外部存储,按用户沉淀 外部知识源,共享 上下文窗口内
服务对象 当前任务状态 个性化经验与偏好 公共事实与文档 当前会话
检索方式 无需检索 语义检索 + 过滤 语义检索 + 权限过滤 无
更新成本 随对话自动滚动 异步抽取,需治理 更新索引即可 无
主要风险 窗口溢出、摘要失真 记忆污染、隐私泄漏 召回错误证据 成本高、注意力稀释

记忆与 RAG 的边界值得单独强调:技术上两者都用向量库和语义检索,但服务对象不同。RAG 从共享知识源取回可引用的证据,回答「世界是什么样的」;长期记忆管理 Agent 与特定用户交互中沉淀的个性化状态,回答「这个用户是什么样的」。实践中两者应当分工:企业事实走 RAG 并带引用,会话偏好走记忆并带来源时间戳,模型才能区分「制度规定」与「上次聊天的约定」。

面试视角

短期记忆和长期记忆怎么划分、各自怎么实现? 短期记忆是上下文窗口内的对话与工具结果,管理手段是滑动窗口、摘要压缩和大结果卸载;长期记忆在外部存储,由异步抽取写入、按 query 检索注入。划分的依据是生命周期:会话结束即失效的放窗口内,跨会话仍有价值的才落库。

记忆系统和 RAG 有什么区别? 技术栈相似但职责不同:RAG 面向共享知识、强调证据与溯源,记忆面向用户态、强调个性化与任务连续性。检索时两者可以分开召回再统一排序,但权限模型、更新链路和注入位置都应独立设计。

什么是记忆污染,怎么治理? 指过时、错误或无关的记忆被注入上下文,带偏当前输出。治理手段:写入端设门槛与置信度复核,检索端设相关性阈值加硬过滤,运维端做时间衰减、冲突标记和定期清理。核心思路是「宁缺毋滥」。

记忆的一致性怎么处理? 多端并发写入时用版本控制或乐观锁合并;事实冲突时新条目生效、旧条目标记废弃而非直接覆盖;重试靠幂等键去重。完全依赖 LLM 判重不可靠。

记忆系统怎么做隐私合规? 写入前 PII 脱敏,条目携带 user_id 与时间戳,支持按用户删除与导出,检索侧由数据层强制租户隔离。敏感场景还要设定 TTL,到期自动遗忘。

小结

  1. 记忆系统在无状态的模型之上构建连续性:短期记忆受窗口约束,靠缩减、卸载、隔离控制膨胀;长期记忆走「异步写入 + 按需检索」两条链路。
  2. 写入要克制、检索要设防、遗忘要常态化——未治理的记忆库比没有记忆更危险。
  3. 记忆与 RAG 分工是个性化与共享知识的分工,不是技术上的二选一。
  4. 一致性与隐私不是附加功能:幂等写入、冲突标记、租户隔离、可删除可导出,都是上线前的必选项。