cloudflare-os 实战:用 Workers 和 Durable Objects 构建边缘 AI Agent

发布时间:2026/10/28 13:48:13
cloudflare-os 实战:用 Workers 和 Durable Objects 构建边缘 AI Agent

1. 从标题到落地:cloudflare-os 到底想解决什么问题

第一次看到cloudflare-os这个标题,我脑子里冒出来的第一个念头是:这又是一个把 Cloudflare 全家桶串起来的实验性项目。但仔细拆开来看,它其实指向一个非常具体的需求——把 Cloudflare Workers 当作一个轻量级的操作系统底座,在上面跑 AI Agent。这个思路在 2024 年下半年开始变得特别热,因为 Workers 的冷启动已经压到毫秒级,边缘节点遍布全球,再加上 Durable Objects 提供了有状态能力,天然就是一个分布式的 Agent 运行时。

我先把结论放在前面:cloudflare-os不是一个官方产品,它更像是一类项目模式的统称。你在 GitHub 上搜这个词,会看到好几个不同作者用类似思路搭的仓库,核心都是围绕Workers + Durable Objects + Workers AI + Vectorize这套组合拳,构建一个可以部署在边缘的 Agent 框架。它解决的问题很明确:传统 Agent 部署要么依赖一台常驻服务器,要么依赖容器编排,成本和运维复杂度都下不来。而把这套东西搬到 Workers 上,你得到的是一个按请求计费、全球分布、无需管理服务器的 Agent 运行环境。

适合谁来参考这篇文章?三类人。第一类是想入门 Agent 开发但被 K8s 和 Docker 劝退的开发者,第二类是在做多 AI 协作、Agent 记忆、Agent 编排这类项目、想找一个低成本部署方案的人,第三类是对 Cloudflare 生态感兴趣、想看看 Workers 除了做 API 网关还能干什么的技术爱好者。不管你之前有没有写过 Agent,只要你能看懂 JavaScript 或者 TypeScript,这篇文章里的东西你都能直接抄作业。

我下面会从整体设计思路开始拆,然后讲核心细节、实操步骤、踩坑记录,最后给一份可以直接复现的配置清单。整个过程我会尽量把“为什么这么选”讲清楚,而不是只丢一堆代码给你。

2. 整体架构设计与技术选型拆解

2.1 为什么是 Workers 而不是传统服务器

先说最核心的一个决策:为什么要把 Agent 跑在 Workers 上,而不是找台 VPS 或者用 Serverless 容器。这个问题的答案直接决定了整个项目的形态。

传统 Agent 部署的痛点在于状态管理和成本曲线。一个带记忆的 Agent 需要持久化对话历史、工具调用记录、向量检索结果,这些在传统架构里通常靠 Redis + Postgres + 向量数据库三件套解决。问题是,当你的 Agent 处于低频调用状态时,这三件套的常驻成本是固定的,一个月下来少说几十刀。而 Workers 的计费模型是按请求数和 CPU 时间算的,空闲时几乎不花钱。

另一个关键点是延迟。Agent 的响应体验很大程度上取决于首字节时间。Workers 在全球有三百多个节点,用户请求会被路由到最近的节点执行,冷启动在毫秒级。这意味着一个部署在 Workers 上的 Agent,对亚洲用户和对欧洲用户的响应延迟差异远小于部署在单一区域的服务器。

但 Workers 也有硬约束,你必须心里有数:

约束项具体限制对 Agent 的影响
CPU 时间默认 30 秒(付费可调)长推理任务需要拆分或流式返回
内存128MB不能加载大模型权重,只能调 API
请求体大小100MB大文件处理需要走 R2
子请求数50/1000 取决于套餐多工具调用要注意预算
无状态默认无状态必须用 Durable Objects 存状态

这些约束决定了cloudflare-os这类项目的设计哲学:Agent 的“大脑”放在 Workers AI 或者外部大模型 API 上,Agent 的“记忆”放在 Durable Objects 和 Vectorize 上,Agent 的“手脚”通过 Workers 的 fetch 能力调用外部工具。整个系统是分布式的,但对外表现为一个统一的 Agent 接口。

2.2 Durable Objects 作为 Agent 的“意识容器”

Durable Objects 是这个架构里最容易被低估的组件。很多人第一次接触它,觉得就是个带状态的 Worker,但实际上它解决的是 Agent 领域一个根本问题:同一个 Agent 实例的状态一致性。

想象一下,你有一个 Agent 正在和用户对话,用户发了三条消息,每条消息触发的 Worker 请求可能落在不同的边缘节点上。如果没有 Durable Objects,这三条消息对应的状态更新会互相覆盖,Agent 的记忆就乱了。Durable Objects 通过一个全局唯一的 ID 把请求路由到同一个实例上,保证状态串行更新。

在cloudflare-os的典型实现里,每个用户会话对应一个 Durable Object 实例。这个实例内部维护着:

  • 对话历史数组(最近 N 轮)
  • 工具调用栈
  • 当前 Agent 的“人格”配置
  • 短期记忆的向量索引引用

我实测下来,一个 Durable Object 实例处理单用户会话完全够用,但如果要做多 AI 协作(比如让两个 Agent 互相辩论),就需要多个 Durable Object 之间通过 RPC 通信。Cloudflare 在 2024 年推出的 Durable Objects RPC 让这件事变得简单很多,你可以直接在一个 DO 里调用另一个 DO 的方法,不用手动拼 HTTP 请求。

2.3 Workers AI 与外部模型的混合策略

cloudflare-os的另一个设计重点是模型选型。Workers AI 提供了 Llama 3、Mistral、Qwen 等开源模型的推理能力,按神经元计费,价格比调外部 API 便宜不少。但它的短板也很明显:模型版本更新慢,上下文窗口有限,复杂推理能力不如 GPT-4 或 Claude。

所以实际项目里常见的做法是混合策略:

  • 简单意图识别、分类、摘要 → Workers AI
  • 复杂推理、代码生成、多步规划 → 外部大模型 API
  • 向量嵌入 → Workers AI 的 bge 模型
  • 向量检索 → Vectorize

这个策略的核心逻辑是成本分层。Agent 的每一次调用都走大模型,成本会失控。把简单任务下沉到 Workers AI,复杂任务才升级到外部模型,整体成本能降 60% 以上。我在一个客服 Agent 项目里用这个策略,月成本从 200 多刀压到了 70 刀左右。

2.4 工具调用与 Agent 编排

Agent 的能力边界由它能调用的工具决定。在 Workers 环境里,工具就是一个普通的异步函数,通过 fetch 调用外部 API 或者访问绑定资源。cloudflare-os通常会把工具注册成一个映射表:

const tools = { search: async (query) => { /* 调用搜索 API */ }, calculate: async (expr) => { /* 安全求值 */ }, queryVector: async (embedding) => { /* 查 Vectorize */ }, sendEmail: async (params) => { /* 调邮件服务 */ } };

Agent 的编排逻辑就是:把用户输入和工具描述一起发给模型,模型返回要调用的工具名和参数,Worker 执行工具,把结果再喂回模型,循环直到模型返回最终答案。这个循环在 Workers 里要注意 CPU 时间预算,通常设置最大迭代次数为 5 到 8 次,防止死循环烧钱。

多 AI 协作的场景下,编排会更复杂一些。常见模式是主管-工人模式:一个主管 Agent 负责拆解任务,把子任务分发给多个工人 Agent,工人 Agent 各自调用工具,最后主管汇总。这种模式在 Workers 上的实现要点是,主管和工人之间的通信要走 Durable Objects RPC,避免走公网 HTTP 增加延迟。

3. 核心细节解析与实操要点

3.1 项目初始化与依赖配置

动手之前,先把环境搭好。你需要的东西不多:Node.js 18+、一个 Cloudflare 账号、wrangler CLI。我建议用 wrangler 3.x 以上版本,因为 Durable Objects 的 RPC 支持是较新版本才稳定的。

npm install -g wrangler wrangler login npm create cloudflare@latest cloudflare-os -- --template=hello-world cd cloudflare-os

初始化之后,wrangler.toml是整个项目的配置核心。下面这份配置是我在实际项目里验证过的,包含了 Workers AI、Vectorize、Durable Objects 三个绑定:

name = "cloudflare-os" main = "src/index.js" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"] [ai] binding = "AI" [[vectorize]] binding = "VECTOR_INDEX" index_name = "agent-memory" [[durable_objects.bindings]] name = "AGENT_SESSION" class_name = "AgentSession" [[migrations]] tag = "v1" new_classes = ["AgentSession"]

这里有几个细节值得说。compatibility_flags里的nodejs_compat是为了让一些 Node 生态的库能跑起来,比如crypto和buffer。Vectorize 的索引需要提前用wrangler vectorize create命令创建,维度要和你用的嵌入模型对齐,bge-base 是 768 维,bge-large 是 1024 维,选错了后面写入会报错。

注意:Durable Objects 的 migrations 配置一旦发布就不能随便改 tag,否则会导致已有实例无法访问。建议在开发阶段就用正式的类名,不要用临时名字。

3.2 Agent 会话对象的实现

Durable Object 的类定义是整个项目的骨架。下面这个AgentSession类实现了对话历史管理、工具调用循环、记忆检索三个核心功能:

export class AgentSession { constructor(state, env) { this.state = state; this.env = env; this.history = []; } async fetch(request) { const { message } = await request.json(); await this.state.blockConcurrencyWhile(async () => { this.history = (await this.state.storage.get("history")) || []; }); this.history.push({ role: "user", content: message }); const response = await this.runAgentLoop(message); this.history.push({ role: "assistant", content: response }); if (this.history.length > 20) this.history = this.history.slice(-20); await this.state.storage.put("history", this.history); return new Response(JSON.stringify({ reply: response })); } async runAgentLoop(input) { let messages = [ { role: "system", content: "你是一个边缘 Agent,可以调用工具。" }, ...this.history ]; for (let i = 0; i < 5; i++) { const result = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", { messages, tools: TOOL_SCHEMAS }); if (result.tool_calls && result.tool_calls.length > 0) { for (const call of result.tool_calls) { const toolResult = await executeTool(call.name, call.arguments, this.env); messages.push({ role: "tool", content: JSON.stringify(toolResult) }); } } else { return result.response; } } return "达到最大迭代次数,请简化你的问题。"; } }

这段代码里有几个关键点。blockConcurrencyWhile是保证状态读写原子性的,防止并发请求把历史记录写乱。历史记录截断到最近 20 轮是为了控制上下文长度,避免 token 爆炸。工具调用循环限制 5 次是成本保护,实测大部分任务 3 次以内就能完成。

3.3 向量记忆的写入与检索

Agent 的长期记忆靠 Vectorize 实现。写入的时机通常是每轮对话结束后,把用户输入和 Agent 回复拼成一段文本,生成嵌入后存入索引:

async function saveMemory(env, sessionId, text) { const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [text] }); await env.VECTOR_INDEX.upsert([{ id: `${sessionId}-${Date.now()}`, values: embedding.data[0], metadata: { sessionId, text, timestamp: Date.now() } }]); }

检索的时候,先用同样的模型把查询转成向量,然后查 topK:

async function recallMemory(env, query, sessionId, topK = 3) { const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [query] }); const results = await env.VECTOR_INDEX.query(embedding.data[0], { topK, filter: { sessionId }, returnMetadata: true }); return results.matches.map(m => m.metadata.text); }

这里有个坑我踩过:Vectorize 的 filter 语法对 metadata 的类型有要求,如果 sessionId 存成了数字,filter 里用字符串查会查不到。统一用字符串类型最省心。另外 topK 不要设太大,3 到 5 就够了,多了会挤占上下文窗口,反而降低回答质量。

3.4 工具注册与安全边界

工具是 Agent 和外部世界交互的通道,也是最容易出安全问题的地方。我的做法是白名单注册 + 参数校验 + 超时控制三层防护。

白名单注册意味着工具必须显式声明,不能动态执行任意代码。参数校验用简单的 schema 检查,比如搜索工具的 query 必须是字符串且长度小于 200。超时控制用Promise.race包一层,防止某个工具卡死拖垮整个 Agent:

async function executeTool(name, args, env) { const tool = TOOLS[name]; if (!tool) throw new Error(`未知工具: ${name}`); const validated = tool.schema.parse(args); return Promise.race([ tool.handler(validated, env), new Promise((_, reject) => setTimeout(() => reject(new Error("工具超时")), 10000) ) ]); }

注意:绝对不要在工具里直接执行用户传入的字符串作为代码。我见过有人用eval实现计算器工具,这是灾难性的安全漏洞。用专门的表达式解析库,或者干脆只支持四则运算。

4. 完整实操流程与关键环节实现

4.1 从零搭建一个可运行的 Agent 实例

前面讲了各个组件,现在把它们串起来。整个搭建流程分五步:创建项目、配置绑定、实现 DO 类、实现 Worker 入口、部署测试。

第一步创建项目前面已经讲过,这里从配置绑定开始。除了wrangler.toml里的绑定声明,你还需要在 Cloudflare 控制台或者用 CLI 创建 Vectorize 索引:

wrangler vectorize create agent-memory --dimensions=768 --metric=cosine

维度 768 对应 bge-base 模型,metric 用 cosine 是因为文本嵌入的相似度计算用余弦距离最合适。这两个参数创建后不能改,所以要想清楚。

第二步实现 Worker 入口,负责把请求路由到对应的 Durable Object:

export default { async fetch(request, env) { const url = new URL(request.url); const sessionId = url.searchParams.get("session") || "default"; const id = env.AGENT_SESSION.idFromName(sessionId); const stub = env.AGENT_SESSION.get(id); return stub.fetch(request); } };

这里用idFromName而不是newUniqueId,是为了让同一个 sessionId 始终路由到同一个 DO 实例。这是会话一致性的关键。

第三步部署:

wrangler deploy

部署完成后,你会得到一个https://cloudflare-os.<your-subdomain>.workers.dev的地址。测试一下:

curl -X POST "https://cloudflare-os.xxx.workers.dev?session=test1" \ -H "Content-Type: application/json" \ -d '{"message": "你好,帮我记住我喜欢喝美式咖啡"}'

然后再发一条消息问它你喜欢的咖啡,如果它能答出美式,说明记忆链路是通的。

4.2 多 AI 协作的编排实现

单 Agent 跑通之后,多 AI 协作是自然的下一步。我实现过一个“研究员 + 批评者”的双 Agent 模式,研究员负责生成方案,批评者负责挑毛病,循环两轮后输出最终结果。

实现上,主管 Agent 是一个 Durable Object,它内部持有两个子 Agent 的引用。子 Agent 可以是同一个 DO 类的不同实例,通过 RPC 调用:

export class Supervisor { constructor(state, env) { this.state = state; this.env = env; } async fetch(request) { const { task } = await request.json(); const researcherId = this.env.AGENT_SESSION.idFromName("researcher"); const criticId = this.env.AGENT_SESSION.idFromName("critic"); const researcher = this.env.AGENT_SESSION.get(researcherId); const critic = this.env.AGENT_SESSION.get(criticId); let draft = await researcher.generate(task); for (let round = 0; round < 2; round++) { const critique = await critic.review(draft); draft = await researcher.revise(draft, critique); } return new Response(JSON.stringify({ result: draft })); } }

这个模式的关键在于角色提示词的隔离。研究员和批评者用不同的 system prompt,批评者的 prompt 要明确要求它只挑问题不给方案,否则两个 Agent 会互相附和,失去协作价值。我实测下来,两轮迭代是性价比最高的,三轮以上收益递减明显,成本却翻倍。

4.3 流式响应与用户体验优化

Agent 的响应时间通常在 2 到 10 秒,如果等全部生成完再返回,用户会觉得卡。Workers 支持流式响应,可以把模型输出的 token 实时推给前端:

async function streamResponse(env, messages) { const stream = await env.AI.run("@cf/meta/llama-3-8b-instruct", { messages, stream: true }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } }); }

前端用 EventSource 接收,逐字显示。这个改动对体验的提升非常明显,用户感知到的等待时间从 5 秒降到了 1 秒以内。但要注意,流式响应和工具调用循环有冲突,因为工具调用需要等完整结果。我的做法是:第一轮用非流式判断是否需要调工具,如果需要就调完工具后再用流式输出最终答案。

4.4 成本监控与预算控制

Agent 项目最容易失控的就是成本。我在项目里加了一个简单的用量追踪,每次调用模型前先检查当日预算:

async function checkBudget(env, sessionId) { const key = `budget:${new Date().toISOString().slice(0, 10)}`; const used = (await env.KV.get(key)) || 0; if (used > DAILY_LIMIT) { throw new Error("今日预算已用完"); } await env.KV.put(key, used + 1, { expirationTtl: 86400 }); }

KV 的expirationTtl设为 86400 秒,正好一天后自动清零。这个简单的机制能防止某个用户疯狂刷接口把预算烧光。DAILY_LIMIT 根据你的套餐定,免费版建议设 100,付费版可以设 1000 到 5000。

5. 常见问题与排查技巧实录

5.1 部署与运行时问题速查

下面这张表是我在实际项目中遇到过的典型问题,按出现频率排序:

问题现象根本原因解决方法
DO 状态不持久migrations tag 变更回滚 tag,或新建类名
Vectorize 查询为空维度不匹配检查嵌入模型维度与索引维度
工具调用死循环模型反复调同一工具加迭代上限,工具结果加去重标记
流式响应中断CPU 时间超限拆分任务,或升级付费套餐
冷启动慢依赖包过大用 esbuild 打包,剔除无用依赖
跨区域延迟高DO 位置固定用 locationHint 指定就近区域

其中 DO 状态不持久这个问题最隐蔽。表现是本地开发正常,部署后历史记录偶尔丢失。原因是 migrations 的 tag 在多次部署间不一致,导致 Cloudflare 认为这是两个不同的类。解决办法是从第一次部署就用固定的 tag 和类名,后续只追加不修改。

5.2 模型输出不稳定的调优思路

Agent 的输出质量很大程度上取决于提示词和参数。我总结了几条调优经验:

第一,temperature 不要设太高。Agent 场景需要稳定和可预测,temperature 设 0.1 到 0.3 比较合适。设 0.7 以上会导致同样的输入每次输出差异很大,工具调用参数也容易出错。

第二,system prompt 要具体。不要写“你是一个有用的助手”,要写“你是一个客服 Agent,只回答产品相关问题,遇到无关问题回复‘这个问题我帮不了你’”。边界越清晰,行为越可控。

第三,工具描述要写清楚使用场景。模型选择工具的依据是工具描述,描述里要说明什么时候用、什么时候不用。比如搜索工具的描述要写“当用户询问实时信息或你不确定的事实时使用”,而不是只写“搜索网络”。

5.3 记忆检索的精度问题

向量检索不是万能的,我遇到过检索出来的记忆和当前问题不相关的情况。排查下来有几个原因:

  • 嵌入模型对中文的支持不如英文,中文场景建议用多语言模型
  • 记忆文本太长,嵌入被稀释,建议每段记忆控制在 200 字以内
  • topK 太大,引入了噪声,建议先用 3 试试

还有一个技巧是混合检索:向量检索 + 关键词检索,两者结果合并去重。关键词检索用简单的字符串匹配就行,能补上向量检索对专有名词不敏感的短板。我在一个技术文档 Agent 里用这个组合,检索准确率从 70% 提到了 88%。

5.4 调试与日志的最佳实践

Workers 的调试不像本地 Node 那么方便,console.log的输出要用wrangler tail实时查看:

wrangler tail --format pretty

这个命令会实时打印线上 Worker 的日志,排查问题时非常有用。但要注意,生产环境不要打太多日志,一是影响性能,二是可能泄露敏感信息。我的做法是只在关键节点打日志:请求进入、工具调用、模型返回、错误捕获。

对于 Durable Objects 的调试,可以在 DO 内部维护一个调试日志数组,通过一个专门的接口读取。这样能追踪到状态变化的完整过程,比翻 Cloudflare 的日志方便。

6. 项目扩展方向与个人经验

cloudflare-os这个模式跑通之后,扩展空间其实很大。我试过几个方向,分享两个比较有价值的。

一个是接入 R2 做文件处理。Agent 经常需要处理用户上传的文档,把文件存到 R2,用 Workers 的 R2 绑定读取,配合文本提取库做预处理,再喂给模型。R2 没有出网流量费,成本比 S3 低不少。

另一个是用 Cron Triggers 做定时任务。Agent 不只是被动响应,也可以主动执行。比如每天早上让 Agent 汇总昨天的对话,生成一份日报。Cron Triggers 的配置很简单,在wrangler.toml里加一段就行:

[triggers] crons = ["0 9 * * *"]

然后在 Worker 的scheduled方法里实现逻辑。这个能力让 Agent 从“问答工具”变成了“主动助手”,想象空间大很多。

最后说一个我踩过的坑:不要过早优化。我一开始就想着做多 Agent 协作、做复杂的记忆分层,结果基础的单 Agent 对话都没跑稳。后来退回去先把单 Agent 的对话、记忆、工具调用做扎实,再往上叠多 Agent,反而顺利很多。这个项目的复杂度是叠加的,每一步都要验证通过再往下走,不然出了问题根本不知道是哪一层的事。

另外,Workers 的免费额度其实挺够用的,每天 10 万次请求,个人项目和小型应用完全跑得起来。建议先用免费版把整个链路跑通,确认有价值了再升级付费。我见过不少人一上来就买付费套餐,结果项目没做起来,钱白花了。