3757 字
约 12 分钟
18
6.1 短记忆、长记忆、外部记忆三层
2026-10-25
无标签

6.1 短记忆、长记忆、外部记忆三层

一句话结论:模型本身没有记忆——它是无状态的函数。所谓"Agent 有记忆",全部是应用在模型外部完成了信息的保存、压缩和检索。理解这一点,你就不会指望"换个模型就有记忆",也不会把上下文和记忆混为一谈。

读这篇之前

需要先读 2.1(模型无状态)、5.5(Session ≠ Context)。这一篇是第 6 章的地基。


一、先看一个让人困惑的现象

用户:我叫小明
AI:你好,小明。

用户:我叫什么?
AI:不知道。

这不是模型"记性差",而是:

每一次调用都是独立的。模型不保存上一次调用的任何东西(2.1)。

所以"AI 记住了小明"这个现象,真相是:应用把"我叫小明"这条消息,在下一次请求里又发了一遍。 [原文 613]

这就是记忆的本质:不是模型有了记忆,而是应用替它记着,并在需要时喂给它。


二、三层记忆:从内到外

业界把 Agent 的记忆分成三层,按"离模型的距离"排列:

层 存在哪 生命周期 容量 存取方式
① 短期记忆(上下文) 消息列表里 本次会话 受窗口限制 直接可见
② 长期记忆 数据库 / 向量库 / 文件 跨会话 几乎无限 需要检索后注入
③ 外部记忆(程序记忆/知识库) 文件、知识库、数据库 长期 几乎无限 按需加载

三层的分工

层 解决什么 典型内容
短期 记住"这次对话在说什么" 本轮消息、工具结果
长期 记住"这个用户是谁、偏好什么" 用户画像、历史决策、上次进度
外部(程序记忆) 记住"这类事该怎么做" 规则、流程、Skill、知识库

第三层最容易和"长期记忆"混。区分方法:

  • 长期记忆是"关于事实的"——用户叫小明、项目用 Java
  • 程序记忆是"关于做法的"——这类任务要先读文件再改、这个项目禁止直接部署

三、为什么必须有第二层

只有短期记忆(也就是只有上下文)会遇到三个硬问题:

问题 后果
窗口有限(2.1) 长任务装不下
会话结束就丢 下次要用户重新说一遍
无法跨任务复用 每次都在重复劳动

第二个问题最影响体验。用户希望的是:"上次我说过我用 Java,它应该记得。"

注意:这需要的是长期记忆,不是"更长的上下文"。上下文再长,会话一结束就没了。


四、五类常见的实现方式

原文总结了开发中最常见的五种实现:[原文 613]

# 方式 做法 优点 缺点
1 全量保留 把历史消息全部带上 简单,不丢信息 很快撑爆窗口
2 滑动窗口 只保留最近 N 轮 成本可控 丢早期信息
3 摘要压缩 定期把历史压成摘要 保留要点 细节丢失(5.2)
4 向量检索 存成向量,按语义召回 能找"意思相近"的 召回不准就失效
5 结构化存储 存成键值/表,按键精确取 精确、可编辑 需要设计 schema

这五种不是互斥的,实践中通常是组合:

最近 3 轮:全量(保证连贯)
稍早历史:摘要(保留要点)
跨会话:结构化存储(用户画像/进度)
知识型:向量检索(相关文档)

这张组合方案值得记住——它同时解决了"连贯性、成本、跨会话、按需检索"四个需求。


五、记忆 ≠ 上下文:一个必须分清的区别

这个区别在 5.5 提过,这里再强调一次,因为它决定了架构:

上下文 记忆
位置 模型输入里 模型外部
谁维护 你的程序每轮拼装 你的程序 + 存储系统
会不会自动进上下文 不会 不会——必须主动注入
上限 窗口大小 存储容量

最关键的一条:记忆不会自动出现在上下文里。 你必须决定"这次任务需要召回哪些记忆",然后注入。

这个决定本身就是一个检索问题——所以第 7 章 RAG 的技术在这里全部适用。


六、一个常见的误解:把上下文当记忆

很多实现是:"把所有历史消息存起来,每次全带上。" 这看起来是"有记忆",实际上会遇到三个问题:

问题 原因
成本线性增长 每次请求都带上全部历史(2.1)
腐化 历史越长,关键信息越难捞(5.2)
无法跨会话 会话一关就没了

正确做法:记忆应该是"存储 + 检索",而不是"全量携带"。


七、常见误区

误区一:模型有记忆,只是被清空了。
不是清空,是根本没有。每次调用都是无状态的(2.1)。

误区二:记忆就是向量数据库。
向量库只是长期记忆的一种实现(第五节第 4 种)。还有结构化存储、摘要等。

误区三:上下文越长记忆越好。
上下文是短期记忆,会话结束就丢;长度增加还会带来腐化(5.2)。

误区四:存了记忆就会自动用上。
不会。必须主动检索并注入上下文。

误区五:记忆越多越好。
记忆多了,检索噪声大、召回不准。记忆也需要治理(6.4)。

误区六:程序记忆和长期记忆是一回事。
前者是"做法",后者是"事实"(第二节已区分)。


八、动手:三层记忆的最小实现

6.1 讲了三层。这里给出每层的最小代码形态。

短期记忆:就是一个列表

short_term = []          # 消息列表,进程内

def push(msg):
    short_term.append(msg)
    if tokens(short_term) > SHORT_LIMIT:
        short_term[:] = compress(short_term)     # 5.2 的压缩

它活在内存里,进程重启即失。

长期记忆:存"事实",不存"对话"

# 错:把原始对话存进长期记忆
long_term.append({"text": "用户:我叫小明\nAI:你好小明"})

# 对:抽成结构化事实
long_term.append({
    "key": "user.name",
    "value": "小明",
    "source": "user_stated",     # 6.4 的来源标记
    "at": "2026-10-25",
})

区别:前者是"复述",后者是"可查询的事实"。

程序记忆:文件 + 按需加载

memory/procedures/
├── code-review.md      # 这类任务该怎么做
├── deploy.md
└── data-cleanup.md

按需加载的方式就是 Skill(9.1)——不必自己发明机制。

三层的存取时机

层 什么时候读 什么时候写
短期 每轮 每轮
长期 任务开始时检索相关事实 用户明确说了新事实时
程序 判断出任务类型时 人工维护

第二行最关键:只在"用户明确说了"时写入,不要"让模型总结一下用户画像再存" —— 那会混进推测(6.4)。


九、动手:三层记忆的完整读写流程

6.1 的三层,串成一次真实交互。

任务开始时的读

① 短期:加载最近 N 条消息(本次会话)
② 长期:按当前任务检索相关事实
   例:任务涉及"部署" → 检索 {部署相关的用户偏好、环境信息}
③ 程序:判断任务类型 → 加载对应 Skill

三步都是"检索",不是"全量加载"。

任务过程中的写

触发 写哪
用户说了一个事实 长期(带 source: user_stated)
完成一个子目标 短期(并考虑检查点 5.5)
发现一条可复用的做法 不写(那属于程序记忆,人工维护)

任务结束时的清理

def finish_task():
    # 短期:压缩后保留,供后续追问
    short_term[:] = summarize(short_term)
    # 长期:本次确认的事实已写入,无需额外动作
    # 程序:不动

一个常见错误:把所有对话都存进长期

错:每轮结束把 messages 全量 append 到 long_term
后果:① 长期记忆变成"对话归档",检索噪声大
     ② 混入了大量过程性叙述
     ③ 上下文压缩后与原文不一致

长期记忆存"事实",不存"对话" —— 这是 6.1 的核心区分。


十、动手:三种记忆方案的选型表

6.1 讲了五类实现。这里给出"什么时候用哪个"的判断。

你的需求 方案 存储 检索方式
只记住本次对话 短期列表 内存 全量
记住最近 N 轮 滑动窗口 内存 全量
记住"大约说过什么" 摘要 内存/文件 全量
按意思找回相关旧事 向量库 数据库 语义
精确取某条记录 键值/表 数据库 主键
记住"做到第几步" 结构化检查点(5.5) 文件 全读

三个常见的错误搭配

错配 问题
用向量库存"进度" 语义检索找不准"第几步",该用结构化
用键值库存"相关文档" 只能精确匹配,找不到同义表达
用摘要存"精确数值" 摘要会丢精度(5.2)

判据一句话:

要"找意思相近的" → 向量
要"取某条具体记录" → 键值
要"记住状态" → 结构化

组合的推荐形态

短期:列表(本次会话)
长期:向量库(相关事实) + 键值(精确偏好,如 "editor": "vim")
程序:文件 + Skill 加载(做法)

两个长期存储并存很常见 —— 因为它们回答两类不同问题。


十一、动手:长期记忆的 schema 设计

6.1 说"只存事实"。但"事实"怎么存,决定了它能不能被用好。

一个可用的最小 schema

{
  "id": "m-001",
  "key": "user.primary_language",
  "value": "Java",
  "text": "用户主要用 Java 开发",
  "source": "user_stated",
  "at": "2026-10-25T10:00:00Z",
  "last_seen": "2026-10-25T10:00:00Z",
  "status": "active",
  "weight": 1.0,
  "embedding": [...]
}

每个字段的必要性

字段 缺了会怎样
key 无法判断"是否同维度",冲突处理失效(6.4)
value 无法做精确匹配查询
text 向量检索需要自然语言,只有 key-value 检索不准
source 无法区分可信度,污染无法清理(6.3)
at / last_seen 无法做时近性排序
status 无法降权而非删除
weight 无法表达重要程度
embedding 无法语义检索

text 和 source 最容易被省 —— 前者一省,语义检索就废了;后者一省,污染就清不掉。

一个常见的设计错误

错:只存 {key: value},把 "text" 当成可推导的
后果:向量检索时不知道拿什么去做 embedding
     → 只能用 "user.primary_language" 这种字段名去搜
     → 完全搜不到"他用什么语言"

text 是给检索用的,key/value 是给逻辑用的,两者都要有。


十二、动手:一次真实的三层协作

把 6.1 的三层放到一次完整交互里看。

场景

用户第 3 次使用这个助手,问:"帮我看看上次那个重构方案还合适吗?"

① 短期记忆(本次会话)

空(新会话)

② 长期记忆(检索)

hits = memory.search("重构方案", top_k=3)
# 返回:
#  [{"text": "用户在 2026-10-20 讨论过 auth 模块的重构",
#    "source": "user_stated", "weight": 1.0},
#   {"text": "当时决定用策略模式替换 if-else", "weight": 1.2},
#   {"text": "约束:不能引入新依赖", "weight": 1.5}]   ← 关键

注意第三条:那是一条约束,权重最高。如果检索时漏了它,后面所有建议都可能违规。

③ 程序记忆(技能加载)

判断任务类型 = 代码重构 → 加载 "refactor" Skill
Skill 里说:先确认现状,再评估方案,最后给改动建议

④ 拼装上下文

[系统规则]
[状态栏:本次任务=评估重构方案]
[长期记忆片段(3 条,带来源)]
[Skill 正文(refactor)]
[用户问题]

⑤ 输出的关键差别

有记忆:

「按你上次的方案(策略模式替换 if-else),现在的现状是……
 但要注意你当时提的约束:不能引入新依赖 —— 这个方案里
 用到的 X 库需要注意。」

没记忆:

「重构 auth 模块,可以考虑策略模式、状态机、或者……
 (一段通用建议,完全没有上下文)」

差别不在"回答质量",而在"是否接着上次继续"。

一个实践建议

在状态栏里显式写"这是第 N 次相关任务" —— 让模型知道"有历史可查"。

【当前状态】
任务:评估重构方案
历史:找到 3 条相关记忆(其中 1 条是约束)

这一行能让模型主动去用检索到的内容,而不是忽略它们。


十三、下一篇

三层里最容易被误解的是"长期记忆"。它有个很反直觉的思路:把上下文当成操作系统的内存来管理。

这就是 MemGPT 的思路。下一篇讲它——以及为什么这个类比如此贴切。

一句话收尾

记忆不是"存得越多越好",而是"在对的时候,取到对的那一条"。 三层结构、来源标记、冲突处理,全都服务于这一句。

6.1 短记忆、长记忆、外部记忆三层
https://clxhxhhr.top/posts/4892/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。