首页 / 资讯中心 / 文章详情

cloudflare-os 开源实践:基于 Durable Objects 构建生产级 AI Agent 运行时

cloudflare-os 开源实践:基于 Durable Objects 构建生产级 AI Agent 运行时 ★ FEATURED ARTICLE
1. 从“cloudflare-os”这个标题说起它到底想解决什么问题第一次看到“cloudflare-os”这个标题我脑子里蹦出来的第一个念头是这到底是一个操作系统还是一个跑在浏览器里的开发环境后来仔细琢磨了一下结合 Cloudflare Workers 这几年的演进路线我大概理解了它想做的事情——把 Cloudflare 的边缘网络能力包装成一套对 AI Agent 友好的“操作系统级”基础设施。说白了它不是让你去装一个 Linux 发行版而是把 Workers、Durable Objects、R2、D1、Vectorize、AI Gateway 这些原本分散的服务用一种更统一、更接近“操作系统抽象”的方式组织起来让开发者写 Agent 的时候不用再关心底层是哪个服务在跑、状态存在哪里、消息怎么路由。你只管写业务逻辑剩下的交给这套“OS”来调度。这个方向其实挺有意思的。过去一年我接触过不少 Agent 项目发现一个共性问题大部分时间不是花在“Agent 有多聪明”上而是花在“怎么让 Agent 稳定地跑起来、记住上下文、调用工具、处理并发”上。cloudflare-os 这个标题背后瞄准的正是这个痛点。它适合那些已经写过一两个 Demo Agent、但一到生产环境就各种翻车的开发者也适合想了解边缘计算和 Agent 架构怎么结合的技术负责人。我下面会从整体设计思路、核心模块拆解、实操落地、以及踩坑经验几个角度把这件事讲透。内容会涉及 Workers 的运行时特性、Durable Objects 的状态管理、Agent 的工具调用编排、以及怎么用开源方案复现一套类似的架构。如果你正在做 Agent 开发或者对 Cloudflare 的边缘生态感兴趣这篇应该能给你不少可直接抄作业的东西。2. 整体设计与思路拆解为什么是“OS”而不是“框架”2.1 从 Workers 到 Agent Runtime 的演进逻辑Cloudflare Workers 刚出来的时候大家把它当成一个“跑在边缘的 Serverless 函数”。你写一个 fetch handler收到请求返回响应完事。这个模型对于 API 网关、静态资源加速、A/B 测试这类场景非常合适因为它是无状态的、短生命周期的。但 Agent 不一样。Agent 需要记忆需要持续运行需要在多个回合之间保持状态还需要调用外部工具并等待结果。这些需求跟传统的 Serverless 模型是冲突的。你不可能每次对话都重新初始化一遍上下文也不可能让一个 Agent 在等待工具返回的时候把整个函数挂起。Cloudflare 的解法是引入 Durable Objects。你可以把它理解成一个有状态的、单实例的、可以长时间存活的对象。每个 Agent 可以对应一个 Durable Object它的状态存在对象内部通过 WebSocket 或者 HTTP 跟外界通信。这样一来Agent 的“记忆”就有了落脚点不需要每次都从数据库里重新读一遍。而“cloudflare-os”这个提法本质上是在 Durable Objects 之上再抽象一层把 Agent 的生命周期、工具注册、消息路由、状态持久化、安全隔离这些通用能力封装成一套类似操作系统的东西。你写 Agent 的时候就像在操作系统上写应用程序不用管内存怎么分配、进程怎么调度只需要关注业务逻辑。这个思路的好处很明显降低心智负担提高复用率。坏处也有抽象层越厚调试越难出问题的时候你很难判断是 Agent 逻辑错了还是 OS 层调度错了。所以我在实际项目中会建议如果你的 Agent 逻辑比较简单直接用 Durable Objects 就够了没必要上太重的抽象。但如果你要做多 Agent 协作、工具市场、权限管理这些复杂场景那这套 OS 层的价值就体现出来了。2.2 核心模块划分与职责边界按照我的理解一套完整的 Agent OS 至少应该包含以下几个模块模块职责对应 Cloudflare 服务运行时执行 Agent 逻辑管理生命周期Workers Durable Objects状态存储保存对话历史、Agent 配置、工具状态Durable Objects Storage D1向量记忆语义检索、长期记忆Vectorize R2工具网关注册、发现、调用外部工具Workers AI Gateway消息总线Agent 之间、Agent 与用户之间的通信Durable Objects WebSocket安全隔离权限控制、沙箱执行Workers 的隔离机制 Access可观测性日志、追踪、指标Workers Analytics Logpush这张表是我根据实际项目经验整理的不一定跟 Cloudflare 官方的规划完全一致但逻辑上是通的。你可以看到每个模块都对应一个具体的 Cloudflare 服务而不是凭空造一个抽象概念。这也是我觉得这套方案比较务实的地方——它没有重新发明轮子而是把现有的积木用一种更合理的方式搭起来。这里特别说一下工具网关。Agent 要干活必须能调用外部工具比如搜索、计算、发邮件、查数据库。传统做法是在 Agent 代码里硬编码这些工具的调用逻辑但这样扩展性很差。更好的做法是搞一个工具注册中心每个工具声明自己的名称、参数 schema、权限要求Agent 运行时根据这些元数据动态调用。Cloudflare 的 AI Gateway 可以在这个环节发挥作用比如做请求缓存、限流、成本统计。2.3 为什么选择开源路线标题里带了“open source”这个关键词我觉得这不是偶然的。Agent 这个领域现在处于标准未定、各家跑马圈地的阶段。如果 Cloudflare 只做一个闭源的托管服务开发者会担心锁定问题——万一哪天涨价了、改 API 了迁移成本太高。开源出来至少让大家能看到底层实现心里有底。而且开源还有一个好处社区可以贡献工具适配器。Agent 要调用的外部服务太多了靠一家公司不可能全部覆盖。开源之后大家可以把自己常用的工具封装成标准插件整个生态就活起来了。这一点上我觉得方向是对的但执行起来挑战也不小——开源项目的治理、版本兼容、安全审核都是坑。我个人的经验是如果你要基于 cloudflare-os 做二次开发先别急着改核心代码而是从写一个自定义工具开始。这样你能快速理解它的插件机制和运行时约束等熟悉了再往深了走。上来就改内核大概率会迷失在抽象层里。3. 核心细节解析与实操要点Agent 运行时到底怎么跑3.1 Durable Objects 的状态管理机制Durable Objects 是这套架构的基石所以必须先把它的脾气摸清楚。每个 Durable Object 是一个单线程、单实例、全局唯一的对象。你通过一个 ID 来访问它Cloudflare 保证同一个 ID 的请求总是路由到同一个实例上。这就意味着你可以在对象内部维护内存状态不用担心并发写冲突。但这里有几个坑我踩过不止一次第一Durable Objects 不是永久存活的。如果一段时间没有请求它会被回收内存状态丢失。所以重要的状态必须写到storage里不能只放在内存变量里。我见过有人把对话历史存在一个数组里结果对象一回收历史全没了。第二storage 的读写是异步的而且有事务限制。你不能在一个事务里读写超过一定数量的键值对。如果 Agent 的对话历史很长需要分页存储不能一次性全塞进去。第三Durable Objects 的冷启动有延迟。虽然比传统 Serverless 好很多但第一次唤醒还是会有几百毫秒的延迟。对于实时性要求高的场景需要做预热。下面是一个简化的 Durable Object 代码示例展示怎么保存和读取对话历史export class AgentSession { constructor(state, env) { this.state state; this.env env; this.history []; } async loadHistory() { const stored await this.state.storage.get(history); this.history stored || []; } async appendMessage(role, content) { this.history.push({ role, content, timestamp: Date.now() }); // 只保留最近 50 条避免无限增长 if (this.history.length 50) { this.history this.history.slice(-50); } await this.state.storage.put(history, this.history); } async fetch(request) { await this.loadHistory(); const { message } await request.json(); await this.appendMessage(user, message); // 这里调用 AI 模型生成回复 const reply await this.generateReply(message); await this.appendMessage(assistant, reply); return new Response(JSON.stringify({ reply }), { headers: { Content-Type: application/json } }); } async generateReply(message) { // 实际项目中这里会调用 Workers AI 或者外部模型 return Echo: ${message}; } }这段代码虽然简单但包含了几个关键点状态加载、消息追加、长度限制、持久化。你在实际项目中还需要考虑错误处理、并发控制、以及 storage 的批量操作优化。3.2 工具调用的编排与安全隔离Agent 要调用工具但工具的执行不能随便来。你肯定不希望 Agent 在用户不知情的情况下删库跑路。所以工具调用需要一套权限声明 沙箱执行 结果校验的机制。我的做法是每个工具在注册的时候必须声明三样东西输入 schema用 JSON Schema 描述参数类型和约束权限标签比如read-only、write、admin执行环境是在当前 Worker 里跑还是转发到另一个 Worker然后 Agent 运行时在调用工具之前先检查当前会话的权限是否覆盖工具要求的权限。如果不覆盖就拒绝调用并返回一个明确的错误信息。const tools { search: { schema: { query: string }, permission: read-only, handler: async (args, env) { // 调用搜索 API return await env.SEARCH.fetch(args.query); } }, sendEmail: { schema: { to: string, subject: string, body: string }, permission: write, handler: async (args, env) { // 发送邮件 return await env.EMAIL.send(args); } } }; async function callTool(name, args, sessionPermission) { const tool tools[name]; if (!tool) throw new Error(Tool ${name} not found); if (!hasPermission(sessionPermission, tool.permission)) { throw new Error(Permission denied for tool ${name}); } // 校验参数 validateSchema(args, tool.schema); return await tool.handler(args, env); }这里的关键是权限模型要简单。我见过有人搞了一套 RBAC结果配置复杂到没人愿意用。对于大多数 Agent 场景read-only、write、admin三档就够了。太细的粒度反而增加维护成本。另外工具的执行结果也要校验。外部 API 可能返回意料之外的数据格式如果不校验直接喂给模型模型可能会产生幻觉。我的习惯是每个工具 handler 返回之前先做一次 schema 校验确保输出符合预期。3.3 多 Agent 协作的消息路由单个 Agent 好办多个 Agent 协作就复杂了。你需要一个消息总线让 Agent 之间能互相发消息还要能追踪消息的因果关系避免死循环。Cloudflare 的 Durable Objects 天然适合做消息路由。你可以搞一个MessageBroker对象所有 Agent 都通过它来收发消息。每条消息带一个traceId方便追踪整条调用链。export class MessageBroker { constructor(state, env) { this.state state; this.env env; this.agents new Map(); } async register(agentId, stub) { this.agents.set(agentId, stub); } async route(from, to, payload, traceId) { const target this.agents.get(to); if (!target) throw new Error(Agent ${to} not registered); // 记录消息日志 await this.state.storage.put(msg:${traceId}, { from, to, payload, timestamp: Date.now() }); return await target.receive({ from, payload, traceId }); } }这里有个经验消息路由一定要有超时和重试上限。Agent 之间互相调用很容易出现 A 等 B、B 等 C、C 又等 A 的情况。如果不设超时整个系统就卡死了。我的做法是每条消息带一个ttl字段超过时间就丢弃并记录一条警告日志。还有一个坑是消息顺序。Durable Objects 是单线程的所以同一个对象内的消息是顺序处理的。但跨对象的消息就不保证了。如果你的业务逻辑依赖消息顺序需要自己在应用层做序列号或者版本号。4. 实操过程与核心环节实现从零搭一个最小可用 Agent OS4.1 环境准备与项目初始化先说环境。你需要一个 Cloudflare 账号安装wranglerCLINode.js 版本建议 18 以上。然后创建一个新的 Workers 项目npm create cloudflarelatest my-agent-os cd my-agent-os npm install在wrangler.toml里配置 Durable Objects 绑定name my-agent-os main src/index.js compatibility_date 2024-01-01 [durable_objects] bindings [ { name AGENT_SESSION, class_name AgentSession }, { name MESSAGE_BROKER, class_name MessageBroker } ] [[migrations]] tag v1 new_classes [AgentSession, MessageBroker]这里注意migrations配置。每次你新增或重命名 Durable Object 类都需要加一个 migration tag否则部署会失败。我一开始不知道这个规则折腾了半小时才搞明白。4.2 核心 Agent 类的实现接下来写 Agent 的核心逻辑。我把它分成三部分初始化、消息处理、工具调用。export class AgentSession { constructor(state, env) { this.state state; this.env env; this.initialized false; } async initialize() { if (this.initialized) return; this.history (await this.state.storage.get(history)) || []; this.config (await this.state.storage.get(config)) || { model: cf/meta/llama-3-8b-instruct, maxTokens: 1024, temperature: 0.7 }; this.initialized true; } async fetch(request) { await this.initialize(); const url new URL(request.url); if (url.pathname /chat) { return await this.handleChat(request); } if (url.pathname /reset) { await this.state.storage.deleteAll(); this.history []; return new Response(Reset done); } return new Response(Not found, { status: 404 }); } async handleChat(request) { const { message } await request.json(); this.history.push({ role: user, content: message }); // 调用模型 const response await this.env.AI.run(this.config.model, { messages: this.history, max_tokens: this.config.maxTokens, temperature: this.config.temperature }); const reply response.response; this.history.push({ role: assistant, content: reply }); // 持久化 await this.state.storage.put(history, this.history); return new Response(JSON.stringify({ reply }), { headers: { Content-Type: application/json } }); } }这段代码里this.env.AI是 Workers AI 的绑定。你需要在wrangler.toml里加上[ai] binding AIWorkers AI 的好处是不用自己管 API Key也不用担心跨区域延迟。但它的模型选择相对有限如果你要用 GPT-4 或者 Claude还是得走外部 API。这时候 AI Gateway 就派上用场了它可以帮你做请求转发、缓存、限流。4.3 工具注册与动态调用工具系统我建议做成声明式的。每个工具一个文件导出一个对象包含名称、schema、权限、handler。然后用一个注册表把它们收集起来。// tools/weather.js export default { name: getWeather, description: 获取指定城市的天气, schema: { type: object, properties: { city: { type: string, description: 城市名称 } }, required: [city] }, permission: read-only, handler: async ({ city }, env) { const resp await fetch(https://api.weather.com/v1/${city}); return await resp.json(); } };然后在 Agent 里根据模型的 function calling 输出动态调用对应的工具async function executeToolCall(toolCall, env) { const tool toolRegistry[toolCall.name]; if (!tool) { return { error: Unknown tool: ${toolCall.name} }; } try { const args JSON.parse(toolCall.arguments); const result await tool.handler(args, env); return { result }; } catch (e) { return { error: e.message }; } }这里有个细节模型的 function calling 输出不一定是合法 JSON。有时候会多一个逗号有时候会少一个引号。所以JSON.parse一定要包在 try-catch 里并且给模型返回一个明确的错误信息让它重新生成。4.4 部署与验证部署很简单npx wrangler deploy部署完之后你可以用 curl 测试curl -X POST https://my-agent-os.your-subdomain.workers.dev/chat \ -H Content-Type: application/json \ -d {message: 北京今天天气怎么样}如果一切正常你会收到一个 JSON 响应包含模型的回复。如果模型决定调用天气工具你还会在日志里看到工具调用的记录。我建议在开发阶段打开wrangler tail实时看日志。这样出问题的时候能快速定位。npx wrangler tail5. 常见问题与排查技巧实录5.1 Durable Object 唤醒失败怎么办这是最常见的问题之一。表现是请求超时或者返回 500 错误。原因通常有几个migration 配置不对检查wrangler.toml里的migrations是否包含了所有 Durable Object 类。ID 生成方式不对如果你用idFromName确保名称是稳定的。如果用newUniqueId每次都会创建新对象状态就丢了。storage 操作超时如果一次读写太多数据可能会超时。建议分批操作每批不超过 100 个键。我的排查顺序是先看wrangler tail的日志确认错误类型然后检查 migration 配置最后检查 storage 操作。5.2 模型输出格式不稳定怎么处理Agent 场景下模型的输出需要被程序解析所以格式稳定性至关重要。我的经验是用 JSON mode如果模型支持强制它输出 JSON。加 few-shot 示例在 prompt 里给一两个正确的输出示例。做后处理用正则或者简单的解析器把模型输出里的多余内容去掉。设置重试如果解析失败把错误信息喂回给模型让它重新生成最多重试 3 次。下面是一个简单的重试逻辑async function getStructuredOutput(prompt, maxRetries 3) { for (let i 0; i maxRetries; i) { const raw await callModel(prompt); try { return JSON.parse(raw); } catch (e) { prompt \n\n上次输出解析失败: ${e.message}。请只输出合法 JSON。; } } throw new Error(Failed to get valid JSON after retries); }5.3 成本失控怎么控制Agent 调用模型和工具都会产生费用。如果不加控制一个月下来账单可能吓死人。我一般会做这几件事设置 token 上限每次请求的 max_tokens 不要设太大根据实际需要来。缓存常见请求用 AI Gateway 的缓存功能相同的请求直接返回缓存结果。限制工具调用次数每个会话最多调用 N 次工具超过就拒绝。监控用量用 Workers Analytics 看每天的请求量和 token 消耗设置告警。问题可能原因解决方法请求超时Durable Object 冷启动预热或增加超时时间状态丢失只存内存未持久化写入 storage工具调用失败权限不足或参数错误检查权限配置和 schema模型输出乱码编码问题或模型幻觉加 JSON mode 和重试成本过高无限制调用加缓存、限流、token 上限5.4 多 Agent 协作时的死锁问题多 Agent 协作最容易出的问题就是死锁。A 等 B 的回复B 等 C 的回复C 又等 A 的回复整个系统就卡住了。我的解法是设置全局超时每个消息带一个 deadline超过就丢弃。限制调用深度用 traceId 记录调用链超过一定深度就拒绝。异步化能异步的就异步不要同步等待。async function routeWithTimeout(target, message, timeoutMs 5000) { return Promise.race([ target.receive(message), new Promise((_, reject) setTimeout(() reject(new Error(Route timeout)), timeoutMs) ) ]); }这个Promise.race的写法很实用能避免无限等待。但要注意超时之后目标 Agent 可能还在处理所以需要幂等设计避免重复执行。6. 我对这套架构的一些个人看法写到这里我想聊几句掏心窝子的话。cloudflare-os 这个方向我觉得是对的但它不适合所有人。如果你的 Agent 只是做个 Demo或者用户量很小直接用简单的 Workers KV 就够了没必要上 Durable Objects 这套重装备。Durable Objects 的计费方式跟传统 Serverless 不一样它是按持续时间和存储量收费的如果对象一直活着费用会持续累积。但如果你要做的是生产级的、多用户的、需要长期记忆的 Agent 应用那这套架构的优势就体现出来了。它的状态管理、消息路由、工具编排都是经过实战检验的模式。你不需要从零造轮子只需要把业务逻辑填进去。我自己的项目里用类似的架构跑了大半年最大的体会是抽象层要适度。太薄了每个 Agent 都要重复写状态管理太厚了出问题的时候排查起来很痛苦。我的建议是先把核心的 Durable Object 和工具注册机制搭好其他的比如向量记忆、多 Agent 协作等真正需要的时候再加。另外开源生态的参与方式很重要。如果你只是想用那就直接看文档和示例如果你想贡献从写一个工具适配器开始别一上来就改核心。这样既能熟悉代码又能给社区带来实际价值。最后分享一个我常用的调试技巧在 Durable Object 里加一个/debug端点返回当前的状态快照包括历史消息数量、工具调用次数、最后一条消息的时间戳。这样出问题的时候不用翻日志就能快速判断状态是否正常。if (url.pathname /debug) { return new Response(JSON.stringify({ historyLength: this.history.length, lastMessage: this.history[this.history.length - 1], config: this.config, uptime: Date.now() - this.startTime }), { headers: { Content-Type: application/json } }); }这个端点我几乎每个项目都会加省了无数排查时间。你可以试试。
阅读完成 · 觉得有帮助?
咨询建站