Cloudflare Workers 全栈实战:用 pnpm 和 wrangler 搭建边缘 AI 应用
1. 从"cloudflare-os"这个名字说起:它到底想解决什么问题
第一次看到cloudflare-os这个仓库名,我下意识以为是某个跑在浏览器里的 Web 桌面系统。点进去翻了翻目录结构才反应过来,它其实是一个把 Cloudflare Workers 当作"操作系统"来用的全栈应用脚手架——用 Workers 当运行时内核,用 D1 当持久化存储,用 R2 当对象存储,用 KV 当缓存层,再用 Workers AI 当"系统级 AI 能力"。整套东西跑在 Cloudflare 的边缘节点上,本地只需要一个pnpm和wrangler就能把整个"操作系统"拉起来。
这个定位很有意思。传统意义上我们要搭一个带 AI 能力的全栈应用,通常得准备一台服务器、装 Node、配数据库、搞反向代理、再单独接一个大模型 API。而cloudflare-os的思路是:把这些基础设施全部抽象成 Workers 的绑定(binding),你写业务逻辑的时候,调用 AI 就像调用一个本地函数一样自然。对于想快速验证 AI Agent、多 AI 协作、边缘计算这类场景的开发者来说,这套脚手架省掉了大量环境搭建的功夫。
它适合谁?我的判断是三类人:一是想入门 Cloudflare Workers 生态但不知道从哪下手的前端/全栈开发者;二是需要快速搭一个 AI Agent 原型、又不想被服务器运维拖住的后端同学;三是已经在用wrangler部署、想进一步把 D1、R2、Workers AI 串起来的中级玩家。如果你连pnpm都还没装明白,别急,后面我会专门讲这块——因为热词里"pnpm 不是内部或外部命令"这个报错,几乎是每个 Windows 用户的第一道坎。
需要先说明一点:cloudflare-os这个项目本身在公开资料里并没有一个官方统一的定义,不同人 fork 出来的版本差异很大。所以下面我讲的架构和实操,是基于 Cloudflare Workers 生态的通用最佳实践 + 这类"OS 式脚手架"最常见的组织方式来展开的,你在实际项目里可以按需裁剪。核心逻辑不会变:Workers 负责计算,绑定负责能力,wrangler 负责部署。
2. 环境准备:pnpm 与 wrangler 这两道坎必须先过
2.1 为什么这类项目偏爱 pnpm 而不是 npm
很多人第一次 clone 这类项目,看到pnpm install就懵了:npm 用得好好的,为什么非要换?原因在于 Workers 项目的依赖树通常又深又碎——wrangler、@cloudflare/workers-types、各种 AI SDK、构建工具链,用 npm 装完node_modules能轻松突破 500MB。pnpm 用的是内容寻址存储 + 硬链接,同一个版本的包在磁盘上只存一份,多个项目共享,装完可能只有几十 MB。
更关键的是 pnpm 的严格依赖隔离。npm 的扁平化node_modules会让你的代码能 import 到那些你根本没在package.json里声明的"幽灵依赖",本地跑得好好的,一部署到 Workers 就报模块找不到。pnpm 默认不允许这种越界访问,等于提前帮你把部署时的坑在本地就暴露出来了。这一点在边缘运行时环境里尤其重要,因为 Workers 的模块解析比 Node 严格得多。
2.2 Windows 上"pnpm 不是内部或外部命令"的完整排查链路
这个报错我在热词里看到好几次,说明踩的人真不少。它的本质只有一个:系统 PATH 里找不到 pnpm 的可执行文件。但导致这个结果的原因有好几种,得按顺序排。
第一步,先确认你到底装没装成功。打开 PowerShell 跑:
npm list -g --depth=0如果列表里没有pnpm,那就是压根没装上,直接npm install -g pnpm。如果列表里有pnpm但命令还是用不了,那就是 PATH 问题。
第二步,查 npm 的全局安装路径:
npm config get prefix假设输出是C:\Users\你的用户名\AppData\Roaming\npm,你需要确认这个路径在系统环境变量 PATH 里。Windows 上改 PATH 的坑在于:改完必须重开终端,老终端不会自动刷新环境变量。我见过太多人改完 PATH 在当前窗口试还是报错,以为没生效,其实开个新窗口就好了。
第三步,如果 PATH 也对、重开终端还是不行,检查是不是装了多个 Node 版本管理器(nvm-windows、fnm 之类)。这类工具切换 Node 版本时,全局包的路径会跟着变,你在这个版本装的 pnpm,切到另一个版本就找不到了。解决办法是在当前使用的 Node 版本下重新npm install -g pnpm。
提示:如果你用的是 PowerShell 而不是 CMD,有时候会遇到执行策略拦截脚本的问题,报错信息不太一样但表现类似。这种情况用
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned放行当前用户即可,不需要动系统级策略。
2.3 pnpm 离线安装:内网环境下的正确姿势
热词里"pnpm 离线安装"出现频率很高,说明不少人在没有外网的环境里干活。pnpm 的离线安装有两条路。
一条是用pnpm store做本地缓存迁移。在有网的机器上先跑一遍完整安装,pnpm 会把所有包存到全局 store(默认在~/.pnpm-store或 Windows 的%LOCALAPPDATA%\pnpm\store)。把这个 store 目录整个拷到离线机器上,然后:
pnpm config set store-dir /path/to/copied/store pnpm install --offline--offline参数会强制 pnpm 只用本地 store,不去网络请求。只要 store 里的包版本和pnpm-lock.yaml完全对得上,就能装成功。
另一条路是直接用pnpm fetch生成一个离线包。在有网环境跑pnpm fetch,它会把 lockfile 里所有依赖下载到一个虚拟 store,打包带走后在离线机pnpm install --offline。这条路更适合跨机器、跨平台的场景,因为pnpm fetch会按目标平台的架构去拉对应的二进制包。
注意:离线安装最容易翻车的地方是平台不一致。你在 Mac 上 fetch 的包,拿到 Windows 上装,那些带原生二进制的依赖(比如 esbuild、workerd)会因为平台不匹配而失败。跨平台离线部署一定要在目标平台上执行 fetch。
2.4 wrangler 的登录与项目初始化
环境通了之后,装 wrangler:
pnpm add -D wrangler然后npx wrangler login走一遍浏览器授权。这一步会拿到一个 OAuth token 存在本地,后续wrangler deploy就不用反复登录了。如果你在 CI 环境里跑,用CLOUDFLARE_API_TOKEN环境变量代替交互式登录,token 在控制台里按最小权限原则生成,只勾选这个项目需要的 Workers、D1、R2 权限就行。
初始化项目时,wrangler.toml(或新版推荐的wrangler.jsonc)是整个"操作系统"的配置文件,所有绑定都在这里声明。下面这张表是我整理的核心绑定类型和它们的用途,先有个全局印象:
| 绑定类型 | 配置字段 | 典型用途 | 类比 |
|---|---|---|---|
| Workers AI | [ai] | 调用大模型推理 | 系统级 AI 服务 |
| D1 | [[d1_databases]] | 关系型数据存储 | 本地硬盘 |
| R2 | [[r2_buckets]] | 对象/文件存储 | 外接存储 |
| KV | [[kv_namespaces]] | 键值缓存 | 内存缓存 |
| Durable Objects | [[durable_objects]] | 有状态协调 | 进程/线程 |
| Vectorize | [[vectorize]] | 向量检索 | 索引数据库 |
3. 把 Workers 当"内核":cloudflare-os 的架构拆解
3.1 请求进来之后,Workers 里发生了什么
理解这套架构,最好的方式是跟着一个请求走一遍。用户在浏览器里发一个请求打到你的*.workers.dev域名,Cloudflare 的边缘网络会把它路由到离用户最近的一个节点,然后你的 Worker 代码在这个节点上被唤醒执行。
这里有个很多人一开始想不通的点:Worker 不是常驻进程。它没有传统服务器的"启动一次、一直运行"的概念,而是每个请求触发一次执行,执行完就销毁。这就是所谓的无状态执行模型。那问题来了——无状态怎么保存用户会话、怎么维护 AI 对话的上下文?
答案就是绑定。D1 存结构化数据(用户表、对话记录),KV 存高频读取的配置和缓存,Durable Objects 存需要强一致性的有状态数据(比如多 AI 协作时的共享黑板)。Worker 本身保持无状态,所有状态外置到这些托管服务里。这个设计的好处是水平扩展几乎零成本——请求量涨十倍,Cloudflare 自动帮你调度,你一行扩容代码都不用写。
3.2 为什么用 D1 而不是直接上外部数据库
有人会问:我直接连一个外部 PostgreSQL 不行吗?技术上可以,但在 Workers 环境里会很别扭。Workers 运行在边缘节点,如果数据库在某个中心机房,每次查询都要跨地域往返,延迟直接飙到几百毫秒。D1 是 Cloudflare 自家的边缘 SQLite,数据分布和计算节点在同一网络里,查询延迟能压到个位数毫秒。
D1 的定位是读多写少、单库规模中等的场景。它基于 SQLite,支持标准 SQL,迁移成本低。但它不是为高并发写入设计的——如果你的场景是每秒几千次写入,D1 会扛不住,这时候要考虑 Durable Objects 做写入聚合,或者干脆换架构。我在实际项目里的经验是:用户数据、配置、日志这类读多写少的,放 D1 非常合适;高频计数器、实时协作状态这类,交给 Durable Objects。
3.3 Workers AI 的调用逻辑与模型选择
cloudflare-os里最吸引人的部分就是 Workers AI。它把模型推理做成了一个绑定,你在代码里这样调用:
export default { async fetch(request, env) { const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', { messages: [ { role: 'system', content: '你是一个简洁的助手' }, { role: 'user', content: '用一句话解释边缘计算' } ] }); return Response.json(response); } }注意env.AI这个对象——它不是 HTTP 请求,而是 Workers 运行时直接注入的能力。这意味着没有网络往返开销,模型推理在 Cloudflare 的 GPU 集群上完成,你只管传参拿结果。
模型选择上,我的建议是分场景:轻量对话和分类任务用llama-3-8b-instruct,速度快成本低;需要更强推理的用llama-3-70b或者 Mistral 系列;做 embedding 检索用bge系列。别一上来就上最大的模型,边缘场景下响应延迟往往比模型能力更重要,用户等三秒和等三百毫秒的体验天差地别。
3.4 多 AI 协作在边缘架构里怎么落地
热词里"多 AI 协作"和"ai agent"是高频词,这正好是cloudflare-os这类架构的强项。多 AI 协作的核心难点是状态共享和任务编排,而 Durable Objects 天生适合干这个。
思路是这样的:给每个协作会话创建一个 Durable Object 实例,它作为"协调者"持有共享上下文。多个 AI Agent 各自是一个 Worker 函数,它们把结果写回这个 Durable Object,协调者负责汇总、去重、决定下一步派发给谁。因为 Durable Object 保证同一时刻只有一个实例在处理某个会话,天然避免了并发写冲突。
export class Coordinator { constructor(state, env) { this.state = state; this.env = env; } async fetch(request) { const { agentId, result } = await request.json(); let board = (await this.state.storage.get('board')) || {}; board[agentId] = result; await this.state.storage.put('board', board); return Response.json({ status: 'ok', board }); } }这个模式我在做多 Agent 原型时反复用过,比用外部消息队列简单得多,因为状态和计算都在同一个边缘节点上,没有跨服务通信的延迟。
4. 从零跑通一个最小可用的 cloudflare-os
4.1 项目骨架与 wrangler 配置
先建目录、初始化:
mkdir cloudflare-os && cd cloudflare-os pnpm init pnpm add -D wrangler然后手写wrangler.toml。这是整个项目的"内核配置",我把它拆开讲:
name = "cloudflare-os" main = "src/index.js" compatibility_date = "2024-09-23" [ai] binding = "AI" [[d1_databases]] binding = "DB" database_name = "cloudflare-os-db" database_id = "你的数据库ID" [[kv_namespaces]] binding = "CACHE" id = "你的KV命名空间ID" [[r2_buckets]] binding = "STORAGE" bucket_name = "cloudflare-os-storage"compatibility_date这个字段新手经常忽略,但它很重要——它决定了 Workers 运行时用哪一套行为规范。日期越新,能用的新特性越多,但某些旧行为也会变。建议锁定一个你测试过的日期,不要随便改,否则可能出现"昨天还好好的,今天部署就报错"的情况。
4.2 创建 D1、KV、R2 资源的命令行操作
配置里那些 ID 不是手填的,得先用 wrangler 创建:
npx wrangler d1 create cloudflare-os-db npx wrangler kv namespace create CACHE npx wrangler r2 bucket create cloudflare-os-storage每条命令执行完,终端会打印出对应的 ID,把它复制到wrangler.toml里。D1 创建后还需要初始化表结构,写一个schema.sql:
CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );然后本地和远程分别执行:
npx wrangler d1 execute cloudflare-os-db --local --file=./schema.sql npx wrangler d1 execute cloudflare-os-db --remote --file=./schema.sql--local是本地模拟环境,--remote才是真正打到云端。开发阶段先用 local 快速迭代,确认没问题再 remote,这个习惯能帮你省下大量调试时间。
4.3 写一个带 AI 对话和持久化的接口
把上面这些串起来,写一个完整的对话接口:
export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/chat' && request.method === 'POST') { const { userId, message } = await request.json(); // 1. 存用户消息 await env.DB.prepare( 'INSERT INTO conversations (user_id, role, content) VALUES (?, ?, ?)' ).bind(userId, 'user', message).run(); // 2. 取最近上下文 const history = await env.DB.prepare( 'SELECT role, content FROM conversations WHERE user_id = ? ORDER BY id DESC LIMIT 10' ).bind(userId).all(); // 3. 调 AI const aiResp = await env.AI.run('@cf/meta/llama-3-8b-instruct', { messages: history.results.reverse().map(r => ({ role: r.role, content: r.content })) }); // 4. 存 AI 回复 await env.DB.prepare( 'INSERT INTO conversations (user_id, role, content) VALUES (?, ?, ?)' ).bind(userId, 'assistant', aiResp.response).run(); return Response.json({ reply: aiResp.response }); } return new Response('Not Found', { status: 404 }); } }这段代码虽然短,但把 D1 读写、上下文管理、AI 调用全串起来了。本地测试用npx wrangler dev,它会起一个本地服务器,D1 和 KV 都用本地模拟,AI 调用会真的打到 Cloudflare(因为本地没法模拟 GPU)。测通了再npx wrangler deploy上线。
4.4 本地开发与线上部署的差异清单
很多人本地跑得好好的,一部署就出问题。我把常见的差异列成表,部署前对着检查一遍:
| 差异点 | 本地 dev | 线上 deploy | 应对 |
|---|---|---|---|
| D1 数据 | 本地 SQLite 文件 | 云端 D1 | 记得 remote 执行 schema |
| KV 数据 | 本地模拟 | 云端 KV | 缓存预热要重新做 |
| 环境变量 | .dev.vars | 控制台 Secrets | 两边都要配 |
| AI 调用 | 真实调用 | 真实调用 | 注意配额消耗 |
| 模块解析 | 宽松 | 严格 | 避免幽灵依赖 |
.dev.vars这个文件专门放本地开发用的密钥,格式和.env一样,但千万别提交到 git,记得加进.gitignore。线上对应的密钥用npx wrangler secret put KEY_NAME逐个上传。
5. 踩坑实录:那些文档里不会写的细节
5.1 pnpm 与 wrangler 的版本兼容陷阱
我遇到过最隐蔽的一个坑:pnpm 装出来的 wrangler 版本和项目要求的对不上,导致wrangler.toml里某些字段被静默忽略。表现是部署成功但绑定不生效,接口报env.DB is undefined。
根因在于 pnpm 的严格依赖解析——如果package.json里 wrangler 写的是^3.0.0,pnpm 可能装到 3.80,而项目实际是按 3.60 的配置写的,中间某些字段语义变了。解决办法是锁定精确版本,把^去掉,或者用pnpm-lock.yaml严格锁定。部署前跑一次npx wrangler --version确认版本,和文档对得上再继续。
5.2 D1 查询的 prepare 与 bind 为什么不能省
新手很容易写出这种代码:
await env.DB.exec(`SELECT * FROM conversations WHERE user_id = '${userId}'`);这是典型的 SQL 注入漏洞,而且 D1 的exec本身也不推荐用于带参数的查询。正确做法永远是prepare().bind():
await env.DB.prepare('SELECT * FROM conversations WHERE user_id = ?').bind(userId).all();prepare会把 SQL 语句预编译,bind把参数作为独立数据传入,数据库引擎不会把参数内容当成 SQL 解析。这不仅是安全问题,性能上也更好——相同结构的查询可以复用预编译结果。
5.3 AI 调用的超时与降级策略
Workers AI 虽然快,但不是百分百稳定。我遇到过高峰期推理排队,请求卡了十几秒才返回。如果这是用户直接等待的接口,体验会很差。
我的做法是加一层超时和降级:
async function safeAI(env, messages, timeoutMs = 8000) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('AI timeout')), timeoutMs) ); try { return await Promise.race([ env.AI.run('@cf/meta/llama-3-8b-instruct', { messages }), timeout ]); } catch (e) { // 降级:返回缓存或默认回复 return { response: '当前请求较多,请稍后再试' }; } }Promise.race让 AI 调用和超时赛跑,谁先完成用谁。超时了就返回兜底文案,至少不会让用户干等。这个模式在任何调用外部服务的场景都适用,值得养成习惯。
5.4 边缘环境下的冷启动与内存限制
Workers 有内存上限(通常 128MB),而且实例会被回收。如果你的代码在模块顶层加载了一个巨大的数据集或初始化了重量级对象,每次冷启动都要重新来一遍,延迟会很难看。
我的经验是:把初始化逻辑尽量推迟到请求处理时,并且用 KV 或 D1 缓存计算结果。比如加载配置,不要const config = loadBigConfig()放在顶层,而是封装成一个带缓存的函数,第一次调用时从 KV 读,之后走内存缓存。这样冷启动时只加载真正需要的那部分。
注意:Workers 的全局变量在同一个实例的多次请求间是共享的,但实例随时可能被回收,所以不能把全局变量当持久存储用,它只是性能优化手段,数据正确性还得靠 D1/KV 保证。
6. 这套架构还能往哪些方向扩展
6.1 接入 Vectorize 做 RAG 检索
cloudflare-os的骨架搭好之后,最自然的扩展就是加 RAG(检索增强生成)。用 Vectorize 存文档向量,用户提问时先检索相关片段,再拼进 AI 的上下文。流程是:文档切块 → 用env.AI.run('@cf/baai/bge-base-en-v1.5')生成 embedding → 存入 Vectorize → 查询时用同样的模型生成查询向量 → 检索 top-k → 拼进 prompt。
这套东西全在 Cloudflare 内部完成,没有外部 API 调用,延迟可控。我在做知识库问答原型时用这个组合,从提问到返回答案能压到一秒以内。
6.2 用 Durable Objects 做实时协作
如果要做多人在线协作、实时聊天这类场景,Durable Objects 是绕不开的。它支持 WebSocket,每个会话一个实例,天然支持广播。配合 Workers AI,可以做出"多人共享一个 AI 助手"的效果——所有人的消息进同一个 Durable Object,AI 的回复广播给所有连接。
6.3 定时任务与队列处理
Workers 支持 Cron Triggers,可以在wrangler.toml里配[triggers] crons = ["0 * * * *"],每小时触发一次。适合做数据清理、缓存刷新、批量 AI 处理这类后台任务。如果任务量大,配合 Queues 做异步消费,避免单次执行超时。
6.4 监控与日志的落地方式
线上跑起来之后,npx wrangler tail能实时看日志,但它是流式的,不适合长期留存。生产环境建议接 Workers Analytics Engine 或者把关键日志写进 D1,自己写个查询页面。我一般会把 AI 调用的耗时、token 消耗、错误码记进一张ai_logs表,方便后续分析成本和优化模型选择。
这套东西我从头搭过好几遍,最大的体会是:边缘架构的爽点在于"没有运维",痛点也在于"没有运维"——你享受了自动扩缩容,但也失去了对底层环境的完全掌控。所以配置的每一个字段、绑定的每一个 ID,都要自己心里有数,别指望出问题了有人帮你兜底。把wrangler.toml当成真正的"内核配置"来对待,把每次部署当成一次系统升级来验证,这套cloudflare-os才能稳稳跑起来。