记忆
要让 agent 长期可用,就必须有记忆(Memory)。没有记忆,每一段对话都从零开始。agent 会忘记用户是谁、它学到了什么、它正在做什么。记忆正是把无状态的 LLM 调用变成持久、有上下文感知能力的 agent 的关键。
Session API 为基于 Cloudflare Agents SDK 构建的 agent 提供记忆层。它管理两类记忆:对话历史(构成一次会话的消息和工具调用)和上下文记忆(注入到系统提示中的持久化块,agent 可以读、写、搜索和加载)。
对话历史
最基础的一种记忆就是对话本身:用户与 agent 之间的消息、agent 发起的工具调用以及它收到的结果。Session 把这些信息都存放在以树状结构组织的消息历史中,后端默认是 SQLite 形式的 Session Provider。
JavaScript
import { Session } from "agents/experimental/memory/session";
// Append messages as the conversation progresses
await session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: "What's the status of the deployment?" }],
});
// Read the full conversation history
const history = session.getHistory();
Explain Code
TypeScript
import { Session } from "agents/experimental/memory/session";
// Append messages as the conversation progresses
await session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: "What's the status of the deployment?" }]
});
// Read the full conversation history
const history = session.getHistory();
Explain Code
对话历史在 Durable Object 的 hibernation 与 eviction 之间持久存在。当 agent 唤醒时,完整历史依然可在 SQLite 中找到,无需重放或重建。
消息通过 parent_id 以树状结构存储,从而支持分支对话。当你以一个已经有子节点的 parentId 来 appendMessage 时,就会形成一个新分支,这对回复重新生成等功能很有用。getHistory(leafId) 可以沿着树中任意一条路径行走。
Session 还提供对对话历史的全文搜索:
JavaScript
const results = session.search("deployment Friday", { limit: 10 });
TypeScript
const results = session.search("deployment Friday", { limit: 10 });
随着对话变长,compaction 会汇总较早的消息,在不丢失底层数据的前提下,把上下文窗口控制在可管理范围内。
上下文记忆
上下文记忆(Context memory)是注入到系统提示中的持久化信息,与对话历史相互独立。它让 agent 在每一轮对话中都能访问到身份信息、指令、已学到的事实、知识库和参考材料。
Session API 支持四种上下文记忆,各自适合不同类型的信息。其类型由支持该上下文块的 provider 决定。Session 会自动检测 provider 的能力。
只读上下文
这就是传统意义上的系统提示:agent 的身份、性格和指令。你可以直接写在代码里,从 R2 中的 SOUL.md 文件加载,或从某个 API 拉取。内容会注入系统提示,agent 无法修改它。
一个编程助手可能拥有一段定义其性格与约束的 soul:
JavaScript
import { Session } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("soul", {
provider: {
get: async () =>
"You are a senior TypeScript engineer. You write concise, " +
"well-tested code. You prefer composition over inheritance. " +
"When you are unsure, you say so rather than guessing.",
},
});
Explain Code
TypeScript
import { Session } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () =>
"You are a senior TypeScript engineer. You write concise, " +
"well-tested code. You prefer composition over inheritance. " +
"When you are unsure, you say so rather than guessing."
}
});
Explain Code
也可以从 R2 加载,这样无需重新部署就能更新 agent 的性格:
JavaScript
const session = Session.create(this).withContext("soul", {
provider: {
get: async () => {
const obj = await env.CONFIG_BUCKET.get("soul.md");
return obj ? obj.text() : "You are a helpful assistant.";
},
},
});
TypeScript
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () => {
const obj = await env.CONFIG_BUCKET.get("soul.md");
return obj ? obj.text() : "You are a helpful assistant.";
}
}
});
只读块通过提供仅含 get() 方法的对象来定义。系统不会生成任何工具,内容直接出现在系统提示里,agent 无法修改它。
可写的短期上下文
可以把它当作 agent 给自己使用的草稿本,用来记录需要记住的内容。就像 Claude Code 维护一份待办事项,或者客服 agent 跟踪在对话中了解到的用户信息一样。
JavaScript
const session = Session.create(this)
.withContext("memory", {
description: "Important facts learned during conversation",
maxTokens: 1100,
})
.withContext("todos", {
description: "Task list, track what needs to be done and what is complete",
maxTokens: 2000,
});
TypeScript
const session = Session.create(this)
.withContext("memory", {
description: "Important facts learned during conversation",
maxTokens: 1100
})
.withContext("todos", {
description: "Task list, track what needs to be done and what is complete",
maxTokens: 2000
});
当你在 builder 中省略 provider 选项时,Session 会自动接入由 SQLite 支持的可写 provider。agent 会获得一个 set_context 工具,可用于替换或追加这些块的内容。token 上限会被强制执行,所以 agent 写入的内容不能超过 maxTokens 的预算。
系统提示在渲染可写块时会带上 token 用量指示,让 agent 知道还有多少空间可用:
══════════════════════════════════════════════
MEMORY (Important facts learned during conversation) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
Deployment target is Cloudflare Workers.
══════════════════════════════════════════════
TODOS (Task list) [12% — 240/2000 tokens] [writable]
══════════════════════════════════════════════
- [x] Set up project scaffolding
- [ ] Add authentication middleware
- [ ] Write integration tests
Explain Code
内容会在多条消息之间持久化,并在 hibernation 后依然存在。它始终出现在系统提示里,因此 agent 在每一轮都能看到,无需额外拉取。
可搜索上下文
当你拥有大量信息(知识库、文档、日志、累积笔记)时,你不会想把它们全部塞进系统提示里。可搜索上下文(Searchable context)只在系统提示里保留一段摘要(例如“42 entries indexed“),并允许 agent 在需要时检索具体条目。
你需要提供一个带 search() 方法的 provider。具体怎么搜索完全由你决定:全文搜索、通过 Vectorize 做向量搜索、调用外部 API,等等。Session 不关心具体实现,只要 provider 有 search() 方法即可。
内置的 AgentSearchProvider 默认使用 Durable Object SQLite + FTS5:
JavaScript
import { AgentSearchProvider } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("knowledge", {
description:
"Searchable knowledge base, search for relevant information before answering",
provider: new AgentSearchProvider(this),
});
TypeScript
import { AgentSearchProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("knowledge", {
description: "Searchable knowledge base, search for relevant information before answering",
provider: new AgentSearchProvider(this)
});
但你也可以基于任何搜索机制实现自己的 provider:
JavaScript
const session = Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: {
get: async () => "Product documentation and FAQs",
search: async (query) => {
// Use Vectorize, an external API, whatever you need
const results = await env.VECTORIZE_INDEX.query(
await generateEmbedding(query),
{ topK: 5 },
);
return results.matches.map((m) => m.metadata.text).join("\n\n");
},
set: async (key, content) => {
// Index new content
},
},
});
Explain Code
TypeScript
const session = Session.create(this)
.withContext("knowledge", {
description: "Searchable knowledge base",
provider: {
get: async () => "Product documentation and FAQs",
search: async (query) => {
// Use Vectorize, an external API, whatever you need
const results = await env.VECTORIZE_INDEX.query(
await generateEmbedding(query), { topK: 5 }
);
return results.matches.map(m => m.metadata.text).join("\n\n");
},
set: async (key, content) => {
// Index new content
}
}
});
Explain Code
agent 会获得一个 search_context 工具用于查询,以及一个 set_context 工具用于索引新条目。它决定要搜什么,你决定怎么搜。
当 agent 需要从一个大集合中检索具体片段、而不是加载整篇文档时,这种方式最合适。
可加载上下文(Skills)
Skills 是较大的上下文块(完整文档、参考指南、运行手册、模板),agent 可以按需发现并加载。可以把它们想象成书架上的参考资料:agent 看到一份带标题和描述的清单,挑出与当前任务相关的那本,加载进来,使用,用完后再卸下。
与 searchable 上下文从大集合中取小片段不同,skills 设计上是被整块加载的。当 agent 加载一个 skill 时,它会把整篇文档放进自己的上下文窗口。
Skills 的支持者是 SkillProvider 接口,该接口有三个方法:
get()返回一个出现在系统提示中的元数据清单(标题与描述)load(key)拉取某个 skill 的完整内容set(key, content, description?)写入或更新一个 skill 条目(可选)
系统提示以清单形式展示可用的 skills。[loadable] 标记告诉 LLM 这些条目不是内联的,需要使用工具来访问完整内容:
══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
- deploy-checklist: Production deployment checklist
agent 看到这些标题,判断哪一项与当前任务相关,然后用 load_context 把完整内容拉进自己的工作上下文。完成后用 unload_context 释放空间。当 skill provider 实现了 set() 时,agent 还可以反向写入,更新已有 skill 或创建新条目。
Agent sees: "- deploy-checklist: Production deployment checklist"
User asks: "Walk me through a production deployment"
Agent calls: load_context({ block: "skills", key: "deploy-checklist" })
→ Full checklist content is loaded into the agent's working context
由 R2 支持的 skills
内置的 R2SkillProvider 把 skills 存储在 Cloudflare R2 bucket 中。每个 skill 都是一个 R2 对象,可选地带有用作描述的自定义 metadata。
JavaScript
import { Session, R2SkillProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () =>
[
"You are a helpful assistant with access to skills.",
"When a user asks you to do something, check the SKILLS section",
"for a relevant skill and use load_context to load it.",
].join("\n"),
},
})
.withContext("memory", {
description: "Learned facts",
maxTokens: 1100,
})
.withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
})
.withCachedPrompt();
Explain Code
TypeScript
import { Session, R2SkillProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () => [
"You are a helpful assistant with access to skills.",
"When a user asks you to do something, check the SKILLS section",
"for a relevant skill and use load_context to load it.",
].join("\n")
}
})
.withContext("memory", {
description: "Learned facts",
maxTokens: 1100
})
.withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })
})
.withCachedPrompt();
Explain Code
prefix 选项把 provider 限定到 bucket 的某个子目录。元数据清单中的 skill key 不会带前缀,因此 skills/api-ref 在系统提示中会显示为 api-ref。
在 Wrangler 配置里加上 R2 bucket 绑定:
JSONC
{
"r2_buckets": [
{
"binding": "SKILLS_BUCKET",
"bucket_name": "my-agent-skills"
}
]
}
TOML
[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-agent-skills"
Skills 就是普通的 R2 对象。可以通过任意 R2 接口上传(Wrangler CLI、Dashboard 或 Workers API):
Terminal window
# Upload a skill from a file
wrangler r2 object put my-agent-skills/skills/style-guide --file ./docs/style-guide.md --content-type text/markdown
要添加描述(显示在元数据清单中),在 R2 对象上设置自定义 metadata:
JavaScript
await env.SKILLS_BUCKET.put("skills/api-ref", content, {
customMetadata: { description: "API Reference documentation" },
});
TypeScript
await env.SKILLS_BUCKET.put("skills/api-ref", content, {
customMetadata: { description: "API Reference documentation" }
});
自定义 skill provider
通过实现 SkillProvider 接口,你可以使用任意存储来支持 skills:
JavaScript
class DatabaseSkillProvider {
db;
constructor(db) {
this.db = db;
}
async get() {
const rows = await this.db
.prepare("SELECT key, description FROM skills ORDER BY key")
.all();
if (rows.results.length === 0) return null;
return rows.results
.map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
.join("\n");
}
async load(key) {
const row = await this.db
.prepare("SELECT content FROM skills WHERE key = ?")
.bind(key)
.first();
return row ? row.content : null;
}
async set(key, content, description) {
await this.db
.prepare(
"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",
)
.bind(key, content, description ?? null, content, description ?? null)
.run();
}
}
Explain Code
TypeScript
import type { SkillProvider } from "agents/experimental/memory/session";
class DatabaseSkillProvider implements SkillProvider {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
async get(): Promise<string | null> {
const rows = await this.db
.prepare("SELECT key, description FROM skills ORDER BY key")
.all();
if (rows.results.length === 0) return null;
return rows.results
.map(r => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
.join("\n");
}
async load(key: string): Promise<string | null> {
const row = await this.db
.prepare("SELECT content FROM skills WHERE key = ?")
.bind(key)
.first();
return row ? (row.content as string) : null;
}
async set(key: string, content: string, description?: string): Promise<void> {
await this.db
.prepare(
"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?"
)
.bind(key, content, description ?? null, content, description ?? null)
.run();
}
}
Explain Code
Session 通过 duck-typing 检测 load() 方法,并自动生成对应的工具。
Skills 与其他记忆类型对比
| 维度 | Skills | 可写上下文 | 可搜索上下文 |
|---|---|---|---|
| 在系统提示中 | 仅元数据清单 | 完整内容 | 摘要数量 |
| 访问方式 | 按 key 加载整篇文档 | 始终可见 | 按查询搜索 |
| 最适合 | 大型文档、参考资料 | 短笔记、偏好 | 由许多小条目组成的大集合 |
| 上下文成本 | 低(加载前) | 与内容长度成正比 | 低(搜索前) |
| agent 是否可写? | 可选(若实现了 set) | 是(通过 set_context) | 是(通过 set_context) |
最关键的区别是:skills 是惰性的。在 agent 决定要使用之前,它在系统提示中的开销几乎为零。这非常适合大型参考资料 —— 任何一次对话通常只用得到其中一小部分。
agent 如何与记忆交互
Session 会基于上下文块的 provider 类型自动生成工具。把这些工具与你自己的应用工具一起传给 LLM:
JavaScript
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };
const result = streamText({
model: myModel,
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(session.getHistory()),
tools: allTools,
});
TypeScript
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };
const result = streamText({
model: myModel,
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(session.getHistory()),
tools: allTools
});
自动生成的工具
Session 会根据存在哪些 provider 类型动态生成工具:
| 工具 | 何时生成 | 作用 |
|---|---|---|
| set_context | 存在任何 writable、skill 或 search 块时 | 向命名块写入内容。对 writable 块,替换或追加;对 skill/search 块,写入按 key 标识的条目。 |
| load_context | 存在任何 skill 块时 | 按 key 把某个文档的完整内容加载到 agent 的上下文。 |
| unload_context | 存在任何 skill 块时 | 通过移除一个之前加载的文档来释放上下文空间。该文档仍可被重新加载。 |
| search_context | 存在任何 search 块时 | 在某个可搜索块内做全文搜索。返回按相关性排序的前若干条结果。 |
| session_search | 使用 SessionManager 时 | 跨所有 session 搜索(跨对话搜索)。 |
工具会附带描述和参数 schema,告诉 LLM 哪些块可用以及它们的用途。agent 自行决定何时及如何使用这些工具。
完整的工具签名和所有 Session 方法,请参阅 Session API 参考。
系统提示
各上下文块会被组装成一个结构化的系统提示,带有清晰的标题和元信息。每个块都有一段带标签的小节,标签指明它的类型与容量:
══════════════════════════════════════════════
SOUL (Identity) [readonly]
══════════════════════════════════════════════
You are a helpful coding assistant who speaks concisely.
══════════════════════════════════════════════
MEMORY (Important facts) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
══════════════════════════════════════════════
KNOWLEDGE (Searchable knowledge base) [searchable]
══════════════════════════════════════════════
12 entries indexed.
══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
Explain Code
标签([readonly]、[writable]、[searchable]、[loadable])告诉 LLM 它对每个块可以进行哪种交互。token 预算让 agent 看到可写块还有多少空间,从而管理自己的记忆。
注意事项
Prompt 缓存
LLM 服务商(Anthropic、OpenAI 等)会缓存系统提示的前缀。当连续请求共享同一段系统提示时,服务商可以跳过对该前缀的重复处理,从而降低延迟和成本。打破缓存(改动系统提示)就会损失这一收益。
Session API 在设计上与 prompt 缓存兼容:
freezeSystemPrompt()在首次调用时根据所有上下文块渲染系统提示,后续调用直接返回缓存值。即使 agent 通过set_context写入了记忆,提示在不同轮之间也保持不变。withCachedPrompt()把冻结后的提示持久化到存储中,以便在 Durable Object hibernation 与 eviction 之后依然存在。agent 唤醒时无需再向所有 provider 拉取,即可加载到相同的提示。
当 agent 通过 set_context 更新一个可写块时,底层 provider 会立即更新(数据已经保存),但被冻结的系统提示不会被重新渲染。LLM 只有在你显式调用 refreshSystemPrompt() 后,才会在下一轮看到这一更新 —— 通常是在对话轮之间执行,而不是中途。
这意味着系统提示在多步工具调用的整轮中保持稳定,从而在每一步都保留 provider 的前缀缓存命中。
JavaScript
const session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt(); // Persist the frozen prompt across hibernation
// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();
// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm
// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();
Explain Code
TypeScript
const session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." }
})
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt(); // Persist the frozen prompt across hibernation
// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();
// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm
// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();
Explain Code
Compaction(压缩)
长对话最终会超出 LLM 的上下文窗口。Compaction 在两个层面应对这个问题:macro-compaction 汇总成段的较早消息,micro-compaction 截断单条过大的消息。
Macro-compaction
Macro-compaction 汇总较旧的消息,但永远不会删除原始消息。
它使用 overlay:汇总结果保存在一张独立的表里,以它覆盖的消息范围为 key。当调用 getHistory() 时,overlay 会在读取时被透明地应用。被压缩的范围会被一条合成的摘要消息替换。底层消息仍保留在 SQLite 中,完整对话依然可用,可用于审计、搜索以及分支。
Messages: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
↓ compaction ↓
Overlay: [1] [2] [SUMMARY of 3-7] [8] [9] [10]
↑ tail protected
要点:
-
非破坏性,原始消息永远不会被删除。完整对话始终在数据库中可查。
-
迭代式,当对话再次变长并触发新一轮 compaction 时,会把已有摘要交给 LLM 更新,而不是从零开始。
-
边界感知,压缩边界会被调整,避免拆开 tool call 与 tool result 的配对。
-
可配置,
protectHead保留前 N 条消息(通常是系统上下文),tailTokenBudget保留最近的消息不被压缩。
JavaScript
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
const session = Session.create(this)
.withContext("memory", { maxTokens: 1100 })
.onCompaction(
createCompactFunction({
summarize: (prompt) =>
generateText({ model: myModel, prompt }).then((r) => r.text),
protectHead: 3,
tailTokenBudget: 20000,
minTailMessages: 2,
}),
)
.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold
Explain Code
TypeScript
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
const session = Session.create(this)
.withContext("memory", { maxTokens: 1100 })
.onCompaction(createCompactFunction({
summarize: (prompt) =>
generateText({ model: myModel, prompt }).then(r => r.text),
protectHead: 3,
tailTokenBudget: 20000,
minTailMessages: 2
}))
.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold
Explain Code
自动 compaction 会在 appendMessage() 之后,当估算 token 数超过阈值时被触发。Compaction 失败不会导致致命错误,因为消息已经保存。
Micro-compaction
Micro-compaction 工作在单条消息的层面,而不是跨范围。它处理两个问题:
读取时截断:truncateOlderMessages() 在把较早的消息发给 LLM 之前,会缩短其中的工具输出和长文本。最近的消息(默认是最近 4 条)会保留完整。该操作作用于副本,存储中的消息不会被改动。
JavaScript
import { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM
TypeScript
import { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM
行大小限制:当一条消息被持久化时(通常是带有大型工具输出的 assistant 消息),会按 SQLite 行大小上限做检查。过大的工具输出会被替换为预览以及一段建议重跑工具的备注。这避免单条消息超出存储上限,同时保持对话流的完整性。
相关资源
Session API 参考 Session 的完整 API 参考,涵盖消息、上下文块、compaction、搜索、工具和自定义 provider。
存储与同步 state 用于更简单的键值持久化与实时同步的 setState()。
Think 通过 configureSession() 内置 Session 集成的、有自身风格的聊天 agent。