用 agents/context 组装 Agent 系统提示词:Blocks、Providers 与冻结提示词实战指南
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
agents/context是 Cloudflare Agents 框架(本仓库agents1/agents)中负责系统提示词组装的模块:它把一段段带标签(label)的提示词文本组织成 Block,每个 Block 背后挂一个存储 Provider,由 Provider 的能力决定该 Block 是只读、可写还是可全文检索,并自动为模型装配对应的set_context/search_context工具。本文以官方文档 docs/agents/context.md 为主体,结合 源码实现 与 测试用例 深入讲解其运行原理与落地姿势,读完你可以在自己的 Agent 中组合出带持久化记忆、可检索知识库且能保持前缀缓存命中的系统提示词。
实验性 API 提示:与
agents/context同级的agents/sessions一样,整个agents/context导出面在 API 稳定前可能随版本变化,生产接入时请锁定版本。
Context 是什么:提示词组装,不是会话存储
agents/context的核心定位是一句话:Context is prompt assembly. It is not conversation storage.它只负责把各 Block 渲染成系统提示词,不负责消息历史的持久化——消息树、流式读取、压缩与附件卸载由agents/sessions负责。
两者是组合而非嵌套的关系:Context 与 Session 句柄组合使用,而不是寄生在 Session 内部。因此一个 Agent 可以"有提示词而没有对话记录",也可以"有对话记录而没有提示词"。这种正交设计在源码注释中被反复强调(见 packages/agents/src/context/index.ts)。
从工程视角看,Context 要解决三个现实问题:
- 提示词的分层管理:把"人格设定(soul)""用户记忆(memory)""知识库(knowledge)"等不同性质的提示片段分开声明、分开存储、分开计 token;
- 持久化:writable 的 Block 能落盘到 Durable Object 的 SQLite,跨会话存活;
- 前缀缓存保持:把渲染好的系统提示词"冻结"为稳定字符串,让模型 Provider 的 prefix cache 跨轮次保持命中,避免每次重新渲染出细微差异导致缓存失效。
Blocks:提示词的最小组织单元
基础用法
Block 的声明方式非常直接。一个 Block 由label(标签)、可选的description(描述)、可选的maxTokens(token 上限)以及provider(存储后端)组成:
import { ContextBlocks } from "agents/context"; const context = new ContextBlocks([ { label: "soul", provider: { get: async () => "You are a helpful assistant." } }, { label: "memory", description: "Facts learned about the user", maxTokens: 1_100, provider: memoryProvider } ]); const system = await context.freezeSystemPrompt(); const tools = await context.tools();每个 Block 渲染为系统提示词中的一个带标签小节(section)。渲染逻辑见 blocks.ts 的 renderPrompt(),标题头(header)由以下部分组成:
- 标签:
label的toUpperCase()大写形式; - 描述:若有
description,以(描述)形式拼接在标签后; - token 占用百分比:设置了
maxTokens时显示[45% — 495/1100 tokens]这样的占比; - 能力标记:
[readonly]、[writable]、[loadable]或[searchable]四选一。
分隔线由 46 个═字符组成,渲染结果形如:
══════════════════════════════════════════════════ MEMORY (Facts learned about the user) [45% — 495/1100 tokens] [writable] ══════════════════════════════════════════════════ 用户偏好 TypeScript,正在使用 Cloudflare Agents空 Block 的渲染规则(重要)
渲染时有一个容易被忽视但很关键的规则:空的只读 Block 会被跳过,而 writable、searchable 的 Block 即使为空也一定渲染。理由很实际——空的只读 Block 不携带任何信息,渲染出来纯属浪费 token;而可写/可检索的 Block 必须让模型知道"这里有工具可以操作它",否则模型不会主动去调用set_context或search_context。该逻辑体现在 renderPrompt() 的跳过条件。
maxTokens 的强制语义
maxTokens不只是渲染时的展示信息,它会在写入时被强制校验。在 setBlock() 中,写入内容经estimateStringTokens估算 token 数后,若超过maxTokens会直接抛出Block "memory" exceeds maxTokens: 1200 > 1100这样的错误,写入被拒绝。测试 context.test.ts 同时验证了只读块写入被拒(readonly错误)与 token 超限被拒两个场景。
Providers:能力驱动行为的存储后端
Provider 是 Block 的行为决定者。agents/context对 Provider 的识别是结构化检查(structural,duck-typing),不是名义类型检查——即不看类型继承关系,只检查对象上有没有对应的方法。官方文档给出的能力矩阵如下:
| Provider shape | Block behavior |
|---|---|
get() | Read-only text in the prompt |
get()+set() | Writable through theset_contexttool |
get()+search(key) | Summary in the prompt,search_contexttool |
get()返回 Block 当前内容,没有内容时返回null;- 可选方法
init(label)会在首次使用前被调用并传入 Block 的 label,因此一个 Provider 类可以同时服务于多个 label(这正是AgentContextProvider复用同一存储类为soul、memory等多个 Block 提供存储的原理)。
源码中的接口定义
在 blocks.ts 中,两个核心接口如下:
export interface ContextProvider { get(): Promise<string | null>; /** Called by the context system to provide the block label before first use. */ init?(label: string): void; } export interface WritableContextProvider extends ContextProvider { set(content: string): Promise<void>; }对应的类型守卫isWritableProvider检查"set" in provider且set是函数(blocks.ts),isSearchProvider检查"search" in provider(search.ts)。所以只要你的对象结构上带这些方法,框架就按对应能力对待它——这为自定义 Provider 提供了极大的自由度。
ContextConfig 的完整字段
从 ContextConfig 接口 可以看到一个 Block 配置的全部可选字段:
label:必填,Block 的键,同时用于工具描述与持久化主键;description?:展示给 AI 的人类可读描述;maxTokens?:token 上限,写入时强制校验;provider?:存储后端。省略时,只要构造ContextBlocks时传入了defaultProvider工厂,就会按 label 自动接线。
defaultProvider:按 label 提供持久化
ContextBlocks构造函数签名是:
constructor( configs: ContextConfig[], promptStore?: WritableContextProvider, defaultProvider?: (label: string) => ContextProvider )第二个参数promptStore用于持久化冻结提示词(见下文),第三个参数defaultProvider是一个(label) => provider的工厂。声明时未带provider的 Block,会在加载时通过 withDefaultProvider() 被自动接线到该工厂返回的实例上——这正是 host(如Think)"仅凭 label 就能提供持久化可写 Block"的机制来源。
Durable SQLite Blocks:落盘到 Durable Object 的持久化记忆
AgentContextProvider把每个 Block 作为一行存储在 Durable Object 自己的 SQLite 数据库中,表名为cf_agents_context_blocks:
import { AgentContextProvider } from "agents/context"; const context = new ContextBlocks([ { label: "memory", provider: new AgentContextProvider(this, "memory") } ]);底层表结构与写入语义
构造函数接受任何带 tagged-templatesql方法的对象——Agent本身就具备,所以可以直接传this。label参数可选,省略时由init()从 Block 声明中补齐。
从 sqlite-provider.ts 可以看到表结构与三个实现细节:
- 建表是惰性的:
ensureTable()在首次get()/set()时才执行CREATE TABLE IF NOT EXISTS,而不是在构造时。测试 providers.test.ts 专门验证了"首次 get 前表不存在、get 后表才出现"的惰性建表行为; - 一 label 一行:
label TEXT PRIMARY KEY,同一个 label 的多次写入走ON CONFLICT(label) DO UPDATE的 upsert,覆盖更新而非累积; - 记录更新时间:
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP在每次 upsert 时被刷新。
CREATE TABLE IF NOT EXISTS cf_agents_context_blocks ( label TEXT PRIMARY KEY, content TEXT NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )测试同时验证了不同 label 落在不同行、互不干扰(providers.test.ts)。
setBlock 的写入路径
ContextBlocks.setBlock(label, content)是框架侧统一入口(blocks.ts),其行为链条是:
- 检查 Block 存在且
writable,否则抛Block "xxx" is readonly; - 检查是否 searchable——keyed provider 必须走
setSearchEntry(),直接setBlock会抛错; - 估算 token 并强制
maxTokens上限; - 更新内存中的 Block,并立即调用
config.provider.set(content)落盘(durable)。
另外还有appendToBlock(label, content)(blocks.ts),在已有内容后追加而非覆盖,会自动在两个片段间补一个换行分隔。
Searchable Blocks:基于 FTS5 的知识库
AgentSearchProvider用 Durable Object 的 FTS5 全文索引表支撑一个 Block:
import { AgentSearchProvider } from "agents/context"; const context = new ContextBlocks([ { label: "knowledge", provider: new AgentSearchProvider(this) } ]);三种方法的行为差异
get():不返回条目本身,而是返回已索引条目的计数(如"2 entries indexed."),渲染进系统提示词只是让模型知道"这个知识库有多大、要不要去搜";search(query):通过search_context工具触发,返回最多10 条按相关性排序(ORDER BY rank)的命中,每条以[key]开头;set(key, content):按 key 替换单条条目,同 key 再次写入会先 DELETE 再 INSERT,不会产生重复条目。
FTS5 表结构
从 search.ts 可以看到虚拟表定义:
CREATE VIRTUAL TABLE IF NOT EXISTS cf_agents_search_fts USING fts5( label UNINDEXED, key UNINDEXED, content, tokenize='porter unicode61' )几个值得注意的设计决策:
label、key标记为UNINDEXED:它们只是过滤字段,不参与全文索引,content才是被索引的主体;所有 label 共享同一张 FTS5 表,靠label列做命名空间隔离。测试 providers.test.ts 验证了docs与notes两个 label 之间互不可见;- 分词器:
porter unicode61,英文词干化 + Unicode 分词; - FTS5 表是唯一存储:源码注释明确解释了为什么不建镜像行表(mirror row table)——镜像表会让每一条索引条目的写入量翻倍,而索引本身已经能回答计数与查询两类问题。这一点与 Sessions 的消息索引是分离的:条目只存在于
cf_agents_search_fts,不混入 Sessions 的消息表; - 查询注入防护:
search()会把查询按空白切词,逐词加双引号转义后再拼接,既保留词间隐式 AND 语义,又防止 FTS5 语法注入(search.ts)。恶意或畸形查询会被 try/catch 吞掉并返回null,而不是让工具调用崩溃。
测试 providers.test.ts 完整验证了索引、搜索、计数汇总与重写去重四条路径。
Frozen Prompts:让前缀缓存保持命中
这是agents/context在成本优化上最有价值的设计。
freezeSystemPrompt()只渲染一次,之后每次调用都返回同一个字符串,因此 Provider 的前缀缓存(prefix cache)能跨轮次保持温热。setBlock()会立即写入 Provider,但刻意不改变已冻结的提示词;需要让新内容生效时,显式调用refreshSystemPrompt()从当前 Block 状态重新渲染。
// 首次调用:加载 providers → 渲染 → 缓存快照 const system = await context.freezeSystemPrompt(); // 修改 Block:写入持久化,但不触碰已冻结的提示词 await context.setBlock("memory", "用户喜欢 Workers"); // 仍然返回旧快照,前缀缓存继续命中 const same = await context.freezeSystemPrompt(); // 显式刷新:重新加载所有 provider、重渲染、覆盖快照 const fresh = await context.refreshSystemPrompt();测试 context.test.ts 精确验证了这一语义:setBlock后freezeSystemPrompt()返回值与冻结前toBe相同,refreshSystemPrompt()后才包含新内容。
promptStore:把冻结提示词也持久化
在构造时传入第二个参数promptStore(任何 writable provider),冻结的提示词本身也会被持久化:
const context = new ContextBlocks( configs, new AgentContextProvider(this, "_system_prompt"), (label) => new AgentContextProvider(this, label) ); const system = await context.freezeSystemPrompt();持久化带来的关键收益是冷唤醒一致性:freezeSystemPrompt()优先返回promptStore中已存的提示词;只有不存在时才加载 providers、渲染并持久化。这样一次冷启动(cold wake)复用到的,是模型已经缓存过的那份逐字节相同的提示词字符串,而不是重新渲染出"略有不同"的版本导致缓存 miss。对应实现见 freezeSystemPrompt()。
refreshSystemPrompt()则会重新加载每一个provider、重新渲染并覆盖存储的提示词(blocks.ts)。
另一个细节:空提示词也会被持久化,测试 context.test.ts 验证了"空缓存也算存在"——避免空快照被误判为"无缓存"而反复重渲染。
Tools:能力驱动的 AI SDK 工具集
tools()返回一个 AI SDK 的ToolSet,工具完全由 Block 的能力自动装配:
set_context:当存在任意 writable Block 时出现;search_context:当存在任意 searchable Block 时出现。
只有只读 Block 的 Agent 一个工具都拿不到。
set_context 的 Schema
从 tools() 实现 可以看到该工具的输入结构:
label(string,enum 限定为所有 writable Block 的 label)——写入目标;content(string,必填)——写入的正文;action(string,enum["replace", "append"],默认replace)——覆盖还是追加;metadata(object,可选,仅当存在 searchable Block 时提供):包含title与description两个字段。title是稳定标识符——相同 title 的条目原地更新,不同 title 创建新条目;description是展示在系统提示词中的一行摘要,帮助模型决定是否加载该条目。
工具描述中会列出所有可写 Block 及其类型(writable 或 searchable, keyed entries),写入完成后返回新内容的 token 占用情况,例如Written to memory. Usage: 45% (495/1100 tokens)。所有错误都会以字符串形式返回给模型而非抛出,保证模型可以自我修正。
search_context 的 Schema
search_context只接受label(enum 限定为 searchable Block)与query(string)两个必填参数,返回命中条目或"No results found."。它会在描述里明确列出哪些 Block 可被搜索,防止模型对只读 Block 发起无效搜索。
keyed entry 的 key 生成逻辑值得单独说明:contextEntryKey()(blocks.ts)优先使用metadata.titleslug 化(最长 60 字符);没有 title 时对内容做 slug + FNV-1a 哈希组合,保证同内容幂等、不同内容不冲突。
Think 集成:在 Agent 类中声明 Context
Think在启动阶段通过configureContext()构建自己的ContextBlocks:
import type { ContextConfig } from "agents/context"; class MyAgent extends Think<Env> { configureContext(): ContextConfig[] { return [ { label: "soul", provider: { get: async () => "You are helpful." } }, { label: "memory", description: "Learned facts", maxTokens: 2_000 } ]; } }两个对使用者透明的默认行为:
- 未声明 provider 的 Block 自动接线到持久化的 per-agent SQLite(即前文
defaultProvider机制在Think中的应用,实现见 think.ts 中configureContext()的调用处); - 冻结系统提示词始终被持久化在
_system_prompt这个 label 下,无需任何显式 opt-in——冷唤醒自动复用已缓存的提示词。
装配好的 Blocks 在onStart()之后通过this.context暴露给 Agent。此外,configureContext()返回空数组时,getSystemPrompt()仍作为回退路径存在(think.ts 中明确标注了这层关系),而一旦配置了 context blocks,getSystemPrompt()就会被忽略。
结合 Session:提示词与对话历史的组合实践
与 Context 最常配套的是agents/sessions。两者明确分工:
- Context:系统提示词的组装、持久化与冻结(本文主题);
- Sessions:对话消息树的持久化、流式与字节预算读取、压缩覆盖层、可选全文检索与附件卸载。
sessions.md同样标注了这条依赖关系:Prompt assembly lives in agents/context and composes with a session handle rather than living inside it。在你自己的 Agent 中,标准组合方式是:ContextBlocks负责"模型永远该知道的"(人格、记忆、知识库),Sessions负责"对话历史"(每条 user/assistant 消息),两者的存储同在一个 Durable Object 的 SQLite 中但各用各的表,互不干扰。
源码与测试索引
想深入研读本文涉及的实现,可以直接在仓库中定位以下文件:
- packages/agents/src/context/blocks.ts:
ContextBlocks主类、ContextProvider/WritableContextProvider接口、渲染与工具装配; - packages/agents/src/context/sqlite-provider.ts:
AgentContextProvider与cf_agents_context_blocks表结构; - packages/agents/src/context/search.ts:
AgentSearchProvider、SearchProvider接口与 FTS5 实现; - packages/agents/src/context/index.ts:模块导出面;
- packages/agents/src/tests/context/context.test.ts:冻结/刷新语义、空提示词持久化、只读与 token 上限校验;
- packages/agents/src/tests/context/providers.test.ts:SQLite 惰性建表、label 隔离、FTS5 索引与搜索验证;
- docs/agents/context.md:官方文档原文;
- docs/agents/sessions.md:与 Context 组合使用的会话存储。
小结
agents/context用极简的抽象解决了 Agent 系统提示词的三个核心痛点:分层组装(Blocks + label)、持久化与能力分层(Provider 的 duck-typing 能力矩阵)与成本控制(冻结提示词 + 前缀缓存保持)。从只读的soul人格设定,到可写的memory持久记忆,再到 FTS5 支撑的knowledge可检索知识库,全部可以由同一套 Block 机制承载,并由框架自动装配对应的工具与提示词渲染——这就是它在Think中被选为默认提示词基础设施的原因。
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考