ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

cloudflare-os 实战:边缘运行时构建 AI agent 操作系统

cloudflare-os 实战:边缘运行时构建 AI agent 操作系统

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

第一次看到cloudflare-os这个标题,我脑子里冒出来的第一个念头是:这大概率不是又一个"把 Linux 塞进浏览器"的玩具项目。因为标题里同时挂着 Cloudflare Workers、AI、agent、pnpm 这几个词,组合起来指向的方向非常明确——在边缘运行时上跑一套面向 AI agent 的操作系统式抽象层。换句话说,它想做的事情,是把"一个 agent 从被唤醒、加载技能、调用工具、维护记忆、到最终产出结果"这一整套生命周期,收敛到一个部署在边缘节点上的统一运行时里。

我先把结论摆在前面:这类项目的核心价值不在于"操作系统"这四个字有多唬人,而在于它试图解决一个真实存在的痛点——agent 的部署和状态管理目前太碎了。你可能用 Python 写了一个 agent,用 Node 写了工具层,记忆存在某个向量数据库,技能散落在各个 prompt 文件里,最后还要自己搭一套调度。cloudflare-os 这类思路的本质,是提供一个"骨架",让 agent 的注册、路由、技能加载、会话状态、工具调用都有一套统一的约定,而底层跑在 Cloudflare Workers 这种无服务器边缘环境上,天然具备全球分布、冷启动快、按需计费的特点。

那它适合谁?我的判断是三类人:第一类是已经在做 agent 开发、但被部署和状态管理折磨过的工程师;第二类是想理解"agent 运行时"到底该怎么设计的架构爱好者;第三类是手里有一堆零散 AI 能力、想把它们组织成一个可复用系统的独立开发者。如果你只是想让 ChatGPT 帮你写个周报,那这个项目跟你没关系;但如果你想搞清楚"agent 框架"和"agent 运行时"的区别,那往下看。

这里必须先厘清一个高频混淆点,因为热搜词里同时出现了harness和agent区别、agent架构、agent框架、agent项目。我的理解是:agent 是"干活的实体",harness 是"约束和驱动这个实体的外壳"。打个比方,agent 是司机,harness 是方向盘加仪表盘加安全带。cloudflare-os 更偏向 harness 这一层——它不负责你的 agent 有多聪明,它负责让 agent 跑得稳、管得住、可观测。这个定位想清楚了,后面所有的技术选型就都顺了。

2. 核心架构拆解:为什么是 Workers + pnpm + 技能化设计

2.1 为什么把运行时放在边缘而不是传统服务器

传统 agent 部署一般是这么干的:一台云主机,跑一个 FastAPI 或者 Express 服务,agent 逻辑全在里面,状态存 Redis 或 Postgres。这套方案我用了很久,问题也很明显——冷启动和扩缩容是两座大山。用户量一上来,你得手动扩容;没人用的时候,机器还在烧钱;跨地域访问延迟高,因为你的服务可能只在一个区域。

Cloudflare Workers 这类边缘运行时的逻辑完全不同。它把你的代码分发到全球几百个节点,请求打到离用户最近的那个节点执行,冷启动通常在毫秒级。对于 agent 这种"请求-响应"特征明显、单次执行时间不长的场景,简直是量身定做。但代价也很明确:运行环境受限。Workers 不是完整的 Node.js,没有文件系统,没有原生 TCP,CPU 执行时间有上限,内存也有限制。所以 cloudflare-os 这类项目在设计时,必须把"什么放在边缘、什么放在外部"这条线划得非常清楚。

我的经验是,边缘层适合放这些东西:请求路由、会话状态(用 KV 或 Durable Objects)、技能注册表、轻量级的 prompt 组装、工具调用的编排。而重计算、大模型推理、向量检索这些,必须外置。这个边界一旦模糊,项目就会变成一个"什么都想塞进边缘"的四不像。

2.2 pnpm 在这里扮演的角色,以及那些安装报错

热搜词里 pnpm 相关的词条特别多,pnpm下载失败、pnpm : 无法将"pnpm"项识别为 cmdlet、pnpm' 不是内部或外部命令、删除pnpm、pnpm离线安装、ubuntu安装pnpm……这说明大量人在第一步就卡住了。我先把这个说透,因为它直接影响你能不能跑起来 cloudflare-os 这类项目。

pnpm 相比 npm 和 yarn 的核心优势是硬链接 + 内容寻址存储。简单说,同一个依赖包在你机器上只存一份,所有项目通过硬链接引用它。这带来的直接好处是磁盘占用小、安装快、依赖树严格(不会出现幽灵依赖)。对于 monorepo 结构的 agent 项目——通常会有packages/core、packages/skills、apps/worker这种划分——pnpm 的 workspace 支持几乎是刚需。

安装报错的根因,九成是环境变量 PATH 没配好。Windows 上用 npm 全局装了 pnpm 之后,如果 npm 的全局 bin 目录不在 PATH 里,就会出现"无法识别为 cmdlet"。解决办法是找到 npm 全局路径(npm config get prefix),把它加进系统环境变量。Ubuntu 上更推荐用 corepack,因为 Node 16.13 之后自带:

corepack enable corepack prepare pnpm@latest --activate

离线安装的场景我也踩过。如果你在内网环境,需要先在能联网的机器上执行pnpm fetch,把依赖缓存下来,再把整个 store 目录拷过去,配合pnpm install --offline。这里有个坑:pnpm 的 store 路径和项目里的.npmrc配置必须一致,否则离线安装会找不到包。我一般会在项目根目录固定一个.npmrc,写明store-dir,避免不同机器行为不一致。

2.3 技能化(skill)设计背后的逻辑

热搜里agent skill教程、claude agent skills: a first principles deep dive这些词,说明"技能"这个概念正在成为 agent 开发的主流抽象。cloudflare-os 这类项目大概率也采用了类似思路:把 agent 的能力拆成一个个独立的 skill,每个 skill 是一个自包含的模块,声明自己的输入、输出、依赖和触发条件。

为什么这么设计?因为单体式 agent 的 prompt 会随着功能增加而爆炸。你想想,一个 agent 既要会查天气、又要会发邮件、又要会做数据分析,如果全塞进一个 system prompt,那 prompt 会长到离谱,模型注意力被稀释,效果反而下降。技能化的本质是按需加载——agent 先看用户意图,判断需要哪个技能,再把这个技能的描述和工具定义注入上下文。这样每次调用的上下文都是精简的、聚焦的。

从工程角度看,技能化还带来一个隐性好处:可测试性。每个 skill 可以单独写单元测试,mock 掉外部依赖,验证输入输出。这在 agent 开发里太重要了,因为 agent 的行为本来就难测,如果连技能都是黑盒,那整个系统就没法维护。

3. 实操落地:从零搭一个最小可用的 agent 运行时

3.1 环境准备与项目初始化

我按最常见的 monorepo 结构来演示,这套结构我在多个 agent 项目里复用,实测很稳。先确认 Node 版本在 18 以上,然后:

corepack enable mkdir cloudflare-os-demo && cd cloudflare-os-demo pnpm init

接着建 workspace 配置。在根目录新建pnpm-workspace.yaml:

packages: - "packages/*" - "apps/*"

然后建三个包:packages/core(核心运行时抽象)、packages/skills(技能集合)、apps/worker(边缘入口)。每个包自己的package.json里,跨包引用用workspace:*协议,这样 pnpm 会自动软链到本地,改代码即时生效,不用反复发包。

提示:如果你在 Windows 上遇到pnpm命令找不到,先别急着重装,执行where pnpm看看路径,再对照npm config get prefix,八成是 PATH 的问题。

3.2 核心运行时的最小实现

packages/core里我一般放三样东西:agent 注册表、技能加载器、会话管理器。先看技能加载器的核心逻辑,这是整个系统的地基:

// packages/core/src/skill-registry.ts export interface Skill { name: string; description: string; triggers: string[]; execute: (input: unknown, ctx: SkillContext) => Promise<unknown>; } export class SkillRegistry { private skills = new Map<string, Skill>(); register(skill: Skill) { if (this.skills.has(skill.name)) { throw new Error(`Skill ${skill.name} already registered`); } this.skills.set(skill.name, skill); } match(intent: string): Skill[] { return [...this.skills.values()].filter((s) => s.triggers.some((t) => intent.toLowerCase().includes(t.toLowerCase())) ); } get(name: string): Skill | undefined { return this.skills.get(name); } }

这段代码看着简单,但有几个设计决策值得说。第一,register里做了重名检查,因为技能冲突是 agent 系统里非常隐蔽的 bug——两个技能触发词重叠,agent 随机选一个,行为就不稳定了。第二,match用的是简单的关键词包含,而不是向量相似度。为什么?因为在边缘环境里,每次请求都做一次 embedding 调用成本太高、延迟太大。先用轻量规则做粗筛,再用模型做精排,这是我在生产环境验证过的性价比最高的方案。

3.3 会话状态管理:Durable Objects 的正确用法

agent 和普通 API 最大的区别是它是有状态的。用户说"帮我改一下刚才那封邮件",你得知道"刚才那封"是什么。在 Workers 环境里,管理这种状态的标准答案是 Durable Objects。

// apps/worker/src/session.ts export class AgentSession { private state: DurableObjectState; private history: Array<{ role: string; content: string }> = []; constructor(state: DurableObjectState) { this.state = state; this.state.blockConcurrencyWhile(async () => { this.history = (await this.state.storage.get("history")) || []; }); } async fetch(request: Request) { const { message } = await request.json(); this.history.push({ role: "user", content: message }); // 这里调用 agent 编排逻辑 const reply = await this.runAgent(message); this.history.push({ role: "assistant", content: reply }); await this.state.storage.put("history", this.history); return new Response(JSON.stringify({ reply })); } }

这里的关键点是blockConcurrencyWhile。Durable Objects 是单线程的,但如果你在构造函数里异步加载状态,而请求已经进来了,就会读到空状态。这个 API 保证在状态加载完成前,所有请求都被阻塞。我见过太多人在这里翻车,表现为"第一次请求正常,第二次就丢历史"。

注意:Durable Objects 的存储有大小限制,历史记录不能无限增长。我的做法是保留最近 N 轮对话,更早的做摘要压缩后存 KV,需要时再取回。

3.4 技能的具体实现示例

光有框架不够,得有个真实技能才能跑通。我拿"天气查询"举例,因为它足够简单又能体现完整链路:

// packages/skills/src/weather.ts import type { Skill } from "@cloudflare-os/core"; export const weatherSkill: Skill = { name: "weather", description: "查询指定城市的当前天气", triggers: ["天气", "气温", "下雨", "weather"], async execute(input: { city: string }, ctx) { const res = await fetch( `https://api.example.com/weather?city=${encodeURIComponent(input.city)}`, { headers: { Authorization: `Bearer ${ctx.env.WEATHER_KEY}` } } ); if (!res.ok) throw new Error(`Weather API failed: ${res.status}`); const data = await res.json(); return { city: input.city, temp: data.temp, desc: data.desc }; }, };

注意execute的签名里带了ctx,这是把环境变量、日志、其他技能引用都透传进来的通道。技能之间不应该直接 import 彼此,而应该通过 ctx 提供的接口调用,否则依赖关系会变成一团乱麻。

4. 踩坑实录:agent 运行时最常见的六类问题

4.1 依赖与构建类问题速查

现象根因解决
pnpm不是内部或外部命令PATH 未包含 npm 全局 bin把npm config get prefix加入 PATH
pnpm install卡住不动registry 不可达或缓存损坏换 registry,或pnpm store prune后重试
离线安装报找不到包store-dir 配置不一致统一.npmrc里的store-dir
workspace 包引用失效未用workspace:*协议改 package.json 依赖声明
构建产物过大把重依赖打进了 worker用动态 import 拆分,重逻辑外置

这张表里的每一条我都在真实项目里遇到过。尤其是最后一条,Workers 的包体积有硬限制,如果你不小心把某个大 SDK 打进去,部署直接失败。我的习惯是在 CI 里加一步wrangler deploy --dry-run,提前看体积。

4.2 agent 行为不稳定的排查思路

agent 最让人头疼的不是报错,而是"有时候对有时候错"。我的排查顺序是这样的:先看上下文,再看技能匹配,最后看模型。上下文问题占七成——要么是历史记录太长把关键信息挤掉了,要么是技能描述写得含糊导致模型选错。技能匹配问题占两成——触发词设计得太宽泛,多个技能抢同一个意图。真正是模型能力问题的,其实不到一成。

有个具体技巧:给每次 agent 调用打上 trace id,把注入的 prompt、匹配到的技能、模型返回全部落日志。这样出问题时能完整回放。我一开始嫌麻烦没做,后来被一个偶发 bug 折腾了两天,从此每个项目第一件事就是搭这套可观测性。

4.3 关于"agent 安全"的几点务实建议

热搜里agent安全是个高频词,但很多讨论停留在概念层面。我说几个能落地的:第一,技能执行要有超时和熔断,一个技能卡住不能拖垮整个会话;第二,外部输入永远不要直接拼进 prompt,要做转义和长度限制,防止提示注入;第三,敏感操作(发邮件、转账、删数据)必须走二次确认,不能让 agent 自主决定。这三点做到了,能挡掉绝大多数现实风险。

5. 多 agent 协作与后续扩展方向

5.1 什么时候需要多 agent,什么时候不需要

多ai协作、agent架构这些词很热,但我得泼盆冷水:大部分场景单 agent 加多技能就够了。多 agent 的价值在于"职责隔离"——比如一个负责规划、一个负责执行、一个负责审查,它们的目标函数不同,互相制衡。如果你只是功能多,那加技能就行,别上多 agent,否则通信开销和状态同步的复杂度会让你怀疑人生。

真要用多 agent,我的建议是从两个开始:一个 planner,一个 executor。planner 只输出结构化计划,executor 只负责执行单步。两者通过一个共享的任务队列通信,而不是直接对话。这样调试起来清晰得多。

5.2 记忆系统的分层设计

agent记忆是另一个绕不开的话题。我的实践是分三层:工作记忆(当前会话,放 Durable Object)、短期记忆(最近几天,放 KV)、长期记忆(沉淀的知识,放向量库)。查询时从近到远逐层检索,命中就停。这样既保证了响应速度,又不会丢失重要信息。全塞进向量库的做法我试过,检索延迟高,而且很多临时信息根本不该进长期库。

5.3 这套架构还能怎么扩展

cloudflare-os 这类项目的想象空间其实挺大。往小了说,你可以把它当成一个"个人 AI 助手后端",接上自己的各种工具。往大了说,它可以变成一个"技能市场"——大家写技能、发布技能、组合技能。我最近在尝试的方向是把技能用声明式配置描述,让非程序员也能通过改 YAML 来组装 agent。这条路走通了,agent 开发的门槛会降一大截。

最后分享一个我踩过的小坑:别在项目初期就追求架构完美。我第一个 agent 项目花了三周设计"完美的技能抽象层",结果真正跑起来发现需求完全不是那样,全部推倒重来。后来我改成先跑通一个最丑但能用的版本,再逐步重构,效率反而高得多。技能注册表、会话管理这些核心抽象可以早点定,但具体的技能接口,一定要等有了两三个真实技能之后再抽象,否则就是拍脑袋。

RELATED READING

延伸阅读

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