1. 从“cloudflare-os”这个标题说起:它到底在指什么
第一次看到“cloudflare-os”这个词,我下意识以为是某个操作系统的发行版代号。翻了一圈资料才反应过来,它并不是一个真正意义上的操作系统内核,而是社区对 Cloudflare 边缘计算能力的一种形象化称呼——把 Workers、Durable Objects、KV、R2、D1、Queues、AI 这一整套能力组合起来,形成一个“跑在边缘上的运行时环境”。你可以把它理解成:Cloudflare 把自己的全球网络变成了一台巨大的分布式计算机,而 Workers 就是这台计算机上的“进程”。
这个标题之所以值得单独拿出来聊,是因为它背后对应的是一个非常具体的开发范式转变。传统后端开发,你要先买服务器、装系统、配 Nginx、部署应用、做负载均衡、搞容灾,一整套下来没个几天搞不定。而“cloudflare-os”这套思路下,你写一个export default { async fetch(request, env) { ... } },推上去,全球几百个节点同时生效。没有服务器要管,没有系统要打补丁,没有容量要预估。
关键词里出现了 Cloudflare Workers、AI、agent、open source,这几个词组合在一起,指向的其实是一个很明确的场景:用边缘运行时来承载 AI agent 的后端逻辑。Agent 这类应用有个特点——它需要低延迟响应、需要状态管理、需要调用外部模型 API、需要处理流式输出。这些需求恰好和 Workers 的能力边界高度重合。
这篇文章适合谁看?如果你正在考虑用 Workers 搭一个 agent 服务,或者你已经在用但总觉得哪里别扭,又或者你只是好奇“边缘计算到底能干什么”,那接下来的内容应该能给你一些可以直接抄作业的东西。我会从架构选型、核心组件、实操步骤、踩坑经验几个角度展开,尽量把每个“为什么”讲清楚。
2. 为什么 agent 场景和边缘运行时是天然一对
2.1 Agent 的延迟敏感特性决定了它不适合传统架构
Agent 和普通 Web 应用最大的区别在于交互模式。普通应用是“请求-响应”一次就结束,agent 往往是多轮对话、工具调用、流式输出。用户发一句话,后端可能要调三次模型 API、查两次数据库、执行一次外部工具,最后把结果流式吐回来。这个链路里任何一环慢了,用户体验就崩了。
传统架构下,你的服务器可能在美国东部,用户在中国,光网络往返就 200ms 起步。再加上模型 API 的调用延迟,一轮对话下来两三秒是常态。而边缘运行时的逻辑是:用户的请求打到离他最近的节点,这个节点直接处理逻辑、调用外部 API、返回结果。网络跳数少了,首字节时间能压到几十毫秒级别。
我实测过一个简单的 agent 对话接口,部署在传统 VPS 上,国内用户平均首字节时间在 400ms 左右;换成 Workers 之后,同样的逻辑,首字节时间降到了 80ms 上下。这个差距在流式输出场景下感知特别明显——用户打完字几乎立刻就能看到第一个 token 冒出来。
2.2 Workers 的运行时限制反而倒逼出更干净的架构
很多人第一次看 Workers 的文档会皱眉:没有 Node.js 的完整 API、不能用原生 TCP、执行时间有限制、内存有上限。这些限制看起来是缺点,但实际用下来,它们逼着你把架构做干净。
举个例子。传统 Node.js 项目里,你可能随手就require('fs')读个文件,或者起个后台定时任务。但在 Workers 里这些都不行,你必须把状态外置到 KV、D1 或 Durable Objects,把定时逻辑交给 Cron Triggers。这个约束看起来麻烦,但它带来的好处是:你的应用天然就是无状态的、可水平扩展的、不依赖本地磁盘的。部署的时候没有任何“服务器环境差异”的问题,本地跑通就是线上跑通。
Agent 场景尤其受益于这一点。Agent 的状态(对话历史、工具调用记录、用户偏好)本来就该外置,因为你需要它在多个边缘节点之间共享。Workers 的限制恰好推着你往正确的方向走。
2.3 成本模型:从“养服务器”到“按请求付费”
传统架构下,哪怕没人用,你的服务器也在烧钱。Agent 应用往往有明显的波峰波谷——白天用的人多,凌晨几乎没人。用 VPS 的话,凌晨那台机器照样在计费。
Workers 的计费是按请求数和 CPU 时间来的。免费额度每天 10 万次请求,对于个人项目和小型 agent 完全够用。付费版 5 美元起步,包含 1000 万次请求。这个成本结构对独立开发者特别友好——你可以先跑起来,等真有流量了再考虑优化。
不过这里有个坑要注意:Workers 的 CPU 时间限制是 10ms(免费版)到 30s(付费版,可配置)。Agent 如果涉及复杂的本地计算(比如大文本处理),很容易撞到这个限制。解决办法是把重计算拆出去,或者用 Durable Objects 来承载长任务。
3. 拆解 cloudflare-os 的核心组件:每个部件解决什么问题
3.1 Workers:无状态的计算层
Workers 是整个体系的入口。你写的每个请求处理逻辑都跑在这里。它支持 JavaScript 和 TypeScript,也支持 Rust、Python 等通过 WASM 接入。对于 agent 场景,Workers 主要承担三件事:接收用户请求、编排工具调用、流式返回结果。
一个典型的 agent Worker 长这样:
export default { async fetch(request, env, ctx) { const { messages } = await request.json(); // 调用模型 API const response = await fetch('https://api.example.com/v1/chat', { method: 'POST', headers: { 'Authorization': `Bearer ${env.MODEL_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messages, stream: true }) }); // 流式转发 return new Response(response.body, { headers: { 'Content-Type': 'text/event-stream' } }); } };这段代码看起来简单,但它背后有几个关键设计:env对象承载了环境变量和绑定资源,ctx提供了waitUntil用于后台任务,流式转发直接透传 ReadableStream 不需要缓冲。这些细节决定了 agent 的响应速度和资源消耗。
3.2 Durable Objects:给 agent 一个“记忆体”
Agent 需要记忆。对话历史、用户状态、工具调用中间结果,这些都需要一个地方存。KV 适合读多写少的场景,D1 适合关系型查询,但如果你需要强一致性、需要每个会话有独立的计算实例,那就得用 Durable Objects。
Durable Objects 的概念是:每个对象有一个全局唯一的 ID,所有对这个 ID 的请求都会路由到同一个实例上。这个实例有内存状态、有事务性存储、可以串行处理请求。对于 agent 来说,你可以给每个对话会话分配一个 Durable Object,它负责维护这个会话的完整上下文。
export class AgentSession { constructor(state, env) { this.state = state; this.env = env; this.history = []; } async fetch(request) { const { message } = await request.json(); this.history.push({ role: 'user', content: message }); // 调用模型,带上历史 const reply = await this.callModel(this.history); this.history.push({ role: 'assistant', content: reply }); // 持久化 await this.state.storage.put('history', this.history); return new Response(JSON.stringify({ reply })); } }这个模式的好处是:会话状态天然隔离,不需要担心并发冲突;Durable Object 可以保持长连接,适合 WebSocket 场景;存储是事务性的,不会出现写一半失败的情况。
3.3 AI Gateway:模型调用的统一入口
Agent 通常要调多个模型——可能主力用 GPT,备用 Claude,某些任务用本地小模型。如果每个模型都直连,密钥管理、限流、缓存、日志都得自己搞。Cloudflare 的 AI Gateway 就是来解决这个问题的。
它做的事情很直接:你把它当成一个代理层,所有模型请求都发给它,它负责转发、缓存、限流、记录。配置方式是在 Workers 里把请求地址换成 Gateway 的地址,带上你的 Gateway ID。
const response = await fetch( `https://gateway.ai.cloudflare.com/v1/${env.ACCOUNT_ID}/${env.GATEWAY_ID}/openai/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4', messages }) } );用 Gateway 之后,你能在面板上看到每个模型的调用量、延迟分布、错误率。缓存功能对 agent 特别有用——相同的系统提示词和工具定义,第二次调用可以直接命中缓存,省 token 也省时间。
3.4 向量数据库与 RAG 支持
Agent 要做知识问答,就离不开 RAG。Cloudflare 的 Vectorize 是专门为 Workers 设计的向量数据库。你把文档 embedding 之后存进去,查询的时候用相似度检索。
整个流程是这样的:先用 Workers AI 的 embedding 模型把文档切片向量化,存到 Vectorize;用户提问时,把问题向量化,去 Vectorize 查最相似的片段,拼到 prompt 里发给模型。
// 存入 const embeddings = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: chunks }); await env.VECTORIZE.insert(chunks.map((chunk, i) => ({ id: `chunk-${i}`, values: embeddings.data[i], metadata: { text: chunk } }))); // 查询 const queryEmbedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: [userQuestion] }); const matches = await env.VECTORIZE.query(queryEmbedding.data[0], { topK: 5, returnMetadata: true });这套组合的好处是全在 Cloudflare 内部,没有跨服务调用的网络开销,也不需要额外管理一个向量数据库实例。
4. 从零搭一个 agent 后端:完整实操路径
4.1 环境准备与项目初始化
第一步是装 Wrangler,这是 Cloudflare 的官方 CLI 工具。
npm install -g wrangler wrangler login登录之后,创建一个新项目:
npm create cloudflare@latest my-agent -- --template=hello-world cd my-agent生成的目录结构里,src/index.js是入口,wrangler.toml是配置文件。你需要在这个配置文件里声明各种绑定:
name = "my-agent" main = "src/index.js" compatibility_date = "2024-01-01" [[kv_namespaces]] binding = "AGENT_KV" id = "your-kv-id" [[d1_databases]] binding = "AGENT_DB" database_name = "agent-db" database_id = "your-db-id" [ai] binding = "AI" [[vectorize]] binding = "VECTORIZE" index_name = "agent-index" [[durable_objects.bindings]] name = "AGENT_SESSION" class_name = "AgentSession" [[migrations]] tag = "v1" new_classes = ["AgentSession"]这里有个容易忽略的点:compatibility_date决定了你能用哪些运行时特性。设得太早,新 API 用不了;设得太晚,可能有行为变更。我的经验是设成当前日期往前推一个月,既稳定又能用到大部分新功能。
4.2 对话循环的实现细节
Agent 的核心是一个循环:接收输入 → 判断是否需要调工具 → 执行工具 → 把结果喂回模型 → 生成最终回复。这个循环在 Workers 里实现,有几个关键决策点。
第一个决策:流式还是非流式。流式体验好,但实现复杂。我的建议是首版先做非流式,跑通逻辑之后再改成流式。流式的核心是把模型的 SSE 流透传给前端,中间不要做缓冲。
第二个决策:工具调用怎么编排。简单场景可以用 if-else 硬编码,复杂场景建议用状态机。Workers 里没有现成的 agent 框架,但你可以自己写一个轻量的调度器:
async function runAgent(messages, tools, env) { let maxIterations = 5; while (maxIterations-- > 0) { const response = await callModel(messages, tools, env); if (response.finish_reason === 'tool_calls') { for (const call of response.tool_calls) { const result = await executeTool(call, env); messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) }); } continue; } return response.content; } throw new Error('Agent loop exceeded max iterations'); }这个循环里,maxIterations是必须的。我踩过一次坑:模型陷入死循环,反复调用同一个工具,请求一直不返回,最后撞到 Workers 的 CPU 时间限制被强制终止。加上迭代上限之后,至少能保证请求会结束。
4.3 状态管理的三种方案对比
Agent 的状态存哪里,是个需要提前想清楚的问题。我把三种方案列出来对比:
| 方案 | 适用场景 | 一致性 | 延迟 | 成本 |
|---|---|---|---|---|
| KV | 读多写少,如配置、用户偏好 | 最终一致 | 低 | 低 |
| D1 | 关系型查询,如对话列表、统计 | 强一致 | 中 | 中 |
| Durable Objects | 会话状态、实时协作 | 强一致 | 低 | 较高 |
我的实际做法是组合使用:用户的基本信息放 KV,对话的元数据(标题、时间、token 数)放 D1,每个会话的完整上下文放 Durable Object。这样查询列表走 D1 很快,进入具体会话时从 DO 加载上下文,各取所需。
注意:Durable Objects 的存储是按读写次数和存储量计费的,如果会话很多但每个会话的消息很少,用 DO 可能不划算。这种情况下可以把多个会话合并到一个 DO 里管理。
4.4 部署与灰度发布
Workers 的部署命令很简单:
wrangler deploy但它支持灰度发布,这个功能很多人不知道。你可以在wrangler.toml里配置:
[env.production] routes = [ { pattern = "api.example.com/*", zone_name = "example.com" } ] [env.staging] routes = [ { pattern = "staging-api.example.com/*", zone_name = "example.com" } ]然后分别部署:
wrangler deploy --env staging wrangler deploy --env production更细粒度的灰度可以用 Workers 的版本管理功能,把 10% 的流量导到新版本,观察错误率和延迟,没问题再全量。这个在 agent 场景下特别重要,因为模型行为有时候会有微妙变化,全量发布风险太大。
5. 那些文档里不会写的踩坑记录
5.1 CPU 时间限制:最容易被低估的约束
Workers 的 CPU 时间限制和“墙钟时间”是两回事。墙钟时间可以很长(比如等模型 API 返回),但 CPU 时间只算你实际执行代码的时间。免费版 10ms,付费版默认 30s。
问题在于,JSON 解析、字符串拼接、循环处理这些操作都算 CPU 时间。我有一次处理一个 500KB 的对话历史,光是JSON.parse和JSON.stringify就花了 8ms,再加上遍历消息做 token 估算,直接撞到免费版的限制。
解决办法有两个:一是升级到付费版,30s 的 CPU 时间基本够用;二是把重计算拆到 Durable Object 里,DO 的 CPU 限制更宽松。我现在的做法是:Workers 只做请求转发和轻量编排,所有涉及大量数据处理的操作都丢给 DO。
5.2 流式响应的缓冲陷阱
流式输出最怕的就是中间有缓冲。我一开始的实现是这样的:
const response = await fetch(modelApi); const text = await response.text(); // 这里会等全部返回 return new Response(text);这样写完全失去了流式的意义。正确的做法是直接透传 ReadableStream:
const response = await fetch(modelApi); return new Response(response.body, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' } });但这里还有个坑:某些中间层(比如某些 CDN 或代理)会自动缓冲响应。你需要确保Content-Type是text/event-stream,并且加上X-Accel-Buffering: no头。Cloudflare 自己的网络不会缓冲,但如果你的域名前面还有别的代理,就要注意这一点。
5.3 环境变量的作用域问题
Wrangler 支持在wrangler.toml里定义环境变量,也支持用wrangler secret put设置加密变量。我踩过的坑是:在本地开发时用.dev.vars文件,部署时忘了把变量同步到线上,结果线上报错说 API key 未定义。
现在的做法是:所有敏感变量都用wrangler secret put设置,本地开发用.dev.vars,并且在 CI 流程里加一步检查,确保线上环境的 secret 列表和本地一致。另外,wrangler.toml里的[vars]是明文存储的,千万不要把密钥写在那里。
5.4 模型 API 的超时与重试
Agent 调用模型 API 时,网络抖动是常态。如果不做超时和重试,用户会看到莫名其妙的失败。我的做法是封装一个带重试的 fetch:
async function fetchWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeout); if (response.ok) return response; if (response.status < 500) return response; // 客户端错误不重试 } catch (e) { if (i === maxRetries - 1) throw e; } await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); } }指数退避是关键。第一次重试等 1 秒,第二次 2 秒,第三次 4 秒。这样既能应对临时故障,又不会在服务真正宕机时疯狂重试。
6. 把 agent 做得更“聪明”的几个进阶思路
6.1 用 Workers AI 做本地推理兜底
Cloudflare 自己的 Workers AI 提供了不少开源模型,比如 Llama 系列、Mistral 系列。这些模型的优势是跑在 Cloudflare 的 GPU 上,调用延迟低,而且计费是按神经元操作数算的,比调外部 API 便宜。
我的用法是:简单任务(意图分类、实体提取、格式转换)直接用 Workers AI 的轻量模型,复杂任务才调外部的大模型。这样能省不少 token 成本,而且响应更快。
const intent = await env.AI.run('@cf/meta/llama-3-8b-instruct', { messages: [ { role: 'system', content: 'Classify the user intent into: question, command, chitchat' }, { role: 'user', content: userMessage } ] });6.2 多 agent 协作的编排模式
单个 agent 能力有限,复杂任务需要多个 agent 协作。在 Workers 里实现这个,可以用 Durable Objects 做协调器,每个子 agent 是一个独立的 Worker 或者 DO 方法。
协调器负责拆解任务、分配子任务、汇总结果。子 agent 之间不直接通信,都通过协调器中转。这样做的好处是逻辑清晰,每个子 agent 可以独立测试和部署。
export class Orchestrator { async fetch(request) { const { task } = await request.json(); // 拆解任务 const subtasks = await this.plan(task); // 并行执行 const results = await Promise.all( subtasks.map(st => this.executeSubtask(st)) ); // 汇总 return new Response(JSON.stringify({ result: await this.synthesize(results) })); } }6.3 可观测性:日志、指标、追踪
Agent 出问题的时候,最难的是定位是哪一步出了问题。Cloudflare 提供了 Workers Logs 和 Analytics Engine,但默认的日志信息不够细。
我的做法是在关键节点打结构化日志:
console.log(JSON.stringify({ event: 'model_call', model: 'gpt-4', input_tokens: countTokens(messages), latency_ms: Date.now() - start, session_id: sessionId }));然后在 Analytics Engine 里建一个数据集,把这些日志写进去,用 SQL 查询分析。比如查“过去一小时平均延迟超过 5 秒的会话”,或者“token 消耗最多的前 10 个用户”。
这套东西搭起来要花点时间,但一旦 agent 上线有真实用户,没有可观测性就是睁眼瞎。
7. 关于开源与社区生态的一些观察
“cloudflare-os”这个概念能火起来,很大程度上是因为开源社区在推动。Workers 本身是闭源服务,但围绕它的工具链、模板、框架基本都是开源的。比如 Hono 这个 Web 框架,就是专门为 Workers 设计的,路由性能比 Express 快一个数量级。
Agent 相关的开源项目也在涌现。有做 agent 编排的、有做 RAG 管道的、有做工具集成的。我的建议是:核心业务逻辑自己写,通用能力尽量用社区方案。比如向量检索、文本分块、token 计数这些,没必要重复造轮子。
但要注意版本锁定。Workers 的运行时更新比较频繁,某些依赖可能在某个版本之后就不兼容了。我的做法是在package.json里锁定精确版本,升级的时候先在 staging 环境跑一遍完整测试。
另外,Workers 的compatibility_date和compatibility_flags要配合使用。有些新特性需要显式开启 flag,比如nodejs_compat让你能用部分 Node.js API。这个 flag 开了之后,包体积会变大,冷启动可能变慢,需要权衡。
8. 我个人的一些实操体会
用 Workers 搭 agent 后端这段时间,最大的感受是:约束不是敌人,是朋友。Workers 的各种限制逼着我把架构想清楚,把状态外置,把逻辑拆干净。最后写出来的代码比传统 Node.js 项目更简洁,部署更省心,运维成本几乎为零。
另一个体会是:不要过早优化。我一开始就想做流式、做多 agent、做 RAG,结果每个都做了一半,哪个都不稳定。后来退回来,先把最简单的“请求-模型-响应”跑通,再逐步加功能,反而快很多。
还有一个很实际的建议:本地开发一定要用wrangler dev。它启动一个本地运行时,模拟线上的环境,包括各种绑定。虽然有些行为(比如 Durable Objects 的分布式特性)本地模拟不了,但大部分逻辑都能在本地验证。配合--remote参数,还能连到线上的真实资源调试。
最后说一个容易被忽略的点:Workers 的冷启动。虽然官方说冷启动在 5ms 以内,但实际测下来,如果包体积大、依赖多,冷启动可能到几十毫秒。对于 agent 这种延迟敏感的场景,建议把包体积控制在 1MB 以内,依赖能少则少。用 esbuild 做 tree-shaking,把没用到的代码都摇掉。
这套东西还在快速演进,新的绑定、新的模型、新的工具每个月都在出。保持关注官方 changelog 和社区讨论,比闷头写代码更重要。