ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

cloudflare-os 实战:基于 Cloudflare Workers 的 AI Agent 架构与 pnpm 部署指南

cloudflare-os 实战:基于 Cloudflare Workers 的 AI Agent 架构与 pnpm 部署指南

1. 从"cloudflare-os"这个名字说起:它到底想解决什么问题

第一次看到cloudflare-os这个仓库名,我下意识以为 Cloudflare 又出了个操作系统。点进去翻了翻代码结构和提交记录才反应过来,这压根不是传统意义上的 OS,而是一套跑在 Cloudflare Workers 运行时之上的 Agent 编排骨架。名字里的 "os" 更接近"面向 Agent 的操作层"这个含义——它把模型调用、工具注册、会话状态、任务调度这几件事,用一套统一的抽象封装起来,让开发者不用每次从零搭 Agent 的轮子。

这个定位其实很聪明。现在做 AI Agent 的人普遍面临一个尴尬:LangChain 那套东西功能全但重,依赖一堆 Python 包,部署起来动辄要一台常驻服务器;而纯手写 fetch 调 API 又太原始,工具调用、多轮记忆、错误重试全得自己造。cloudflare-os卡在中间——它借 Cloudflare Workers 的边缘运行时特性,把 Agent 的"大脑"和"手脚"都放在离用户最近的节点上跑,冷启动接近零,按请求计费,不需要你维护任何长驻进程。

我拿它跟几个主流方案做了个粗略对比,方便你判断它适不适合自己的场景:

维度cloudflare-osLangChain/LlamaIndex纯手写 fetch
运行时Workers 边缘Node/Python 服务器任意
冷启动毫秒级秒级到十几秒取决于宿主
状态管理内置会话抽象需接外部存储全手写
工具注册声明式装饰器/链式全手写
部署成本按请求计费常驻实例费用视宿主
学习曲线中等偏陡低但工作量大

适合谁来用?我的判断是三类人:一是已经在用 Workers 生态、想快速加个 Agent 能力的全栈开发者;二是做轻量级 AI 应用、不想为了一点点推理逻辑养一台服务器的独立开发者;三是想研究 Agent 架构但不想被框架绑架、希望代码足够透明的人。如果你要做的是重度的 RAG、复杂多 Agent 协作、或者需要大量本地文件操作,那这套东西可能不是最优解,后面我会具体说边界在哪。

关键词里出现了pnpm、agent、Cloudflare Workers、AI这几个高频词,说明大家最关心的其实是两件事:怎么把这个项目跑起来,以及它的 Agent 架构到底怎么设计的。下面我就按这两条线展开,中间穿插我自己踩过的坑。

2. 环境准备:pnpm 这一关为什么总有人卡住

2.1 为什么这个项目偏偏用 pnpm

先回答一个很多人会问的问题:npm、yarn 用得好好的,为什么非要上 pnpm?这不是跟风。cloudflare-os这类 Agent 项目有个特点——依赖树又深又宽,模型 SDK、工具库、类型定义、构建插件层层嵌套。npm 的扁平化 node_modules 在这种场景下容易出现"幽灵依赖"(你 import 了一个没写在 package.json 里的包,居然能跑),换台机器就崩。pnpm 用内容寻址存储 + 硬链接的方式,每个包只在全局 store 里存一份,项目里通过符号链接引用,既省磁盘又杜绝了幽灵依赖。

更实际的一点:Workers 项目的构建对依赖体积敏感。pnpm 的严格依赖隔离能让你更早发现"这个包其实没被真正用到",从而在打包时砍掉。我实测过一个中等规模的 Agent 项目,从 npm 切到 pnpm 之后,node_modules从 480MB 降到 190MB 左右,构建产物也小了一圈。

2.2 安装 pnpm 的三种姿势与常见报错

关键词里pnpm : 无法将"pnpm"项识别为 cmdlet、pnpm' 不是内部或外部命令这两个报错出现频率极高,本质都是装完了但没进 PATH。我按操作系统分开说。

Windows 下,最稳的方式是用 npm 全局装:

npm install -g pnpm

装完如果还是提示"不是内部或外部命令",八成是 npm 的全局 bin 目录没在 PATH 里。用npm config get prefix查一下路径,把它加到系统环境变量。另一个坑是 PowerShell 的执行策略,有时候脚本被拦,用管理员权限跑一次Set-ExecutionPolicy RemoteSigned就好。别用corepack在 Windows 上折腾,我试过几次,版本切换时容易出玄学问题。

macOS / Linux 下,官方脚本最省心:

curl -fsSL https://get.pnpm.io/install.sh | sh -

装完记得source一下 shell 配置文件,或者重开终端。如果你用 zsh,脚本有时会写到.zshrc,但当前会话没生效,导致你以为装失败了。

离线安装这个需求关键词里也提到了。pnpm 支持从本地 store 离线装包,前提是你之前已经缓存过。做法是先把node_modules和 pnpm store 一起打包带走,目标机器上设置pnpm config set store-dir /path/to/store,然后pnpm install --offline。注意--offline模式下如果 store 里缺任何一个包,它会直接失败而不是回退到网络,所以打包前务必确认依赖完整。

提示:如果你在 CI 环境里遇到 pnpm 版本和 lockfile 不匹配的报错,别急着删 lockfile。先看报错里的lockfileVersion,用packageManager字段锁定版本,比手动升级靠谱得多。

2.3 拉取项目与首次构建

环境齐了之后,标准流程是这样:

git clone <repo-url> cloudflare-os cd cloudflare-os pnpm install pnpm run dev

这里有个容易被忽略的点:Workers 项目通常需要wrangler这个 CLI 来做本地开发和部署。pnpm run dev背后一般就是wrangler dev。第一次跑会提示你登录 Cloudflare 账号,本地开发可以用wrangler dev --local走本地模拟运行时,不消耗线上额度。我建议新手先用--local把逻辑跑通,确认 Agent 能正常响应了,再切到线上环境。

构建阶段如果报"找不到某个类型定义",大概率是tsconfig的types字段没包含 Workers 的类型包。检查一下有没有装@cloudflare/workers-types,并在 tsconfig 里加上。这个坑我踩过,报错信息指向业务代码,实际根因在类型配置,排查起来挺费时间。

3. Agent 架构拆解:这套骨架的四个核心部件

3.1 请求入口与路由分发

cloudflare-os的入口是一个标准的 Workersfetchhandler。所有进来的请求先经过一层路由,根据路径或请求体里的意图字段,决定交给哪个 Agent 处理。这个设计的好处是多 Agent 共存——你可以在同一个 Worker 里挂好几个不同职责的 Agent,比如一个负责问答、一个负责工具调用、一个负责定时任务,互不干扰。

路由层通常还会做几件事:请求体解析、鉴权校验、限流。限流这块 Workers 生态有现成的方案,基于 Durable Objects 或者 KV 都能实现。我自己的做法是在路由层加一个轻量的 token 计数,超过阈值直接返回,避免 Agent 被刷爆导致账单失控。这一点很关键,Agent 调用模型是按 token 计费的,没有限流等于把钱包敞开。

3.2 会话状态:Agent 的"记忆"放在哪

Agent 和普通 API 最大的区别就是有状态。多轮对话要记住上下文,工具调用要记住中间结果。cloudflare-os在这块的抽象值得细说。

它一般会提供一个 Session 接口,底层可以接不同的存储:KV 适合存短期的会话快照,Durable Objects 适合需要强一致性的场景,D1 适合需要结构化查询的历史记录。选哪个取决于你的需求:

  • KV:读写快、便宜,但最终一致,适合"记个大概"的对话历史。
  • Durable Objects:单点强一致,适合需要串行处理的任务队列、协作编辑这类场景。
  • D1:SQLite 底座,适合要按条件查历史、做统计分析的场景。

我个人的经验是,对话上下文用 KV,任务状态用 Durable Objects。因为对话历史偶尔读到旧版本问题不大,但任务状态如果出现并发写冲突,会导致 Agent 重复执行工具,那就要命了。

3.3 工具注册与调用协议

Agent 的"手脚"就是工具。cloudflare-os里工具通常是声明式注册的——你定义一个工具的名字、描述、参数 schema,框架负责把它转成模型能理解的 function calling 格式,并在模型返回工具调用意图时执行对应函数。

这里有个设计细节很关键:工具的 schema 描述质量直接决定 Agent 的调用准确率。我见过太多人把工具描述写得含糊,比如一个查天气的工具描述只写"获取天气",模型根本不知道要传城市还是经纬度。正确的做法是把描述写清楚,参数用 JSON Schema 严格约束:

const weatherTool = { name: "get_weather", description: "查询指定城市的当前天气,返回温度和天气状况", parameters: { type: "object", properties: { city: { type: "string", description: "城市名称,如'北京'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["city"] } };

参数里加enum约束、给每个字段写description,这些看似啰嗦的动作,实测能把工具调用的错误率降一大截。

3.4 模型调用层与多模型切换

Agent 的"大脑"是模型。cloudflare-os一般会把模型调用抽象成一层,支持切换不同的模型提供方。Workers 生态里可以直接调 Workers AI,也可以走外部 API。抽象层的好处是换模型不改业务代码,今天用这个模型,明天想换那个,只改配置。

多模型协作是关键词里提到的另一个点。所谓多 AI 协作,本质是让不同模型扮演不同角色——一个负责规划、一个负责执行、一个负责审查。cloudflare-os的架构支持这种模式,但要注意token 成本会成倍上升。我的建议是,只有在任务确实复杂、单模型搞不定的时候才上多模型,简单的问答用单模型就够了,别为了架构好看而烧钱。

4. 从零跑通一个最小 Agent:完整实操链路

4.1 定义你的第一个工具

光看架构不够,得动手。我带你跑一个最小的例子:一个能查时间、能算数的 Agent。

先定义工具。查时间这个工具不需要参数,算数需要两个操作数和一个运算符:

const tools = [ { name: "get_current_time", description: "获取当前服务器时间,返回 ISO 格式字符串", parameters: { type: "object", properties: {} } }, { name: "calculate", description: "执行基础四则运算", parameters: { type: "object", properties: { a: { type: "number", description: "第一个操作数" }, b: { type: "number", description: "第二个操作数" }, op: { type: "string", enum: ["+", "-", "*", "/"] } }, required: ["a", "b", "op"] } } ];

注意get_current_time的parameters我写了个空对象而不是省略,这是为了兼容某些模型对 schema 的严格校验,省略了反而可能报错。

4.2 实现工具执行器

工具定义好了,得有东西去执行它。执行器本质就是一个 switch:

function executeTool(name, args) { switch (name) { case "get_current_time": return new Date().toISOString(); case "calculate": { const { a, b, op } = args; const ops = { "+": a + b, "-": a - b, "*": a * b, "/": a / b }; if (op === "/" && b === 0) return "错误:除数不能为零"; return String(ops[op]); } default: return `未知工具:${name}`; } }

除零判断这个细节别省。模型有时候会生成除以零的调用,你不处理它就会返回Infinity,然后模型拿着Infinity继续推理,输出一堆莫名其妙的东西。加个判断,返回明确的错误信息,模型反而能自己纠正。

4.3 组装 Agent 主循环

Agent 的核心是一个循环:把用户输入和工具定义发给模型,模型要么直接回答,要么返回工具调用意图,执行工具后把结果塞回对话,再问模型,直到模型给出最终答案。

async function runAgent(userInput, history = []) { const messages = [...history, { role: "user", content: userInput }]; const maxTurns = 5; for (let i = 0; i < maxTurns; i++) { const response = await callModel(messages, tools); if (response.toolCalls && response.toolCalls.length > 0) { messages.push(response.message); for (const call of response.toolCalls) { const result = executeTool(call.name, call.arguments); messages.push({ role: "tool", toolCallId: call.id, content: result }); } continue; } return { reply: response.content, history: messages }; } return { reply: "任务步骤过多,已中止", history: messages }; }

maxTurns这个上限是必须的。我见过没有上限的 Agent 陷入死循环,反复调用同一个工具,几分钟烧掉几十块钱的 token。设个 5 到 10 轮的上限,超了就中止并返回提示,这是保命的设计。

4.4 本地调试与线上部署

本地用wrangler dev --local跑起来之后,用 curl 或者 Postman 发个请求测试:

curl -X POST http://localhost:8787/agent \ -H "Content-Type: application/json" \ -d '{"input": "现在几点?顺便帮我算一下 23 乘以 47"}'

如果 Agent 正确调用了两个工具并给出答案,说明链路通了。这时候再wrangler deploy推到线上。部署后记得在 Cloudflare 控制台配置环境变量,把模型 API 的密钥通过 secret 方式注入,千万别硬编码在代码里,Workers 的代码是可以被查看的。

5. 那些文档不会告诉你的坑

5.1 冷启动与超时的真实表现

Workers 的冷启动确实快,但首次调用模型 API 的网络往返才是真正的耗时大头。我实测下来,一个简单的单轮问答,端到端延迟在 800ms 到 2s 之间,取决于模型提供方的响应速度。如果你的 Agent 要跑多轮工具调用,延迟会累加,用户等个五六秒是常事。

Workers 有 CPU 时间限制(免费版和付费版不同),但注意这个限制算的是CPU 执行时间,不包括等待网络 IO 的时间。所以模型调用这种 IO 密集的操作一般不会触发 CPU 超时,但如果你的工具里有大量同步计算,就要小心了。我的做法是把重计算拆出去,用队列或者异步任务处理。

5.2 会话状态的并发陷阱

前面提到用 KV 存对话历史,这里有个坑:KV 是最终一致的,写入后立刻读可能读到旧值。如果你的 Agent 在同一个请求里先写后读,可能拿到脏数据。解决办法是同一个请求内的状态用内存变量传递,只在请求结束时才落盘到 KV。Durable Objects 就没这个问题,但成本更高,按需选择。

另一个坑是会话 ID 的生成。别用时间戳,并发下会撞。用crypto.randomUUID(),Workers 运行时原生支持。

5.3 工具调用的参数校验不能省

模型生成的工具参数不可信。它可能给你传个字符串 "23" 而不是数字 23,可能漏传必填字段,可能传个 enum 里没有的值。我一开始偷懒没做校验,结果 Agent 时不时崩在a + b上,因为 a 是字符串,"23" + 47变成了"2347"。后来加了严格的类型转换和校验,稳定性立刻上来了。

function validateArgs(args, schema) { for (const key of schema.required || []) { if (args[key] === undefined) throw new Error(`缺少参数:${key}`); } // 类型转换与 enum 校验按需补充 return args; }

5.4 成本控制的几个实操手段

Agent 烧钱是真实存在的。我总结了几个有效的手段:

  • 限制 maxTurns:前面说过,5 到 10 轮足够大多数任务。
  • 限制上下文长度:对话历史不能无限增长,超过一定轮数就做摘要或者截断。
  • 缓存常见问答:高频问题直接命中缓存,不走模型。
  • 监控 token 消耗:在每次模型调用后记录 token 数,设个日限额告警。

注意:缓存 Agent 响应要谨慎。如果响应依赖实时数据(比如查天气),缓存会导致返回过期信息。只缓存那些确定性的、不随时间变化的问答。

6. 这套架构的边界:什么时候不该用它

说了这么多好处,也得泼点冷水。cloudflare-os这套基于 Workers 的 Agent 架构,有几个明确的边界。

第一,不适合重计算任务。Workers 的 CPU 时间有限,如果你要做本地模型推理、大规模数据处理、复杂图像运算,它扛不住。这类任务应该放到专门的算力节点上,Workers 只做编排和调度。

第二,不适合需要长连接常驻的场景。Workers 是无状态的请求-响应模型,虽然有 Durable Objects 能做有状态,但如果你要的是 WebSocket 长连接、实时推送这类,得额外设计,复杂度上升。

第三,生态依赖。你被绑在 Cloudflare 的生态里,KV、Durable Objects、D1 这些服务虽然好用,但迁移成本不低。如果你的项目未来可能要迁到别的云,得提前做好抽象隔离。

第四,调试体验。本地--local模式和线上真实环境还是有差异,尤其是涉及 KV、DO 这些服务时,本地模拟和线上行为不完全一致。我遇到过本地跑得好好的,上线就报错的情况,最后发现是本地 KV 模拟的 TTL 行为和线上不同。所以关键逻辑上线前一定要在预发环境验证。

7. 我个人的几点使用体会

折腾这套东西有一阵子了,说几个真实感受。

Agent 这个领域现在概念满天飞,什么 agent 架构、agent 记忆、agent skill,听着高大上,落到代码上其实就那么几件事:怎么调模型、怎么存状态、怎么执行工具、怎么控制循环。cloudflare-os的价值不在于它发明了什么新概念,而在于它把这四件事用一套干净的抽象串起来了,代码量不大,读一遍就能懂,改起来也方便。这种"透明"在现在的 Agent 框架里反而稀缺。

pnpm 那一关,我建议新手别跳过。很多人一遇到命令找不到就换 npm,结果后面依赖冲突更头疼。花十分钟把 PATH 配好,一劳永逸。

最后分享一个小技巧:调试 Agent 的时候,把每次模型调用的完整 messages 数组打印出来。你会直观看到模型是怎么"思考"的——它先看到什么、决定调什么工具、拿到结果后怎么调整。这个习惯帮我定位了至少一半的诡异问题,比看任何日志都管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进