很多文章一上来就讲「短期记忆 / 长期记忆 / 向量检索」,读完你还是不知道自己的 Agent 该存什么、什么时候存、存错了怎么办。

这篇换个路子:先给你看三个真实翻车现场,让你建立"为什么需要记忆"的体感;再用一个三问框架把记忆系统设计的所有决策点串起来;最后用一张全景图对齐主流方案(mem0 / MemGPT / ChatGPT memory / LangChain / Zep),告诉你它们各自回答了哪几个问题、留了哪些坑。

读完你应该能拿着纸笔,画出一套属于自己业务场景的 Memory 设计,而不是闭眼背概念。


一、业务场景:三个翻车现场,三个问题

忘掉所有术语。想象你在用一个 AI 助手,看三个真实会发生的"翻车现场"。

翻车 1:记不住(跨会话失忆)

你:我对花生过敏,帮我记住。
助手:好的,我已经记住了。
(第二天,新开对话)
你:帮我订个午餐。
助手:好的!为您推荐宫保鸡丁——花生满满,很香哦!
你:???

为什么翻车:大多数 Agent 的"记忆"就是本次对话的消息列表。新开对话,消息列表清空——昨天的"花生过敏"随着上次对话结束就消失了。

用户感受:它每次都像第一次见我。

翻车 2:装不下(对话太长)

第 1 轮:描述问题
第 2-20 轮:各种尝试、报错、日志
第 21-49 轮:缩小范围,锁定方向
第 50 轮:你:所以按我们刚才的分析,下一步该查什么?
助手:(开始胡言乱语,忘记前面分析过的结论)

为什么翻车:模型一次能读的内容有物理上限(上下文窗口)。对话越长,塞回的历史越多:超限报错;没超限但很长 → 费用暴涨 + 注意力稀释(lost in the middle,模型对开头第 3 轮的约束经常视而不见)。

用户感受:聊得越久它越笨,而且越聊越贵。

翻车 3:管不了(记忆出错)

你:(口误)我对花生过敏。
助手:(记住了)
(此后每次推荐食谱都避开花生)
你:其实我说错了,我不过敏。
助手:(新记忆写进去,但旧记忆还在)
(之后行为随机:有时避开花生,有时不避)

多人场景更可怕:

同事 A:(说错)数据库密码每周三轮换
Agent:(记住了)
同事 B/C/D:(未来 3 个月,每次问密码都被告知"每周三轮换")
3 个月后才发现错了 → 已误导全团队 3 个月

为什么翻车:记住的只是文字,没有配套管理:不知道谁写的、错了怎么改、过时了怎么清。

用户感受:它记错的东西比它忘掉的东西更可怕——因为你会信任它。

三个翻车,三个问题,一个根源

翻车 问题 一句话
现场 1 记不住 会话结束,消息列表清零
现场 2 装不下 历史全量塞回,有上限、贵、笨
现场 3 管不了 光存文字不行,要能溯源/更正/过期

三个问题指向同一个根源:

模型是一条"金鱼"——每次调用都是一张白纸,只认识你这次塞给它的内容。

记忆系统就是围绕这条金鱼搭建的"外部设备":把重要的存到窗外(解决记不住),把塞回去的裁剪到装得下(解决装不下),把存的管理起来(解决管不了)。

你设计 Memory 要解决的,就是这三个问题,没有第四个。 后面所有的方案、框架、代码,都是对这三个问题的不同回答。


二、解决思路:一个本质定义 + 一个三问框架

2.1 本质定义:记忆系统 = 窗内窗外的搬运工

先建立物理直觉。模型不是流式"听"你说话,每次调用:

你发起一次调用:
   把【所有内容】一次性发给模型
        ↓
   模型读完这一坨,生成回复
        ↓
   调用结束。模型对这次调用的记忆【立即清零】

三条关键认知:

  1. 一次性:读的是这次塞给它的完整内容,不是边听边想
  2. 无后台:这次没塞的信息,对模型等于不存在(它不会"去查"、“去回忆”)
  3. 即焚:回复完就失忆,下次调用又是一张白纸

那个"一次能塞多大"的物理上限,就是上下文窗口。窗口里装什么:

┌─────────────── 上下文窗口(比如 128K token)───────────────┐
│ system prompt(人设和规则)                                   │
│ 工具清单(有哪些工具可调)                                     │
│ 历史消息(这个对话之前说过什么)          ← 会话越长这里越胖   │
│ 注入的外部内容(搜到的资料、取回的记忆)   ← 记忆系统的主角    │
│ 你刚说的话                                                    │
└────────────────────────────────────────────────────────────┘

窗口是稀缺资源,有三重稀缺:

  • 物理上限(硬的):超限直接报错,没有商量。
  • 钱(每轮重复计费):第 1 轮发 1K 付费 1K,第 50 轮发 50K 付费 50K,50 轮总付费 ≈ 1+2+…+50 ≈ 1275K,平方级增长
  • 注意力(越长越笨):lost in the middle,塞 100 轮历史,模型对开头的约束经常视而不见。窗口大了 ≠ 窗口好用了。

三重稀缺合起来:窗口不仅要"塞得下",还要"塞得少而准"。

窗口之外的所有信息(昨天的对话、用户档案、知识库)就是窗外

        ┌────────────┐
        │  上下文窗口  │ ← 模型唯一能看到的世界
        └────────────┘
              ↑ ↓ 只有两条路
┌─────────────────────────────┐
│           窗外               │
│  · 昨天的对话(已结束)        │
│  · 用户的档案、偏好           │
│  · 上上周的任务记录           │
│  · 公司知识库 / 文档          │
└─────────────────────────────┘

于是记忆系统有了本质定义:

记忆系统 = 一套在"窗内"和"窗外"之间搬运信息的机制。

搬运只有两个方向:

写入(窗内 → 窗外):这次对话里出现了重要的东西,挑出来存好
读取(窗外 → 窗内):下次需要时,把它取回来拼进窗口

行业黑话对照(后面会反复用):

黑话 白话
注入(inject) 把窗外取回的内容拼进这次的请求里
召回(recall) 读取方向的主力动作
提取(extract) 写入方向"挑出重要东西"的动作

所有主流方案本质都是这个搬运工,差别只在三件事:搬什么、怎么搬、何时搬。 这三件事就是下面的三问框架。

2.2 三问框架:存什么 / 怎么取 / 怎么忘

任何记忆方案都只是对同样三个问题的不同回答。学会用这三问去拆,就不会迷路。

             ┌─────────────────────────────────────┐
             │           窗外(记忆库)              │
             └─────────────────────────────────────┘
                ↑ 问1        ↓ 问2          问3
             存什么?      怎么取?       怎么忘?
             (写入)      (读取)      (遗忘/治理)
                │              │              │
             挑重要的      需要时搬回     过时的、错的
             搬出去        窗内           清理掉/管起来

三问顺序 = 一条信息的生命周期:进来(存)→ 被用(取)→ 退出(忘)

问 1:存什么?(写入)

四件事要拍板:

a. 存原材料,还是存提炼物?

方案 A:把原始聊天记录整段存      (存录像)
方案 B:从对话提炼出一条条事实存  (存要点)

A 忠实但臃肿,检索时要再加工,没法直接管理;B 有损但干净,天生适合检索、去重、更新。主流选 B——长期记忆不存原始消息,存提炼后的结构化条目(带 subject / type / provenance / status)。原始消息归原始消息(会话日志/checkpoint),长期记忆的归长期记忆(提炼条目)。

b. 谁来判断"重要"? 三种流派:

流派 谁判断 例子
规则 关键词/格式命中 偏好关键词表(“我喜欢”“我偏好”)
模型判 LLM 读对话后决定 mem0 的提取管线
显式 用户/模型主动说"记住这个" ChatGPT memory、save_memory 工具

c. 什么时候写? 每轮写 / 会话结束写 / 模型随时调工具写。

d. 写之前查不查旧账? 新事实和已有记忆矛盾怎么办——直接并存会导致行为随机。要做冲突消解(mem0 用 LLM 决策 ADD/UPDATE/DELETE/NOOP,也可以用 subject 键比对 + supersede 标记取代)。

问 2:怎么取?(读取)

三件事要拍板:

a. 怎么知道"需要"? 匹配手段三档进化:

第 1 档:关键词匹配     内容含"过敏"就召回          (简单可测)
第 2 档:向量相似       语义接近就召回              (主流,需 embedding)
第 3 档:知识图谱       顺着关系推理召回            (高级,v2 候选)

b. 谁来发起取? 双通道:

被动通道:每轮自动把相关记忆拼进窗口(模型无感知,保鲜)
主动通道:给模型一个 search 工具,它觉得需要就自己查(灵活省空间)

成熟系统两条都要。

c. 取回来放哪、放多少? 记忆注入要吃窗口预算——注入 2K 记忆,可能就要先压缩掉 2K 旧历史。注入是只读搬运:每轮重新检索、重新注入、用完即弃,store 是唯一信息源(绝不把注入的记忆存回 state,否则下次提取会自我复制污染记忆库)。

问 3:怎么忘?(遗忘与治理)

两个层次:

a. 正常遗忘(信息过时):

方式 机制 例子
TTL 过期 写时定好有效期,到期自动失效 “促销到周五”
新旧取代 新事实进来,旧的标记退役 “过敏"→"不过敏”
窗口滑出 只保留最近 K 条 滑动窗口
人工删除 用户/管理员手动清 GDPR

b. 治理(记忆出错/被滥用):

溯源:谁写的?什么时候?从哪次对话来的?   (provenance)
更正:错了怎么改?直接删=销毁证据,正确做法是标记取代、留痕
权限:谁能读/写/删?个人记忆和共享记忆规则一样吗?

不对称原则:忘掉一条正确记忆,损失是一条信息;留着一条错误记忆,污染的是之后每一次回答。治理严格程度随共享范围升级。

2.3 分层模型:把三问安放到时间尺度上

三问框架告诉你"要回答哪些问题",但没告诉你"不同生命周期的信息要不要分开存"。这是下一个常见错误:

一个 ArrayList 存所有记忆:
  - 今天的对话消息
  - 上周的对话消息
  - 用户偏好("喜欢深色模式")
  - 任务工作状态("正在等 CI 通过")
  - 公司知识库摘要
全混在一起,统一存取

为什么不行?生命周期完全不同:

今天的对话消息    活几小时
上周的对话消息    活几天
用户偏好          活几个月甚至永久
任务工作状态      活几分钟到几天
公司知识库        永久,更新慢

混在一起的两类灾难:

灾难 A(过度保留):今天的临时消息被当成永久记忆,半年后还在污染检索结果
灾难 B(误清永久):清理过期任务状态时,顺手把用户偏好也清了

分层不是为了好看,是因为生命周期不同的东西硬塞一起必然互相伤害。 照搬人脑(Atkinson-Shiffrin 模型:感觉记忆 → 短期记忆 → 长期记忆),Agent 三层:

┌─────────────────────────────────────────────────────────┐
│ 第 1 层:Working Memory(工作记忆)                      │
│   = 一次 Agent run 内的消息列表                           │
│   生命周期:一个 run(几十秒到几分钟)                     │
│   存什么:当前这次推理的对话                              │
│   怎么取:不需要取,它本来就在窗内                        │
│   怎么忘:run 结束自动消失                                │
├─────────────────────────────────────────────────────────┤
│ 第 2 层:Session Memory(会话记忆)                      │
│   = 一次会话的多轮消息累积                                 │
│   生命周期:一次会话(几小时到几天)                       │
│   存什么:这个会话从开头到现在的所有消息                   │
│   怎么取:每轮 run 开始时,拼进工作记忆                    │
│   怎么忘:会话结束归档或丢弃                               │
├─────────────────────────────────────────────────────────┤
│ 第 3 层:Long-term Memory(长期记忆)                    │
│   = 跨会话持久化的事实条目                                 │
│   生命周期:跨会话、跨 run,长期持久                       │
│   存什么:提炼后的结构化条目(不存原始消息)               │
│   怎么取:按需检索注入                                    │
│   怎么忘:TTL 过期 / supersede 取代 / 管理员删除           │
└─────────────────────────────────────────────────────────┘

三层之间的搬运关系:

       用户消息
          ↓
    ┌─────────────┐
    │ Working Mem │ ← run 内活跃
    └─────┬───────┘
          │ run 结束同步回
          ↓
    ┌─────────────┐
    │ Session Mem │ ← 会话内累积
    └─────┬───────┘
          │ 会话结束提取
          ↓
    ┌─────────────┐
    │ Long-term   │ ← 跨会话持久
    └─────────────┘
          ↑
          │ 下次会话开始时检索注入
          └────── 回到 Working Mem

设计启示:你至少要为这三层各设计一套存取机制。很多系统只做了第 1 层(消息列表)就号称有"记忆",其实是翻车现场 1。

2.4 单会话内的问题:压缩 + 归档(解决翻车 2)

长对话的死亡螺旋:

第 1 轮:装窗 1K token
第 10 轮:装窗 10K token
第 50 轮:装窗 50K token  ← 三重稀缺全中

三种解法:

解法 思路 优点 缺点
滑动窗口 只保留最近 K 条,老的直接丢 简单 丢得太狠
摘要替换 老消息总结成摘要替换原文 保留要点 丢了原文
压缩+归档 老消息总结成摘要 + 原文归档 + 保留最近 K 条 要点留窗内、原文可回溯 实现稍复杂

光"丢"不行,光"摘要"也不行,"摘要+归档"才是闭环。 这是 pi 的 compaction 思路,完整流程:

1. 估预算:所有消息字符数 / 4 > budgetTokens ?
      否 → 原样返回(大多数轮次走这条)
      是 → 进入压缩

2. 切分消息:
      system 消息           → 全部保留(人设/规则不能动)
      前 (n - keepRecent) 条 → 待归档段(要被压缩的)
      后 keepRecent 条       → 近期保留段(原样留窗内,默认 2~6)

3. 调模型总结:待归档段 → 拼成长文本 → 调模型 → 拿到摘要
      失败 → 降级截断(前 500 字符),不抛异常
      (记忆系统的故障不应该拖垮主流程)

4. 重组窗口:[system..., summary(伪装成 user 消息), 最近 K 条]

5. 归档原文(关键!):
      被压缩掉的原文 → 存成长期记忆条目(type=SUMMARY)→ 落库

两个关键设计点

  1. 就地改写 state,而不是只改请求副本。因为持久化(Checkpoint)存的就是 state。如果只改请求副本不改 state:压缩时请求里是摘要+最近 K 条,但 state 里还是全部 50 条 → Checkpoint 存了 50 条 → 下次恢复读回 50 条 → 又超窗 → 又压缩 → 永远原地打转。就地改写 = 让持久化的 = 模型实际见过的

  2. 摘要有损,归档兜底回溯。压缩本质是一种"受控遗忘":从工作记忆遗忘(腾空间),但归档到长期记忆(保可回溯)。

compaction 本质是三问框架里"怎么忘"在单会话内的回答:窗内的老消息被摘要替换(遗忘),原文归档进长期记忆(可回溯)。

2.5 跨会话的长期记忆:提取/检索/注入流水线(解决翻车 1)

翻车 1 的根因:会话结束 AgentState 丢弃,第二天新会话模型不知道昨天的"花生过敏"。需要两段管道:

写入管道(会话 A 结束时):从对话挑出"花生过敏",存进长期记忆库
读取管道(会话 B 开始时):从记忆库把"花生过敏"捞回,注入窗口
写入管道:四步走(写谨慎)
会话消息
  ↓ 第 1 步:提取候选
  │   只看 USER 消息(assistant 回复可能含幻觉,记下来等于把幻觉固化)
  │   关键词/模型判定命中 → 生成候选条目(type/importance/subject)
  ↓ 第 2 步:闸门 1——重要性阈值
  │   importance >= 阈值 ?  否 → 丢弃
  ↓ 第 3 步:闸门 2——频控(去重)
  │   已有相同 subject 且 content 完全相同 ?  是 → 丢弃
  ↓ 第 4 步:闸门 3——冲突取代(supersede)
  │   已有相同 subject 但 content 不同 ?
  │     是 → 旧条目标记 SUPERSEDED(不物理删!)+ 新条目写 ACTIVE
  │     否 → 直接写 ACTIVE
store.write(entry)

四步全在写路径——写谨慎。 矛盾记忆不能并存,否则模型行为随机。

读取管道:三步走
1. 检索:
   recall(scopes=[user:u1, channel:c1])
     → 隔离过滤(scope 不在列表里绝不返回)
     → 只回 ACTIVE
     → TTL 惰性过滤
     → 关键词/向量匹配
     → 按 createdAt 倒序 + limit

2. 渲染:
   命中条目 → 拼成 "[Known memories]\n - [PREFERENCE] 记住我对花生过敏"
            → 包装成一条 user 消息

3. 注入:
   窗口 = [system, [Known memories], 历史消息..., 用户刚说的话]
   注入位置:system 之后、历史之前

关键:注入不落盘。 [Known memories] 这条消息不加进 AgentState。如果注入的记忆被存回 state,下次提取会从 messages 里再提取出这条 → 写回 store → 自我复制 → 记忆库被污染。注入是只读搬运,每轮重新检索、重新注入、用完即弃。

跨会话记忆本质是三问框架的完整闭环:存什么(四步写入)、怎么取(三步读取)、怎么忘(TTL + supersede)。

2.6 治理:记忆出错了怎么办(解决翻车 3)

翻车 3 的根因:光存文字不行,要能溯源/更正/过期。核心不对称:

遗忘(信息过时了):
  - 损失:丢一条信息
  - 代价:可接受(用户重说一遍就行)

治理(记忆出错了/被滥用了):
  - 损失:错误信息被反复放大,污染每一次回答
  - 代价:不可接受(信任被破坏,且很难发现错了)

忘掉一条正确记忆,损失是一条信息;留着一条错误记忆,污染的是之后每一次回答。

治理三件套:

工具 1:溯源(provenance)——这条记忆哪来的?

每条记忆带四元组:

sourceType  怎么来的
  - USER_SAID     用户在对话里说的
  - TOOL_RESULT   从工具执行结果提取的
  - MODEL_DERIVED 模型总结/压缩/主动存的
  - ADMIN_EDIT    管理员手写或修改的

actor       谁说的 / 哪个工具 / 哪个模型
runId       从哪次 run 提取的(管理员手写的为 null)
at          什么时候记录的

四个排查场景:

场景 A:"这条记忆对不对?"      → 看 sourceType
场景 B:"谁说的这条?"          → 看 actor
场景 C:"哪次对话产生的?"      → 看 runId
场景 D:"这条是 3 个月前记的"    → 看 at

provenance 是治理的地基。没有它,四个问题全部答不上来。

工具 2:supersede——错了怎么改?(关键机制)

最直觉的改法"删旧写新"为什么不行:

1. 销毁证据:3 个月后想查"为什么一直推荐错" → 旧记忆已删,查不到
2. 无法回滚:万一"不过敏"才是错的 → 旧的已删,回不去
3. 审计断裂:合规要求可追溯 → 删了就断了

正确做法 supersede(标记取代)

旧记忆:"对花生过敏" → status 从 ACTIVE 改为 SUPERSEDED(不物理删!)
新记忆:"不过敏"     → status = ACTIVE, provenance 标记谁更正的

结果:
  - 检索只回 ACTIVE → 模型只见"不过敏"(行为正确)
  - 列出所有状态 → 管理员可见完整历史(可审计)
  - 需要回滚?把旧的改回 ACTIVE、新的标 REJECTED 即可

supersede 的本质是"软删除 + 留痕"。物理删除只在 GDPR 场景才用。

工具 3:TTL——过时了怎么自动失效?

有些记忆天然有保质期。TTL 机制:写入时设 expireAt,查询时惰性过滤

查询时:
  entry.expireAt != null && now >= expireAt ?
    是 → 跳过(不返回)
    否 → 正常返回

为什么"惰性过滤"而不是"主动清理"?零额外组件(不需要后台线程/锁),代价是过期条目仍占内存(但不影响正确性)。TTL 的核心不是"怎么删",而是"怎么让它不再生效"——失效比删除重要。

治理本质是给"怎么忘"加上了"怎么管":supersede 留痕取代 + TTL 惰性过期 + provenance 溯源 + 管理面板审核。

2.7 多人共享一个 Agent 的记忆:scope + 待审闸

当 Agent 从个人助手变成团队 Agent,三个新问题:

问题 1:A 的个人隐私怎么保护?    → 需要:个人记忆和频道记忆分开存
问题 2:A 说错了怎么办?          → 需要:共享记忆写入后不能立刻生效,要先审
问题 3:B 怎么看到 A 留下的团队知识? → 需要:频道记忆对频道内所有人可见

三个问题指向同一个解:scope 机制 + 待审闸门

scope 统一隔离与共享

scope 就是一个字符串:

agent:weather-bot    Agent 自己的常识
user:u1              u1 的个人记忆(只对 u1 可见)
session:s1           会话级事实
task:r42             任务工作记忆
channel:c1           频道共享记忆(频道内所有人可见)

隔离与共享是同一个机制的两面

隔离(user:u1):
  B 的查询列表 = [user:u2, channel:c1]
  store.query 绝不返回 user:u1 的条目
  → A 的个人隐私 B 看不到 ✅

共享(channel:c1):
  A 的查询列表 = [user:u1, channel:c1]
  B 的查询列表 = [user:u2, channel:c1]
  两人都把 channel:c1 放进查询列表
  → 频道记忆两人都能看到 ✅

隔离和共享的差别,仅仅是scope 在不在你的查询列表里。不需要两套系统。

隔离的实现位置:store 强制,不靠自觉。 store.query 内部那一行 .filter(e -> query.scopes().contains(e.scope())) 就是隔离的全部。调用方传 [user:u2],store 绝不返回 user:u1 的条目。

待审闸门:为什么共享记忆要先审后用

个人 vs 共享的风险不对称:

个人记忆写错了:
  - 只影响自己
  - 损失:一人一次回答偏了
  → 可以宽松:写完就 ACTIVE

共享记忆写错了:
  - 影响频道内所有人
  - 被反复检索、反复放大
  - 可能几个月才发现
  → 必须严格:先审后用

待审机制完整流程:

T1  A 说"密码每周三轮换"
    → scope=channel:c1 → status=PENDING_REVIEW
    → store: [PENDING_REVIEW] 密码每周三轮换

T2  B 检索 channel:c1
    → store.query 只回 ACTIVE
    → B 看不到这条 ✅ 防污染

T3  管理员审核:
    → 正确 → approve(id) → ACTIVE
    → 错误 → reject(id) → REJECTED

T4  审核后:
    → 正确:B 能看到了 ✅
    → 错误:B 永远看不到 ✅(错误被挡在门外)

待审闸门的本质:在"写入"和"生效"之间插入人工审核——写入可以自动,但生效必须人工确认。个人记忆可以"先写后治",共享记忆必须"先审后用"。治理时机的前移。

问题 解法 机制
A 的隐私 个人记忆 scope=user:u1,B 查询列表不含它 scope 隔离
A 说错了 频道记忆写入默认 PENDING_REVIEW 待审闸 + supersede
B 看到团队知识 频道记忆 scope=channel:c1,B 查询列表含它 scope 共享

三个问题,一个 scope 机制全解。


三、主流框架实现方案:一张全景图 + 三问拆解

讲完设计思路,来看看主流方案各自回答了三问里的哪些、留了哪些坑。用三问去拆,就不会迷路。

3.1 主流方案一页全景图

方案 派系 存什么 怎么取 怎么忘 治理 设计借鉴点
LangChain Memory 家族 上下文管理 不存(就是消息) buffer/window/summary 窗口滑出 反面教材:只做单会话,不解决跨会话
pi compaction 上下文管理 超窗归档原文 压缩后注入 压缩归档 归档可回溯 压缩+归档闭环的范本
MemGPT/Letta 操作系统 分层存储,模型自编辑 模型自检索 模型自删 self-editing 思想(给模型 memory 工具)
mem0 知识提取 LLM 提炼 + ADD/UPDATE/DELETE/NOOP 向量检索注入 LLM 决策删除 溯源 提取-冲突消解管线同构
ChatGPT memory 知识提取 双通道(saved + 历史引用) saved 全量注入 + 历史语义检索 用户设置可删 用户可见可删 双通道读取
Claude memory tool 知识提取 文件目录,模型显式写 模型显式读 模型/用户删 显式保存豁免阈值
Zep/Graphiti 知识提取 时序知识图谱 图检索 双时间轴过期 时序审计 v2 候选(时序+图谱)
Claude Tag 频道记忆 共享治理 频道共享条目 scope 检索 管理员管理 强(核心) scope 隔离 + 待审闸

3.2 用三问拆解四个代表方案

mem0(知识提取派代表):

存什么:会话结束 → LLM 提炼事实 → 与旧记忆比对 → ADD/UPDATE/DELETE/NOOP
怎么取:向量化 → 语义相似检索 → 每轮注入
怎么忘:UPDATE/DELETE 由 LLM 决策 + 用户管理

特点:写入用 LLM 做冲突消解(四选一决策),读取用向量检索。治理有溯源但偏弱。适合想要"自动提取 + 语义检索"的场景,但 LLM 决策写入有成本和不确定性。

ChatGPT memory(双通道代表):

存什么:双通道——模型自动存的 saved memories + 历史对话语义引用
怎么取:saved memories 每轮全量注入 + 历史按语义检索
怎么忘:用户在设置里逐条可见、可删

特点:saved memories 全量注入(量小但重要),历史对话按需语义检索(量大但参考性)。治理交给用户可见可删。适合"用户偏好少而精 + 历史可语义回溯"的个人助手场景。

MemGPT/Letta(操作系统派代表):

存什么:分层存储(core memory / archival memory),模型用工具自编辑
怎么取:模型用工具自检索(觉得需要就自己查)
怎么忘:模型用工具自删

特点:把记忆管理权交给模型自己(self-editing),像操作系统管内存一样管记忆。治理弱。适合"长程自主 Agent"场景,但对模型能力要求高、不可控风险大。

LangChain Memory 家族(上下文管理派代表):

存什么:不存(就是消息列表本身)
怎么取:buffer(全量)/ window(最近 K 条)/ summary(摘要)
怎么忘:窗口滑出

特点:只解决单会话内的上下文管理(翻车 2),完全不解决跨会话(翻车 1)和治理(翻车 3)。是最基础的,也是最容易用错的——很多人以为接了 LangChain Memory 就有"记忆"了,其实只是个消息裁剪器。

3.3 怎么选:按你优先解决哪个翻车

你的痛点是"聊太久变笨变贵"(翻车 2)
  → 上下文管理派:pi compaction(压缩+归档)/ LangChain window+summary

你的痛点是"跨会话记不住用户"(翻车 1)
  → 知识提取派:mem0(LLM 提取+向量检索)/ ChatGPT memory(双通道)

你的痛点是"记忆出错污染全团队"(翻车 3)
  → 共享治理派:Claude Tag(scope+待审)/ 自建治理三件套(provenance+supersede+TTL)

你的痛点是"长程自主 Agent 要自己管记忆"
  → 操作系统派:MemGPT/Letta(self-editing)

现实是组合:成熟的 Memory 系统不是复刻某一个方案,而是按三问框架从各派取最合适的部件组合——

pi 的 compaction            (解决装不下)
+ mem0 的提取-冲突消解管线    (解决记什么,判定器可用规则或 LLM)
+ MemGPT 的 self-editing 思想 (给模型 memory 工具,主动存取)
+ Claude Tag 的频道治理       (scope 隔离 + 待审)
+ 治理三件套                  (provenance/supersede/TTL)

3.4 向量检索:现在要不要用

第 5 层检索的硬伤是字面匹配:

硬伤 1:存了"我对花生禁忌",查"过敏" → 查不到
硬伤 2:存了"用户不喜欢坚果类",查"花生" → 查不到

向量检索(embedding)怎么解决:

步骤 1:向量化——"我对花生过敏"和"花生是我的饮食禁忌"向量很接近
步骤 2:存进向量库(每条记忆多存 vector 字段)
步骤 3:检索时查询也变成向量,找距离最近的条目

v1 要不要用向量?三个考量

理由 1(用):语义匹配,解决关键词的字面硬伤
理由 2(不用):依赖重——需要 embedding 模型 + 向量库(FAISS/Milvus/pgvector)
理由 3(关键):检索方式别焊死——query 抽象好,v1 关键词、v2 向量、v3 图谱,调用方都不动

设计启示:检索方式是可替换的实现细节,不是架构。把 query(MemoryQuery) 抽象留好,v1 用关键词保证可测,v2 换向量不动调用方。

3.5 RAG 与 Memory 的边界

容易混淆的两个概念:

RAG(Retrieval-Augmented Generation)
  检索的是:外部世界知识(文档库 / 知识库 / 网页)
  例子:"公司报销政策是什么?" → 检索报销制度文档
  回答:"世界上存在什么"
  比方:查百科全书(客观知识,谁查都一样)

Memory
  检索的是:Agent / 用户的过去(之前的对话 / 提取的事实 / 偏好)
  例子:"帮我订午餐" → 检索"用户对花生过敏"
  回答:"我们之间发生过什么"
  比方:翻日记本(私人记忆,因人而异)
维度 RAG Memory
检索内容 文档 chunks 记忆条目
隔离需求 强(个人记忆严格隔离)
治理需求 弱(文档错了改文档) 强(要溯源/更正/待审)
时效性 低(文档更新慢) 高(用户刚说的下一轮就要用)
写入方式 离线灌库 运行时实时提取

Memory 和未来的 RAG 文档库是两套东西,不混做。


四、设计清单:照着画你自己的 Memory

读完上面,你应该能回答下面这张清单。如果有一格答不上,说明你的设计还有洞。

4.1 三问清单

三问 你要拍的板 参考选项
存什么 原材料 or 提炼物? 主流选提炼物(结构化条目)
存什么 谁判断重要? 规则 / 模型判 / 显式
存什么 什么时候写? 每轮 / 会话结束 / 模型调工具
存什么 矛盾怎么消解? LLM 四选一 / subject 键 + supersede
怎么取 怎么匹配? 关键词 / 向量 / 图谱
怎么取 谁发起? 被动注入 / 主动 search 工具 / 双通道
怎么取 注入放哪、放多少? system 之后历史之前;吃预算要压缩
怎么取 注入落不落盘? 不落盘(防自我复制污染)
怎么忘 过时怎么失效? TTL 惰性过滤 / 滑动窗口
怎么忘 错了怎么改? supersede 留痕取代(别物理删)
怎么忘 谁能读/写/删? scope 查询列表 + 管理面板

4.2 分层清单

你用什么承载 生命周期 存取机制想清楚没
Working Memory 消息列表 / AgentState 一个 run ——
Session Memory 会话历史 一次会话 run 开始拼回 / run 结束同步
Long-term Memory 记忆库 / Store 跨会话持久 写入管道 + 读取管道

4.3 治理清单

治理项 你的机制 触发时机
溯源 provenance 四元组(sourceType/actor/runId/at) 写入时必须带
更正 supersede(旧 SUPERSEDED + 新 ACTIVE) 发现错误时
过期 TTL 惰性过滤 查询时
共享审核 channel scope 默认 PENDING_REVIEW 写入时定状态
隔离 store.query 强制 scope 过滤 查询时

4.4 防错清单(踩过的坑)

  • 长期记忆别存原始消息,存提炼后的结构化条目
  • 只提取 USER 消息,别提取 assistant 回复(可能含幻觉)
  • 注入的记忆别存回 state(防自我复制污染)
  • compaction 要就地改写 state,别只改请求副本(防 Checkpoint 死循环)
  • 压缩失败要降级截断,别抛异常(记忆故障不拖垮主流程)
  • 错误记忆用 supersede 别物理删(保审计/可回滚)
  • 隔离靠 store.query 强制,别靠调用方自觉
  • 共享记忆先审后用,个人记忆先写后治
  • 检索方式别焊死,query 抽象留好(v1 关键词 → v2 向量)
  • Memory 和 RAG 分开做,别混

五、收束:一句话记住整篇

记忆系统的本质是一句话:

模型是一条只有窗口那么大记忆的金鱼;记忆系统就是围绕这条金鱼干活的搬运工——把重要的搬出去存好,把需要的搬进来,顺手把窗口收拾得又小又准,把存的管理得能溯源、能更正、能过期。

所有主流方案本质都是这个搬运工,差别只在三件事:搬什么(存什么)、怎么搬(怎么取+怎么忘)、何时搬(写入时机+注入时机)

你设计 Memory 要做的,就是拿着三问框架 + 分层模型 + 治理三件套,对着自己的业务场景(三个翻车里你优先解决哪个),从主流方案里取最合适的部件组合——而不是闭眼复刻某一个框架。

三个翻车,三问框架,一张全景图。这就是设计 Agent Memory 的全部。

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐