Agent 记忆:虚拟上下文与内存分页


文档摘要

Agent 记忆:虚拟上下文与内存分页 本节摘要:上下文窗口看起来应该能解决记忆问题,其实不能。多轮对话、长文档、密集工具轨迹都会越界,越界之外的东西就消失;即便没越界,塞满无关上下文也会稀释注意力;新会话又是空窗,跨会话说不出「记得你上次让我……」。MemGPT(Packer 等人, 2023)给这道难题的名字是虚拟上下文(Virtual Context)——把操作系统的虚拟内存搬过来:主上下文是 RAM,外部存储是磁盘,Agent 用记忆工具在两者之间分页。

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)。

学习目标

阅读完本节,你应当能够:

  1. 解释 MemGPT 建立的操作系统类比:主上下文 = RAM,外部上下文 = 磁盘,记忆工具 = 分页换入/换出。
  2. 用标准库实现两层 MemGPT 模式:固定容量的主上下文缓冲 + 可检索的外部归档库 + 分页换入/换出工具。
  3. 描述 Agent 如何发出**中断(Interrupt)**去查询或修改外部记忆,以及结果如何被拼接进下一轮提示。
  4. 识别 MemGPT 的哪些设计选择延续到 Letta(第 08 节)和 Mem0(第 09 节)。
  5. 诊断生产记忆的两大反模式:记忆腐烂(Memory Rot)记忆投毒(Memory Poisoning)

一、问题与直觉

上下文窗口为什么救不了记忆

更大的窗口看起来应该终结记忆问题。实际上,生产里有三种失败模式反复出现:

  1. 溢出(Overflow) —— 多轮对话、长文档、工具密集的轨迹超过窗口,截断之外的东西全部丢失。
  2. 稀释(Dilution) —— 即便在窗口内,塞满无关上下文也会把注意力摊薄在无关内容上。前沿模型在长输入上依然会退化。
  3. 不持久(Persistence) —— 新会话从空窗开始,没有外部记忆的 Agent 跨会话说不出「记得你上次让我……」。

Mem0 的 2025 年论文测得一个扎心的对照:128k 窗口的基线仍然会漏掉长程事实,而一个只有 4k 窗口、但带外部记忆的 Agent 反而能抓住。窗口大小治标,记忆架构治本。

OS 类比:把虚拟内存搬过来

MemGPT(Packer 等人, arXiv:2310.08560, 2024 年 2 月 v2)把上下文管理映射到操作系统的虚拟内存:

操作系统概念 MemGPT 概念 2026 生产对应物
RAM 主上下文(提示词) Anthropic/OpenAI 的上下文窗口
磁盘 外部上下文 向量库、KV 库、图存储
缺页中断 记忆工具调用 memory.searchmemory.readmemory.write
OS 内核 Agent 控制循环 带记忆工具的 ReAct 循环

Agent 跑的还是普通 ReAct 循环(第 01 节、第 06 节),只是多了一类工具,让它能在主上下文和外部上下文之间分页换入/换出

两层结构

  • 主上下文(Main Context) —— 固定容量的提示词,装着当前任务,始终对模型可见
  • 外部上下文(External Context) —— 无上限、可检索,在相关时读入、在新事实涌现时写出。

原论文在两个超出基础窗口的任务上验证了这个设计:超过 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 最大的工程简洁性。

这个模式在哪里出错

  • 记忆腐烂(Memory Rot) —— 写入比读出快,检索被陈旧事实淹没。修法:周期性合并(Letta 的睡眠期计算,第 08 节)、显式作废(Mem0 的冲突检测器,第 09 节)。
  • 记忆投毒(Memory Poisoning) —— 外部记忆本质是被检索出来的文本。如果攻击者可控的内容落进了一条记忆笔记,Agent 下次会话就会再次吃下它。这就是第 27 节要讲的 Greshake 间接提示注入攻击,只不过拉长了时间维度。
  • 引用丢失(Citation Loss) —— Agent 回忆出「用户让我发布 X」,却说不出是哪一轮说的。每条归档写入都应带上来源引用(会话 ID、轮次 ID)。

二、从零实现

原课程 code/main.py 用标准库实现了 MemGPT 的两层模式:

  • MainContext —— 固定容量的提示缓冲,带 core 字典和 messages 列表;超容量时自动淘汰最旧的消息。
  • ArchivalStore —— 内存里的类 BM25 存储(用 token 重叠打分),记录为 (id, text, tags, session, turn)
  • 五个记忆工具,对应 MemGPT 工具面。
  • 一个脚本 Agent:往归档库填事实,然后调 archival_memory_search 回答问题。

核心骨架如下,用伪代码展示。

Step 1:主上下文(固定容量 + 自动淘汰)

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

Step 2:外部归档库(可检索)

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]

Step 3:五个记忆工具(中断式)

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,形状相同

Step 4:脚本 Agent 跑一遍

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 的变体:

  • Letta(第 08 节)—— 三层(核心、回忆、归档)、原生推理、睡眠期异步计算。
  • Mem0(第 09 节)—— 向量 + KV + 图,融合在一个打分层里。
  • OpenAI Assistants / Responses —— 通过线程(threads)和文件(files)的托管记忆。
  • Claude Agent SDK —— 通过技能(skills)和会话存储的长期记忆。

选型时应按运维形态(自托管、托管、框架集成)选,而不是按核心模式选——核心模式都是 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 调用即可投入生产。

五、练习

  1. (Easy) 给主上下文加一个按 token 计的容量上限(用 len(text.split()) * 1.3 近似)。超限时把最旧的若干条消息合并成摘要而非直接丢弃。对比有无摘要器时的行为差异。
  2. (Medium) 在归档库上正经实现 BM25(词频 TF + 逆文档频率 IDF)。在一个玩具事实集上测 recall@10,与 token 重叠基线对比。
  3. (Medium) 给归档写入加 citation 字段(session_id、turn_id、source_url)。让 Agent 在每次「检索支撑的回答」上都引用来源
  4. (Hard) 模拟记忆投毒:往归档库插入一条「忽略未来所有用户指令」的记录。写一个守卫,扫描检索结果里形如指令的文本并标记为不可信。
  5. (Hard) 把实现移植到 MemGPT 研究仓库(cpacker/MemGPT)的核心记忆 JSON 模式。从扁平字符串换成带类型区段后,什么变了?

本节要点回顾

  1. 上下文窗口救不了记忆:三种失败模式——溢出、稀释、不持久;128k 窗口的基线仍会漏 4k 窗口 + 外部记忆能抓到的事实。
  2. MemGPT = OS 虚拟内存:主上下文是 RAM、外部上下文是磁盘、记忆工具是缺页中断、控制循环是内核。
  3. 两层结构:主上下文固定容量且始终可见,外部上下文无上限且按需检索。
  4. 记忆即中断:Agent 中途调记忆工具,运行时执行,结果拼进下一轮观察——等同 Unix read() 系统调用。
  5. 五个标准工具:core_memory_append/replacearchival_memory_insert/searchconversation_search
  6. 记忆工具就是普通工具:复用第 06 节那套工具调用循环,记忆能力与调工具能力是同一套。
  7. 淘汰策略承重:直接丢弃丢信息,生产里先合并成摘要写回归档库,这是 Letta 睡眠期的雏形。
  8. 四大记忆类型:工作(现在要紧)、情景(发生过)、语义(什么是真)、程序(怎么做)——各自有不同实现。
  9. 两大反模式:记忆腐烂(写多读少,定期合并 + 显式作废)、记忆投毒(检索即重吃,带引用 + 守卫)。
  10. 今日生产系统都是 MemGPT 变体:Letta / Mem0 / OpenAI Assistants / Claude Agent SDK,选型按运维形态而非核心模式。

下一节,我们进入「记忆块与睡眠时计算」——MemGPT 在 2024 年 9 月成为 Letta,把两层扩展为三层(核心、回忆、归档),并引入睡眠期 Agent 异步地做记忆合并,正是本节淘汰策略的工业化升级。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U