Agent 记忆:虚拟上下文与内存分页 本节摘要:上下文窗口看起来应该能解决记忆问题,其实不能。多轮对话、长文档、密集工具轨迹都会越界,越界之外的东西就消失;即便没越界,塞满无关上下文也会稀释注意力;新会话又是空窗,跨会话说不出「记得你上次让我……」。MemGPT(Packer 等人, 2023)给这道难题的名字是虚拟上下文(Virtual Context)——把操作系统的虚拟内存搬过来:主上下文是 RAM,外部存储是磁盘,Agent 用记忆工具在两者之间分页。
本节摘要:上下文窗口看起来应该能解决记忆问题,其实不能。多轮对话、长文档、密集工具轨迹都会越界,越界之外的东西就消失;即便没越界,塞满无关上下文也会稀释注意力;新会话又是空窗,跨会话说不出「记得你上次让我……」。MemGPT(Packer 等人, 2023)给这道难题的名字是虚拟上下文(Virtual Context)——把操作系统的虚拟内存搬过来:主上下文是 RAM,外部存储是磁盘,Agent 用记忆工具在两者之间分页。本节从生产里的三种失败模式(溢出、稀释、不持久)讲起,吃透 MemGPT 的 OS 类比、两层结构、中断式记忆工具,并用标准库从零实现一个迷你 MemGPT:固定容量的主上下文缓冲、可检索的外部归档库、五个记忆工具、一个会写会读的脚本 Agent。读完本节,你应能识别今日所有生产记忆系统(Letta、Mem0、Zep、LangMem)背后的同一个 MemGPT 骨架。
对应原课程:Phase 14 · Lesson 07 ·
memory-virtual-context-memgpt(原英文phases/14-agent-engineering/07-memory-virtual-context-memgpt/docs/en.md)。
阅读完本节,你应当能够:
更大的窗口看起来应该终结记忆问题。实际上,生产里有三种失败模式反复出现:
Mem0 的 2025 年论文测得一个扎心的对照:128k 窗口的基线仍然会漏掉长程事实,而一个只有 4k 窗口、但带外部记忆的 Agent 反而能抓住。窗口大小治标,记忆架构治本。
MemGPT(Packer 等人, arXiv:2310.08560, 2024 年 2 月 v2)把上下文管理映射到操作系统的虚拟内存:
| 操作系统概念 | MemGPT 概念 | 2026 生产对应物 |
|---|---|---|
| RAM | 主上下文(提示词) | Anthropic/OpenAI 的上下文窗口 |
| 磁盘 | 外部上下文 | 向量库、KV 库、图存储 |
| 缺页中断 | 记忆工具调用 | memory.search、memory.read、memory.write |
| OS 内核 | Agent 控制循环 | 带记忆工具的 ReAct 循环 |
Agent 跑的还是普通 ReAct 循环(第 01 节、第 06 节),只是多了一类工具,让它能在主上下文和外部上下文之间分页换入/换出。
原论文在两个超出基础窗口的任务上验证了这个设计:超过 10 万 token 的文档分析、跨数天的多会话持久聊天。
MemGPT 的精髓是记忆即中断(Memory-as-Interrupt):对话进行到一半,Agent 可以调用一个记忆工具,运行时执行它,把结果作为新的观察拼接进下一轮助手回合。这在概念上等同于 Unix 的 read() 系统调用——阻塞进程、返回字节、进程继续。
标准的记忆工具面:
core_memory_append(section, text) —— 往提示词的某个持久区段写。core_memory_replace(section, old, new) —— 编辑某个持久区段。archival_memory_insert(text) —— 往可检索的外部库写。archival_memory_search(query, top_k) —— 从外部库检索。conversation_search(query) —— 扫描过往对话轮。💡 设计要点:这些工具不是新机制,它们就是第 06 节那套工具调用的特例。把记忆操作封装成工具,意味着 Agent 的「记忆能力」与它的「调用工具能力」是同一套循环——这是 MemGPT 最大的工程简洁性。
原课程 code/main.py 用标准库实现了 MemGPT 的两层模式:
MainContext —— 固定容量的提示缓冲,带 core 字典和 messages 列表;超容量时自动淘汰最旧的消息。ArchivalStore —— 内存里的类 BM25 存储(用 token 重叠打分),记录为 (id, text, tags, session, turn)。archival_memory_search 回答问题。核心骨架如下,用伪代码展示。
class MainContext: def __init__(self, cap=8): self.cap = cap # 最多保留多少条消息 self.core = {} # 持久命名区段,如 {"persona":"...", "user":"..."} self.messages = [] # 对话消息列表 def append(self, role, content): self.messages.append({"role": role, "content": content}) while len(self.messages) > self.cap: evicted = self.messages.pop(0) # 淘汰最旧 # 生产里这里应是「合并成摘要」而非直接丢弃 def render(self): # 把 core 区段拼成系统提示,再接消息 core = "\n".join(f"[{k}]\n{v}" for k, v in self.core.items()) return [{"role": "system", "content": core}] + self.messages
class ArchivalStore: def __init__(self): self.records = {} # id -> {text, tags, session, turn} def insert(self, text, tags=None, session=None, turn=None): rid = f"rec_{len(self.records)}" self.records[rid] = {"text": text, "tags": tags or [], "session": session, "turn": turn} return rid def search(self, query, top_k=3): # 类 BM25:token 重叠打分(生产里换成真正的 BM25) q_tokens = set(query.lower().split()) scored = [] for rid, rec in self.records.items(): t_tokens = set(rec["text"].lower().split()) score = len(q_tokens & t_tokens) scored.append((score, rid, rec)) scored.sort(reverse=True) return scored[:top_k]
def core_memory_append(ctx, section, text): ctx.core.setdefault(section, "") ctx.core[section] += text + "\n" return f"已写入 core[{section}]" def archival_memory_insert(store, text, session, turn): rid = store.insert(text, session=session, turn=turn) return f"已归档 {rid}" def archival_memory_search(store, query, top_k=3): hits = store.search(query, top_k) return "\n".join(f"{rid}: {rec['text']}" for _, rid, rec in hits) # 另有 core_memory_replace / conversation_search,形状相同
1. archival_memory_insert("用户偏好暗色主题") # 写事实 2. archival_memory_insert("用户在做 Agent 记忆项目") 3. archival_memory_insert("项目截止 2026-08-15") 4. 连续对话,直到主上下文超 cap,触发淘汰 5. 问:"用户的项目截止什么时候?" 6. Agent 调 archival_memory_search("项目 截止") 7. 取回第 3 条,拼进下一轮观察,答出 2026-08-15
运行 python3 code/main.py 会看到 Agent 写入三条事实、把主上下文填到上限(强制淘汰)、然后通过检索归档库回答后续问题——无需任何真实 LLM 就复现了 MemGPT 的工作流。
💡 设计要点:淘汰策略是承重的。直接丢弃最旧消息会丢信息;生产里应在淘汰前合并成摘要写回归档库,这样信息以压缩形式留在外部上下文里,需要时还能检索回来。这就是 Letta 睡眠期计算(第 08 节)的雏形。
今日的生产记忆系统几乎都是 MemGPT 的变体:
选型时应按运维形态(自托管、托管、框架集成)选,而不是按核心模式选——核心模式都是 MemGPT。
分页解决了容量问题,但没解决「该存什么」的问题。生产系统里反复出现四种记忆类型,各自回答不同的问题:
| 记忆类型 | 回答的问题 | 典型实现 | 怎么做 |
|---|---|---|---|
| 工作记忆(Working) | 现在什么要紧? | MemGPT / Letta | 通过记忆工具在固定提示预算里换入换出(本节、第 08 节) |
| 情景记忆(Episodic) | 发生过什么? | Zep | 时序知识图——事实带有效期,「某时为真」可查 |
| 语义记忆(Semantic) | 什么是真的? | Mem0 | 抽取管线在向量/KV/图库里去重、更新事实(第 09 节) |
| 程序记忆(Procedural) | 这事怎么做? | LangMem | 后台抽取事实与行为规则,作为 Agent 跨轮参考 |
| 系统 | 主攻类型 | 思路 |
|---|---|---|
| MemGPT / Letta | 工作 | 固定预算换页 |
| Zep | 情景 | 时序知识图,事实带有效期 |
| Mem0 | 语义 | 抽取管线,跨存储去重更新 |
| LangMem | 语义 + 程序 | 后台抽取事实与行为规则进库 |
| agentmemory | 情景 + 语义 | 会话即跑即捕获,合并成可检索的带类型记录 |
⚠️ 选型警示:别被「我们有自己的记忆系统」这话唬住。先问它分了几层、淘汰策略是什么、怎么去重、引用带没带——这四个答案一摆,99% 的「创新」都退回 MemGPT 加一个打分层。
本节产出一份可复用技能(原课程 outputs/skill-virtual-memory.md):
skill-virtual-memory.md:给定任意目标运行时,产出正确的两层记忆脚手架(主上下文 + 归档库 + 工具面),内置淘汰策略与引用字段。包含一份设计检查清单(主上下文容量怎么定?淘汰前要不要合并?检索打分用 BM25 还是向量?写入要不要带来源?),以及一份记忆腐烂的监控清单(读写比、陈旧记录占比、检索命中率)。Python 代码(code/main.py)是独立可运行的两层记忆骨架,工具面、淘汰逻辑、类 BM25 检索都是厂商无关的;把玩具打分换成真实向量库、把脚本 Agent 换成真实 LLM 调用即可投入生产。
len(text.split()) * 1.3 近似)。超限时把最旧的若干条消息合并成摘要而非直接丢弃。对比有无摘要器时的行为差异。citation 字段(session_id、turn_id、source_url)。让 Agent 在每次「检索支撑的回答」上都引用来源。cpacker/MemGPT)的核心记忆 JSON 模式。从扁平字符串换成带类型区段后,什么变了?read() 系统调用。core_memory_append/replace、archival_memory_insert/search、conversation_search。下一节,我们进入「记忆块与睡眠时计算」——MemGPT 在 2024 年 9 月成为 Letta,把两层扩展为三层(核心、回忆、归档),并引入睡眠期 Agent 异步地做记忆合并,正是本节淘汰策略的工业化升级。