Cloudflare Workers 上构建 AI Agent 后端:架构设计与工程实践
1. 从cloudflare-os这个名字说起它到底想解决什么问题第一次看到cloudflare-os这个项目名我脑子里冒出来的第一个念头是这不会又是一个把一堆配置脚本打包起来、起个唬人名字的仓库吧。毕竟现在叫XX-OS的项目太多了真正能跑起来、能长期维护的没几个。但把关键词铺开一看——Cloudflare Workers、AI、agent、pnpm——这几个词凑在一起指向性其实非常明确这是一个围绕Cloudflare Workers 边缘运行时搭建的、面向AI Agent 场景的工程化脚手架或者说操作系统层。先把概念对齐一下。这里的OS不是传统意义上管进程、管内存、管文件系统的操作系统而是面向 Agent 应用的运行时抽象层。你可以把它理解成Cloudflare Workers 提供了最底层的算力和网络能力相当于裸机而cloudflare-os想做的事情是在这层裸机之上把 Agent 开发中反复要写的东西——路由分发、会话状态、工具调用、模型接入、任务编排——抽象成一套统一的接口和约定。这样你写 Agent 的时候不用每次都从fetch事件开始手搓。为什么这件事值得做因为 Agent 开发和传统的 Web API 开发有一个本质区别Agent 是有状态的、多轮的、会调用外部工具的。一个普通的 REST 接口请求进来、查库、返回生命周期就结束了。但一个 Agent 的一次对话可能横跨十几轮交互中间要调用搜索、要读写记忆、要执行代码、要等待人工确认。这些状态如果全塞在客户端安全性和一致性都没法保证如果全塞在单个服务器进程里又和 Workers 这种无状态、按请求计费、随时可能被回收的边缘运行时天然冲突。cloudflare-os的价值就在于它试图用 Workers 生态里现成的原语——Durable Objects 做有状态协调、KV 做冷存储、R2 做大对象、Queues 做异步任务、AI Gateway 做模型代理——拼出一套能承载 Agent 生命周期的骨架。关键词里出现pnpm说明它的工程组织大概率是 monorepo 形态用 pnpm workspace 管理多个 package这在 Workers 项目里已经是事实标准了。而agent、agent开发、agent架构、agent记忆这些热搜词恰好对应了这套骨架要覆盖的几个核心模块。所以这篇文章我不打算写成官方文档翻译而是站在一个真正动手搭过 Agent 后端的人的角度把cloudflare-os这类项目背后的设计逻辑、落地时会遇到的坑、以及怎么把它用在自己的项目里一层层拆开讲。适合两类人看一是已经会用 Workers 写简单接口、想往 Agent 方向走的开发者二是被 Agent 的状态管理和工具编排折磨过、想找一套现成结构参考的人。2. 为什么 Agent 后端特别适合放在 Workers 上2.1 边缘运行时的三个特性恰好对上 Agent 的三个痛点很多人对 Workers 的印象还停留在跑个轻量 API、做个 CDN 回源改写。但 Agent 场景其实和边缘运行时非常契合原因有三点我一条条说。第一冷启动极快适合突发式调用。Agent 的请求模式是典型的脉冲式用户发一句话触发一次模型调用模型返回后可能立刻触发三四个工具调用然后又是模型调用。这种密集的、短时间内的多次函数执行如果放在传统容器里每次冷启动几百毫秒到几秒体验会非常割裂。Workers 的 isolate 冷启动通常在毫秒级这个差距在 Agent 的多轮循环里会被放大成肉眼可见的卡顿差异。第二按请求计费适合长尾低频的 Agent。大部分 Agent 应用的真实负载是很不均匀的白天可能每分钟几十次调用凌晨可能几小时没人用。如果用常驻服务器闲置成本很高。Workers 的计费模型是按实际执行时间和请求数算的闲置基本不花钱。对于个人开发者或者早期项目这一点直接决定了你能不能把成本压到可接受范围。第三全球分布适合需要低延迟工具调用的场景。当 Agent 需要调用外部 API比如查天气、查汇率、查库存时如果计算节点离数据源近往返延迟会明显降低。Workers 的节点分布意味着你的 Agent 逻辑可以跑在离用户和离工具都相对近的地方。但这里有个前提你必须接受无状态这个约束并且学会用 Durable Objects 来管理需要状态的部分。这是cloudflare-os这类项目最核心的设计取舍也是新手最容易翻车的地方。我见过太多人把会话历史直接存在 Worker 的全局变量里本地测试没问题一上线就发现每次请求都失忆。原因很简单Workers 的 isolate 随时可能被回收全局变量不保证跨请求存活。2.2 Durable ObjectsAgent 记忆的落点agent记忆是热搜词里出现频率很高的一个。在 Workers 体系里做 Agent 记忆的标准答案就是Durable Object。你可以把它理解成一个有唯一名字的、单线程执行的、自带持久化存储的小服务。每个会话对应一个 DO 实例这个实例内部可以维护内存状态同时通过storageAPI 把关键数据落盘。为什么 DO 适合做记忆因为它保证了同一时刻只有一个请求在处理同一个会话。这对 Agent 至关重要——如果两个请求同时修改同一段对话历史很容易出现覆盖或者顺序错乱。DO 的单线程模型天然避免了这个问题你不需要自己加锁。在cloudflare-os这类架构里典型的做法是Worker 收到请求后根据sessionId计算出一个 DO 的 ID然后把请求转发给对应的 DO。DO 内部负责加载历史、拼接上下文、调用模型、执行工具、写回历史。Worker 本身只做路由和鉴权保持轻薄。// 简化的路由逻辑示意 export default { async fetch(request, env) { const url new URL(request.url); const sessionId url.searchParams.get(session); if (!sessionId) return new Response(missing session, { status: 400 }); // 用 sessionId 稳定映射到同一个 DO 实例 const id env.AGENT_SESSION.idFromName(sessionId); const stub env.AGENT_SESSION.get(id); return stub.fetch(request); } };这段代码看着简单但里面有个关键点idFromName保证同样的sessionId永远落到同一个 DO。如果你用newUniqueId()那每次请求都会创建新实例记忆就断了。这个坑我在早期项目里踩过排查了半天才发现是 ID 生成方式用错了。2.3 模型接入AI Gateway 解决的是什么问题关键词里有AI、ai大模型基础理论、ai agent。Agent 离不开模型调用而模型调用在生产环境里会遇到几个现实问题密钥管理、限流、重试、成本追踪、多模型切换。如果每个 Agent 都自己直连模型厂商的 API这些问题会重复出现而且密钥散落在各处安全风险很高。Cloudflare 的 AI Gateway 就是冲着这些痛点去的。它在你的 Worker 和模型厂商之间加了一层代理你只需要配置一次密钥之后所有请求走 Gateway。Gateway 帮你做缓存、做限流、做日志、做 fallback。对于cloudflare-os这种要支撑多个 Agent 的操作系统来说把模型接入统一收敛到 Gateway 是合理的选择。实操上要注意一点Gateway 的缓存对 Agent 场景要慎用。因为 Agent 的请求往往带有上下文同样的 prompt 在不同会话里语义可能完全不同盲目命中缓存会导致答非所问。我的建议是只对确定无状态的、纯工具类的模型调用开缓存对话类调用一律关掉。3. 用 pnpm workspace 组织 Agent 项目的实际结构3.1 为什么是 pnpm而不是 npm 或 yarn热搜词里pnpm相关的一大堆pnpm下载失败、pnpm安装、pnpm离线安装、删除pnpm、pnpm 不是内部或外部命令。这说明很多人在 pnpm 的环境配置上就卡住了。先解决这个再谈项目结构。pnpm 相比 npm 和 yarn 的核心优势是硬链接 内容寻址存储。简单说同一个依赖版本在你的磁盘上只存一份所有项目通过硬链接引用它。对于一个包含多个 package 的 monorepo这能省下大量磁盘空间安装速度也更快。更重要的是pnpm 的依赖隔离更严格——它默认不允许你访问没有在package.json里声明的依赖这能有效避免幽灵依赖问题。在 Agent 项目里依赖往往很多模型 SDK、工具库、类型定义幽灵依赖一旦出现构建和部署时会出现莫名其妙的模块找不到。关于pnpm 不是内部或外部命令这个报错本质是 PATH 没配好。Windows 上用 npm 全局安装 pnpm 后需要确认 npm 的全局 bin 目录在 PATH 里。可以用npm config get prefix查到路径然后手动加进环境变量。Ubuntu 上更推荐用 corepackcorepack enable corepack prepare pnpmlatest --activatecorepack 是 Node 自带的包管理器版本管理工具用它启用 pnpm 比全局安装更干净也不会污染全局环境。至于pnpm离线安装如果你的部署环境不能联网可以先用pnpm fetch把依赖下载到本地 store再把整个 store 目录打包带过去目标机器上设置--offline标志安装。3.2 一个能落地的 monorepo 目录划分cloudflare-os这类项目我建议的目录结构是这样的cloudflare-os/ packages/ core/ # Agent 核心抽象会话、消息、工具接口 runtime/ # Workers 运行时适配DO、KV、Queue 封装 tools/ # 内置工具集搜索、计算、HTTP 调用 providers/ # 模型提供商适配层 apps/ gateway/ # 对外入口 Worker dashboard/ # 管理面板可选 pnpm-workspace.yaml package.json这样划分的理由是关注点分离。core只定义接口和类型不依赖任何 Cloudflare 特有的东西这样将来想换运行时比如换到别的边缘平台时核心逻辑不用动。runtime负责把 core 的抽象映射到具体的 DO、KV 上。tools和providers是插件式的加一个新工具或换一个模型厂商只需要新增一个 package不影响其他部分。pnpm-workspace.yaml的内容很简单packages: - packages/* - apps/*然后在根package.json里用pnpm -r递归执行脚本比如pnpm -r build会按依赖顺序构建所有 package。这里有个经验一定要在根目录用workspace:*协议引用内部包而不是写相对路径。这样 pnpm 会正确处理包之间的依赖关系构建顺序不会乱。3.3 依赖版本锁定Agent 项目尤其不能马虎Agent 项目依赖的模型 SDK 更新非常频繁而且经常有破坏性变更。我强烈建议在 monorepo 里统一锁定关键依赖的版本用pnpm.overrides强制所有 package 用同一个版本{ pnpm: { overrides: { some-model-sdk: 1.2.3 } } }不这么做的话core里用的是 1.2.0providers里用的是 1.3.0类型对不上构建时报一堆莫名其妙的错。这个坑我在一个多模型适配的项目里踩得很深最后花了一整天才理清楚版本关系。4. Agent 核心循环在 Workers 上的实现细节4.1 一次完整的 Agent 请求到底经历了什么把 Agent 的一次交互拆开看大致是这样一条链路用户消息到达入口 WorkerWorker 鉴权、解析 sessionId转发给对应 DODO 加载历史消息构造上下文调用模型经 AI Gateway模型返回判断是否需要调用工具如果需要执行工具把结果追加到上下文回到第 4 步如果不需要把最终回复写回历史返回给用户这个循环里第 5 步到第 6 步的循环是最容易出问题的地方。因为模型可能连续要求调用多个工具也可能陷入调用工具→模型又要求调用同一个工具的死循环。cloudflare-os这类框架通常会在 core 层设置一个最大迭代次数比如 10 次超过就强制终止并返回当前结果。这个保护机制必须有否则一个设计不当的 prompt 就能让你的 Worker 一直跑到超时白白烧钱。4.2 工具调用的超时与错误处理工具调用是 Agent 和外部世界交互的窗口也是最不稳定的环节。外部 API 可能超时、可能返回格式错误、可能限流。在 Workers 环境里每个请求有 CPU 时间限制工具调用如果同步等待太久整个请求会被杀掉。我的做法是给每个工具调用包一层超时控制用Promise.race实现function withTimeout(promise, ms) { const timeout new Promise((_, reject) setTimeout(() reject(new Error(tool timeout)), ms) ); return Promise.race([promise, timeout]); }超时后不要直接让整个 Agent 失败而是把工具调用超时作为一条工具结果返回给模型让模型自己决定是重试、换工具还是告诉用户。这样 Agent 的鲁棒性会好很多。实测下来把工具超时设成 8 到 10 秒比较合理太短会误杀正常的慢接口太长会拖垮整个请求。4.3 流式输出在 DO 里的处理Agent 的体验很大程度上取决于流式输出。用户希望看到模型一个字一个字往外蹦而不是等十几秒突然出现一大段。在 Workers 里做流式要用ReadableStream和TransformStream。难点在于流式输出和工具调用是冲突的。如果模型一边流式输出一边要求调用工具你没法把已经流出去的文字收回来。常见的处理方式是分两阶段第一阶段让模型决定是否需要工具不流式如果需要就执行工具第二阶段拿到最终答案后再流式输出给用户。这样用户会先看到一个思考中的指示然后才看到流式回答。体验上略有妥协但逻辑清晰、不易出错。在 DO 里做流式还要注意DO 的fetch返回的 Response body 可以是流但你要确保在流结束前 DO 不被回收。实践中只要流还在被消费DO 就会保持活跃这一点不用太担心。5. 那些文档里不会写的踩坑记录5.1 DO 的存储写入不是免费的Durable Object 的storage.put是有延迟和成本的。我见过有人在 Agent 的每一轮循环里都把完整历史写一遍结果一个十轮的对话写了十次全量数据成本和延迟都上去了。正确做法是只追加增量用storage.put存单条消息用storage.list按 key 前缀读取历史。key 可以设计成msg:${timestamp}:${id}这种形式天然有序。另外DO 的存储写入默认是异步的如果你需要确保写入完成再返回要await。但在 Agent 场景里历史写入其实可以异步做不阻塞用户看到回复。这个取舍要看你的业务对一致性的要求。5.2 上下文长度是会爆的Agent 跑久了历史消息会越来越长最终超过模型的上下文窗口。这时候必须做上下文压缩。常见策略有三种滑动窗口只保留最近 N 轮、摘要压缩把早期对话总结成一段话、关键信息提取只保留实体和结论。cloudflare-os这类框架一般会提供钩子让你自定义压缩策略。我的经验是滑动窗口 摘要组合最实用。保留最近 5 到 8 轮完整对话更早的内容用一次额外的模型调用总结成 200 字以内的摘要。这样既保留了长期记忆的脉络又控制了 token 消耗。注意摘要调用本身也要算成本不要每轮都重新总结可以每积累 5 轮总结一次。5.3 本地开发和线上行为的差异wrangler dev本地跑的时候DO 和 KV 都是本地模拟的行为和线上有细微差别。最典型的是并发行为本地基本是单请求串行线上是真并发。有些在本地测不出来的竞态问题一上线就暴露。我的建议是涉及 DO 的关键逻辑一定要在线上用wrangler dev --remote模式测一遍虽然慢一点但能提前发现并发问题。还有一个坑是环境变量的读取方式。本地.dev.vars和线上wrangler secret的加载时机不同如果你在模块顶层就读env本地可能读到 undefined。正确做法是在fetch处理函数内部读env不要在外面缓存。5.4 关于 agent 安全的一点提醒热搜词里有agent安全。Agent 能调用工具就意味着它能产生副作用——发邮件、改数据、调支付。永远不要把高危工具的调用权限直接交给模型自主决定。我的做法是给工具分级只读类工具查询、搜索可以让模型自由调用写入类工具发消息、改状态必须经过人工确认或者额外的权限校验。在cloudflare-os的架构里这个校验可以放在 DO 内部作为工具执行前的一道关卡。6. 把 cloudflare-os 用起来的几条实操建议如果你打算基于这套思路搭自己的 Agent 后端我给几条具体的建议。先从最小闭环开始。不要一上来就把所有工具、所有模型、所有记忆策略都接上。先跑通用户消息→模型→回复这一条链路确认 DO 的会话映射正确、历史能存能读。然后再加工具调用再加流式再加压缩。每加一层都单独验证出问题时才好定位。把模型调用抽象成接口。不要在你的业务代码里直接写某个厂商的 SDK 调用。定义一个ModelProvider接口把chat方法抽象出来具体实现放在providers包里。这样将来换模型、加模型、做 A/B 测试都只需要改一处。cloudflare-os的providers目录就是干这个的。日志要打全但要脱敏。Agent 出问题时你需要知道模型收到了什么、返回了什么、调用了哪些工具。这些日志对排查至关重要。但日志里可能包含用户隐私一定要在写入前做脱敏处理尤其是存到 KV 或 R2 的长期日志。成本监控要前置。Agent 的 token 消耗很容易失控尤其是工具调用循环。建议在 AI Gateway 层配置用量告警或者在自己的 DO 里累计 token 计数超过阈值就降级或拒绝。不要等到账单出来才发现问题。版本升级要谨慎。Workers 运行时和 wrangler 工具链更新很快有时候一个小版本升级就会改变某些 API 的行为。生产项目建议锁定 wrangler 版本升级前先在测试环境跑一遍完整的 Agent 流程。最后分享一个我自己用下来很顺手的调试技巧在 DO 里加一个特殊的调试指令比如用户发送/debug时返回当前会话的完整状态——历史消息数、当前上下文 token 估算、最近一次工具调用结果。这个功能在排查Agent 为什么突然变傻这类问题时特别有用比翻日志快得多。搭 Agent 这件事工具和框架只是起点真正决定体验的是你对状态、上下文和错误处理的理解深度。