news 2026/9/18 4:16:48

用 agents/context 组装 Agent 系统提示词:Blocks、Providers 与冻结提示词实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 agents/context 组装 Agent 系统提示词:Blocks、Providers 与冻结提示词实战指南

用 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 要解决三个现实问题:

  1. 提示词的分层管理:把"人格设定(soul)""用户记忆(memory)""知识库(knowledge)"等不同性质的提示片段分开声明、分开存储、分开计 token;
  2. 持久化:writable 的 Block 能落盘到 Durable Object 的 SQLite,跨会话存活;
  3. 前缀缓存保持:把渲染好的系统提示词"冻结"为稳定字符串,让模型 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)由以下部分组成:

  • 标签labeltoUpperCase()大写形式;
  • 描述:若有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_contextsearch_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 shapeBlock 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复用同一存储类为soulmemory等多个 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 providerset是函数(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本身就具备,所以可以直接传thislabel参数可选,省略时由init()从 Block 声明中补齐。

从 sqlite-provider.ts 可以看到表结构与三个实现细节:

  1. 建表是惰性的ensureTable()在首次get()/set()时才执行CREATE TABLE IF NOT EXISTS,而不是在构造时。测试 providers.test.ts 专门验证了"首次 get 前表不存在、get 后表才出现"的惰性建表行为;
  2. 一 label 一行label TEXT PRIMARY KEY,同一个 label 的多次写入走ON CONFLICT(label) DO UPDATE的 upsert,覆盖更新而非累积;
  3. 记录更新时间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),其行为链条是:

  1. 检查 Block 存在且writable,否则抛Block "xxx" is readonly
  2. 检查是否 searchable——keyed provider 必须走setSearchEntry(),直接setBlock会抛错;
  3. 估算 token 并强制maxTokens上限;
  4. 更新内存中的 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' )

几个值得注意的设计决策:

  • labelkey标记为UNINDEXED:它们只是过滤字段,不参与全文索引,content才是被索引的主体;所有 label 共享同一张 FTS5 表,靠label列做命名空间隔离。测试 providers.test.ts 验证了docsnotes两个 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 精确验证了这一语义:setBlockfreezeSystemPrompt()返回值与冻结前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 时提供):包含titledescription两个字段。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 } ]; } }

两个对使用者透明的默认行为:

  1. 未声明 provider 的 Block 自动接线到持久化的 per-agent SQLite(即前文defaultProvider机制在Think中的应用,实现见 think.ts 中configureContext()的调用处);
  2. 冻结系统提示词始终被持久化_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:AgentContextProvidercf_agents_context_blocks表结构;
  • packages/agents/src/context/search.ts:AgentSearchProviderSearchProvider接口与 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 4:16:19

MCP3901A0-E/ML选型指南:24位模拟前端核对要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 4:11:46

鸿蒙预览场景模拟:@ohos/hamock 模拟框架详解与源码级实践指南

鸿蒙预览场景模拟&#xff1a;ohos/hamock 模拟框架详解与源码级实践指南 【免费下载链接】cann-recipes-harmony-infer 本项目为鸿蒙开发者提供基于CANN平台的业务实践案例&#xff0c;方便开发者参考实现端云能力迁移及端侧推理部署。 项目地址: https://gitcode.com/cann/…

作者头像 李华