前两章我们拿到了两把钥匙:一个自由的循环,一张确定性的图。
但合上书,一个更扎心的问题浮上来——如果对话进行到第 100 轮,上下文窗口爆了,你的 Agent 还记得你是谁、说过什么吗?
一、一个厨房里最常见的尴尬
先接着上一章的厨房往下讲。
客人第一天来,说了一句:「我不吃香菜。」
大厨点点头,照做了。菜很好。
第二天客人又来了。大厨一脸茫然——他完全不记得这个人昨天说过什么。
这不是他态度不好,是物理上装不下。这位大厨的工作记忆只有一道菜那么长,做完就清空。
现实中的解法有两个,都很笨:
- 在菜单上一直往上写——写到后来,菜单比桌子还长,大厨根本看不过来;
- 把写满的菜单塞进抽屉——抽屉倒是能装,但大厨压根不知道抽屉里有啥。
这正是所有 Agent 面临的困境:
模型记不住东西。
不是「记性差」,是物理上装不下。一个 128k 的上下文窗口,塞进 100 轮对话的工具返回、中间思考、冗余日志,早就爆了。
工业界最常见的解法是「截断最老的」或者「把历史丢给向量库」——但这两招都很粗糙:
- 截断 = 用户说过的话永远消失了;
- 向量库 = 模型根本不知道你丢给它的那段话是谁说的、什么时候说的。
那么问题来了:在真实的世界里,「记不住就装不下」这个问题,操作系统是怎么解决的?
答案是——内存分层。
flowchart LR
CPU[CPU] -->|只跟内存打交道| RAM[内存 RAM<br/>快,小,贵]
RAM -.->|装不下就换出| DISK[磁盘 Disk<br/>慢,大,便宜]
OS[操作系统] -.->|按需换页| RAM
OS -.->|按需调取| DISK
style CPU fill:#E8F0FE,stroke:#4285F4,color:#1A1A1A
style RAM fill:#FFF4E5,stroke:#FB8C00,color:#1A1A1A
style DISK fill:#E6F4EA,stroke:#34A853,color:#1A1A1A
style OS fill:#F1F3F4,stroke:#9AA0A6,color:#1A1A1A
- 速度快的装不下(内存),容量大的够不着(磁盘);
- CPU 只跟内存打交道,磁盘由操作系统按需调度;
- 装不下的时候,操作系统会明确地告诉进程:「内存压力高,请你自己清理」。
MemGPT 这篇论文干的事,就是把这一整套计算机体系结构,原封不动地搬给了大模型。它现在的开源继任者叫 Letta。
这一章我们就把它最本质的两件事讲透:类操作系统的内存分层,以及自我修改内存。
二、把后厨改造成一个「操作系统」
现在把上面这套内存分层,原样搬进厨房。
假设我们要给这位失忆的大厨,配一整套记忆设施。最直觉的方案是分四层:
flowchart TB
subgraph IN[在灶台边 - 模型每一轮都能直接看见]
direction LR
CM[便签条<br/>Core Memory]
WC[手边的食材<br/>Working Context]
end
subgraph OUT[在隔壁房间 - 模型知道有,但得去取]
direction LR
RS[冰箱<br/>Recall Storage]
AS[储藏室<br/>Archival Storage]
end
CM -->|抬眼就能看见| LLM[大厨]
WC -->|伸手就够得着| LLM
LLM -.->|按编号去取| RS
LLM -.->|按意思去找| AS
RS -.->|取回来摆上台面| WC
AS -.->|取回来摆上台面| WC
style IN fill:#FFF4E5,stroke:#FB8C00,color:#1A1A1A
style OUT fill:#E6F4EA,stroke:#34A853,color:#1A1A1A
style LLM fill:#E8F0FE,stroke:#4285F4,color:#1A1A1A
四个区域,两句话说完:
| 存储 | 厨房对应 | 谁看得见 | 怎么读写 |
|---|---|---|---|
| 核心记忆 | 贴在灶台前的便签条 | 大厨抬眼就能看见 | 用工具改写:追加 / 替换 |
| 工作上下文 | 灶台上正在用的食材 | 大厨伸手就够得着 | 追加;放不下了自动收走 |
| 回忆存储 | 塞进冰箱的历史 | 知道里面有,得去拿 | 按编号精确取 |
| 档案存储 | 储藏室的长期资料 | 知道里面有,得去翻 | 按「意思」语义检索 |
注意一个关键区别:前两块是「摆在眼前」的,后两块是「存在别处」的。
大厨能直接读到的,永远是前两块。后两块必须先「取回来」,摆到前两块里,他才能看见。
这就是整个记忆体系的核心机制。接下来两节,讲清楚两个最关键的零件。
三、零件一:便签条 —— 让记忆「一直摆在眼前」
3.1 大厨要记住「客人不吃香菜」,最笨也最有效的办法是什么?
把这句话写在一张便签条上,贴在灶台正前方。
大厨每做一道菜,抬头就能看见。他不需要「回忆」,因为这句话从来没离开过他的视野。
这就是核心记忆(Core Memory)的全部设计。
它由若干「记忆块」组成,每一块是一张便签:
结构体 记忆块:
标签 : 文本 # 便签的名字,比如 "客人" / "我自己"
内容 : 文本 # 便签上写的东西
字数上限 : 数字 # ★ 硬边界——便签就这么大,写满了就是写满了
只读 : 真假 # 只读的块,大厨改不了
那个 字数上限 字段,就是「内存」二字的物理含义。它不是一个建议,是一道硬墙。
3.2 便签上的字,是谁写的?
这是 MemGPT 最反直觉、也最精彩的地方。
记忆的写入者,不是用户,是模型自己。
传统的做法是:你的信息写死在 System Prompt 里,想改就得改代码、重新部署。
MemGPT 把「写记忆」变成了大厨的一个工具。大厨读完岗位守则,发现「哦,这个客人的偏好我还没记」,就主动调用工具,把这句话物理写进便签条。
函数 追加记忆(记忆库, 标签, 新内容):
当前内容 = 记忆库.取块(标签).内容
新内容 = 当前内容 + 换行 + 新内容
记忆库.更新块(标签, 新内容)
返回 新内容
看清楚——这里没有任何魔法,就是字符串拼接加写回对象。 十几行代码。
但它的意义是巨大的:不用改一行部署代码,大厨自己在运行时决定「该记什么、该忘什么、该改什么」。
这就叫自我修改内存。
3.3 轴心:便签是数据,岗位守则是它的投影
这里有一个必须点破的设计细节。
便签上的内容,是怎么变成大厨能看见的东西的?答案是每一轮重新「编译」一次:
函数 编译成守则(记忆库):
结果 = "<核心记忆>\n"
对于 每一块 在 记忆库 里:
结果 += " <" + 块.标签 + " 已用=" + 块.已用字数 + " 上限=" + 块.上限 + ">\n"
结果 += " " + 块.内容 + "\n"
结果 += " </" + 块.标签 + ">\n"
结果 += "</核心记忆>"
返回 结果
编译出来的东西长这样:
<核心记忆>
<我自己 已用=16 上限=2000>
你是忠实记录客人偏好的助理。
</我自己>
<客人 已用=30 上限=2000>
客人不吃香菜
</客人>
</核心记忆>
▍这里是整个「自我演化」的轴心。
记忆是数据,岗位守则是它的渲染产物。大厨改的不是「守则字符串」,而是背后的记忆对象;下一轮做菜前重新编译一遍,守则就自动带上了新记忆。
你不需要任何「把修改写回提示词」的代码——因为提示词从来不是源头,它只是投影。
而且注意那个 已用 / 上限——大厨能看见自己还剩多少空间。 这是后面「压力告警」的基础。
四、零件二:冰箱 —— 让记忆「该收就收」
便签条就这么大,写满了怎么办?
4.1 换页:不是删掉,是收进冰箱
工作上下文(灶台上手边的食材)也一样会满。满了之后,最老的东西不是被删掉,而是被收进冰箱。
函数 放入工作区(消息):
循环 只要 当前占用 + 消息.大小 > 预算:
最老的 = 工作区.弹出最早的一条()
冰箱.存入(最老的) # ★ 不是删除,是换页
打印("已换页到冰箱:" + 最老的.内容)
工作区.追加(消息)
「换出去」和「删掉」是两件完全不同的事。 换出去的东西还在冰箱里,需要的时候按编号能精确取回来。
这就是「回忆存储」(Recall Storage)的角色——它是历史消息的仓库,大厨知道里面有东西,需要时按编号去拿。
4.2 压力告警:让大厨知道自己快满了
换页还伴随一个特别关键的动作。
当便签条或者灶台接近上限时,系统会往岗位守则里注入一条显式的告警:
系统告警:记忆压力过高,请用工具压缩或清理旧记忆。
为什么必须有这条告警?
因为大厨对「灶台快满了」这件事毫无知觉——他只能看见台面上的东西,看不见台面的边界。
系统必须像操作系统的信号一样,主动把压力通知给他,他才会想起来去调用工具压缩记忆。
这是「让模型管理自己的内存」的前提:先让压力对模型可见。
于是就有了完整的闭环:
sequenceDiagram
participant OS as 记忆系统
participant M as 大厨(模型)
participant MEM as 便签条
OS->>OS: 检查:便签已用 52 / 上限 60
OS->>M: 在守则末尾注入「记忆压力过高」
Note over M: 大厨读到告警
M->>OS: 调用工具:把这几条旧记忆压缩成一行
OS->>MEM: 物理改写便签内容
MEM-->>OS: 52 字 → 35 字
Note over OS: 压力解除,告警消失
OS->>M: 下一轮守则里不再有告警
操作系统负责盯压力、发信号;大厨负责动手清理。 分工明确。
五、把三块拼起来:一次完整对话
现在把便签条、灶台、冰箱拼在一起,跑一遍「客人第二次上门」的完整流程:
sequenceDiagram
participant U as 客人
participant C as 控制器
participant M as 大厨(模型)
participant MEM as 便签条(核心记忆)
participant R as 冰箱(回忆存储)
Note over U,R: ── 第 1 次上门 ──
U->>C: "我不吃香菜"
C->>M: 守则(含空白便签)+ 历史
M->>MEM: 调用工具:追加记忆「客人不吃香菜」
Note over MEM: 便签被物理改写<br/>已用 0 → 6 字
M-->>U: "好的,记下了"
Note over C: 第一轮结束,重新编译守则<br/>便签内容已经进了提示词
Note over U,R: ── 中间隔了 8 轮闲聊 ──
C->>R: 灶台放不下,老消息陆续收进冰箱
Note over MEM: 便签一直贴在灶台前<br/>从头到尾没被换出去过
Note over U,R: ── 第 10 次上门 ──
U->>C: "你还记得我不吃什么吗?"
C->>M: 守则(便签上写着「客人不吃香菜」)
M-->>U: "记得,您不吃香菜。"
Note over U,M: 全程没有任何外部数据库查询<br/>答案就在每一轮都编译进守则的便签上
「记性好」的真相,其实不是记性好,是「没被换出去过」。
大厨能答上来,唯一的原因是那句话每一轮都物理地待在提示词里——它不是被「回忆」起来的,它是一直摆在眼前的。
这就是「常驻上下文」的全部秘密。
六、完整的控制器长什么样
把上面所有零件组装起来,一个完整的记忆型 Agent 控制器,主干只有这么长:
函数 跑一轮(用户输入):
轮次 += 1
放入工作区(用户消息) # ① 客人说话 → 进灶台
循环 最多 内层步数 次:
守则 = 拼装守则(
模板 = 基础岗位守则,
核心记忆 = 编译成守则(记忆库), # ② 便签重新渲染
告警 = 检查内存压力()) # ③ 快满了就注入告警
动作 = 大厨.作答(守则, 工作区快照) # ④ 问大厨
如果 动作 是 记忆工具:
执行记忆工具(动作) # 物理改写便签
把结果写回工作区
继续循环 # ★ 心跳:让大厨再想一步
如果 动作 是 对客人说话:
把回复写回工作区
返回 回复 # 本轮结束
报错("超过上限仍未对客人说话")
这里有三个地方值得停下来看一眼。
6.1 心跳:记忆工具执行完,本轮为什么不结束?
看那个 继续循环。大厨写完便签之后,系统不结束这一轮,而是把控制权交回给他,让他再想一步。
为什么?因为便签刚被改写,大厨还没「看见」新版本。让他再转一圈,重新编译一遍守则,他就能看见自己刚写下的东西,再决定要不要跟客人说话。
这就是「心跳」(heartbeat)机制——一个工具执行完,模型有权决定「我要不要再想一步」。
6.2 两类工具,两种命运
flowchart LR
A[大厨决定<br/>调用工具] --> B{是什么工具?}
B -->|记忆工具| C[改写便签<br/>-> 继续循环 - 心跳]
B -->|说话工具| D[回复客人<br/>-> 本轮结束]
C --> A
style B fill:#FFF4E5,stroke:#FB8C00,color:#1A1A1A
style C fill:#E8F0FE,stroke:#4285F4,color:#1A1A1A
style D fill:#E6F4EA,stroke:#34A853,color:#1A1A1A
- 记忆工具(追加 / 替换):改完继续转,因为「记东西」不是目的,「用记下来的东西」才是;
- 说话工具:一说话,这一轮就结束了,因为客人已经拿到答复了。
6.3 便签是数据,不是字符串
再看一眼那句 编译成守则(记忆库)。它是每一轮重新调用一次的。
这意味着:大厨改的不是提示词,而是背后的数据结构;提示词只是它每次的渲染结果。
这个设计带来一个非常舒服的性质——你想改大厨的行为,不用改代码,改便签就行;而改便签这件事,大厨自己就能干。
七、小结
7.1 三章叠起来看
到这里,三章合起来,一条清晰的进化线浮出来了:
| 维度 | 01 · 循环 | 02 · 确定性图 | 03 · 记忆分层 |
|---|---|---|---|
| 核心抽象 | 一个 while 循环 + 交接 | 一张图 + 状态 + 合并规则 | 内存分层 + 自我编辑工具 |
| 记忆 | 自由字典 | 可落盘的快照 | 便签 / 灶台 / 冰箱 / 储藏室 |
| 谁控制循环 | 模型 | 图结构 + 条件边 | 模型(心跳)+ 规则 |
| 上下文爆了怎么办 | 无解,硬塞 | 检查点 / 压缩 | 换页 + 告警,让模型自己清 |
| 这一章递来的钥匙 | 循环 | 确定性 | 自我演化 |
一句话串起来:
第一章证明了「循环」是 Agent 的最小骨架;第二章证明了「控制流」可以是显式的图;第三章则把目光从「流程」移向了「状态」——Agent 最稀缺的资源,是上下文窗口里那几 KB 内存,而它必须学会自己管理它。
7.2 真实 Letta 还做了什么
和上篇一样,列一下工业级 Letta 有、而这一章没展开的东西——如果真要在生产里用,缺的正是这几块:
| 还没做的设施 | 真实 Letta 里是什么 | 解决什么问题 |
|---|---|---|
| 精确 token 计数 | 按真实 token 数算窗口 | 字符近似在长英文上会偏差 |
| 档案存储的语义检索 | 记忆进向量库,按意思召回 | 长期记忆不能靠字符串匹配 |
| 数据库持久化 | 消息、记忆块、Agent 状态全落库 | 进程一挂,记忆全丢 |
| 工具描述自动生成 | 从函数注释自动推导出参数 schema | 手写参数表容易写错 |
| 心跳规则的体系化 | 终止型 / 子调用型 / 退出前必需型等规则 | 只靠模型自觉,循环可能失控 |
| 提示词缓存优化 | 记忆没变就不重建守则 | 每轮重建会打爆缓存、烧钱 |
7.3 下一章要解决的问题
现在四块存储里,有一块还只是个空壳。
储藏室(档案存储) —— 我们说的是「按意思去找」。可什么叫「按意思」?储藏室里有十万份食材,大厨说「给我来点提鲜的」,怎么在毫秒之间把最对味的那几样挑出来?
这就是向量库。它会是我们第二幕(组件篇)的开篇主角。
但在那之前,还有一个更硬的问题没解决:这一章的大厨,便签写得再好、冰箱收得再整齐,他依然伸不出手——他改不了一个真实文件,跑不了一条真实命令。
下一章,我们给他装上一双手。
核心结论:记忆不是「存起来」,而是「每一次都摆在眼前」。Agent 的自我演化,本质上是把「记忆」从只读的数据,变成了模型可以自己改写的工具。