跳转到内容
搜索文档

对话状态与内存

最后更新 查看 MarkdownAgent 设置

Agent 需要内存才能在时间上持续有用。没有内存,每次对话都从零开始——Agent 会忘记用户是谁、学到了什么、正在做什么。内存将无状态 LLM 调用变为持久、上下文感知的 Agent。

Session API 为基于 Cloudflare Agents SDK 构建的 Agent 提供内存层。它管理两类内存:对话历史(构成会话的消息与工具调用)与上下文内存(注入系统提示词的持久块,Agent 可读、写、搜索与加载)。

当你需要的不只是简单同步状态或扁平聊天历史时使用本页。小型 UI 状态用存储与同步状态。基础聊天持久化由 AIChatAgent 替你存储消息。有明确主张的长期内存由 Think 基于 Session 与上下文块构建。

对话历史

最基本的内存类型是对话本身:用户与 Agent 之间的消息、Agent 发起的 tool 调用及其收到的结果。Session 将所有内容存入由 Session Provider 支持的树形消息历史,默认为 SQLite。

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 = await session.getHistory();
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 = await session.getHistory();

对话历史在 Durable Object 休眠与驱逐后仍保留。Agent 唤醒时完整历史可在 SQLite 中获取,无需重放或重建。

消息通过 parent_id 以树结构存储,支持分支对话。对已有子节点的 parentId 调用 appendMessage 会创建分支,适用于响应重新生成等功能。getHistory(leafId) 沿树中任选路径遍历。

Session 还提供对话历史的全文搜索:

const results = await session.search("deployment Friday", { limit: 10 });
const results = await session.search("deployment Friday", { limit: 10 });

对话变长时,压缩 会摘要较早消息,在保留底层数据的同时控制上下文窗口。

上下文内存

上下文内存是注入系统提示词的持久信息,与对话历史分离。它让 Agent 在每一轮次都能访问身份、指令、已学事实、知识库与参考资料。

Session API 支持四类上下文内存,各适用于不同信息。类型由支撑上下文块的 提供商 决定。Session 自动检测提供商能力。

只读上下文

这是传统系统提示词:Agent 的身份、个性与指令。可直接写在代码库、从 R2 的 SOUL.md 加载,或从 API 获取。内容注入系统提示词,Agent 无法修改。

编程助手可有定义个性与约束的 soul:

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.",
	},
});
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.",
	},
});

或从 R2 加载,以便在不重新部署的情况下更新 Agent 个性:

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.";
		},
	},
});
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 自用的 scratchpad,用于记下需要记住的内容——类似 Claude Code 维护待办列表,或客服 Agent 在对话中跟踪对用户的了解。

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,
	});
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,
	});

构建器中省略 provider 时,Session 自动连接到 SQLite 支持的可写提供商。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

内容跨消息持久化并在休眠后存活。始终在系统提示词中可见,Agent 每轮次都能看到,无需额外获取。

可搜索上下文

当有大量信息(知识库、文档、日志、累积笔记)不宜全部塞入系统提示词时,可搜索上下文在系统提示词中保留摘要(例如「已索引 42 条」),并在需要时让 Agent 检索具体条目。

你提供带 search() 方法的提供商。搜索实现完全由你决定:全文搜索、通过 Vectorize 的向量搜索、调用外部 API 等。Session 只关心提供商是否有 search() 方法。

内置 AgentSearchProvider 默认使用 Durable Object SQLite 与 FTS5:

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),
});
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),
});

也可实现由任意搜索机制支持的自定义提供商:

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
		},
	},
});
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
		},
	},
});

Agent 获得用于查询的 search_context 工具与用于索引新条目的 set_context 工具。由 Agent 决定搜什么,由你决定如何搜索。

当 Agent 需从大型集合中查找具体信息而非加载整份文档时,这是合适选择。

可加载上下文(Skills)

技能是大型上下文(完整文档、参考指南、运行手册、模板),Agent 可按需发现与加载。可将其视为书架上的参考资料:Agent 看到标题与描述列表,选取与当前任务相关者,加载、使用,完成后卸载。

与从大型集合检索小块的可搜索上下文不同,技能设计为整份加载。Agent 加载技能时,整份文档进入其上下文窗口。

技能由 SkillProvider 接口支持。技能提供商有三个方法:

  • get() 返回出现在系统提示词中的元数据列表(标题与描述)
  • load(key) 获取特定技能的完整内容
  • set(key, content, description?) 写入或更新技能条目(可选)

系统提示词将可用技能显示为列表。[loadable] 标签告诉 LLM 这些条目不是内联的,需要使用工具访问完整内容:

══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
- deploy-checklist: Production deployment checklist

Agent 看到标题,决定哪个技能与当前任务相关,并使用 load_context 将完整内容拉入工作上下文。完成后使用 unload_context 释放空间。当技能提供商实现 set() 时,Agent 也可写回,更新现有技能或创建新技能。

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 的技能

内置 R2SkillProvider 将技能存储在 Cloudflare R2 存储桶中。每个技能是一个 R2 对象,可选自定义元数据用于描述。

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();
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();

prefix 选项将提供商限定到存储桶中的子目录。元数据列表中的技能键显示时不带前缀,因此 skills/api-ref 在系统提示词中显示为 api-ref

使用 keysget()load() 允许列表指定相对前缀的技能:

new R2SkillProvider(env.SKILLS_BUCKET, {
	prefix: "skills/",
	keys: ["deploy-checklist", "api-ref"],
});
new R2SkillProvider(env.SKILLS_BUCKET, {
	prefix: "skills/",
	keys: ["deploy-checklist", "api-ref"],
});

在 Wrangler 配置中添加 R2 存储桶绑定:

{
  "r2_buckets": [
    {
      "binding": "SKILLS_BUCKET",
      "bucket_name": "my-agent-skills"
    }
  ]
}
[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-agent-skills"

技能是普通 R2 对象。通过任意 R2 接口(Wrangler CLI、仪表板或 Workers API)上传:

# 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 对象上设置自定义元数据:

await env.SKILLS_BUCKET.put("skills/api-ref", content, {
	customMetadata: { description: "API Reference documentation" },
});
await env.SKILLS_BUCKET.put("skills/api-ref", content, {
	customMetadata: { description: "API Reference documentation" },
});

自定义技能提供商

通过实现 SkillProvider 接口,可用任意存储支撑技能:

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();
	}
}
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();
	}
}

Session 通过鸭子类型检测 load() 方法并自动生成相应工具。

技能与其他内存类型对比

方面 技能 可写上下文 可搜索上下文
在系统提示词中 仅元数据列表 完整内容 摘要计数
访问模式 按键加载整份文档 始终可见 按查询搜索
适用场景 大型文档、参考资料 短笔记、偏好 大量小条目集合
上下文成本 低(加载前) 与内容成正比 低(搜索前)
Agent 可写? 可选(若实现 set 是(通过 set_context 是(通过 set_context

关键区别:技能是惰性的。在 Agent 决定需要某个技能之前,系统提示词中几乎不占成本。这使它们非常适合大型参考资料,其中仅子集与任意对话相关。

Agent 如何与内存交互

Session 根据上下文块的提供商类型自动生成工具。将这些工具与自有应用特定工具一并传给 LLM:

const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };

const result = streamText({
	model: myModel,
	system: await session.freezeSystemPrompt(),
	messages: await convertToModelMessages(await session.getHistory()),
	tools: allTools,
});
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };

const result = streamText({
	model: myModel,
	system: await session.freezeSystemPrompt(),
	messages: await convertToModelMessages(await session.getHistory()),
	tools: allTools,
});

生成的工具

Session 根据存在的提供商类型动态生成工具:

工具 生成条件 作用
set_context 存在任意可写、技能或搜索块 向命名块写入内容。可写块替换或追加。技能/搜索块写入带键条目。
load_context 存在技能块 按键将文档完整内容加载到 Agent 上下文。
unload_context 存在技能块 移除先前加载的文档以释放上下文空间。文档仍可重新加载。
search_context 存在搜索块 在可搜索块内全文搜索。返回按相关性排序的前若干结果。
session_search 使用 SessionManager 跨所有会话搜索(跨对话搜索)。

这些工具包含描述与参数 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

标签([readonly][writable][searchable][loadable])告诉 LLM 每个块可进行的交互类型。Token 预算显示可写块中剩余空间,帮助 Agent 管理自身内存。

注意事项

Prompt 缓存

LLM 提供商(Anthropic、OpenAI 等)缓存系统提示词前缀。连续请求共享相同系统提示词时,提供商可跳过重新处理该前缀,降低延迟与成本。破坏缓存(更改系统提示词)会丧失此收益。

Session API 设计为与提示词缓存配合:

  • freezeSystemPrompt() 在首次调用时从所有上下文块渲染系统提示词,后续调用返回缓存值。即使 Agent 通过 set_context 写入内存,提示词在轮次之间也不变。
  • withCachedPrompt() 将冻结的提示词持久化到存储,使其在 Durable Object 休眠与驱逐后存活。Agent 唤醒时加载相同提示词,无需从所有提供商重新获取。

当 Agent 使用 set_context 更新可写块时,底层提供商立即更新(数据已保存),但冻结的系统提示词 不会重新渲染。LLM 仅在显式调用 refreshSystemPrompt() 时在下一轮次看到更新,通常你在对话轮次之间而非轮次中间调用。

这意味着系统提示词在整个多步工具使用轮次中保持稳定,保留提供商的前缀缓存。

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();
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();

压缩

长对话最终会超出 LLM 的上下文窗口。压缩在两个层面解决:宏观压缩摘要较旧消息范围,微观压缩截断过大的单条消息。

宏观压缩(Macro-compaction)

宏观压缩摘要较旧消息,但从不删除原文。

它使用 覆盖层:摘要在单独表中存储,以其覆盖的消息范围为键。调用 getHistory() 时,覆盖层在读取时透明应用。被压缩的范围由合成摘要消息替换。底层消息保留在 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

要点:

  • 非破坏性,原始消息从不删除。完整对话始终在数据库中可用。
  • 迭代式,对话再次增长并触发另一次压缩时,现有摘要传给 LLM 更新,而非从头替换。
  • 边界感知,压缩边界会偏移,避免拆分工具调用 / 工具结果对。
  • 可配置protectHead 保留前 N 条消息(通常是系统上下文),tailTokenBudget 保持最近消息完整。
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
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

自动压缩在 appendMessage() 后、估计 token 数超过阈值时触发。压缩失败非致命 — 消息已保存。

微观压缩(Micro-compaction)

微观压缩在单条消息层面而非跨范围工作。它处理两个问题:

读取时截断truncateOlderMessages() 在发送给 LLM 前缩短较旧消息中的工具输出与长文本。最近消息(默认最后 4 条)保持完整。这在副本上操作,不修改已存储消息。

import { truncateOlderMessages } from "agents/experimental/memory/utils";

const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM
import { truncateOlderMessages } from "agents/experimental/memory/utils";

const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM

行大小强制:持久化消息时(通常是带大型工具输出的 assistant 消息),检查 SQLite 行大小限制。过大的工具输出替换为预览与建议重新运行工具的说明。这防止单条消息超出存储限制,同时保留对话流程。

相关

Session API 参考

Session 完整 API 参考,涵盖消息、上下文块、压缩、搜索、工具与自定义提供商。

Think

通过 configureSession() 内置 Session 集成的有明确主张的聊天 Agent。

这篇文档对您有帮助吗?