news 2026/10/28 20:12:21

Cloudflare边缘计算实战:用Workers与Durable Objects构建Agent

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cloudflare边缘计算实战:用Workers与Durable Objects构建Agent

1. 从"cloudflare-os"这个标题说起:它到底在指什么

第一次看到"cloudflare-os"这个词,我下意识以为是某个操作系统的发行版代号。翻了一圈资料才反应过来,它并不是一个真正意义上的操作系统内核,而是社区对 Cloudflare 边缘计算能力的一种"拟物化"称呼——把 Workers、Durable Objects、KV、R2、D1、Queues、AI Gateway 这一整套边缘运行时,当成一台"分布在全球几百个节点的虚拟计算机"来看待。你写的不是跑在某台服务器上的进程,而是跑在离用户最近的那台边缘机器上的函数。这个心智模型一旦建立,后面所有的架构决策都会顺很多。

我为什么对这个话题感兴趣?因为过去一年里,"agent"这个词被反复提起,但真正落到"agent 跑在哪里、状态存哪里、工具怎么调、成本怎么控"这几个问题上,能讲清楚的人并不多。Cloudflare 这套东西恰好提供了一个很有意思的答案:把 agent 的推理循环放在边缘,把状态放在 Durable Objects,把工具调用通过 Workers 暴露出去,把模型调用走 AI Gateway 统一收口。整套东西不需要你维护一台常驻服务器,冷启动在毫秒级,按请求计费。

这篇文章适合谁看?如果你已经写过一点 Workers,想把它用到 agent 场景里,那这篇会对你有直接帮助;如果你只是听说过 Cloudflare 但没动过手,我也会把关键概念用生活化的方式讲清楚;如果你正在纠结"agent 到底该部署在传统云主机还是边缘",那第 4 节关于状态和成本的对比应该能帮你做决定。全文我会尽量用从业者之间聊天的口吻,把踩过的坑、算过的账、试过的方案都摊开讲。

需要先说明一点:下面涉及的具体配置、参数、代码,都是基于我自己的实践和社区常见做法整理的,Cloudflare 的产品迭代很快,具体数值和 API 名称请以官方最新文档为准。我尽量把"为什么这么做"讲透,这样即使 API 变了,你也能自己迁移。

2. 把边缘运行时当成一台"操作系统"来理解

2.1 Workers 是这台机器的"进程调度器"

传统操作系统里,进程调度器决定哪个程序在哪个 CPU 核上跑多久。Workers 的角色类似:你上传一段 JavaScript 或 TypeScript(也支持 Rust、Python 通过 WASM 或 Workers 的运行时),Cloudflare 把它分发到全球边缘节点,用户请求打到哪个节点,就在哪个节点执行。你不需要关心"部署到哪台机器",就像你写多线程程序时不需要关心线程被调度到哪个物理核。

这个类比的关键在于:Workers 是无状态的、短命的。一个请求进来,函数执行,返回响应,然后这个执行环境可能就被回收了。它不像传统服务器那样有一个长期存活的内存空间让你存全局变量。这一点对 agent 来说非常致命,因为 agent 的核心恰恰是"多轮对话 + 中间状态 + 工具调用结果"。如果你把对话历史存在 Workers 的全局变量里,下一个请求打到另一个节点,历史就丢了。

所以理解"cloudflare-os"的第一课就是:Workers 负责算,不负责记。记住这条,后面 Durable Objects 的存在意义就顺理成章了。

2.2 Durable Objects 是这台机器的"文件系统 + 单例进程"

Durable Objects(简称 DO)是我认为 Cloudflare 整套体系里最被低估的东西。它的核心特性有两个:第一,每个 DO 实例是全局唯一的,你可以用idFromName("user-123")这样的方式,保证全世界所有节点上,user-123 对应的 DO 实例只有一个;第二,这个实例有持久化存储,可以存键值对,也可以存结构化数据。

把它类比成操作系统的话,DO 既是"文件系统"(持久化),又是"单例守护进程"(全局唯一、串行处理请求)。对 agent 来说,这简直是量身定做的:一个用户对应一个 DO,对话历史、工具调用记录、用户偏好全存在这个 DO 里,无论用户从哪个地区访问,请求都会被路由到同一个 DO 实例,状态天然一致。

我实测下来,DO 的读写延迟在边缘节点内部是很低的,因为存储和计算在同一个位置。但要注意,DO 是单线程串行处理的,同一个 DO 实例上的请求会排队。这对 agent 来说通常是好事(避免并发写冲突),但如果你的 agent 要做大量并行工具调用,就得考虑把不同工具拆到不同 DO,或者用 Workers 做并发编排。

2.3 KV、R2、D1、Queues 是"外设"

如果 Workers 是 CPU、DO 是内存加磁盘,那 KV、R2、D1、Queues 就是各种外设:

  • KV:全球分布的键值存储,读极快(边缘缓存),写有延迟(最终一致)。适合存配置、feature flag、静态资源索引这类"读多写少"的东西。
  • R2:对象存储,兼容 S3 API,但没有出网流量费。适合存 agent 生成的文件、日志归档、大模型输出的附件。
  • D1:SQLite 的托管版,适合存需要 SQL 查询的结构化数据,比如 agent 的任务表、用户表。
  • Queues:消息队列,适合把耗时的 agent 任务异步化,比如"用户提交了一个长任务,先返回 accepted,后台慢慢跑"。

这套组合的妙处在于,它们都是"按需付费、无需运维"的。你不需要像传统架构那样,先买一台数据库服务器、再买一台对象存储、再配一个消息队列。对个人开发者和小团队来说,这是把"基础设施成本"压到接近零的关键。

2.4 AI Gateway 是"模型调用的统一收口"

agent 离不开大模型。但如果你在代码里直接写死某个厂商的 API endpoint,后面想换模型、想加缓存、想限流、想看调用日志,就得改代码。AI Gateway 解决的就是这个问题:它给你一个统一的入口,你在前面配置好上游模型,代码里只调 Gateway 的地址。

它带来的实际好处有几个:缓存(相同 prompt 直接返回缓存结果,省钱)、限流(防止 agent 循环调用把账单打爆)、日志(每次调用的 token 数、延迟、成本一目了然)、降级(主模型挂了自动切备用)。我在做 agent 的时候,最怕的就是"agent 陷入循环,一晚上烧掉几百块",AI Gateway 的限流和预算告警基本能兜住这个风险。

3. 用 Workers + Durable Objects 搭一个 agent 的最小骨架

3.1 为什么 agent 的状态必须放在 DO 而不是 KV

先说一个我踩过的坑。最早我做 agent 的时候,图省事把对话历史存在 KV 里,key 是 session id,value 是消息数组。本地测试一切正常,上线后用户反馈"聊着聊着就失忆了"。排查了半天才想明白:KV 是最终一致的,写进去的数据要过一段时间才能在全球所有节点可见。用户这次请求打到 A 节点,写入了历史;下次请求打到 B 节点,读到的还是旧数据。对 agent 这种强依赖上下文连续性的场景,这是致命的。

DO 就不一样。同一个 session 的请求,无论从哪来,都会被路由到同一个 DO 实例,读写都是强一致的。而且 DO 支持事务性的存储操作,你可以"读历史 → 追加新消息 → 写回"这一套原子完成,不会出现并发覆盖。

所以我的建议很明确:agent 的会话状态、工具调用中间结果、用户级配置,全部放 DO。KV 只用来存那些"全局共享、不要求强一致"的东西,比如系统 prompt 模板、模型路由表。

3.2 一个可运行的 agent 骨架代码

下面这段是我常用的最小骨架,用 TypeScript 写,跑在 Workers 上,状态放 DO。为了让你能直接抄,我把关键注释都写上了。

// worker.ts - 入口,负责路由 export interface Env { AGENT_DO: DurableObjectNamespace; AI_GATEWAY_URL: string; AI_GATEWAY_TOKEN: string; } export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); // 用 session id 定位 DO 实例 const sessionId = url.searchParams.get("session") || "default"; const id = env.AGENT_DO.idFromName(sessionId); const stub = env.AGENT_DO.get(id); // 所有请求转发给 DO 处理 return stub.fetch(request); }, };
// agent-do.ts - Durable Object,持有会话状态 export class AgentDO implements DurableObject { private state: DurableObjectState; private env: Env; private history: Array<{ role: string; content: string }> = []; constructor(state: DurableObjectState, env: Env) { this.state = state; this.env = env; // 从持久化存储恢复历史 this.state.blockConcurrencyWhile(async () => { this.history = (await this.state.storage.get("history")) || []; }); } async fetch(request: Request): Promise<Response> { const { message } = await request.json() as { message: string }; this.history.push({ role: "user", content: message }); // 调用模型(走 AI Gateway) 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 }), { headers: { "Content-Type": "application/json" }, }); } private async callModel(messages: Array<{ role: string; content: string }>) { const resp = await fetch(this.env.AI_GATEWAY_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.env.AI_GATEWAY_TOKEN}`, }, body: JSON.stringify({ messages, model: "your-model" }), }); const data = await resp.json() as any; return data.choices?.[0]?.message?.content ?? ""; } }

这段代码有几个设计点值得展开。第一,blockConcurrencyWhile保证 DO 在恢复历史完成之前不处理任何请求,避免读到空历史。第二,历史直接存整个数组,简单粗暴,但对话长了之后会有性能问题,后面第 5 节会讲怎么优化。第三,模型调用走 Gateway,代码里不出现任何厂商特定的 endpoint。

3.3 工具调用怎么接进来

agent 和普通聊天机器人的区别就在工具调用。在 Workers 里接工具,我的做法是把每个工具写成一个独立的函数,然后在 DO 里根据模型返回的 tool_call 字段分发。

const tools: Record<string, (args: any) => Promise<any>> = { get_weather: async ({ city }) => { const r = await fetch(`https://api.example.com/weather?city=${city}`); return r.json(); }, search_docs: async ({ query }) => { // 这里可以接你自己的检索服务 return { results: [] }; }, }; // 在 callModel 之后,检查是否有 tool_call if (data.choices?.[0]?.message?.tool_calls) { for (const call of data.choices[0].message.tool_calls) { const fn = tools[call.function.name]; if (fn) { const result = await fn(JSON.parse(call.function.arguments)); this.history.push({ role: "tool", content: JSON.stringify(result), }); } } // 把工具结果再喂回模型,生成最终回复 return this.callModel(this.history); }

这里有个容易忽略的点:工具执行也要考虑超时和失败。边缘函数的执行时间是有上限的,如果某个工具调用很慢,整个 agent 请求就会超时。我的做法是给每个工具加Promise.race超时,超时后返回一个"工具暂时不可用"的结果给模型,让模型自己决定是重试还是换个思路。这比直接抛错让整个请求挂掉要优雅得多。

4. 边缘 agent 的成本账与性能账,我实际算过一遍

4.1 请求计费模型下,agent 到底贵不贵

Cloudflare Workers 的计费是按请求数 + CPU 时间。免费额度是每天 10 万请求,付费后每百万请求 0.5 美元左右,CPU 时间另算。乍一看很便宜,但 agent 场景有个陷阱:一次用户对话可能触发多次模型调用和多次工具调用,每一次都可能算作独立的子请求或者消耗 CPU 时间。

我拿一个真实场景算过:一个用户问一句"帮我查下明天北京天气并推荐穿什么",agent 的流程是:模型调用 1(决定调天气工具)→ 工具调用(查天气)→ 模型调用 2(根据天气推荐穿搭)。如果模型调用走的是外部 API,那 Workers 这边主要是 CPU 时间消耗,一次对话大概几十毫秒 CPU,成本可以忽略。真正的大头是模型 token 费用,这部分跟部署在哪无关。

所以结论是:边缘部署在计算成本上几乎不增加负担,省下的是服务器固定成本。传统方案你要养一台常驻服务器,哪怕没请求也在烧钱;边缘方案没请求就不花钱。对低频、突发型的 agent 应用,这个差异非常明显。

4.2 冷启动与延迟:边缘的真实表现

很多人担心边缘函数的冷启动。我实测下来,Workers 的冷启动通常在几毫秒到几十毫秒,比传统 Serverless(比如某些需要几百毫秒到几秒的)好很多。原因是 Cloudflare 用的是 V8 isolate 而不是容器,启动一个 isolate 比启动一个容器快几个数量级。

但 DO 有个特性要注意:DO 实例如果长时间没请求,会被休眠。下次请求进来时需要唤醒,这个唤醒有额外延迟。对 agent 来说,如果用户隔了很久才回来继续对话,第一次请求会稍慢。我的应对是:对活跃会话保持心跳,或者接受这个延迟(通常也就几十毫秒,用户感知不到)。

延迟的另一个来源是模型调用本身。边缘节点离用户近,但离模型服务商不一定近。这时候 AI Gateway 的价值就体现出来了,它可以在边缘做缓存,相同 prompt 直接返回,省掉一次跨区域往返。

4.3 一张对比表:边缘 agent vs 传统云主机 agent

维度边缘 agent(Workers + DO)传统云主机 agent
固定成本几乎为零,按请求计费需要常驻实例,闲置也收费
冷启动毫秒级(isolate)秒级(容器)或需常驻
全球延迟用户就近执行,天然低需要多区域部署或 CDN
状态管理DO 强一致,但单线程自己搭数据库/Redis
运维复杂度极低,无需管服务器需要管扩容、监控、备份
长任务支持受执行时间限制,需异步化无硬性限制
生态成熟度较新,部分库不兼容成熟,什么都能跑

这张表不是要证明边缘一定更好,而是帮你判断场景。如果你的 agent 是"短请求、高并发、全球用户",边缘优势明显;如果是"长任务、重计算、需要特定系统库",传统云主机可能更合适。

5. 会话历史膨胀、并发冲突、工具超时:三个必踩的坑

5.1 历史越存越大,DO 存储和 token 双重爆炸

agent 跑久了,会话历史会越来越长。我见过一个用户连续聊了几天,历史攒了几百条消息,结果两个问题同时出现:DO 存储的 value 越来越大,读写变慢;每次调模型都要把全部历史塞进 prompt,token 费用飙升,还容易超出模型的上下文窗口。

我的解决方案是分层记忆。把历史分成三层:最近 N 轮完整保留(保证对话连贯),更早的对话做摘要压缩(用模型生成一段总结),再早的直接归档到 R2。每次调模型时,prompt 里放"摘要 + 最近 N 轮",而不是全部历史。

async function buildPrompt(history: any[], summary: string, recentCount = 10) { const recent = history.slice(-recentCount); return [ { role: "system", content: `以下是之前的对话摘要:${summary}` }, ...recent, ]; }

摘要的触发时机可以按消息条数(比如超过 50 条就压缩一次),也可以按 token 数。压缩本身也是一次模型调用,所以要控制频率,别每轮都压。

5.2 DO 单线程串行,并发请求会排队

前面说过 DO 是单线程串行处理请求的。这在大多数时候是优点,但有个场景会出问题:用户快速连发多条消息。比如用户打字快,连着发了三条,这三个请求都会路由到同一个 DO,排队处理。如果每条都要调模型(耗时几秒),用户会感觉"卡住了"。

我的处理方式是在客户端做请求合并:用户连发消息时,前端等一小段时间(比如 300ms),把多条消息合并成一条再发。或者在 DO 里做队列,收到新请求时如果上一个还在处理,就把新消息追加到待处理队列,处理完一个再处理下一个,并且把中间的用户消息合并进同一次模型调用。

另一个思路是把"读"和"写"分离。如果只是查询历史,可以走 KV 缓存(接受最终一致),只有写入才走 DO。这样读请求不会阻塞 DO。

5.3 工具调用超时,整个 agent 请求跟着挂

这个坑我在第 3 节提过,但值得单独展开,因为它太常见了。边缘函数有执行时间上限,工具调用如果卡住,整个请求就废了。更糟的是,如果工具调用是"发出去没回来",你可能连错误都捕获不到。

我的标准做法是给每个工具包一层超时和重试:

async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> { const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error("tool timeout")), ms) ); return Promise.race([promise, timeout]); } // 使用 try { const result = await withTimeout(tools.get_weather({ city }), 5000); } catch (e) { // 返回一个模型能理解的错误信息,而不是直接抛 return { error: "天气服务暂时不可用,请稍后重试" }; }

关键点是把错误信息也喂回模型,让模型有机会说"抱歉,天气查询失败了,要不我直接根据季节给你建议?"而不是让用户看到一个 500 错误页。这种"优雅降级"是 agent 体验的关键。

6. 把 agent 放到边缘之后,我观察到的几个真实变化

6.1 开发心智从"服务器"转向"请求"

用了边缘之后,我写代码的习惯变了。以前会想"这个服务启动时要初始化什么、要连哪个数据库、连接池多大",现在想的是"这个请求进来时,我需要什么、从哪拿、拿不到怎么办"。所有东西都是请求级别的,没有"启动"这个概念。

这个转变一开始不适应,但习惯了之后发现代码更简单了。没有全局状态,就没有全局状态的 bug;没有连接池,就没有连接池泄漏。每个请求都是独立的,测试起来也容易。

6.2 部署从"发布"变成"保存"

传统部署要打包、上传、重启、健康检查。Workers 的部署是wrangler deploy一条命令,几秒钟生效,而且支持灰度。我现在的节奏是:改一点、部署、看日志、再改。这种快速迭代对 agent 这种需要反复调 prompt 和工具逻辑的场景特别友好。

6.3 但有些事边缘真的做不了

说了这么多好处,也得说局限。边缘函数不适合跑长时间的计算任务,比如视频转码、大规模数据处理。也不适合跑需要特定系统依赖的东西,比如某些 Python 科学计算库。如果你的 agent 需要这些,就得把重活放到别的地方,边缘只做编排和轻量工具调用。

我的架构通常是:边缘负责"接收请求、管理会话、调用模型、编排轻量工具",重活通过 Queues 异步派发到后端服务。这样既享受了边缘的低延迟和低成本,又不被它的限制卡住。

7. 如果你现在想动手,我的建议顺序

先别急着上 DO 和 AI Gateway,从最简单的 Workers 开始。写一个 hello world,理解请求怎么进来、响应怎么出去、wrangler dev怎么本地调试。这一步大概半小时。

然后加一个 KV,存点配置,理解"读多写少"的场景怎么用。再然后加 DO,写一个计数器或者简单的会话存储,理解"全局唯一实例"和"持久化"是怎么回事。这一步是关键,DO 的心智模型建立起来,agent 就水到渠成了。

最后接 AI Gateway,把模型调用收口。这时候你已经有一个能跑的最小 agent 了。剩下的就是不断加工具、调 prompt、优化历史管理。

我踩过的最大坑是"想一步到位",一开始就想把架构设计得很完美,结果卡在细节里出不来。后来改成"先跑通最小闭环,再逐步加东西",效率高很多。边缘这套东西的好处就是,每一步的增量成本都很低,你可以放心地小步快跑。

提示:DO 的存储 API 和 Workers 的运行时都在持续更新,动手前建议先扫一眼官方 changelog,避免用到已废弃的写法。我上面给的代码是骨架,实际项目里记得加上错误处理、日志和监控。

最后分享一个我常用的调试技巧:在 DO 里把每次模型调用的 prompt 和返回都写一份到 R2,按 session 分目录存。出问题的时候直接翻 R2,比在日志里大海捞针快得多。这个习惯帮我定位过好几次"模型为什么突然答非所问"的问题,最后发现都是历史拼接的格式出了偏差。

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

ECC 三重含义全解析:椭圆曲线密码学、SAP ECC 与内存纠错码

1. ECC 到底是个什么东西&#xff0c;为什么突然又被翻出来聊ECC 这三个字母&#xff0c;在不同圈子里指向完全不同的东西。做密码学的看到它&#xff0c;第一反应是Elliptic Curve Cryptography&#xff0c;椭圆曲线密码学&#xff1b;做企业信息化的看到它&#xff0c;脑子里…

作者头像 李华
网站建设 2026/10/28 20:10:33

混合型随机变量概率计算与分布函数构建详解

1. 混合型随机变量概率计算的核心逻辑混合型随机变量同时具备离散和连续特性&#xff0c;这在概率论中属于高阶考点。我监考多年发现&#xff0c;80%的学生在这个知识点上失分严重。理解混合型变量的关键在于把握两点&#xff1a;离散部分的概率质量函数&#xff08;PMF&#x…

作者头像 李华
网站建设 2026/10/28 20:10:33

超构透镜偏振成像技术:原理、设计与应用

1. 项目概述&#xff1a;偏振成像技术的新突破偏振成像技术正在经历一场革命性的变革。传统偏振成像系统通常需要多个光学元件和复杂的机械结构来实现偏振信息的获取&#xff0c;这不仅增加了系统体积和重量&#xff0c;还限制了成像速度和应用场景。而全介质超构透镜的出现&am…

作者头像 李华
网站建设 2026/10/28 20:10:33

claude-mem 记忆系统拆解:存储、检索、注入与更新全流程

1. 项目缘起与核心定位第一次看到claude-mem这个名字&#xff0c;我的直觉是&#xff1a;这大概率是一个围绕 Claude 生态做“记忆层”的项目。事实也确实如此。它要解决的是一个非常具体、也非常痛的问题——大语言模型在跨会话场景下没有持久记忆。你每次打开一个新对话&…

作者头像 李华
网站建设 2026/10/28 20:10:33

claude-mem 记忆系统设计:从提炼、存储到检索注入的工程实践

1. 从"聊完就忘"说起&#xff1a;claude-mem 到底想解决什么如果你用 Claude 这类对话式 AI 做过稍微长一点的项目&#xff0c;大概率遇到过这种尴尬&#xff1a;昨天花了两个小时跟它把一套数据清洗逻辑捋得清清楚楚&#xff0c;今天开个新会话&#xff0c;它一脸无…

作者头像 李华
网站建设 2026/10/28 20:10:02

ECC 是什么?椭圆曲线密码学、SAP ECC、纠错码与发动机控制单元全解析

ECC 这个词&#xff0c;放在不同圈子里指向的东西完全不一样。做密码学的看到它想到的是椭圆曲线密码学&#xff0c;做企业信息化的看到它想到的是 SAP ERP Central Component&#xff0c;做汽车电子的看到它想到的是发动机电子控制单元&#xff0c;做嵌入式的看到它想到的是纠…

作者头像 李华