资讯详情

资讯详情

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 cloudflarelatest cloudflare-os -- --templatehello-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() } }]); }检索的时候先用同样的模型把查询转成向量然后查 topKasync 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包一层防止某个工具卡死拖垮整个 Agentasync 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 --dimensions768 --metriccosine维度 768 对应 bge-base 模型metric 用 cosine 是因为文本嵌入的相似度计算用余弦距离最合适。这两个参数创建后不能改所以要想清楚。第二步实现 Worker 入口负责把请求路由到对应的 Durable Objectexport 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?sessiontest1 \ -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 万次请求个人项目和小型应用完全跑得起来。建议先用免费版把整个链路跑通确认有价值了再升级付费。我见过不少人一上来就买付费套餐结果项目没做起来钱白花了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →