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 的思路。下一篇讲它——以及为什么这个类比如此贴切。
一句话收尾
记忆不是"存得越多越好",而是"在对的时候,取到对的那一条"。 三层结构、来源标记、冲突处理,全都服务于这一句。