第六话里,我们把"流程控制"从模型手里收回来,交给了 StateGraph。但有一个问题从第五话开始就一直存在,我们假装它不存在:
模型的记忆。
回顾第五话的代码,我们的"记忆"就是那个 messages 列表——每次对话,把所有历史都塞进 prompt 喂给模型:
state.add_message(msg) # 工具调用也塞进历史
state.add_message({"role": "tool", ...}) # 观察结果也塞进历史
前几轮没事。但对话到 20 轮、30 轮呢?prompt 越来越长,直到撞上上下文窗口上限——这时会发生什么?模型开始"忘记"最早说的话,或者更糟:被中间的信息干扰,答错。
我先说结论:
上下文窗口 ≠ 记忆。 模型本身不"记得"任何东西,它只是每次被我们喂一坨 prompt。 真正的记忆分三层:
- 上下文(Working):当前窗口内,最易失——塞不下就丢
- 短期(Episodic):会话内关键事实,显式提取——比全量历史省 token、抗干扰
- 长期(Semantic):跨会话持久,检索式访问——不检索的记忆等于不存在
这一篇,我们写一个带三层记忆的最小 Agent,亲眼看看"记忆是怎么补位的"。
一、旧世界:把历史塞进上下文的问题
先看最朴素的做法——全量历史注入。它有两个硬伤:
硬伤 1:上下文窗口有限。 假设窗口 8K token,对话到第 30 轮时历史已经 10K token——塞不进去了。要么截断最老的(等于"失忆"),要么压缩(损失细节)。窗口是物理限制,记忆是软件设计。
硬伤 2:Lost in the Middle(中间迷失)。 2023 年一篇论文(arXiv 2309.04271)发现一个反直觉现象:模型对长上下文的利用呈 U 型——开头和结尾的信息记得好,中间的信息最容易丢,性能差距可达 25-30%,即使 100K 窗口的模型也一样。把关键信息放在 30 条消息的中间?它很可能被"淹没"。
关键洞察:把历史当"水管"一样无脑灌,是新手 Agent 最大的坑。塞不下和塞中间,都是"假记忆"——看似有,实则不可靠。
二、转折:三层记忆——各管一段
认知科学早就给了我们答案:人的记忆也不是一个池子,而是分层的。Agent 照搬这个分层:
| 层 | 存什么 | 生命周期 | 访问方式 | 类比 |
|---|---|---|---|---|
| 上下文(Working) | 当前轮次消息 | 窗口内 | 直接注入 prompt | 你正在读的这句话 |
| 短期(Episodic) | 会话内关键事实 | 会话级 | 显式提取 + 摘要注入 | 今天上午聊过的事 |
| 长期(Semantic) | 用户偏好/知识点 | 跨会话(近乎永久) | 向量检索 | 你记得朋友爱喝什么 |
关键设计原则:
① 短期记忆 = 显式提取,不是全量存储。 全量历史是"录像带"——什么都录了,但长、贵、噪音多。短期记忆是"笔记"——只记关键事实(名字、偏好、决定),结构化、省 token、抗干扰。
② 长期记忆 = 检索式访问,不是注入式。 你不可能把"用户爱喝龙井、职业是 Java 工程师、博客写 AI Agent"全塞进每次 prompt。正确做法是:提问时检索出相关的几条,只注入那几条。这就是 RAG(检索增强生成)的核心思想——记忆库很大,但每次只"想"起相关的部分。
③ 记忆要有生命周期:巩固与遗忘。 不是所有信息都值得长期保存。工程实践通常给记忆打分(importance):高价值 → 升级到长期;低价值 → 过期遗忘。遗忘不是 bug,是 feature——没有遗忘,记忆库会堆满垃圾,检索质量反而下降。
关键洞察:三层记忆不是"三个数据库",而是三种访问模式——上下文是注入式(全量)、短期是摘要式(精炼)、长期是检索式(按需)。访问模式决定了它们各自解决什么问题。
三、原理讲透:为什么"不检索的记忆等于不存在"
很多人以为"我的 Agent 存了聊天记录,所以它有记忆"。错。存储 ≠ 记忆,检索才是记忆。
想想人的记忆:你知道"朋友爱喝龙井"这件事,不是因为你把这句话背下来了,而是因为在需要的时候你能想起来——聊天聊到茶、点单、送礼,这些场景会自动唤起这条记忆。
Agent 也一样。长期记忆必须支持按语义检索:用户问"他喜欢喝什么?",Agent 要从记忆库里找出“喜欢喝茶,尤其是龙井"这条。怎么找?最基础的方法是向量相似度:
1. 把记忆条目转成向量(embedding)——语义相近的文本向量也相近
2. 把用户问题也转成向量
3. 计算余弦相似度,返回最相近的 N 条
这就是"检索"的本质。演示里我们用纯 Python 实现一个最小版(中文 bigram 分词 + 余弦相似度,零依赖),让你看到机制本身:
def tokenize(text): # 中文按相邻两字(bigram)切分
return {text[i:i+2] for i in range(len(text)-1) if 中文字符}
def cosine(a, b): # 向量相似度
return len(a & b) / (sqrt(len(a)) * sqrt(len(b)))
注意演示级实现的局限:纯 bigram 对同义改写敏感——“爱喝"和"喜欢喝"切出来的 bigram 完全不同,匹配不到。生产环境用真实 embedding(如 OpenAI/DeepSeek embedding API),同义词会投影到相近的向量空间,就没有这个问题。机制相同,精度不同。
顺带说一句检索之外的另一半:巩固(Consolidation)与遗忘(Forgetting)。记忆不是存进去就完事了——它有一个生命周期:
对话产生信息 → 编码 → 存储 → 检索 → 巩固(低→高价值迁移)→ 遗忘(过期清理)
工程上最常见的做法是给每条记忆打 importance 分数:
- 巩固:对话中出现的用户偏好/重要决定,打分 ≥ 0.7 → 从短期升级到长期;分数低的留在短期,会话结束即丢。这模拟了人脑"重要的事记牢,琐事随风散”。
- 遗忘:长期记忆也有容量和时效。时间衰减(太久没被检索到的降权)、容量上限(满了淘汰最不重要的)。没有遗忘的记忆库,最终会被垃圾淹没,检索质量断崖式下降——这跟搜索引擎要处理"死链"是一个道理。
很多 Agent 项目死在"只存不捡”:记忆越堆越多,检索命中率越来越差。好的记忆系统不是存得多,而是记得准、忘得掉。
四、动手:三层记忆 Agent 实测
代码:https://github.com/renxin2024/GYA/tree/main/c07-memory(memory_demo.py,纯标准库)
export DEEPSEEK_API_KEY=sk-你的key
python3 memory_demo.py
场景 1:短期记忆补位
用户第一轮说"我叫张三" → Agent 提取存入短期记忆。然后聊两轮无关话题(写文字)——此时张三的名字已经不在模型上下文里了。再问"我是谁?我叫什么名字?":
模型(带记忆): 你是张三。
对比(无记忆,没有任何上下文):
模型(无记忆): 在没有上下文的情况下,我无法知道你是谁,也不知道你的名字。
这就是短期记忆的价值:名字被"挤出"上下文后,显式提取的记忆补上了。注意我们没有把整个对话历史塞回去——只注入了一条关键事实,更省 token、更抗干扰。
场景 2:长期记忆向量检索
向记忆库存入三条用户画像,然后提问检索:
记忆库:
- 用户喜欢喝茶,尤其是龙井
- 用户职业是 Java 后端工程师,擅长并发编程
- 用户的博客主题是 AI Agent 开发
问『用户喜欢喝什么?』→ 检索到: 用户喜欢喝茶,尤其是龙井
问『用户职业是什么?』→ 检索到: 用户职业是 Java 后端工程师...
问『博客写什么?』→ 检索到: 用户的博客主题是 AI Agent 开发
每条问题都准确"想"起了对应的记忆——这就是检索式访问。如果记忆库有 1000 条,我们不会全塞给模型,只注入检索到的那 1-2 条。
场景 3(诚实说明):Lost-in-the-Middle 实测
演示脚本里也放了一个"信息放中间 vs 开头"的对比。诚实地说:deepseek-v4-flash 对短 padding 抗性很好,两头都答对了——这个效应要在 100K 级长上下文才明显(论文数据:中间 vs 两端性能差 25-30%)。demo 放它是让你知道这个坑的存在,正文用论文数据支撑结论。
关键洞察:三层记忆合起来的完整流程是——提问时,短期记忆给摘要、长期记忆给检索结果,一起注入上下文,模型基于"当前问题 + 记得的事"作答。模型始终只面对一个"够用的小 prompt",而不是越来越长的全量历史。
五、工程化延伸:生产级记忆系统
demo 是概念演示,生产要补的东西不少:
| 能力 | demo(本篇) | 生产要补的 |
|---|---|---|
| 短期记忆 | 内存 dict | 会话级存储 + 上限管理(FIFO/TTL) |
| 长期记忆 | 内存 list + bigram | 向量库(Qdrant/Chroma)+ 真实 embedding |
| 巩固 | 无 | 重要性打分(如 ≥0.7 升长期)+ 定期 Consolidation |
| 遗忘 | 无 | 时间衰减/容量上限,防止记忆库膨胀 |
| 记忆 vs RAG | 混在一起 | 明确边界:Memory 存对话产生的个人事实,RAG 存外部知识库 |
记忆 vs RAG 的边界值得单独强调——这是最常见的混淆:
- Memory(记忆):对话中自然产生的——“用户叫张三"“用户爱喝龙井”。换个用户,答案就变。
- RAG(检索增强):外部知识库——“什么是 ReAct"“Java 并发怎么调优”。所有用户共享,答案不变。
把用户偏好存进 RAG?你会得到"所有用户都爱喝龙井”。把外部知识存进 Memory?每次都要重新"对话产生”,浪费。各司其职。
下一步
阶段 2(Agent 循环)到此收尾:我们有循环(第五话)、有状态(第六话)、有记忆(第七话)——一个"会干活、能记住"的 Agent 成型了。
但还有一个痛苦没解决:工具的接入。第四话我们手写了注册表,接一个新工具要写代码、注册、测试。如果你要接 10 个不同的服务(GitHub、数据库、日历、邮件),每个都要写适配代码——N×M 的集成地狱。
下一篇进入阶段 3:MCP 协议——为什么需要它。一句话预告:MCP 是"工具的 USB-C 接口",让工具接入标准化。
演示代码:
https://github.com/renxin2024/GYA/tree/main/c07-memory(memory_demo.py,纯标准库;DeepSeek 官方 API,默认模型deepseek-v4-flash)。Java 21 等价实现见独立仓库https://github.com/renxin2024/GYA-Java(c07-memory/,Gradle 工程,gradle run)。 参考:Lost in the Middle 论文 arXiv 2309.04271(U 型注意力、中间性能差 25-30%);四层记忆与巩固/遗忘机制来自 Agent 记忆系统工程实践(hello-agents 讲义延伸);Memory vs RAG 边界为行业共识;demo 输出为 2026-05-26 本机实测(deepseek-v4-flash,Python 与 Java 双跑)。

留言
欢迎分享你的想法。评论提交后会在审核通过后显示。