Agent 的记忆系统怎么设计?

深入系统设计原理约 7 分钟读完

一句话回答

短期记忆就是当前上下文:本次任务的消息、工具结果和中间结论,受上下文窗口限制,靠截断、摘要、压缩来管理。长期记忆存在外部(数据库、向量库、文件),跨会话保留,按内容可以分为事实(用户偏好、项目约定)、经历(做过的任务和结果)、技能(做事的方法和流程)。设计长期记忆要回答四个问题:写入什么、什么时候写、怎么检索、怎么更新和遗忘。错误或过时的记忆会持续误导后续对话,所以要记录来源,并让用户能查看和删除。

详细解析

短期记忆和长期记忆

短期记忆 长期记忆
在哪里 上下文窗口里 外部存储:数据库、向量库、文件
生命周期 一次会话或一次任务 跨会话保留
管理方式 截断、摘要、清理旧的工具结果 写入、检索、更新、遗忘
主要问题 容量有限,越长越贵,中间的信息容易被忽略 写错、过时、检索不到或检索到不相关的内容

多轮对话的历史怎么截断和摘要,属于短期记忆的管理,见 多轮对话的上下文怎么管理。下面重点讲长期记忆。

长期记忆存什么

类型 例子 适合的存储和检索方式
事实 用户偏好 TypeScript、项目用 pnpm、用户所在的时区 结构化存储,按键精确读取和覆盖;数量少时每次都加载
经历 上次部署失败是因为缺了环境变量、某个报错是怎么解决的 带摘要和时间的记录,按相似度检索
技能 本项目的发布步骤、周报的格式要求 写成说明文档或脚本,任务相关时再加载

编程 Agent 的项目说明文件(如 CLAUDE.md)本质上就是一份由人和 Agent 共同维护的长期记忆。

四个设计问题

  1. 写入什么:只存以后还有用、稳定、可信的信息。用户明确表达的偏好可以存;一次性的请求、临时状态、模型推测出来的结论不要存;密码、证件号等敏感信息不存
  2. 什么时候写:一种是对话中实时写,给模型一个"保存记忆"的工具,用户说"记住……"时调用;另一种是会话结束后由后台任务抽取,可以批量去重、合并,代价是要到下次会话才生效
  3. 怎么检索:少量核心事实每次都放进上下文;其余的按当前问题检索,可以综合相关度、时间远近和重要程度排序;也可以提供"搜索记忆"工具,让模型按需查询
  4. 怎么更新和遗忘:新信息和旧记忆冲突时(比如用户换了城市),更新而不是追加;有时效的信息设置过期时间;长期用不到的记忆降低权重或清理;用户可以查看、修改、删除自己的记忆

错误记忆的风险

  • 写错:模型理解错了用户的话,或者把推测当成事实写进去,之后每次对话都会被误导
  • 过时:用户的情况变了,旧记忆没有更新
  • 被投毒:网页、邮件里的恶意内容被当成"记忆"写入,就成了一次持续生效的提示注入,见 什么是数据投毒
  • 泄露:记忆没有按用户隔离,A 的信息出现在 B 的对话里

应对:每条记忆记录来源和时间;只从可信来源写入,比如用户本人明确的表达,外部内容不直接写入;按用户隔离存储和检索;放进上下文时注明"以下是历史记忆,可能已过时";提供查看和删除的入口。

代码示例

TypeScript
interface Memory {
  id: string
  userId: string
  type: 'fact' | 'episode' | 'skill'
  key?: string // 事实类记忆的键,如 preferred_language,同一个键只保留最新的值
  content: string
  source: string // 来源:哪次对话、用户原话还是后台抽取
  updatedAt: Date
}

// 给模型用的记忆工具。执行时 userId 从登录态获取,不作为参数暴露给模型
const saveMemoryTool = {
  name: 'save_memory',
  description:
    '保存在以后的对话中仍然有用的信息,如用户明确表达的偏好。不要保存一次性的请求、敏感信息或你自己的推测。同一类信息使用相同的 key,新值会覆盖旧值。',
  parameters: {
    type: 'object',
    properties: {
      key: { type: 'string', description: '记忆的键,如 preferred_language' },
      content: { type: 'string', description: '要记住的内容,写成完整的一句话' },
    },
    required: ['key', 'content'],
  },
}

// 每次对话开始时组装记忆:按用户隔离,事实全部加载,经历按问题检索(store 是记忆存储的封装)
async function buildMemoryContext(userId: string, question: string) {
  const facts: Memory[] = await store.list({ userId, type: 'fact' })
  const episodes: Memory[] = await store.search({ userId, type: 'episode', query: question, topK: 3 })
  const lines = [...facts, ...episodes].map((m) => `- ${m.content}(${m.updatedAt.toISOString().slice(0, 10)})`)
  return `以下是关于该用户的历史记忆,可能已经过时,仅供参考;与用户当前的说法冲突时,以用户为准:\n${lines.join('\n')}`
}

面试官可能追问

长期记忆都用向量库存就行了吗?

不一定。事实类记忆有明确的键,用关系型数据库或键值存储更合适,能精确读取和覆盖更新;向量检索适合按语义查找相似的经历。也有不少 Agent 直接用文件存记忆,由模型自己读写和整理,简单、可读,方便人工检查。实际系统往往组合使用。

记忆和 RAG 有什么区别?

检索技术可以一样,区别在数据本身。RAG 的知识库由人整理,相对静态,多个用户共享;记忆是 Agent 从交互中自己写入的,按用户隔离,不断变化,所以要额外处理更新、冲突和遗忘。

上下文压缩和长期记忆是什么关系?

压缩发生在一次会话内部:上下文快满时把早期内容摘要成要点,腾出空间。长期记忆是把信息存到会话之外,下次还能用。两者可以配合:压缩时顺便把值得长期保留的信息抽取出来,写入长期记忆。

用户要求"忘掉我的某个信息",要怎么做?

提供查看和删除记忆的界面。删除要彻底:数据库、向量库、缓存里的副本都要删掉,由这条记忆生成的摘要也要处理;对话历史里如果也有,要一并处理。同时要满足所在地区个人信息保护的要求。

易错点

  • 把所有对话原样存下来当记忆,检索出来的噪声远多于有用信息
  • 记忆只增不改,新旧矛盾的信息同时进入上下文
  • 记忆没有按用户隔离
  • 把检索到的记忆当成确定的事实甚至指令,而不是"可能过时的参考"

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。