一个所有人都遇到过的瞬间
你让 AI 帮你规划了一次"给爸妈买保险"的方案,聊了半个小时,它甚至记得你爸爸的年纪、你们家的预算。然后你不小心刷新了页面,新开一个会话,问它:"上次说的那个重疾险,我们比到哪一步了?"
它一脸茫然:"我们……之前聊过吗?"
不是它笨,是它本来就什么都没有。这一瞬间的"失忆",是理解整个 Agent 记忆体系最好的入口。
LLM 是"无状态"的:它没有上次,只有这次
先把话说透:一个大语言模型,本质上是一个函数:
f(一段文字) → 一段文字
同样的输入,永远得到同样的输出。它不保存任何"调用之间"的状态——没有记忆体、没有变量、没有上次。你在它眼里永远是第一次见面。
那为什么平时用 ChatGPT 好像它"记得"你说过什么?因为客户端把历史聊天记录也一并塞进了这次的输入。它没有"想起你",它只是把你们之前的对话又"读"了一遍,然后接着往下写。
用代码看更直观:
// 第一轮
await model.invoke([human("我叫李四,是一名设计师")]);
// 第二轮:不把历史带上 —— 模型根本不认识李四
const reply = await model.invoke([human("我是谁?")]);
// 🤖 抱歉,我不知道你是谁
而"看起来有记忆"的做法,是第二轮把第一轮的问答一起带进去:
await model.invoke([
human("我叫李四,是一名设计师"),
ai("你好李四,很高兴认识你!"),
human("我是谁?"), // 🤖 你是李四,一名设计师
]);
所以这里要建立一个观念:"多轮记忆"不是模型的能力,是外面那层代码的工程问题。 这也正是为什么记忆要从"模型"里拆出来,单独做成一套系统。
Agent = LLM + Harness:记忆属于脚手架,不属于大脑
光聊天可能还体会不到记忆多重要。但当你开始做 Agent(让 AI 不只是"回答",而是"干活"),记忆就成了地基。
一个被广泛接受的公式是:
Agent = LLM + Harness(tool + RAG + memory + ...)
- LLM 是大脑:负责理解、推理、生成。
- Harness(可以叫脚手架/框架/外壳)是大脑之外的一切:给模型接工具的
tool、帮它查资料的RAG、以及让它可以"记得"的memory。
为什么要这么拆?因为大脑有一个致命缺陷——每次对话它都得重新醒来。工具、知识、记忆,全都要靠外面的脚手架帮它接上。打个比方:LLM 是一个随时会被敲晕的天才,Harness 是它身边的经纪人,负责提醒它"你以前认识谁、你手头有什么工具、上次那件事办到哪了"。
而 memory 又是 Harness 里最底层的一块:RAG 是"基于问题去外部知识里检索相关片段放进 prompt",本质是一种记忆的读取;工具调用要能连续多轮,本质也要短期记忆撑住上下文。很多能力,追根溯源都长在记忆这棵树上。
于是记忆要解决三件事
既然"带历史进 prompt"就能让模型显得有记忆,那是不是把历史无限拼进去就完了?
不行。三个现实问题会立刻拦住你:
| 问题 | 具体表现 |
|---|---|
| 上下文窗口有限 | 模型一次只能"读"有限的内容(比如 200k token),聊得足够久,历史迟早放不下 |
| 开销随长度暴涨 | 上下文越长,每次请求越慢、越贵(token 是按量计费的),还容易在长上下文里"迷失重点" |
| 持久化缺失 | 服务重启、会话关闭,内存里的历史就没了——更别说跨天、跨设备、多用户 |
这三件事分别对应了记忆系统的三块核心能力:
- 管理:窗口装不下时怎么办?——不能只会"全塞",要学会截断、总结、压缩。
- 存储:出了内存、出了进程、出了会话,历史放哪?——要有外存(文件、数据库、向量库)。
- 检索:长期记忆那么多,怎么把"这次最该用到的"找回来?——不是全量搬进 prompt,而是精准召回。
你可以把这三块记成一个记忆系统的骨架:存得下、管得住、取得回。
记忆的两种基本形态
面向工程,记忆可以简化成两大类,这也是后面整个系列的两条主线:
① 短期记忆(工作记忆)—— 这一场对话"最近在聊什么"
它就住在上下文窗口里,跟着这一次请求走:最近的几条用户消息、助手回复、工具调用结果。特点是量小、要新、变化快。它是 Agent 完成当前任务的"工作台"。
② 长期记忆 —— 跨会话的"你是谁、我们聊过什么"
用户的基本信息、偏好、历史对话的要点。它的特点是量可以很大、要跨越很多次会话、要求"想用的时候能被想起来"。所以它必须落到外存(文件 / 数据库 / 向量库),并且靠检索而不是全量读入来使用。
学界和工业界其实还有更细的划分:情景记忆(发生过的事)、语义记忆(事实和概念)、程序记忆(怎么做事的技能)。但对第一版 Agent 系统,先抓住"短期 / 长期"这一层就够用了——复杂分层往往是业务逼出来的,不是一开始设计出来的。
还有一个很多人会忽略的点,值得单独说:记忆系统真正难的不是"存",而是"取"和"忘"。 存,往数据库里写一行谁都会;难的是:窗口满了,丢哪些、留哪些、要不要先把丢掉的总结一下?长期库里上万条历史,凭什么把"红烧肉火候"那条在问"这周学什么菜"时捞出来?这些问题会在系列后面的每一篇里反复出现。
一条最自然的成长路线(我踩出来的)
这部分算我的"踩坑地图",也是这个系列为什么按现在的顺序排的原因。我把一个 Agent 记忆模块从无到有搭了一遍,几乎每一步都是在"不知道坑在哪"的情况下被现实教育出来的:
- 从裸 messages 数组开始 —— 直接把对话数组拼给模型。能用,但很快发现:消息类型混杂(用户/助手/工具)、加删除都不方便、没有统一结构。
→ 于是引入
ChatMessageHistory一类的内存历史对象来管理。 - 想让它跨会话 —— 内存一重启就没。于是把历史写成 JSON 文件落盘,下次启动读回来。
- 会话一长就爆窗口 / 爆 token 费 —— 开始研究上下文管理:先是最粗暴的"只留最近 N 条"(截断),发现老信息丢了可惜。
- 于是学"总结" —— 把被截掉的老消息喂给模型,总结成一段摘要继续带着走,甚至让摘要跨轮滚动累积,越滚越接近一个"会话的压缩档案"。
- 可文件检索太弱 —— 文件只能整段读,没法"按相关度捞一条"。于是上了向量数据库(Milvus)+ embedding,把历史按语义存进去,提问时按语义相似度召回。这一步其实就是在 Agent 里做 RAG。
- 最后是"长成工程" —— 把上面所有东西拆层:短期记忆封装成类、长期记忆抽象成接口(Milvus / 内存两种实现随意切换)、拼 prompt 单独抽一个纯函数。至此它才从一个 demo,变成"可以演进、可以换后端"的东西。
你会发现:每一步都是在前面方案的痛点上自然长出来的,没有一步是拍脑袋的架构。这其实是最好的学习顺序——因为每个坑你都亲自踩过,后面的方案你才真的理解它解决了什么。