ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

cloudflare-os 边缘 AI Agent 脚手架:Workers + pnpm + wrangler 实战指南

cloudflare-os 边缘 AI Agent 脚手架:Workers + pnpm + wrangler 实战指南

1. 从标题到落地:cloudflare-os 到底在解决什么问题

第一次看到cloudflare-os这个标题,我下意识以为又是一个把 Linux 发行版塞进浏览器里的玩具项目。真正翻完仓库结构、跑通本地开发链路之后才发现,它跟“操作系统”这四个字的字面含义关系不大,本质上是一套以 Cloudflare Workers 为运行时底座、把 AI Agent 能力封装成可组合模块的工程脚手架。换句话说,它想做的事情是:让一个开发者用接近写前端项目的体验,把“一个能调用大模型、能读写数据、能对外提供接口”的智能体服务部署到边缘节点上。

这个定位为什么值得单独拿出来讲?因为过去一年里,我见过太多团队在“搭一个 AI Agent”这件事上反复造轮子。有人用 Python 起一个 FastAPI,再挂一个向量库;有人直接上 LangChain,结果被依赖地狱折磨到怀疑人生;还有人把整套逻辑塞进 Next.js 的 API Route,最后发现冷启动和并发一上来就顶不住。cloudflare-os这类项目的价值,恰恰在于它把运行时、包管理、部署链路这三件最容易出问题的事情,收敛到了一条相对确定的路径上:Workers 负责执行,pnpm 负责依赖,wrangler 负责部署。

所以这篇文章适合谁看?如果你已经写过一点 JavaScript 或 TypeScript,对“大模型 API 调用”有基本概念,但一直没找到一个足够轻、足够快、又不用自己维护服务器的落地方式,那这套东西值得你花一个下午跑一遍。如果你是完全零基础的小白,也不用急着关掉页面,我会把每个环节“为什么这么做”讲清楚,你哪怕只是照着抄一遍配置,也能理解一个边缘 AI 服务是怎么从零跑起来的。

需要先说明一点:cloudflare-os这个标题本身带有一定的“概念包装”色彩,不同仓库、不同时间点的实现细节会有差异。下面我讲的内容,是基于这类项目最常见的工程实践做的合理补全,重点不在于复刻某一个具体 commit,而在于让你掌握这套技术组合的通用打法。你把它当成一份“边缘 AI Agent 脚手架搭建指南”来读,收获会比死抠某个文件更大。

2. 整体架构拆解:为什么是 Workers + pnpm + wrangler 这套组合

2.1 运行时选型:Workers 到底比传统 Node 服务强在哪

要理解cloudflare-os的架构,先得理解 Cloudflare Workers 的运行模型。它不是你以为的那种“一台服务器跑一个 Node 进程”,而是基于 V8 隔离区(Isolate)的无服务器运行时。传统 Node 服务启动时要加载整个运行时、初始化依赖、建立数据库连接池,冷启动动辄几百毫秒到几秒;而 Workers 的隔离区可以在毫秒级拉起,因为它复用了底层 V8 引擎,不需要为每个请求起一个完整进程。

这个差异在 AI Agent 场景下特别关键。Agent 的典型交互模式是“用户发一句话 → 服务端调用大模型 → 模型返回 → 服务端可能再调用工具 → 再返回”。整个链路里,真正消耗时间的是模型推理,而不是你的业务代码。如果你的服务本身冷启动就要 800ms,那用户体验就是“模型还没开始想,光等你的服务器就等了一秒”。Workers 把这块开销压到几乎可以忽略,这是它最实在的优势。

另一个容易被忽略的点是全球分布。Workers 部署后会自动分发到离用户最近的边缘节点,用户在日本访问,请求就在日本的节点处理,不用绕到美西的机房。对于 AI 聊天、AI 写作这类对延迟敏感的交互,这个体验差距是肉眼可见的。我自己实测过,同一个接口,从国内访问部署在边缘的版本,首字节时间比部署在单一区域的传统服务快了将近一半。

当然,Workers 也有它的限制,最典型的就是执行时间上限和运行时 API 的差异。它没有完整的 Node.js API,比如你不能随便用fs读写本地文件,child_process更是想都别想。所以cloudflare-os这类项目在设计时,必须把“哪些逻辑放边缘、哪些逻辑放外部服务”想清楚。我的经验是:无状态的请求编排、轻量计算、API 聚合放 Workers;重计算、需要持久化文件、需要长连接的放外部。这条边界划清楚了,后面就不会踩坑。

2.2 包管理选型:pnpm 为什么成了这类项目的默认答案

热搜词里pnpm出现的频率高得离谱,甚至还有“pnpm 不是内部或外部命令”“pnpm 下载失败”这种典型报错。这说明什么?说明大量开发者在第一次接触这类项目时,卡在了包管理这一步。那为什么cloudflare-os这类项目偏爱 pnpm,而不是 npm 或 yarn?

核心原因是磁盘效率和依赖隔离。pnpm 用硬链接加内容寻址的方式存储依赖,同一个版本的包在磁盘上只存一份,多个项目共享。一个典型的 AI Agent 项目,依赖树里可能有几百个包,用 npm 装完轻松几百 MB,pnpm 能把这个数字压到三分之一甚至更低。更重要的是,pnpm 的node_modules结构是严格的,它不会像 npm 那样把未声明的依赖“提升”到顶层,这就避免了“我明明没装这个包,为什么能 import 到”的幽灵依赖问题。在多人协作的项目里,这个特性救过我不止一次。

但 pnpm 也是新手最容易翻车的地方。最常见的两个报错,一个是pnpm : 无法将“pnpm”项识别为 cmdlet,一个是'pnpm' 不是内部或外部命令。这两个本质上是同一个问题:pnpm 没有正确安装,或者装了但没进 PATH。Windows 上用 npm 全局装 pnpm 之后,有时候需要重启终端,甚至重启系统,PATH 才会刷新。如果你用的是 PowerShell,还可能遇到执行策略限制,需要先跑一句Set-ExecutionPolicy -Scope CurrentUser RemoteSigned。这些细节官方文档不会重点讲,但实际卡住的人一大片。

2.3 部署工具:wrangler 的角色和它的隐藏价值

wrangler是 Cloudflare 官方的命令行工具,负责本地开发、构建、部署、查看日志这一整套流程。很多人把它当成一个“部署按钮”,其实它的价值远不止于此。wrangler dev可以在本地起一个模拟 Workers 运行时的环境,你改代码它热重载,还能直接连远程的 KV、D1、R2 这些存储做调试。这意味着你不需要每次改一行代码就部署一次,本地就能把大部分逻辑验证完。

我特别想强调wrangler.toml这个配置文件。它是整个项目的“中枢神经”,里面定义了入口文件、兼容性日期、环境变量、绑定资源(KV 命名空间、D1 数据库、R2 存储桶等)。很多新手部署失败,问题就出在这个文件上:要么compatibility_date写得太老,导致某些新 API 不可用;要么绑定资源的 ID 填错,运行时直接报“binding not found”。我的建议是,每次新增一个绑定,就在本地用wrangler dev跑一遍,确认能读到再部署,别攒一堆改动一起上,出了问题很难定位。

3. 核心细节解析:一个边缘 AI Agent 的关键构件

3.1 入口与路由:请求是怎么被分发的

一个典型的cloudflare-os风格项目,入口文件通常长这样:导出一个带有fetch方法的对象,所有进来的请求都从这里进入。这个fetch方法接收request、env、ctx三个参数,分别代表请求对象、环境绑定、执行上下文。路由分发就在这个函数里做,常见做法是用一个轻量的路由库,或者干脆手写一个switch判断url.pathname。

这里有个实操细节值得说:路由匹配要放在最前面,且要处理 OPTIONS 预检请求。因为 AI Agent 服务经常要被前端页面跨域调用,如果你不处理 CORS,浏览器会先发一个 OPTIONS 请求,你的代码如果直接当成业务请求处理,就会返回错误,前端看到的就是“跨域失败”。正确做法是在路由最前面判断request.method === 'OPTIONS',直接返回带 CORS 头的空响应。这个坑我踩过,当时排查了半天以为是模型接口的问题,结果是预检没过。

3.2 环境变量与密钥管理:别把 API Key 写进代码

AI Agent 必然要调用大模型 API,这就涉及密钥管理。新手最容易犯的错,就是把 API Key 直接硬编码在代码里,然后一不小心提交到了公开仓库。正确的做法是用wrangler secret put把密钥存成加密的环境变量,代码里通过env.XXX读取。这样密钥不会出现在代码里,也不会出现在wrangler.toml里。

但这里有个容易混淆的点:wrangler.toml里的[vars]和wrangler secret是两回事。[vars]是明文变量,适合放一些非敏感的配置,比如模型名称、超时时间;secret是加密的,适合放 API Key、数据库密码。我见过有人把 API Key 放进[vars],然后以为这样就安全了,其实它只是没写死在代码里,但依然是明文存储的。判断标准很简单:这个值泄露了会不会造成损失?会,就用 secret。

3.3 调用大模型:流式响应是体验的分水岭

AI Agent 最核心的动作就是调用大模型。这里我要重点讲流式响应,因为它是“能用”和“好用”的分界线。非流式的做法是:等模型把整段话生成完,一次性返回给前端。用户看到的就是转圈圈等好几秒,然后突然蹦出一大段文字。流式的做法是:模型每生成一个 token,就通过 SSE(Server-Sent Events)推给前端,用户看到文字像打字一样一个个冒出来。

在 Workers 里实现流式响应,关键是用TransformStream把上游的响应流转发下去。代码结构大致是:拿到模型返回的ReadableStream,通过pipeThrough接一个转换流,再作为响应体返回。这里有个坑:必须设置正确的响应头,Content-Type要是text/event-stream,还要加Cache-Control: no-cache和Connection: keep-alive,否则某些代理或浏览器会缓冲整个响应,流式就变成了“假流式”。我调试这个的时候,前端一直收不到增量数据,最后发现是少了X-Accel-Buffering: no这个头。

3.4 工具调用与多 AI 协作:Agent 的“手脚”怎么接

热搜词里有“多 AI 协作”和“ai agent”,这正好对应 Agent 架构里最有意思的部分:工具调用。一个只会聊天的模型是“嘴”,能调用工具的模型才是“手脚”。在 Workers 里实现工具调用,通常的做法是:在发给模型的请求里带上工具定义(function schema),模型判断需要调用某个工具时,会返回一个结构化的调用请求,你的代码解析后执行对应函数,再把结果塞回对话历史,发起下一轮请求。

多 AI 协作则是更进一步:让一个“调度者”模型把任务拆解,分发给不同的“执行者”模型或工具,最后汇总结果。这套逻辑在 Workers 里完全跑得通,因为 Workers 支持fetch调用任意 HTTP 接口,也支持waitUntil做后台任务。但要注意,Workers 的单次请求有 CPU 时间限制,如果你的多轮协作链路太长,可能会超时。我的经验是把长链路拆成多个请求,用队列或者状态存储来串联,而不是硬塞在一次请求里。

4. 实操过程:从零跑通一个最小可用版本

4.1 环境准备:pnpm 和 wrangler 的正确安装姿势

先把地基打好。第一步是装 Node.js,建议用 18 或 20 的 LTS 版本,别用太新的奇数版本,兼容性容易出问题。装完之后验证一下node -v和npm -v都能正常输出。

接下来装 pnpm。最稳的方式是用 Node 自带的 corepack:corepack enable然后corepack prepare pnpm@latest --activate。这样装出来的 pnpm 和 Node 版本绑定,不容易出现 PATH 问题。如果你用npm install -g pnpm装完发现命令找不到,先别急着重装,试试关掉终端重新开一个,Windows 用户尤其要注意这一点。

装完 pnpm 之后,验证pnpm -v能输出版本号。如果还是报“不是内部或外部命令”,那就是 PATH 没配好。Windows 上可以手动把 npm 的全局目录加到系统环境变量里,路径通常是C:\Users\你的用户名\AppData\Roaming\npm。这一步搞定,后面就顺了。

wrangler 不需要全局装,作为项目依赖装就行:pnpm add -D wrangler。这样每个项目用自己锁定的 wrangler 版本,不会因为全局版本升级导致构建行为变化。装完之后用pnpm wrangler --version验证。

4.2 项目初始化:目录结构和关键文件

一个最小可用的项目结构大概是这样:

cloudflare-os-demo/ ├── src/ │ └── index.ts # 入口文件 ├── wrangler.toml # 部署配置 ├── package.json ├── tsconfig.json └── .dev.vars # 本地开发的环境变量(不要提交)

package.json里关键的是 scripts 部分,通常配三个命令:dev对应wrangler dev,deploy对应wrangler deploy,tail对应wrangler tail用来看线上日志。tsconfig.json里要把types指向@cloudflare/workers-types,这样编辑器里才有 Workers 专属 API 的类型提示,不然env、ctx这些参数全是红的。

wrangler.toml是最需要仔细写的。一个基础版本包含name(项目名)、main(入口文件路径)、compatibility_date(兼容性日期,建议写当前日期往前推一两个月,太新可能有未稳定的行为)。如果要绑定 KV 或 D1,就在这里加对应的配置块。

4.3 写第一个接口:从请求到模型调用的完整链路

入口文件里,我们先写一个最简单的健康检查接口,确认部署链路通了,再加模型调用。健康检查就是判断路径是/health时返回一个 JSON。跑pnpm dev,本地访问http://localhost:8787/health,能看到返回就说明基础环境没问题。

然后加模型调用接口。核心逻辑是:读取请求体里的用户消息,拼成模型需要的格式,用fetch调用模型 API,把返回流转发出去。这里要注意,模型 API 的 Key 从env里读,本地开发时放在.dev.vars文件里,格式是MODEL_API_KEY=xxx。.dev.vars一定要加进.gitignore,这是血泪教训。

流式转发的代码骨架:

const upstream = await fetch(modelEndpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${env.MODEL_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); return new Response(upstream.body, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no', }, });

这段代码看起来简单,但每一行都有讲究。upstream.body直接透传,不做缓冲,这是流式的关键。响应头那三行,少一个都可能出问题。

4.4 部署上线:wrangler deploy 之后要做的三件事

pnpm deploy跑完,wrangler 会给你一个xxx.workers.dev的地址。别急着庆祝,先做三件事。

第一,用wrangler tail看实时日志,然后访问一下线上接口,确认没有报错。线上环境和本地环境最大的差异是环境变量,本地.dev.vars里的东西不会自动同步到线上,需要用wrangler secret put重新设置一遍。我见过太多次“本地好好的,一上线就 500”,九成是密钥没配。

第二,测一下流式响应是不是真的流式。用curl -N访问接口,看输出是不是逐块出现的。如果是一次性全出来,检查响应头是不是被某个环节改掉了。

第三,测一下冷启动延迟。Workers 虽然冷启动快,但第一次请求还是会比后续慢一点。如果这个延迟在你的可接受范围内,就可以进入下一步优化了。

5. 常见问题与排查技巧实录

5.1 pnpm 相关报错的速查表

报错信息根本原因解决方法
pnpm 不是内部或外部命令PATH 未包含 pnpm 安装目录重启终端,或手动添加 npm 全局目录到 PATH
无法将“pnpm”项识别为 cmdletPowerShell 执行策略限制运行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
pnpm 下载失败网络问题或镜像源不可达切换镜像源,或使用离线安装包
ERR_PNPM_NO_MATCHING_VERSION依赖版本冲突检查 package.json 里的版本约束,用pnpm why定位

关于“pnpm 离线安装”,如果你的网络环境确实不稳定,可以在一台能正常下载的机器上跑pnpm store path找到存储目录,把整个 store 拷到目标机器,再设置pnpm config set store-dir指向它。这样即使断网也能装依赖。这个方法我在内网环境里用过,很稳。

5.2 Workers 运行时的典型坑

坑一:compatibility_date太老导致 API 不可用。有些新特性是分阶段开放的,如果你的日期写得太早,某些 API 会直接不存在。解决办法是把日期更新到较近的时间,但别用未来日期。

坑二:绑定资源读不到。报错通常是binding XXX not found。检查wrangler.toml里的绑定名称和代码里env.XXX的名称是否完全一致,大小写敏感。另外,本地开发时某些绑定需要加--remote参数才能连到真实资源。

坑三:CPU 时间超限。Workers 对单次请求的 CPU 时间有限制,如果你的 Agent 逻辑里有大量循环或复杂计算,会触发Exceeded CPU Limit。解决办法是把重计算拆出去,或者用ctx.waitUntil把非关键逻辑放到响应之后执行。

5.3 流式响应的三个隐形杀手

第一个是中间层缓冲。如果你的请求经过了某些反向代理,它们可能会缓冲整个响应再转发。解决办法是加X-Accel-Buffering: no头,或者确认代理层关闭了缓冲。

第二个是响应头缺失。Content-Type必须是text/event-stream,否则浏览器不会按 SSE 解析。这个错误很隐蔽,因为接口本身是通的,只是前端收不到增量。

第三个是前端读取方式不对。流式响应不能用普通的fetch().then(r => r.json()),要用response.body.getReader()逐块读取,或者用EventSource。很多人后端写对了,前端读错了,然后以为是后端的问题。

5.4 我踩过的三个真实坑

第一个坑是在 Workers 里用了 Node 的Buffer。本地开发时因为兼容层存在,居然跑通了,一部署就报错。后来才知道 Workers 用的是 Web 标准的Uint8Array和TextEncoder,不是 Node 的 Buffer。这个教训是:本地能跑不代表线上能跑,涉及运行时 API 的地方一定要查文档。

第二个坑是把大文件塞进环境变量。我想把一个提示词模板放进[vars],结果因为太长导致部署失败。环境变量有大小限制,长文本应该放在 KV 或者 R2 里,运行时读取。

第三个坑是忘记处理模型 API 的错误响应。模型接口偶尔会返回 429 或 500,如果代码里直接upstream.body透传,前端收到的是一个错误流,解析会出问题。正确做法是先判断upstream.ok,不 OK 就返回一个结构化的错误信息。

6. 性能与成本:边缘 AI 服务的账怎么算

6.1 延迟优化的几个着力点

延迟是 AI 服务体验的核心。除了前面说的流式响应,还有几个地方可以优化。一是减少不必要的中间调用,比如你的 Agent 如果只是转发模型响应,就别在中间再套一层自己的 API。二是合理使用缓存,对于相同或相似的请求,可以用 KV 缓存模型响应,命中缓存直接返回,省掉一次模型调用。三是就近部署,Workers 本身是全球分布的,但如果你调用的模型 API 在特定区域,可以考虑用智能路由把请求导向延迟最低的端点。

6.2 成本结构的真实拆解

Workers 的计费主要看请求数和 CPU 时间,免费额度对个人项目和小规模测试完全够用。真正的大头是模型 API 的调用费用。这里有个容易被忽略的点:流式响应并不会降低模型费用,因为模型该生成多少 token 还是多少。省钱的关键在于控制输入长度和输出长度,比如精简系统提示词、限制历史对话轮数、对输出做长度约束。

我自己的做法是给每个 Agent 设一个 token 预算,超过预算就截断或者提示用户。这个逻辑放在 Workers 里做很合适,因为它在请求入口,能在调用模型之前就把不该发的请求拦下来。

6.3 监控与日志:上线之后怎么知道它好不好

wrangler tail是最基础的实时日志工具,但它只能看当前时刻。要做长期监控,可以把关键指标(请求量、错误率、模型调用耗时)写到 Analytics Engine 或者外部监控服务。我的习惯是在每个请求的关键节点打一个结构化日志,包含请求 ID、耗时、状态,这样出问题的时候能快速定位是哪个环节慢。

另外,Workers 的日志默认保留时间有限,如果你需要长期留存,得主动导出。对于个人项目,wrangler tail加上偶尔手动检查基本够用;对于正式服务,建议接入一个日志聚合方案。

7. 这套东西还能怎么扩展

跑通最小版本之后,cloudflare-os这类项目的想象空间其实很大。往存储方向走,可以接 D1 做对话历史持久化,接 R2 做文件上传和读取,接 Vectorize 做向量检索,这样就能搭出一个带知识库的问答 Agent。往协作方向走,可以用 Durable Objects 做多用户会话状态管理,让多个用户共享同一个 Agent 的上下文。往工具方向走,可以把常用的外部 API 封装成工具函数,让模型按需调用。

我自己最近在试的一个方向,是把 Agent 的配置也做成动态的:提示词模板、工具列表、模型参数都存在 KV 里,运行时读取。这样调整 Agent 行为不需要重新部署,改 KV 就行。这个模式在需要频繁调优的场景下特别省事,推荐你也试试。

最后分享一个我在实际使用中的体会:边缘 AI 服务最大的价值不是省钱,而是把“想法到上线”的距离缩短到了分钟级。以前搭一个 Agent 服务,光是配服务器、配环境、配部署就要折腾半天,现在pnpm dev写完,pnpm deploy就上线了。这个反馈速度对迭代效率的提升,比任何单点优化都来得实在。

RELATED READING

延伸阅读

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