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 Objects:Agent 记忆的落点
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 上更推荐用 corepack:
corepack enable corepack prepare pnpm@latest --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.0,providers里用的是 1.3.0,类型对不上,构建时报一堆莫名其妙的错。这个坑我在一个多模型适配的项目里踩得很深,最后花了一整天才理清楚版本关系。
4. Agent 核心循环在 Workers 上的实现细节
4.1 一次完整的 Agent 请求,到底经历了什么
把 Agent 的一次交互拆开看,大致是这样一条链路:
- 用户消息到达入口 Worker
- Worker 鉴权、解析 sessionId,转发给对应 DO
- DO 加载历史消息,构造上下文
- 调用模型(经 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 这件事,工具和框架只是起点,真正决定体验的是你对状态、上下文和错误处理的理解深度。