为 Agent 添加 Agent Memory,以便其可以跨对话召回持久上下文。
本指南使用 Agents SDK 及其 Session API,将内存召回作为模型可调用的工具提供。如果您使用其他 Agent 框架,这也同样适用:使用 ingest() 或 remember() 存储内存,通过您的 Agent 工具之一公开 recall(),并使用系统提示词告诉模型何时搜索内存。
- 注册 Cloudflare 账户 ↗。
- 安装
Node.js↗。
Node.js 版本管理器
使用 Volta ↗ 或 nvm ↗ 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。
您还需要获得 Agent Memory 的使用权限。
当模型需要相关内存来回答或行动时,使用 recall()。当您拥有对话消息,并希望 Agent Memory 自动提取持久内存时,使用 ingest()。当您的 Agent 已经知道要存储的确切内存时,使用 remember()。
不要在每次模型轮次后都调用 ingest()。相反,应在用户处于空闲状态、对话被压缩时,或在其他自然的检查点进行批量摄取。
创建一个 Worker 项目:
npm create cloudflare@latest -- memory-agentyarn create cloudflare memory-agentpnpm create cloudflare@latest memory-agent进行设置时,请选择以下选项:
- 对于 What would you like to start with?,选择
Hello World example。 - 对于 Which template would you like to use?,选择
Worker only。 - 对于 Which language do you want to use?,选择
TypeScript。 - 对于 Do you want to use git for version control?,选择
Yes。 - 对于 Do you want to deploy your application?,选择
No(部署前我们还会做一些修改)。
进入项目目录:
cd memory-agent安装本指南中使用的依赖:
npm i agents ai workers-ai-provideryarn add agents ai workers-ai-providerpnpm add agents ai workers-ai-providerbun add agents ai workers-ai-provider命名空间 (Namespace) 限制了您的应用程序的内存配置文件的范围。使用 Wrangler 创建一个:
npx wrangler agent-memory namespace create my-agentyarn wrangler agent-memory namespace create my-agentpnpm wrangler agent-memory namespace create my-agent您将在 Worker 绑定中使用该命名空间名称 my-agent。
在您的 Wrangler 配置中添加 agent_memory 绑定。如果您使用的是 Agents SDK,还要注册您的 Agent Durable Object。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "memory-agent",
"main": "src/server.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": [
"nodejs_compat"
],
"ai": {
"binding": "AI"
},
"agent_memory": [
{
"binding": "MEMORY",
"namespace": "my-agent"
}
],
"durable_objects": {
"bindings": [
{
"name": "ChatAgent",
"class_name": "ChatAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"ChatAgent"
]
}
]
}name = "memory-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = ["nodejs_compat"]
[ai]
binding = "AI"
[[agent_memory]]
binding = "MEMORY"
namespace = "my-agent"
[[durable_objects.bindings]]
name = "ChatAgent"
class_name = "ChatAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatAgent"]为您绑定的服务生成本地 TypeScript 类型:
npx wrangler typesyarn wrangler typespnpm wrangler types不能仅因为您的应用程序拥有内存绑定,模型就能使用内存。您需要通过一个工具公开召回功能,并指导模型何时调用它。
通过 Agents SDK Session API,添加一个可搜索的上下文提供者 (Context Provider)。Session 会将该提供者的 search() 方法转换为模型的 search_context 工具。
创建 src/server.ts 并添加召回设置:
import { Agent, routeAgentRequest } from "agents";
import { Session } from "agents/experimental/memory/session";
const INSTRUCTIONS = "You are a helpful assistant.";
const MEMORY_CONTEXT = `Long-term memory is available through the search_context tool.
MEMORY POLICY
- Search memory with search_context when the user asks what you know or remember about them.
- Search memory when the request depends on prior sessions, preferences, project state, conventions, decisions, or long-running tasks.
- Phrase memory searches as concise topics, not questions.
- Do not search memory to repeat something the user just said in the current conversation.
- When search_context returns results, always incorporate them into your response. The results are real memories from previous conversations.
- Treat recalled memories as helpful context, not guaranteed truth. If a memory is important for an irreversible action, confirm with the user.`;
const MEMORY_PROFILE_NAME = "demo-user";
export class ChatAgent extends Agent {
initialState = { cursor: 0, nextIngestAt: null };
session = Session.create(this)
.withContext("instructions", {
provider: { get: async () => INSTRUCTIONS },
})
.withContext("memory", {
description:
"Searchable durable memory: facts, events, instructions, and tasks from prior conversations.",
provider: {
get: async () => MEMORY_CONTEXT,
search: async (query) => {
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
const { answer } = await profile.recall(query, {
responseLength: "short",
});
return answer || "No relevant memories found.";
},
},
})
.withCachedPrompt();
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { Agent, routeAgentRequest } from "agents";
import { Session } from "agents/experimental/memory/session";
const INSTRUCTIONS = "You are a helpful assistant.";
const MEMORY_CONTEXT = `Long-term memory is available through the search_context tool.
MEMORY POLICY
- Search memory with search_context when the user asks what you know or remember about them.
- Search memory when the request depends on prior sessions, preferences, project state, conventions, decisions, or long-running tasks.
- Phrase memory searches as concise topics, not questions.
- Do not search memory to repeat something the user just said in the current conversation.
- When search_context returns results, always incorporate them into your response. The results are real memories from previous conversations.
- Treat recalled memories as helpful context, not guaranteed truth. If a memory is important for an irreversible action, confirm with the user.`;
const MEMORY_PROFILE_NAME = "demo-user";
type ChatAgentState = {
cursor: number;
nextIngestAt: number | null;
};
export class ChatAgent extends Agent<Env, ChatAgentState> {
initialState: ChatAgentState = { cursor: 0, nextIngestAt: null };
session = Session.create(this)
.withContext("instructions", {
provider: { get: async () => INSTRUCTIONS },
})
.withContext("memory", {
description:
"Searchable durable memory: facts, events, instructions, and tasks from prior conversations.",
provider: {
get: async () => MEMORY_CONTEXT,
search: async (query: string) => {
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
const { answer } = await profile.recall(query, {
responseLength: "short",
});
return answer || "No relevant memories found.";
},
},
})
.withCachedPrompt();
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;系统提示词与工具本身同样重要。它告诉模型何时调用 search_context、何时不要调用它,以及如何对待召回的内存。
接下来,为您的 Agent 提供一种添加持久内存的方式。在聊天 Agent 中,通常的做法是将对话存储在 Session 中,然后当用户空闲后调用 ingest()。
修改 agents 导入并添加 AI SDK 导入。保留第 4 步中的 Session 导入。
import { Agent, getAgentByName, routeAgentRequest } from "agents";
import { convertToModelMessages, generateText, stepCountIs } from "ai";
import { createWorkersAI } from "workers-ai-provider";import { Agent, getAgentByName, routeAgentRequest } from "agents";
import { convertToModelMessages, generateText, stepCountIs } from "ai";
import type { UIMessage } from "ai";
import { createWorkersAI } from "workers-ai-provider";在文件顶部附近、导入语句下方,添加摄取延迟:
const MEMORY_INGEST_DELAY_SECONDS = 10;const MEMORY_INGEST_DELAY_SECONDS = 10;然后,使用以下结构更新 ChatAgent。注释标记了保留第 4 步中的 Session 设置的位置。
export class ChatAgent extends Agent {
initialState = { cursor: 0, nextIngestAt: null };
// Keep the `session = Session.create(this)` setup from step 4 here.
async chat(message) {
const userMessage = {
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: message }],
};
await this.session.appendMessage(userMessage);
await this.scheduleIngest();
const workersai = createWorkersAI({ binding: this.env.AI });
const result = await generateText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
system: await this.session.freezeSystemPrompt(),
messages: await convertToModelMessages(await this.session.getHistory()),
tools: await this.session.tools(),
stopWhen: stepCountIs(5),
});
const assistantMessage = {
id: `assistant-${crypto.randomUUID()}`,
role: "assistant",
parts: [{ type: "text", text: result.text }],
};
await this.session.appendMessage(assistantMessage);
return result.text;
}
async ingestScheduledMemory() {
await this.runIngest();
}
async scheduleIngest() {
await this.cancelPendingIngest();
await this.schedule(
MEMORY_INGEST_DELAY_SECONDS,
"ingestScheduledMemory",
{},
);
this.setState({
...this.state,
nextIngestAt: Date.now() + MEMORY_INGEST_DELAY_SECONDS * 1000,
});
}
async cancelPendingIngest() {
const pending = await this.listSchedules();
for (const schedule of pending) {
if (schedule.callback === "ingestScheduledMemory") {
await this.cancelSchedule(schedule.id);
}
}
}
async runIngest() {
const history = await this.session.getHistory();
const messages = history
.slice(this.state.cursor)
.filter(
(message) => message.role === "user" || message.role === "assistant",
)
.map((message) => ({
role: message.role,
content: message.parts
.map((part) => (part.type === "text" ? part.text : ""))
.filter(Boolean)
.join("\n\n"),
}))
.filter((message) => message.content);
if (messages.length === 0) {
this.setState({ ...this.state, nextIngestAt: null });
return { ingested: 0 };
}
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
await profile.ingest(messages, { sessionId: this.name });
this.setState({
...this.state,
cursor: history.length,
nextIngestAt: null,
});
return { ingested: messages.length };
}
}export class ChatAgent extends Agent<Env, ChatAgentState> {
initialState: ChatAgentState = { cursor: 0, nextIngestAt: null };
// Keep the `session = Session.create(this)` setup from step 4 here.
async chat(message: string): Promise<string> {
const userMessage: UIMessage = {
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: message }],
};
await this.session.appendMessage(userMessage);
await this.scheduleIngest();
const workersai = createWorkersAI({ binding: this.env.AI });
const result = await generateText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
system: await this.session.freezeSystemPrompt(),
messages: await convertToModelMessages(
(await this.session.getHistory()) as UIMessage[],
),
tools: await this.session.tools(),
stopWhen: stepCountIs(5),
});
const assistantMessage: UIMessage = {
id: `assistant-${crypto.randomUUID()}`,
role: "assistant",
parts: [{ type: "text", text: result.text }],
};
await this.session.appendMessage(assistantMessage);
return result.text;
}
async ingestScheduledMemory() {
await this.runIngest();
}
private async scheduleIngest() {
await this.cancelPendingIngest();
await this.schedule(
MEMORY_INGEST_DELAY_SECONDS,
"ingestScheduledMemory",
{},
);
this.setState({
...this.state,
nextIngestAt: Date.now() + MEMORY_INGEST_DELAY_SECONDS * 1000,
});
}
private async cancelPendingIngest() {
const pending = await this.listSchedules();
for (const schedule of pending) {
if (schedule.callback === "ingestScheduledMemory") {
await this.cancelSchedule(schedule.id);
}
}
}
private async runIngest(): Promise<{ ingested: number }> {
const history = (await this.session.getHistory()) as UIMessage[];
const messages = history
.slice(this.state.cursor)
.filter(
(message) => message.role === "user" || message.role === "assistant",
)
.map((message) => ({
role: message.role,
content: message.parts
.map((part) => (part.type === "text" ? part.text : ""))
.filter(Boolean)
.join("\n\n"),
}))
.filter((message) => message.content);
if (messages.length === 0) {
this.setState({ ...this.state, nextIngestAt: null });
return { ingested: 0 };
}
const profile = await this.env.MEMORY.getProfile(MEMORY_PROFILE_NAME);
await profile.ingest(messages, { sessionId: this.name });
this.setState({
...this.state,
cursor: history.length,
nextIngestAt: null,
});
return { ingested: messages.length };
}
}将默认导出替换为一个简单的测试端点。每个 conversationId 都会映射到拥有其自己 Session 历史记录的独立 Agent 实例。
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/chat") {
const { message, conversationId = "default" } = await request.json();
if (!message) {
return Response.json({ error: "Missing message" }, { status: 400 });
}
const agent = await getAgentByName(env.ChatAgent, conversationId);
const response = await agent.chat(message);
return Response.json({ response });
}
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/chat") {
const { message, conversationId = "default" } =
(await request.json()) as {
message?: string;
conversationId?: string;
};
if (!message) {
return Response.json({ error: "Missing message" }, { status: 400 });
}
const agent = await getAgentByName(env.ChatAgent, conversationId);
const response = await agent.chat(message);
return Response.json({ response });
}
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;摄取路径是 scheduleIngest() 和 runIngest()。每个用户消息都会取消上一个挂起的摄取并安排一个新的摄取,因此 Agent Memory 会在用户进入空闲状态后处理对话,而不是在每个 Agent 轮次后都进行处理。
runIngest() 使用了游标 (cursor),因此每个批次仅包含尚未摄取的消息。sessionId 在共享的内存配置文件内按对话对内存进行分组。
本演示在多个对话中跨用了一个 Agent Memory 配置文件 demo-user。在生产环境中,请选择与您的应用范围(如用户、团队、租户或组织)相匹配的配置文件名称。
您还可以从 Session 压缩钩子 (compaction hook) 中调用摄取逻辑。重要的约束是在自然检查点以批处理方式摄取,而不是在每个 Agent 轮次后都进行。在生产环境中,请选择与您应用程序的用户体验相匹配的摄取延迟。
对大多数应用而言,自动摄取已足够。如果您希望模型立即存储特定内存,请添加一个服务器端工具,其执行函数会调用 remember()。
当 Agent 已经知道要存储的准确内存时,可以使用此方法。例如,在用户说“记住我更喜欢简洁的答案”后,模型可能会调用 rememberMemory 工具。
如果模型能够调用内存写入工具,请添加系统提示词指令,定义什么值得被记住以及何时请求确认。对于许多 Agent 而言,自动对话摄取比直接给模型一个写入内存的工具更简单、更安全。
启动本地开发服务:
npx wrangler devyarn wrangler devpnpm wrangler dev向第一个对话发送请求,让其记住一个持久的偏好:
curl -X POST "http://localhost:8787/chat" \
-H "Content-Type: application/json" \
-d '{"conversationId":"first-chat","message":"I prefer TypeScript examples and concise answers."}'在发送下一个请求之前,请等待至少 30 秒。代码安排在用户闲置 10 秒后运行摄取,随后 Agent Memory 需要额外的时间来提取、分类并索引这些内存,之后这些内容才可供召回。
在另一个对话中提出一个依赖于持久内存的问题。此请求使用不同的 Session 历史记录,但使用同一个 Agent Memory 配置文件。
curl -X POST "http://localhost:8787/chat" \
-H "Content-Type: application/json" \
-d '{"conversationId":"second-chat","message":"What do you know or remember about me and my preferences?"}'模型应当调用 search_context,从 Agent Memory 中接收召回的内存,并在其响应中应用该上下文。第二个对话与第一个对话没有共享的 Session 历史,因此关于用户偏好的任何知识都来自于 Agent Memory。