对应 Maps to:Stanford CS349D AI Inference Infrastructure(Spring 2026),#16 Wed May 20 Memory。必读:S. Eyuboglu, R. Ehrlich, S. Arora, N. Guha, D. Zinsley, E. Liu, W. Tennien, A. Rudra, J. Zou, A. Mirhoseini, C. Ré, Cartridges: Lightweight and general-purpose long context representations via self-study(2025);P. Chhikara, D. Khant, S. Aryan, T. Singh, D. Yadav, Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory(2025)。选读:C. Packer, S. Wooders, K. Lin, V. Fang, S. G. Patil, I. Stoica, J. E. Gonzalez, MemGPT: Towards LLMs as Operating Systems(2023);Q. Zhang, C. Hu 等, Agentic Context Engineering: Evolving Contexts for Self-Improving Language Models(ICLR 2026)。 依据说明:原课没有公开 slides。本讲依据四篇论文的全文(Cartridges 用 arXiv v3,ACE 用 arXiv v3)。§1 的代价分析、§6 的系统视角和 §7”之后的发展”是我补充的。讲义里的纸上数字全部来自配套代码的实际运行;论文里的数字写成”论文报告”。 配套代码 Code:
code/lecture14/(numpy;4 个脚本 + 2 个模块:memory_lab.py是代价模型、玩具长期对话世界与五种记忆系统、前缀缓存模拟;cartridge_lab.py是 Cartridges 的玩具版,真的用梯度下降训练一小块 KV。代码 04 约 110 秒,其余几秒以内) 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说清 LLM 的”记忆”为什么是一个系统问题:上下文就是工作记忆,而上下文的每个 token 都要占 KV cache、拖慢 prefill 和 decode、压低整台 GPU 的吞吐。
- 区分四种让模型”记住”东西的做法——放进上下文、存在外面再检索、离线压进参数、把经验整理成不断演化的上下文——并说出各自把代价放在了哪里。
- 解释 MemGPT 怎样借用操作系统的虚拟内存(主上下文 / 外部存储、换入换出、中断与事件),以及这种设计在推理服务端意味着什么(更多的 LLM 调用、提示词中间的修改)。
- 解释 mem0 的”抽取 → 增 / 改 / 删 / 不变”流水线和它的图版本,理解”在写入时做功”与”在读取时做功”的权衡。
- 解释 Cartridges 的思路:把一份语料离线训练成一小块 KV cache(self-study:合成对话 + 上下文蒸馏),以及它在什么条件下划算。
- 解释 ACE 怎样用”生成—反思—整理”和增量更新避免上下文坍缩,并用前缀缓存的原理说明”只追加”为什么对服务端最友好。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 第 1 讲:KV cache 的大小、prefill 与 decode 的 roofline | 向量数据库与近似最近邻检索的算法 |
| 第 3 讲:PagedAttention、前缀缓存(RadixAttention) | 长上下文模型本身的训练(位置编码外推等) |
| 第 9 讲:上下文缓存的分层存储(显存 / 内存 / SSD) | KV cache 的有损压缩(只在 §4 作为对比提到) |
| 第 13 讲:智能体程序、多次 LLM 调用 | 提示词优化的算法细节(GEPA、DSPy 只讲与 ACE 的关系) |
| 前置讲 P3:虚拟内存、分页、换入换出 | 记忆系统的隐私与”被遗忘权”(§6 只点到) |
0.3 本讲地图 Map
§1 为什么"记忆"是一个系统问题:上下文的代价 ← 代码 01
§2 MemGPT:把 LLM 当成操作系统 ← 代码 02、03
§3 mem0:抽取、合并、检索 ← 代码 02
§4 Cartridges:离线把语料训练成一小块 KV cache ← 代码 04、01
§5 ACE:不断演化的上下文"手册" ← 代码 03
§6 系统视角:记忆的层级、写入与读取、可变的上下文
§7 之后的发展
§8–12 小结、常见误区、练习、延伸阅读、下一讲预告
0.4 记号 Notation
| 符号 | 含义 |
|---|---|
| ICL(in-context learning) | 把语料或历史原样放进上下文,让模型”现场读” |
| 主上下文 main context | MemGPT 里放进提示词的部分(相当于内存);外部上下文 external context 是提示词之外的存储(相当于磁盘) |
| 记忆 memory(mem0) | 从对话里抽取出来的一条短事实,带时间戳和嵌入向量 |
| cartridge,大小 p | 一块可训练的 KV cache,相当于 p 个”虚拟 token” |
| self-study | Cartridges 的训练方法:合成对话 + 上下文蒸馏 |
| 手册 playbook、要点 bullet | ACE 的上下文:一条条带元数据(有用 / 有害计数)的要点 |
| 前缀缓存命中 / 复用 | 提示词里与上一次调用相同的前缀,KV 可以直接复用,不用重新 prefill |
0.5 关于数字 · About the numbers
本讲的数字有三个来源:(1)第 1 讲的纸上 GPU(Llama-3-8B / H100 的 roofline)算出的显存、延迟和吞吐;(2)一个没有真正 LLM 的玩具长期对话世界:读者、抽取、增删改都是带错误率的规则,它能说明机制和代价,不能说明真实系统的准确率;(3)一个真的做梯度下降的 Cartridges 玩具:一层 softmax 注意力、随机的”键 → 值”事实。论文里的数字写成”论文报告”。
1. 为什么”记忆”是一个系统问题 · Why memory is a systems problem
1.1 LLM 没有记忆,只有上下文
一个训练好的 LLM 是一个固定的函数:给它一串 token,它给出下一个 token 的分布。它”知道”的东西只有两类:训练时写进权重的,和这一次调用写在上下文里的。两次调用之间,它什么也不记得——你昨天告诉助手”我对乳制品过敏”,今天它照样推荐奶酪披萨(mem0 论文图 1 的例子),除非有人把这句话放回今天的提示词里。
需要”记住”的东西大致有三类:
| 记什么 | 例子 | 特点 |
|---|---|---|
| 一个用户 | 对话历史、偏好、个人信息 | 不断增长、会变化(搬家、换工作),新信息要覆盖旧信息 |
| 一份语料 | 代码库、病历、财报、教科书 | 很长(10 万–100 万 token)、基本不变、被反复查询 |
| 经验 | 做任务时学到的策略、踩过的坑 | 从反馈中积累,要整理、去重,错误的经验要剔除 |
本讲的四篇论文正好覆盖这三类:MemGPT 和 mem0 记”一个用户”,Cartridges 记”一份语料”,ACE 记”经验”。
1.2 把一切都放进上下文要付出什么
最简单的做法是把要记的东西全放进上下文。现在的模型有 128k 甚至 1M token 的窗口,为什么还不够?代码 01 用第 1 讲的纸上 GPU 算了一笔账(Llama-3-8B / H100):
显存(演示 1)。Llama-3-8B 每个 token 的 KV cache 是 128 KiB(32 层 × 8 个 KV 头 × 128 维 × K、V 两份 × 2 字节):
| 上下文 | token | KV(Llama-3-8B) | 占 H100 的 KV 空间 |
|---|---|---|---|
| 一段 LOCOMO 对话 | 26,000 | 3.41 GB | 6.1% |
| 一本书 | 128,000 | 16.78 GB | 30.0% |
| MTOB 教科书 | 484,000 | 63.44 GB | 113.4%(放不下,也超出窗口) |
延迟(演示 2,batch = 1)。没有前缀缓存时,12.8 万 token 的 prefill 要 8.8 秒(其中约 6 秒是注意力的二次项),250 token 只要 7 毫秒。前缀缓存能消掉 prefill(同一份上下文第二次用时 TTFT 只剩 12 毫秒),但消不掉 decode:每生成一个 token 都要把整份 KV 读一遍,200 个 token 从 1.22 秒变成 2.47 秒。
吞吐(演示 3)——这是最要命的一项。很多用户同时使用、每人带着自己的一份上下文时,所有人的 KV 都要常驻显存:
| 每个请求的上下文 | 能同时服务的请求 | 吞吐(token/s) | 相对 1k |
|---|---|---|---|
| 1,000 | 341 | 13,480 | 1 |
| 16,000 | 26 | 976 | 1/14 |
| 120,000 | 3 | 126 | 1/107 |
| 120,000,所有人共享同一份 | 1,227 | 8,843 | 1/1.5 |
每一步 decode 的时间几乎不变(约 25 毫秒,都在把显存读满),变化的是这些字节被多少个请求分摊。Cartridges 论文在 SGLang 上实测从 1k 到 120k 下降 77 倍,与我们的 107 倍同一量级。最后一行说明了一个关键区别:如果长上下文是共享的(同一份公司手册),KV 只存一份,吞吐几乎不受影响(第 3 讲的前缀共享、第 4 讲的级联注意力);真正的难题是每个人有自己的一份——自己的对话历史、自己的病历、自己的代码库。
还有一个质量问题:上下文长了,模型也不一定”读得进去”。MemGPT 和 mem0 都引用了 lost-in-the-middle 的发现:模型对上下文开头和结尾的信息记得最好,中间的容易漏掉。
1.3 四种做法
| 做法 | 代表 | 记忆在哪里 | 读取时的代价 | 写入 / 准备的代价 | 主要风险 |
|---|---|---|---|---|---|
| 放进上下文 | 长上下文 ICL | KV cache | 高:显存、prefill、decode 都随长度增长 | 无 | 超出窗口、读不进去、吞吐崩溃 |
| 存在外面、用时检索 | RAG、MemGPT、mem0 | 外部存储(文本 / 向量 / 图) | 低:只放进几百到几千 token;但可能要多次调用 | RAG 无;mem0 每对消息都要调 LLM 抽取、更新 | 检索不到、检索到过时的信息 |
| 离线压进参数 | Cartridges(以及微调) | 一小块训练出来的 KV | 很低:相当于 p 个 token 的前缀 | 很高:每份语料一次离线训练 | 训练成本要靠大量查询摊销;语料变了要重训 |
| 演化的上下文 | ACE、Dynamic Cheatsheet | 提示词里的一本”手册” | 随手册增长,但大部分可以被前缀缓存复用 | 每个任务后反思、整理 | 坍缩、被错误的反馈污染 |
这张表的每一格,后面都会用论文和代码展开。一个贯穿全讲的问题是:代价放在写入时还是读取时,以及哪一部分上下文会被修改。
2. MemGPT:把 LLM 当成操作系统 · MemGPT
论文卡片 Paper card · MemGPT
- 问题:上下文窗口有限(论文写作时开源模型多在 2k–8k),长对话和长文档放不下;直接加长窗口代价是二次的,而且模型不一定用得好。
- 关键想法:借用操作系统的虚拟内存:把提示词当作”内存”(主上下文),把提示词之外的数据库当作”磁盘”(外部上下文);让 LLM 自己通过函数调用在两者之间搬数据,提供”无限上下文”的假象。
- 机制:主上下文 = 只读的系统指令 + 可读写的工作上下文 + 先进先出的消息队列;队列管理器在快满时发”内存压力”警告、满了就移出旧消息并生成递归摘要;函数执行器解析 LLM 输出的函数调用;“心跳”让 LLM 连续调用多个函数再回复用户;事件(用户消息、系统警告、定时器)触发推理。
- 结果(论文报告):多会话聊天的深层记忆检索任务上,GPT-4 从 32.1% 提高到 92.5%;嵌套键值检索任务里,GPT-4 在 3 层嵌套时降到 0%,MemGPT + GPT-4 不受嵌套层数影响。
- 局限:依赖模型的函数调用能力(GPT-3.5 版本明显变差);常常没翻完检索结果就停下;每一步记忆操作都是一次额外的 LLM 调用。
2.1 从虚拟内存到虚拟上下文
回忆前置讲 P3 的虚拟内存:程序以为自己有一大块连续的内存,实际上操作系统只把正在用的页放在物理内存里,其余放在磁盘上;访问不在内存里的页时触发缺页中断,操作系统把它换进来、必要时把别的页换出去。MemGPT 把这套想法搬到上下文上(论文图 3):
LLM 的有限上下文窗口(例如 8k token)
┌───────────────┬──────────────────┬───────────────────────────────┐
│ 系统指令 │ 工作上下文 │ 先进先出队列 │ ← 提示词(主上下文)
│ (只读) │ (可读写,只能 │ [递归摘要] 消息 消息 … 消息 │
│ │ 通过函数修改) │ (由队列管理器写入) │
└───────────────┴──────────────────┴───────────────────────────────┘
↕ 函数调用:working_context.append/replace、recall_storage.search、archival_storage.insert/search
┌──────────────────────────┐ ┌──────────────────────────┐
│ 回忆存储 recall storage │ │ 档案存储 archival storage │ ← 外部上下文
│ (全部历史消息) │ │ (任意文本,向量检索) │
└──────────────────────────┘ └──────────────────────────┘
- 工作上下文是一块固定大小的读写区,用来记”关键事实”:用户的名字、生日、偏好,以及助手自己的人设。论文图 4:用户说”我和 James 分手了”,LLM 调用
working_context.replace("Boyfriend named James", "Ex-boyfriend named James")。 - 先进先出队列放滚动的消息历史,第一条是之前被移出的消息的递归摘要。
- 队列管理器:提示词超过”警告阈值”(例如窗口的 70%)时,在队列里插一条”内存压力”的系统消息,让 LLM 有机会把要紧的内容存进工作上下文或档案存储(论文图 1);超过”刷新阈值”(100%)时,移出一批最旧的消息(例如窗口的 50%),用旧摘要和被移出的消息生成新的递归摘要。被移出的消息仍在回忆存储里,可以搜回来。
- 函数执行器:LLM 的输出被当作函数调用解析、校验、执行,结果(包括错误,例如”工作上下文已满”)再喂回给 LLM——LLM 从反馈里学会调整。检索结果是分页的,防止一次检索就把窗口撑爆。
- 控制流:事件(用户消息、内存警告、用户登录、定时任务)触发推理;函数调用可以带一个
request_heartbeat=true参数,要求执行完后立刻再推理一次——这样 LLM 可以连续翻好几页检索结果、或者先存记忆再回复,而不用等下一条用户消息。
这和操作系统的对应关系很直接:主上下文 ↔ 物理内存;外部存储 ↔ 磁盘;内存压力警告 ↔ 低内存信号;移出 + 摘要 ↔ 换出;检索 ↔ 缺页后的换入;事件 ↔ 中断。不同的是,换入换出的决策由 LLM 自己做,而不是由操作系统按 LRU 之类的规则做。
2.2 评测
- 深层记忆检索(DMR):在多会话聊天数据集 MSC 的 5 次会话之后,问一个只有记得之前对话才能回答的问题。基线只能看到前 5 次会话的有损摘要,MemGPT 可以分页搜索完整历史。论文报告的准确率:GPT-3.5 Turbo 38.7% → 66.9%,GPT-4 32.1% → 92.5%,GPT-4 Turbo 35.3% → 93.4%。
- 对话开场白:用记住的用户信息写下一次会话的第一句话,与人写的开场白相当。
- 多文档问答:固定上下文的基线受限于检索器排在前 K 的文档;MemGPT 可以翻页,理论上不受此限,但论文观察到它”常常在翻完检索结果之前就停下”。
- 嵌套键值检索:140 对 UUID 键值(约 8k token),一个键的值可能又是另一个键。GPT-3.5 在 1 层嵌套时就降到 0%,GPT-4 在 3 层时降到 0%;MemGPT + GPT-4 通过反复查询不受层数影响。
2.3 在服务端看 MemGPT
从推理服务的角度看,MemGPT 有两个直接的后果。
第一,更多的 LLM 调用。 每一次换入(搜索)、每一次写工作上下文,都是一次函数调用 + 一次”心跳”推理。代码 02 的 MemGPT 式记忆在回答问题时平均要 1.31 次调用;代码 03 模拟的一次 600 轮会话(8k 窗口)里有 767 次调用。这些调用的提示词几乎相同——都是整个主上下文——所以前缀缓存至关重要(代码 03 演示 1):
| LLM 调用 | prefill 的 token | prefill 的 GPU 时间 | |
|---|---|---|---|
| 没有前缀缓存 | 767 | 4,520,991 | 102.1 s |
| 有前缀缓存 | 767 | 497,359 | 13.9 s |
第二,提示词中间会被修改。 前缀缓存只能复用”和上次一模一样的前缀”。MemGPT 的工作上下文在提示词的前部(系统指令之后),每次 append / replace 都让它后面整个消息队列(几千 token)重新 prefill。在剩下的 49.7 万 token 里:
| 缓存断在哪里 | prefill 的 token | 占比 |
|---|---|---|
| 被修改的工作上下文 | 375,034 | 75% |
| 新追加的消息(无法避免) | 77,264 | 16% |
| 移出后换新的递归摘要 | 45,061 | 9% |
能不能换个布局?代码 03 演示 3 比较了三种(括号里是”一条含事实的消息被立刻存进工作上下文”的概率):
| 布局 | 很少改(10%) | 中等(50%) | 很频繁(100%) |
|---|---|---|---|
| 系统 | 工作上下文 | 摘要 | 队列(论文) | 260,851 | 497,359 | 795,921 |
| 系统 | 摘要 | 队列 | 工作上下文 | 483,804 | 669,205 | 807,584 |
| 修改作为新消息追加,移出时合并成快照 | 139,657 | 139,877 | 142,580 |
我原以为把工作上下文挪到最后会省很多,结果相反:新消息总是插在它前面,它在每一次调用里都要重算(平均约 700 token × 767 次)。真正便宜的是第三种(不是论文的设计,是我们按”只追加”原则加的对比):不去改提示词中间的任何东西,把修改作为一条短消息追加到末尾,等到移出时(反正整个队列都要失效)再合并进新的工作上下文快照。
另一个旋钮是一次移出多少(代码 03 演示 2):每次只移出窗口的 10% 时,89 次移出让 prefill 涨到 146 万 token;一次移出 75% 时只有 13 次、36.5 万 token,但主上下文里平均留下的历史从约 7,400 token 降到约 5,100 token。论文默认的 50% 是一个折中——和操作系统里”批量换出比频繁小量换出便宜”是同一个道理。
3. mem0:抽取、合并、检索 · mem0
论文卡片 Paper card · mem0
- 问题:长期对话里,完整上下文太慢太贵;RAG 检索原始对话片段,噪声多、可能拿到过时的信息;已有的记忆系统要么不准,要么慢、要么记忆库臃肿。
- 关键想法:在写入时做功:每来一对消息,就用 LLM 抽取出简短的”候选事实”,再和已有的相似记忆比较,让 LLM 通过工具调用决定 ADD / UPDATE / DELETE / NOOP,维护一个一致、紧凑的记忆库;读取时只检索少量相关记忆。图版本 mem0g 把记忆存成”实体—关系—实体”的有向带标签图。
- 结果(论文报告):LOCOMO 上 LLM 评判分数(J)mem0 66.9、mem0g 68.4,最好的 RAG 约 61,完整上下文 72.9;p95 总延迟 mem0 1.44 秒 vs 完整上下文 17.1 秒;每段对话的记忆库约 7k token(mem0g 约 14k,Zep 超过 60 万)。
- 局限:质量仍低于完整上下文;写入路径每对消息都要调用 LLM;抽取和增删改的判断出错时,读取时无法补救。
3.1 两个阶段
抽取(论文 §2.1)。新来一对消息 (通常是用户一句、助手一句)时,构造一个提示词:对话摘要 (由一个异步模块定期刷新)+ 最近 条消息 + 这对新消息,交给 LLM 抽取出一组候选事实 。摘要提供全局的主题,最近的消息提供局部的细节。
更新。对每个候选事实,从向量库里取出最相似的 条已有记忆,连同候选事实一起交给 LLM,通过函数调用选择一个操作:
| 操作 | 什么时候 |
|---|---|
| ADD | 没有语义相同的记忆:新增一条 |
| UPDATE | 已有记忆的补充:用信息更多的版本替换 |
| DELETE | 新信息与已有记忆矛盾:删掉旧的 |
| NOOP | 已经知道了,或者无关紧要 |
论文的实验用 GPT-4o-mini 做所有 LLM 操作。注意这里的设计哲学:不用单独的分类器,让 LLM 自己判断两条信息的语义关系。
mem0g(论文 §2.2)把记忆存成有向带标签图:节点是实体(人、地点、事件……,带类型、嵌入和创建时间),边是关系三元组(“Alice —lives_in→ San Francisco”)。抽取分两步(先抽实体,再生成关系);新关系与已有关系冲突时,由 LLM 判断是否让旧关系失效——标记为无效而不物理删除,以便做时间推理。检索也有两条路:以实体为中心(找到问题里的实体对应的节点,沿入边和出边展开子图),以及把整个问题和所有三元组的文本嵌入做相似度匹配。
3.2 论文的评测
LOCOMO 有 10 段长对话,每段约 600 轮、2.6 万 token,平均约 200 个问题,分单跳、多跳、时间推理、开放领域四类。论文主要用 LLM 评判(J)打分,因为 F1、BLEU 这类词面重叠指标会给”Alice 生于七月”(正确答案是三月)打很高的分。几组关键数字(论文表 1、表 2):
| 方法 | 每问的上下文 token | p95 总延迟 | 总体 J |
|---|---|---|---|
| 完整上下文 | 26,031 | 17.1 s | 72.9 |
| RAG(256 token × 2 块) | 约 512 | 1.91 s | 61.0 |
| RAG(8192 × 2) | 约 16,384 | 9.94 s | 60.5 |
| Zep | 3,911 | 2.93 s | 66.0 |
| mem0 | 1,764 | 1.44 s | 66.9 |
| mem0g | 3,616 | 2.59 s | 68.4 |
论文的结论是:mem0 以完整上下文的一小部分代价,拿到了接近的质量;图记忆在时间推理上有帮助(J 58.1 vs 55.5),在单跳、多跳上没有。
3.3 我们的玩具复现:机制在哪里
代码 02 造了一个仿照 LOCOMO 规模的玩具世界(memory_lab.Conversation):16 个用户属性(一半说过一次就不变,一半会变:搬家、换工作)、4 个人物角色(伴侣、好友、上司、室友——会换人,每个人有家乡、职业两个事实),中间夹着大量闲聊和”谈到话题但没给答案”的消息;嵌入是 256 维的随机向量加噪声。问题分三类:当前值(“现在住在哪”)、之前的值(“搬家前住在哪”)、两跳(“伴侣的家乡是哪”——要先知道伴侣是谁,而”Alex 来自 Ohio”这句话里只有人名、没有”伴侣”)。读者是”理想读者”:看到的材料里有答案所需的事实就答对,按时间戳取最新的有效值。
代码 02 演示 1(10 段对话、389 个问题):
| 系统 | 当前值 | 之前的值 | 两跳 | 全部 | token / 问 | 调用 / 问 | 写入调用 / 段 |
|---|---|---|---|---|---|---|---|
| 完整上下文 | 1.000 | 1.000 | 1.000 | 1.000 | 25,755 | 1.00 | 0 |
| RAG 8192×2 | 0.620 | 0.477 | 0.637 | 0.584 | 14,617 | 1.00 | 0 |
| RAG 512×2 | 0.330 | 0.083 | 0.188 | 0.231 | 1,077 | 1.00 | 0 |
| mem0(删除旧事实) | 0.895 | 0.009 | 0.588 | 0.584 | 200 | 1.00 | 370 |
| mem0g 式(标为失效) | 0.895 | 0.761 | 0.588 | 0.794 | 247 | 1.00 | 370 |
| mem0g 式 + 图检索 | 0.895 | 0.761 | 0.787 | 0.835 | 278 | 1.00 | 370 |
| MemGPT 式 | 0.910 | 0.899 | 0.887 | 0.902 | 6,289 | 1.31 | 52 |
这张表里有四个机制,比绝对数字重要得多:
- RAG 的主要失败是”找到了过时的陈述”。同一个属性在历史上被说过好几次,还有很多只谈话题不给答案的消息;检索按相似度排序,最新的那次变化不一定排在前面。mem0 在写入时就把”哪个是最新的”处理好了,所以当前值的问题只读 200 token 就答对 0.895。
- 删除会丢掉历史。mem0 用 DELETE 维护一致性,“之前是什么”的问题几乎全错(0.009);像 mem0g 那样标为失效而不删除,回到 0.761。但”保留历史”需要配套的检索:我的第一版把失效的记忆也放进相似度排序,长对话里旧版本越积越多,把当前值挤出了前 10 条(README 第 3 条)。
- 两跳需要沿实体展开。“伴侣的家乡”这个问题的嵌入和”Alex 来自 Ohio”不像;单次向量检索 0.588,沿检索到的人物做一跳扩展(mem0g 的以实体为中心的检索)0.787。MemGPT 式的智能体可以先搜人、再搜人名,两跳也有 0.887。
- 代价从读取搬到了写入。mem0 每段对话写入时要调用 LLM 约 370 次(每对消息一次抽取,每条事实一次更新);MemGPT 式只在移出消息时整理(约 52 次);完整上下文和 RAG 一次也不用。
在这个长度上,MemGPT 式是最好的记忆系统(0.902)——它的主上下文已经装下了最近约 6 千 token 的对话,覆盖了大部分近期的变化。但它每问读的 token 是 mem0 的 20 多倍,而且对话越长退化得越快(演示 4)。
一个对论文的补充:调好的 RAG 强得多。 mem0 只比较了 k = 1、2 的 RAG(理由是 k 再大就覆盖了大半段对话)。代码 02 演示 2 固定 128 token 的小块、多取几块:
| k | 当前值 | 之前的值 | 全部 | token / 问 |
|---|---|---|---|---|
| 2 | 0.325 | 0.055 | 0.193 | 306 |
| 8 | 0.895 | 0.761 | 0.715 | 1,229 |
| 16 | 0.990 | 0.917 | 0.892 | 2,459 |
| 64 | 0.995 | 1.000 | 0.990 | 9,806 |
128 × 16(约 2,500 token)就超过了 mem0g 式 + 图检索(0.835),多读约 9 倍的 token。同样约 2 千 token 的预算,大块的 2048 × 1 只有 0.154:小块的嵌入更”纯”,检索更准。这个结论有一个重要的前提:我们的读者不会被多出来的材料干扰。另外,论文里中等块大小最差、两端较好的 U 形,我们的玩具没有复现。
部件不完美时(代码 02 演示 3)。mem0 式记忆的质量取决于写入路径上两个 LLM 判断:抽取召回从 1.0 降到 0.7,正确率从 1.000 降到 0.674;把”一次变化”误判为”已经知道了”(新值被丢掉)的概率升到 60%,正确率降到 0.681。这些判断在写入时一次做完,读取时无法补救——与之对比,完整上下文把原始材料都留着,只要读者够好就能补救。我们用一个参数 alpha 模拟”读长材料会漏看”:alpha = 0.5 时完整上下文仍高于 mem0g 式(0.877 vs 0.835),alpha = 1 时降到 0.689。alpha 是假设,不是测出来的;真实模型的 lost-in-the-middle 程度取决于模型和任务。
对话越来越长(代码 02 演示 4,每种长度 3 段对话):
| 会话数 | 平均 token | 完整上下文(窗口 128k) | RAG 8192×2 | mem0g 式 + 图(写入调用) | MemGPT 式 |
|---|---|---|---|---|---|
| 20 | 25,773 | 1.00 | 0.69 | 0.93(370) | 0.86 |
| 60 | 77,623 | 1.00 | 0.38 | 0.88(1,081) | 0.67 |
| 200 | 257,253 | 0.80 | 0.13 | 0.81(3,520) | 0.57 |
超出窗口后,完整上下文只能截断最旧的部分,很早说过、之后再没变的事实就丢了;mem0g 式每问读的 token 几乎不变(约 250–280),正确率缓慢下降,但写入调用与对话长度成正比;MemGPT 式的工作上下文容量固定,记不下越来越多的事实,退化得最快。
4. Cartridges:离线把语料训练成一小块 KV cache · Cartridges
论文卡片 Paper card · Cartridges
- 问题:用户反复对同一份大语料(代码库、病历、财报、教科书)提问时,ICL 每次都要带着整份语料的 KV cache,显存和吞吐代价巨大;现有的 prompt 压缩和 KV 压缩方法在压缩超过约 2 倍时质量就迅速下降。
- 关键想法:每份语料离线训练一小块 KV cache(cartridge,p 个可训练的”虚拟 token”),推理时把它当作前缀加载。训练成本可以在同一份语料的所有查询上摊销。直接在语料上做下一个 token 预测只会”背诵”,所以提出 self-study:让模型对着语料的片段自问自答生成合成对话,再用上下文蒸馏训练 cartridge。
- 结果(论文报告):在 LongHealth、QASPER、MTOB 等基准上与 ICL 质量相当,显存平均少 38.6 倍、峰值吞吐高 26.4 倍;在 48.4 万 token 的教科书上(超出 Llama-8B 的 128k 窗口)比截断的 ICL 高 11.0 chrF;独立训练的 cartridge 可以直接拼接使用。
- 局限:训练远比 prefill 贵(未优化的实现在一个 8 × H100 节点上约 30 分钟);语料变化后要重新训练;需要大量查询才回本。
4.1 问题设定:同一份语料,很多次查询
Cartridges 关心的场景:一份语料 (例如 AMD 的 10-K 财报,近 10 万 token),用户对它提出各种各样的问题——查事实、做计算、做总结、甚至写首诗。目标有两个:回答的质量不低于 ICL,同时显存占用尽量小。§1.2 已经算过,后者直接决定了吞吐和成本。
已有的两类压缩各有问题:prompt 压缩(截断、摘要、按信息量过滤 token)直接丢信息;KV cache 压缩(丢掉或合并注意力分数低的 KV,例如 DuoAttention)用很少的计算做压缩,论文发现它们在压缩超过约 2 倍时质量就迅速下降。Cartridges 的出发点是一个经济学观察:既然同一份语料会被问很多次,就值得花大量计算去压缩它。
4.2 参数化:一块可训练的 KV cache
cartridge 是一组可训练的键和值向量 (每一层各一组),它的显存和一个 p token 的提示词的 KV 一样大。训练时冻结模型的所有参数,只把损失反向传播到这些 KV 上——本质上是简化的 prefix-tuning。几个细节:
- 初始化很关键:用语料前 p 个 token 的真实 KV 初始化。论文在 LongHealth 上:随机向量 29.9%、随机 token 的 KV 51.3%、前 p 个 token 55.3%。
- 冻结注意力汇点(第一个 token 的 KV)能让训练更稳定。
- 为什么不用 LoRA:显存相同时,KV 参数化在语料相关的问题上更好(MTOB 上高 4.5 chrF),而且对无关任务的损害小得多——LoRA 版本随大小增加 MMLU 从 54.7 跌到 45.3,KV 版本只到 54.3。
- 服务端几乎不用改:cartridge 就是一个 KV 前缀,现有引擎管理前缀缓存的机制(第 3、9 讲)可以直接加载它;而服务很多不同的 LoRA 需要专门的基础设施(第 12 讲提到的多 LoRA 服务)。
4.3 self-study:合成对话 + 上下文蒸馏
为什么不直接在语料上训练? 论文图 3:用下一个 token 预测在语料原文上训练的 cartridge,能把语料”背”得几乎完美(困惑度很低、显存小 107 倍),但对其他类型的问题(总结、推理、创作)泛化很差——它学会的是”接着原文往下写”,而不是”回答关于原文的问题”。
合成数据(论文算法 1)。把语料切成 512–4,096 token 的片段 ;让同一个模型扮演 A、B 两个角色:A 的提示词里有片段和一个种子提示词(例如”请就上面的文档提一个问题”),B 的提示词里只有片段;两者交替生成一段对话(论文的主要实验只用一问一答)。切成片段有两个作用:让模型专注于语料的不同部分,以及让比上下文窗口还长的语料也能用来生成数据。种子提示词有 5 类:结构化、总结、提问、用例、创作——都是通用的,不针对具体语料;比只用一个种子提示词在 MTOB 上高 7.9 chrF(24.1 → 32.0)、LongHealth 上高 4.8 个点。
上下文蒸馏(论文公式 3)。老师是”带着片段的模型” ,学生是”带着 cartridge 的模型” ,在合成对话的每个位置上最小化两者下一个 token 分布的 KL 散度:
与在同样的合成数据上做下一个 token 预测相比,蒸馏在 MTOB 上高 8.6 chrF(24.9 → 33.5)。直觉是:老师的完整输出分布比”正确答案是哪个 token”包含更多信息。
4.4 结果
- 质量—显存的前沿(论文图 4):在 LongHealth 和 QASPER 上,都能找到比 ICL 更好、显存却小得多的 cartridge 大小(最多省 10 倍和 100 倍);平均下来与 ICL 质量相当时显存少 38.6 倍、峰值吞吐高 26.4 倍。
- 扩展有效上下文(§5.2):MTOB 的 48.4 万 token 教科书超出 Llama-8B 的 128k 窗口,靠分块的 self-study 训练出的 cartridge 比截断到前 13 万 token 的 ICL 高 11.0 chrF,和 ICL 读人工精选的 6 万 token 版本相当。
- 组合(§5.4):两份 10-K 各自训练的 cartridge 直接拼接,就能回答跨两份文档的问题,比只带一个 cartridge、比截断的 ICL 都好。
- 计算越多越好(图 5):训练步数越多,质量稳定上升。
- 理论直觉(附录 E):在多查询关联回忆(MQAR)问题上,用梯度下降更新的固定大小记忆可以精确解决线性注意力(“把关联加起来”的更新规则)解决不了的情形。这解释了为什么一块固定大小的 cartridge 能做到线性注意力架构做不到的事。
4.5 我们的玩具版:真的用梯度下降训练一块 KV
我们没法在笔记本上训练 Llama,但可以保留 Cartridges 的关键结构(cartridge_lab.py):“文档”是 512 条随机的”键 → 值”关联,每条事实是一个 token 的 (key, value);“模型”是一层 softmax 注意力,再在所有可能的值上输出分布;“用户的问题”是换了说法的 key(加噪声,与原 key 的余弦相似度约 0.96)。ICL 就是把 512 对 KV 全放进缓存;cartridge 是 p 对可训练的 KV,用手写的反向传播和 Adam 训练(自检里用数值差分验证过梯度)。
代码 04 演示 1(正确率:原文的说法 / 换一种说法;ICL 是 1.000 / 1.000):
| p | 压缩比 | 截断到前 p 个 | 背诵(原文 + 硬标签) | self-study(换说法 + 蒸馏) |
|---|---|---|---|---|
| 16 | 32× | 0.034 | 0.418 / 0.052 | 0.185 / 0.086 |
| 32 | 16× | 0.060 | 0.932 / 0.208 | 0.801 / 0.549 |
| 64 | 8× | 0.121 | 0.976 / 0.470 | 0.990 / 0.922 |
| 128 | 4× | 0.253 | 0.917 / 0.674 | 0.998 / 0.986 |
线性记忆(一个 64 × 64 矩阵,显存相当于 32 个 token)把关联加起来是 0.282,用最小二乘”训练”这个矩阵是 0.317。
这张表把论文的几个论点都演示出来了:截断的正确率就是 p / N;背诵式训练用原文问时 16 倍压缩就几乎全对,换个说法就掉到 0.208——正是论文图 3 的现象;self-study 在 8 倍压缩下换说法也有 0.922。
演示 2 的消融(p = 64,换说法问):
| 设置 | 正确率 |
|---|---|
| 默认(蒸馏、前 p 个 token 初始化、训练问题换说法) | 0.922 |
| 硬标签代替蒸馏 | 0.906 |
| 随机 p 个 token 的 KV 初始化 | 0.923 |
| 标准正态随机向量初始化 | 0.122 |
| 训练问题不换说法 | 0.518 |
初始化的影响比论文报告的还强:64 维标准正态向量的长度约为 8,注意力的 logits 一开始就被放大几十倍,softmax 饱和,梯度几乎为零。蒸馏比硬标签只好一点(p = 32 时在种子噪声的范围内):玩具里的”值”之间没有任何关系,老师的输出分布除了正确答案几乎没有额外的信息——而真实语言里”差一点对的答案”包含大量信息,所以论文里蒸馏的好处大得多。训练越久越好,边际收益递减:1,000 步 0.663,3,000 步 0.922,8,000 步 0.985。
演示 3 把文档加长到 2,048 条事实,而”上下文窗口”只有 512:ICL 只能放进前 512 条(0.253);老师每次只看一个 512 条的片段,p = 256 的 cartridge 训练 3,000 步 0.515、6,000 步 0.745,还在上升。在这个玩具里分块的老师和看整份文档的老师一样好(3,000 步时分别是 0.515 和 0.509)——一个问题只和一条事实有关;真实文档有跨片段的依赖,这正是论文设计”面向全局结构的种子提示词”的原因。
演示 4 测组合:A、B 两份文档各训练一个 p = 64 的 cartridge,直接拼接后平均 0.499,比截断的 ICL(0.121)、只带 A 的 cartridge(0.422)都好,与论文一致;但不如在 A + B 上联合训练的同样大小的 cartridge(0.788)——拼接后 A 的问题有 35.6% 的注意力被 B 的 token 分走。论文没有和联合训练比较。
4.6 什么时候划算
Cartridges 用离线计算换在线显存。代码 01 演示 4 按论文的数字(省 38.6 倍显存、训练约 30 分钟 × 8 块 H100 ≈ 14,400 GPU·秒)算了回本点(Llama-3-8B / H100,GPU 满载时每次查询摊到的 GPU·秒):
| 文档 | ICL,每次 prefill | ICL,KV 已缓存 | cartridge | 回本:对每次 prefill | 回本:对已缓存 |
|---|---|---|---|---|---|
| 32,000 token | 1.44 | 0.42 | 0.019 | 10,104 次 | 36,255 次 |
| 128,000 token | 10.45 | 1.67 | 0.050 | 1,384 次 | 8,877 次 |
| 484,000 token | 放不进窗口 | 放不进窗口 | 0.168 | — | — |
要上千次查询才回本,文档越短越难回本。这不是反对 Cartridges,而是说明它的定位:适合被反复查询、内容稳定的语料(代码库、病历、财报、法规),训练可以放在夜间闲置的 GPU 上(这时 GPU·秒的”价格”更低);训练如果能优化 10 倍(论文认为空间很大,例如用共享前缀的注意力内核),回本点也缩小 10 倍。只问几次的文档,直接放进上下文更划算;超出窗口的文档,cartridge 是少数可行的办法之一。
5. ACE:不断演化的上下文”手册” · Agentic Context Engineering
论文卡片 Paper card · ACE
- 问题:通过修改上下文(系统提示、记忆)而不是权重来改进 LLM 应用,是越来越常用的做法;但已有方法有简短偏差(优化器偏爱简洁、通用的提示词,丢掉领域细节)和上下文坍缩(每一步让 LLM 整体重写上下文,积累的知识可能突然被压成一小段摘要)。
- 关键想法:把上下文当作一本不断演化的手册:一条条带元数据的要点。三个角色分工:生成器(做任务、标出哪些要点有用或误导)、反思器(从成功和失败里提炼教训)、整理器(把教训变成小的增量条目);增量由非 LLM 的确定性逻辑合并,定期用嵌入去重(“增长并精炼”)。
- 结果(论文报告):AppWorld 智能体基准上平均比强基线高 10.6%,金融推理基准高 8.6%;用开源的 DeepSeek-V3.1 达到排行榜第一名(基于 GPT-4.1 的生产级智能体)的平均水平;适应延迟比 GEPA 低 82.3%、比 Dynamic Cheatsheet 低 91.5%。
- 局限:依赖足够强的反思器和可靠的反馈信号;没有可靠反馈时,上下文会被错误的信号污染而变差;不是所有任务都需要长而详细的上下文。
5.1 简短偏差与上下文坍缩
“上下文适应”(context adaptation,也叫上下文工程 context engineering)是指不改权重、只改输入来改进模型:更清楚的指令、结构化的推理步骤、领域知识、过去的经验。代表方法:Reflexion(反思失败来改进规划)、TextGrad(用文字形式的”梯度”优化提示词)、GEPA(基于执行轨迹反思、用遗传—帕累托搜索演化提示词)、Dynamic Cheatsheet(在推理时积累策略和代码片段的外部记忆)。
ACE 指出两个问题。简短偏差:很多提示词优化器把”简洁”当作优点,反复收敛到几乎相同的、通用的指令(“写单元测试确保方法行为正确”),丢掉了领域里的启发式、工具用法和常见错误。上下文坍缩:让 LLM 在每一步整体重写累积的上下文时,它倾向于把长上下文压成短摘要。论文图 2 的例子(AppWorld,Dynamic Cheatsheet):第 60 步上下文有 18,282 token、准确率 66.7;下一步被重写成 122 token,准确率跌到 57.1——比完全不适应的 63.7 还低。
ACE 的立场与”简洁的提示词更好”相反:LLM 不像人,它更擅长从长而详细的上下文里自己挑出相关的部分,所以上下文应该是全面的手册,而不是简洁的摘要。
5.2 生成、反思、整理
任务 ──▶ 生成器 ──轨迹(标出用到的要点是否有用)──▶ 反思器 ──教训(可多轮精炼)──▶ 整理器
▲ │
└──────────── 手册 ◀── 确定性合并(追加新要点、更新计数、去重)◀── 增量条目
- 要点(bullet):一条小的知识单元(一个可复用的策略、一个领域概念、一个常见错误),带唯一编号和”被标为有用 / 有害的次数”等元数据。
- 增量更新:整理器每次只产生几条新要点(“增量上下文”),由非 LLM 的逻辑合并进手册——不重写整本手册,所以不会坍缩,也不用花生成整本手册的 token。因为更新是局部的,多个增量可以并行合并。
- 增长并精炼:新要点追加,已有要点就地更新计数;去重通过比较要点的嵌入完成,可以每次更新后做,也可以”懒惰地”只在超出窗口时做。
- 支持离线(在训练集上多轮优化系统提示词)和在线(测试时边做边学,相当于智能体的记忆)两种用法;反馈可以来自真实标签,也可以只来自执行结果(代码是否跑通、API 是否报错)。
5.3 论文的评测
- AppWorld(API 调用、代码生成、与环境交互的智能体任务):ReAct 基线平均 42.4;离线 ACE 59.4(比 ICL、GEPA 分别高 12.3%、11.9%);在线 ACE 59.5(比 Dynamic Cheatsheet 高 7.6%);不用真实标签、只用执行反馈时离线 57.2。
- 金融推理(FiNER 的 XBRL 实体标注、Formula 的数值计算):有真实标签时离线 81.9(基线 69.1);但没有可靠反馈时,ACE 和 Dynamic Cheatsheet 都可能变差(例如在线、无标签时 DC 的平均从 69.1 降到 65.4)——上下文会被错误的信号污染。
- 消融:去掉反思器和多轮适应,离线平均从 59.4 降到 55.1。
- 代价:AppWorld 离线适应的延迟比 GEPA 低 82.3%、试运行(rollout)次数少 75.1%;FiNER 在线适应的延迟比 DC 低 91.5%、token 费用少 83.6%。
- 长上下文 ≠ 高服务成本:用 GPT-5.1 的提示词缓存测量,评测时 91.8% 的输入 token 来自缓存,计费的输入成本比按原始长度计算低 82.6%。
5.4 在服务端看 ACE
ACE 的两个代价论点——“适应便宜”和”长上下文不贵”——都可以用前缀缓存的原理解释。代码 03 演示 4 模拟手册随 300 个在线任务演化(每个任务:生成器读”系统 | 手册 | 任务”、输出 300 token;每步以 60% 的概率新增一条 40 token 的要点):
| 更新方式 | 最终手册 | 缓存复用率 | 生成器 prefill | 更新手册的输出 | GPU 时间 |
|---|---|---|---|---|---|
| 整体重写(Dynamic Cheatsheet 式) | 6,760 | 11.7% | 1,129,660 | 1,045,720 | 7,231 s |
| 增量,计数器写在提示词里 | 7,240 | 31.5% | 904,780 | 8,280 | 641 s |
| 增量,计数器只在元数据里 | 6,760 | 92.4% | 97,260 | 7,920 | 620 s |
| 增量 + 每 50 步去重一次 | 5,000 | 90.0% | 110,140 | 8,070 | 619 s |
- 整体重写的代价主要在生成:每一步都要把整本手册重写一遍,300 步一共输出约 105 万 token,decode 逐个 token 进行,GPU 时间是增量更新的 11.7 倍。这就是 ACE”适应延迟低 82–91%“的主要来源。
- 增量更新时,有没有把会变的东西写进提示词决定了缓存复用率:如果每条要点后面写着”有用 3 次、有害 1 次”,每一步被用到的几条要点都会变,而它们分布在手册各处,缓存从第一处修改就断了(31.5%);计数器只留在元数据里、提示词只追加新要点时,复用率 92.4%,和论文测到的 91.8% 很接近。
- 定期去重让缓存失效一次,之后又是只追加,复用率只略降。
但”长上下文不贵”只对 prefill 成立:decode 时每个 token 仍要读一遍整本手册的 KV(§1.2),在批量服务时,手册越长,同时能服务的请求越少。
6. 系统视角 · A systems view
6.1 记忆的层级
把本讲的四种做法和计算机系统里熟悉的存储层级放在一起看:
| 计算机系统 | LLM 的”记忆” | 容量 | 访问代价 |
|---|---|---|---|
| 寄存器 / 缓存 | 当前提示词里的 token(KV cache 在显存里) | 窗口大小 | 每个 token 都参与每一步计算 |
| 内存 | 前缀缓存里的 KV(第 3 讲)、CPU 内存里的 KV(第 9 讲) | GB–TB | 加载或重算 |
| 磁盘 / 数据库 | 回忆存储、档案存储、向量库、图数据库 | 无限 | 检索 + 再放进提示词(又一次 prefill) |
| 编译好的代码 | cartridge、微调后的权重 | 小 | 几乎免费,但要离线”编译”(训练) |
MemGPT 是”操作系统管理分页”;mem0 是”带写入时规范化的数据库”;Cartridges 是”预先编译”;ACE 是”只追加的日志 + 定期压实”(log-structured storage 的思路)。
6.2 在写入时做功,还是在读取时做功
| 写入时 | 读取时 | |
|---|---|---|
| 完整上下文 | 无 | 读全部(可被前缀缓存部分复用) |
| RAG | 切块、算嵌入(便宜) | 检索 + 读几块原文;要自己分辨新旧 |
| mem0 | 每对消息一次抽取、每条事实一次更新(LLM 调用) | 检索几条干净的事实 |
| MemGPT | 移出时整理(LLM 调用) | 主上下文 + 可能多次检索调用 |
| Cartridges | 每份语料一次离线训练(非常贵) | 几乎免费 |
哪种划算,取决于读写比:一段对话被问 200 次(LOCOMO)时,mem0 的 370 次写入调用很划算;一段对话写了很多、几乎不被回顾时就不划算。一份语料被问上万次时,cartridge 划算;只问几次时不划算(§4.6)。这和数据库里”写优化”与”读优化”的索引选择是同一个问题。
6.3 可变的上下文与前缀缓存
本讲反复出现一个推理服务的原则:前缀缓存只能复用没有变过的前缀,所以提示词里会变的东西应该放在后面、做成只追加的。
- MemGPT 的工作上下文在前部且频繁修改,75% 的 prefill 来自它(§2.3);
- ACE 的计数器如果写进提示词,复用率从 92.4% 降到 31.5%(§5.4);
- 整体重写的上下文(Dynamic Cheatsheet)几乎无法缓存;
- 反过来,cartridge 是一个完全不变的前缀,是对缓存最友好的形式。
这和第 13 讲的结论相呼应:智能体的提示词结构、调度和缓存是一起设计的。一个实用的检查清单:系统指令和稳定的知识放最前;会增长的历史只追加;会修改的小状态放在末尾或写成追加的日志;需要大改时批量做(像移出一样,一次失效、之后长期复用)。
6.4 什么时候选什么
- 语料共享给很多用户(公司手册、产品文档):直接放进上下文,靠前缀共享和级联注意力(第 3、4 讲);
- 语料每人一份、被反复查询、很少变化(病历、代码库):考虑 cartridge 或微调;
- 长期对话、个人助手(会变化的事实):抽取式记忆(mem0),注意保留历史;需要时用智能体式检索(MemGPT)补充;
- 积累经验的智能体:演化的手册(ACE),确保有可靠的反馈信号,保持只追加;
- 对话不长、预算允许:完整上下文仍然是质量最好、最简单的选择。
7. 之后的发展 · Since then
- 产品化的记忆:MemGPT 的作者成立了 Letta,继续发展”有状态智能体”;mem0 有开源和托管版本;Zep 用时间知识图(Graphiti)做记忆;主流聊天产品也都加入了跨会话的记忆功能(mem0 论文就把 OpenAI 的记忆功能作为基线)。
- 利用空闲时间:与 Cartridges”用闲置算力离线压缩”相似的思路还有 sleep-time compute(Lin et al., 2025,Letta 与 UC Berkeley):在用户提问之前,利用空闲时间对上下文做预先的推理,把结果存起来,减少查询时的计算。
- 上下文工程成为一个独立的话题:GEPA、DSPy 的提示词优化,Dynamic Cheatsheet、ACE 的在线适应,以及”怎样组织提示词才能被缓存”的工程实践。
- 把记忆做进架构:线性注意力、状态空间模型(第 10 讲)用固定大小的状态代替增长的 KV;Titans、测试时训练(TTT)在推理时用梯度下降更新记忆——Cartridges 附录 E 讨论了它们与 cartridge 的关系,区别是后者不用从头训练模型。
- KV cache 压缩持续发展(丢弃、合并、量化、低秩),与”训练出来的小 KV”形成一个从”几乎不花计算”到”花大量计算”的连续谱。
8. 小结 · Summary
- LLM 的记忆就是它的上下文;每个 token 都占 KV cache。每人一份 12 万 token 的上下文,会让一块 H100 的峰值吞吐下降两个数量级(代码 01:107 倍;论文实测 77 倍),共享的长上下文则几乎没有这个问题。
- MemGPT 借用虚拟内存:主上下文 / 外部存储,由 LLM 自己通过函数调用换入换出。代价是更多的 LLM 调用和提示词中间的修改——在我们的模拟里,前缀缓存省掉 89% 的 prefill,剩下的 75% 来自工作上下文的修改(代码 03)。
- mem0 在写入时抽取事实并决定增 / 改 / 删 / 不变,读取时只读几百 token。玩具复现的机制:RAG 常常检索到过时的陈述;删除丢掉历史,标为失效而不删除能保住(0.009 → 0.761);两跳需要沿实体展开;代价从读取搬到写入(每段对话约 370 次 LLM 调用)。调好的 RAG(小块、多取)是比论文更强的基线(代码 02)。
- Cartridges 离线把语料训练成一小块 KV(self-study:分块的合成对话 + 上下文蒸馏),显存少 38.6 倍、吞吐高 26.4 倍(论文报告)。玩具复现:背诵式训练换个说法就不行,self-study 在 8 倍压缩下答对 0.922;初始化至关重要;直接拼接可用但不如联合训练(代码 04);回本需要上千次查询(代码 01)。
- ACE 把上下文当作只追加、定期精炼的手册,避免整体重写造成的坍缩和高昂的生成代价;只追加时前缀缓存复用 92.4%(论文测得 91.8%),整体重写的适应代价是增量的 11.7 倍(代码 03)。
- 共同的系统原则:分清在写入时还是读取时做功(取决于读写比);会变的上下文放在后面、做成只追加的。
9. 常见误区 · Common pitfalls
- “上下文窗口有 1M token 了,记忆问题就解决了。” 放得进去不等于付得起:每人一份长上下文时吞吐下降两个数量级(代码 01),而且模型不一定读得进去。
- “前缀缓存让长上下文变得免费。” 它省掉的是 prefill;decode 每个 token 都要读整份 KV,显存也一直占着(代码 01 演示 2、3)。
- “记忆系统总比完整上下文便宜。” 读取便宜了,写入可能很贵:mem0 每对消息都要调 LLM(代码 02);cartridge 要上千次查询才回本(代码 01)。
- “DELETE 让记忆库更干净,所以更好。” 也删掉了”之前是什么”的答案;需要历史时应标为失效(代码 02)。
- “RAG 不行,所以要专门的记忆系统。” 论文比较的 RAG 配置偏弱;小块、多取的 RAG 在我们的玩具里很强。真正的区别在于”谁来分辨新旧”——写入时的规范化还是读取时的读者(代码 02 演示 2)。
- “在语料上微调(或训练 cartridge)就能让模型回答关于语料的问题。” 直接在原文上训练只学会背诵;要用多样的合成问题和蒸馏(代码 04 演示 1、2)。
- “上下文越简洁越好。” 对 LLM 不一定;整体重写还会坍缩。ACE 主张详细的、只追加的手册(§5.1)。
- “把会变的状态放在提示词哪里都一样。” 放在前部会让后面的缓存全部失效;但简单地挪到末尾也可能更贵(每次都重算),只追加 + 定期合并最好(代码 03 演示 3)。
10. 练习 · Exercises
练习 1、2、3、5 的参考答案里的数字来自代码的实际输出或
memory_lab.py/hw_model.py的函数。
练习 1(70B 的显存账)。Llama-3-70B 每个 token 的 KV 是 320 KiB,BF16 权重约 141 GB。用 8 块 H100(每块 80 GB,按 90% 利用率)做张量并行,权重之外还能放多少 token 的 KV?能同时放下几份 12.8 万 token 的上下文?
参考答案
8 × 80 × 0.9 = 576 GB,减去 141 GB 剩约 435 GB;每 token 327,680 字节,约 133 万 token;12.8 万 token 一份约 41.9 GB(代码 01 演示 1),能放下 10 份。也就是说,8 块 H100 服务 70B 模型时,同时只能有约 10 个用户各自带着一本书——每个人的 KV 比模型权重的三分之一还大。
练习 2(mem0 的读写账)。代码 02 里 mem0 每段对话写入时约 370 次 LLM 调用,每次抽取调用的提示词约 1,000(固定部分)+ 最近 10 条消息约 430 + 这对消息约 85 token,每次更新调用约 500 + 200 token。(a) 一段对话的写入大约要 prefill 多少 token?(b) 完整上下文每个问题要读约 2.6 万 token;问多少个问题时,mem0 的写入 prefill 总量等于完整上下文的读取 prefill 总量?(c) 为什么这个比较对完整上下文不太公平?
参考答案
(a) 约 300 次抽取 × 1,500 ≈ 45 万,加约 70 次更新 × 700 ≈ 5 万,合计约 50 万 token(代码里 build 返回的 prefill 可以直接打印出来核对)。(b) 50 万 / 2.6 万 ≈ 19 个问题;LOCOMO 每段对话约 200 个问题,远超过回本点。(c) 完整上下文的 2.6 万 token 在同一段对话的多个问题之间是相同的前缀,可以被前缀缓存复用(代码 01 演示 2:缓存后 TTFT 只剩问题本身);这时它的读取 prefill 几乎为零,剩下的是 decode 时读 KV 的代价和显存占用。所以 mem0 的真正优势在于显存和 decode(每问 200 token vs 2.6 万 token),以及超出窗口后的可扩展性。
练习 3(一次移出多少)。代码 03 演示 2:移出比例 10% 时 89 次移出,50% 时 19 次。用”每次移出后整个队列都要重新 prefill”估算两种设置下移出本身造成的 prefill,再与表中的总 prefill 比较。差额来自哪里?
参考答案
移出比例 10% 时,队列总是接近满的(窗口 8,000 减去系统指令、工作上下文和摘要,约 6,000 token),每次移出后约 5,400 token 要重算:89 × 5,400 ≈ 48 万;50% 时移出一半后只剩约 2,400 token:19 × 2,400 ≈ 4.6 万。代码里按”缓存断在哪里”的分类(memgpt_session 返回的 cause)正好是 48.1 万和 4.5 万。表中的总 prefill 是 146 万和 50 万,差额主要来自工作上下文的修改(91.1 万 vs 37.5 万):10% 时队列始终接近满,每次修改让更长的队列失效;而且移出频繁,每次移出前的”内存压力”警告又带来几次修改。各种失效的代价是相互放大的。
练习 4(cartridge 的回本点对假设的敏感性)。代码 01 演示 4 里,12.8 万 token 的文档对”KV 已缓存的 ICL”要约 8,900 次查询回本。(a) 如果训练优化了 10 倍呢?(b) 如果夜间闲置 GPU 的价格只有白天的三分之一(训练在夜间做)呢?(c) 如果文档每周更新一次、每次都要重训,每周至少要多少次查询?
参考答案
回本点 = 训练成本 / 每次查询省下的成本。(a) 训练成本除以 10,回本点约 890 次。(b) 训练成本按价格折算为三分之一,约 2,960 次;两者叠加约 300 次。(c) 每周重训一次,就要求每周至少约 8,900 次查询(按原始假设)——对一个每天被查几十次的个人文档不划算,对一个全公司每天查上千次的代码库就划算。
练习 5(计数器写在提示词里)。手册有 n 条要点,每一步随机修改其中 3 条(位置均匀分布)。(a) 第一处修改的位置的期望大约在手册的什么位置?(b) 据此估算生成器的缓存复用率,并与代码 03 演示 4 的 31.5% 比较。
参考答案
(a) 3 个均匀随机位置的最小值,期望约在 1/(3+1) = 1/4 处。(b) 缓存只能复用系统指令(500 token)加上手册的前约 1/4;手册平均约 3,400 token(从 0 增长到约 6,800),任务 300 token 每次都是新的,所以复用率约 (500 + 0.25 × 3,400) / (500 + 3,400 + 300) ≈ 32%,与模拟的 31.5% 一致。
练习 6(玩具 cartridge 的初始化)。cartridge_lab.py 里 β = 20、key 是 64 维单位向量。(a) 用真实 token 初始化时,一个问题和”它自己的那个 key”的注意力 logit 约是多少?和其他 key 的呢?(b) 用标准正态随机向量初始化时,logit 的典型大小是多少?为什么训练不动?
参考答案
(a) 问题与自己的 key 的余弦相似度约 0.96,logit ≈ 20 × 0.96 ≈ 19;与其他随机 key 的点积约 0 ± 1/√64 = ±0.125,logit 约 ±2.5——softmax 能把注意力集中到正确的 key 上,梯度有意义。(b) 随机向量的长度约 √64 = 8,与单位向量问题的点积约 ±1,logit 约 ±20,而且对所有问题都是随机的;softmax 几乎把全部注意力给了某一个碰巧点积最大的槽位,其余槽位的梯度接近 0,训练只能缓慢地移动少数槽位(代码 04 演示 2:0.122)。
练习 7(思考:编程智能体的记忆)。为一个跨多次会话帮你维护代码库的编程智能体设计记忆:代码库本身(20 万 token)、你的编码偏好、它做过的修改和踩过的坑。哪些放进上下文、哪些检索、哪些考虑 cartridge、哪些用手册?提示词怎样排布才对缓存友好?
参考答案(要点)
代码库:太大、但每次只用到一部分,适合按文件 / 符号检索(它变化频繁,cartridge 要不断重训,不划算;如果有一个稳定的大型依赖库,可以考虑 cartridge)。编码偏好:少量、稳定,直接放在系统指令后面(mem0 式抽取,变化时更新)。修改记录与踩过的坑:ACE 式的手册,带执行反馈(测试是否通过),只追加、定期去重。排布:系统指令 → 编码偏好 → 手册(只追加)→ 本次会话的历史(只追加)→ 检索到的代码片段与当前任务(每次都变,放最后)。偏好或手册需要大改时,在会话开始时一次性改,之后保持不变。
练习 8(思考:删除、失效与被遗忘权)。mem0 用 DELETE、mem0g 标为失效。如果用户要求”忘掉我的地址”(隐私法规里的被遗忘权),这两种设计各要怎样处理?cartridge 和 ACE 的手册呢?
参考答案(要点)
抽取式记忆里,删除是一条明确的操作,但标为失效的旧版本也要真正删除(以及回忆存储里的原始消息、向量库里的嵌入、各种备份);“失效”只适合正常的信息变化,不适合合规删除。ACE 论文也指出,可读的上下文让”选择性遗忘”变得容易:直接删掉相关要点。cartridge 和微调后的权重最难:信息被分散在参数里,无法定位删除,只能从去掉这部分数据的语料重新训练——这是”把记忆写进参数”的一个隐藏代价。
11. 延伸阅读 · Further reading
本讲的必读材料
- S. Eyuboglu, R. Ehrlich, S. Arora, N. Guha, D. Zinsley, E. Liu, W. Tennien, A. Rudra, J. Zou, A. Mirhoseini, C. Ré. Cartridges: Lightweight and general-purpose long context representations via self-study. arXiv:2506.06266,2025。
- P. Chhikara, D. Khant, S. Aryan, T. Singh, D. Yadav. Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory. arXiv:2504.19413,2025。
选读
- C. Packer, S. Wooders, K. Lin, V. Fang, S. G. Patil, I. Stoica, J. E. Gonzalez. MemGPT: Towards LLMs as Operating Systems. arXiv:2310.08560,2023。
- Q. Zhang, C. Hu, S. Upasani, B. Ma, F. Hong, V. Kamanuru, J. Rainton, C. Wu, M. Ji, H. Li, U. Thakker, J. Zou, K. Olukotun. Agentic Context Engineering: Evolving Contexts for Self-Improving Language Models. ICLR 2026(arXiv:2510.04618)。
相关工作
- N. F. Liu et al. Lost in the Middle: How Language Models Use Long Contexts. TACL 2024。
- A. Maharana et al. Evaluating Very Long-Term Conversational Memory of LLM Agents(LOCOMO). ACL 2024。
- X. L. Li, P. Liang. Prefix-Tuning: Optimizing Continuous Prompts for Generation. ACL 2021。
- M. Suzgun et al. Dynamic Cheatsheet: Test-Time Learning with Adaptive Memory. 2025。
- L. A. Agrawal et al. GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning. 2025。
- P. Rasmussen et al. Zep: A Temporal Knowledge Graph Architecture for Agent Memory. 2025。
- K. Lin et al. Sleep-time Compute: Beyond Inference Scaling at Test-time. 2025。
背景
- 第 1 讲:KV cache 与 roofline;第 3 讲:前缀缓存;第 4 讲:级联注意力;第 9 讲:上下文缓存的分层存储;第 10 讲:状态空间模型;第 13 讲:智能体程序;前置讲 P3:虚拟内存。
12. 下一讲预告 · Next lecture
到目前为止,推理服务的”客户”都是人或者人写的程序。第 15 讲:强化学习的推理系统换一个客户:训练。今天的推理模型(DeepSeek-R1 等)是用强化学习训练出来的,而 RL 的每一轮都要先让模型生成大量的回答(rollout),再打分、再训练——生成往往占了一轮的大部分时间,生成长度的长尾又让整个集群等待最慢的几个样本。我们读两篇系统论文:HybridFlow(verl)用”节点间单控制器 + 节点内多控制器”的混合编程模型表达 RLHF 的数据流,并在训练和生成之间零冗余地重新切分模型;RLHFuse 把不同阶段融合起来,让长尾的生成和已经完成的样本的打分、训练重叠。我们会用模拟器看长尾 rollout 的代价,以及各种”不让 GPU 等最慢样本”的办法。