数据截至 (上游 commit 35efe178d76b)
记忆子系统:短期缓冲、持久化、工作记忆与语义检索
30 秒导读: 大模型每次调用都是「失忆」的——你不把上文喂进去,它就什么都不记得。 VoltAgent 的记忆子系统就是那台「喂料机」:把这一轮产生的消息先攒在内存缓冲里, 生成结束后异步刷进数据库(LibSQL / Postgres / Supabase 任选),下一轮再从库里 捞出来拼进 prompt。在这条主干上,它再挂三样「记得更准 / 更省」的能力:语义检索 (按意思召回老消息)、工作记忆(把要点写进一块结构化便签)、摘要压缩 (历史太长就先总结再喂)。
本章只讲「记忆」这一层。agent 主循环怎么把这些记忆拼进一次生成,见 01-agent-runtime.md;工具系统见 02-tools-and-mcp.md。
1. 这是什么(零基础也能懂)
一句话定义: 记忆子系统 = agent 的「上下文持久层」,负责把对话消息存下来、取回来, 并在取回时做得更聪明(只取相关的、只取要点、太长就压缩)。
为什么需要它? 大模型本身无状态。你问「我叫什么名字」,它只能看这一次请求里带了什么。 要让 agent 跨越很多轮、甚至跨越很多天还记得你,就必须有人在每轮之间把消息落盘、 在每轮开头把消息读回。这个「有人」就是记忆子系统。
它把「记忆」分成两个层次,正好对应人脑的两种记忆:
| 层次 | 对应人脑 | 在 VoltAgent 里是什么 | 存在哪 |
|---|---|---|---|
| 短期 | 工作台上的便签 | ConversationBuffer(本轮消息缓冲) | 进程内存,单次生成期间 |
| 长期 | 笔记本 / 档案柜 | Memory + 存储适配器(全部历史) | LibSQL / Postgres / Supabase… |
用起来什么样: 配一个 Memory,把它交给 agent,记忆就自动转起来了——你只要在每次调用
时带上 userId 和 conversationId,框架就知道「这是谁、哪段对话」,自动读旧的、存新的。
// 示意,非源码:最小配置——一句话开启「能跨轮记忆」的 agent
const memory = new Memory({
storage: new LibSQLMemoryAdapter({ url: "file:./voltagent.db" }), // 存哪里
});
const agent = new Agent({ name: "assistant", model, memory });
// 同一个 conversationId,第二轮就能记得第一轮说过的话
await agent.generateText("我叫小明", { userId: "u1", conversationId: "c1" });
await agent.generateText("我叫什么?", { userId: "u1", conversationId: "c1" }); // → 小明
一句话直觉: 把 ConversationBuffer 当内存(RAM)、把存储适配器当磁盘,
把 MemoryPersistQueue 当那个「攒够了 / 该收尾了就把 RAM 写回磁盘」的刷盘线程——
整套记忆就是一个为对话定制的「读缓存 + 写回」体系。
2. 顶层全景(它大概怎么转)
先看一次生成里,记忆是怎么在两端各出场一次的。
怎么读这张图: 从左到右是一次 generateText 的时间线;记忆在开头读、
结尾写,中间的生成过程只跟内存里的 ConversationBuffer 打交道。
┌────────────────────────── 一次 agent 生成 ──────────────────────────┐
│ │
│ ① 读历史 ② 本轮攒消息 ③ 异步落库 │
│ │
│ 存储适配器 ──读──► ConversationBuffer ──drain──► MemoryPersist │
│ (DB / 库) (内 存·短期缓冲) Queue │
│ ▲ │ ▲ │ │
│ │ │ │ 每步 addModelMessages │ debounce │
│ │ 模型生成/工具调用 │ 200ms │
│ │ ▼ │
│ └──────────────── 写 ◄─────────── saveMessage ── 存储适配器 │
│ │
└──────────────────────────────────────────────────────────────────┘
叠加能力(读的时候可选):
· 语义检索 —— 用 embedding+vector 按「意思」召回老消息
· 工作记忆 —— 读/写一块结构化便签,注入 system 指令
· 摘要压缩 —— 历史超阈值就先总结,再喂给模型
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Memory | 记忆总门面,统管消息 / 会话 / 向量 / 工作记忆 | packages/core/src/memory/index.ts:76 |
StorageAdapter | 可插拔「磁盘」:落库消息、会话、工作记忆、工作流状态 | packages/core/src/memory/types.ts:397 |
VectorAdapter | 可插拔「向量库」:存向量、按余弦相似度检索 | packages/core/src/memory/adapters/vector/types.ts:4 |
EmbeddingAdapter | 可插拔「向量化器」:文本 → 向量 | packages/core/src/memory/adapters/embedding/types.ts:11 |
MemoryManager | agent 侧包装:接 OpenTelemetry、后台队列、生成标题 | packages/core/src/memory/manager/memory-manager.ts:28 |
ConversationBuffer | 本轮短期缓冲:合并 tool 调用/结果、标记待落库 | packages/core/src/agent/conversation-buffer.ts:62 |
MemoryPersistQueue | 异步刷盘:防抖 + 串行,把缓冲写进存储 | packages/core/src/agent/memory-persist-queue.ts:31 |
applySummarization | 历史超阈值时生成/注入摘要 | packages/core/src/agent/apply-summarization.ts:42 |
主线走一遍(高层):
- 生成开始,
MemoryManager.prepareConversationContext从存储读回最近若干条消息 (memory-manager.ts:564,默认contextLimit=10)。 - 生成过程中,模型每吐一 段 / 每次工具调用,都
ConversationBuffer.addModelMessages进缓冲,同时标记成「待落库」。 - 流式期间边生成边
scheduleSave(防抖攒批),生成收尾flush(强制刷盘),MemoryPersistQueue把缓冲里「待落库」的消息交给MemoryManager.saveMessage写库。
3. 三类可插拔适配器:记忆的「三块插槽」
这节讲什么: VoltAgent 把「记忆」拆成三个正交的接口,像三个插槽——你插什么进去, 就得到什么能力。这是整个子系统最重要的设计。
3.1 三块插槽各管什么
Memory 构造时只认三个可选件(memory/types.ts:257 MemoryConfig):
| 插槽 | 接口 | 负责 | 不配会怎样 |
|---|---|---|---|
| storage(必填) | StorageAdapter | 消息、会话、工作记忆、工作流状态的落库 | 无法构造 |
| embedding(可选) | EmbeddingAdapter | 把文本变成向量 | 语义检索 / RAG 关闭 |
| vector(可选) | VectorAdapter | 存向量 + 相似度检索 | 语义检索 / RAG 关闭 |