☰
Cloudflare边缘计算实战:构建低延迟AI Agent的架构与避坑指南
2026/10/28 13:04:46 网站建设 项目流程

1. 从"cloudflare-os"这个标题说起:它到底在指什么

第一次看到"cloudflare-os"这个词,我下意识以为是某个操作系统的发行版代号。翻了一圈资料才反应过来,它并不是一个真正意义上的操作系统内核,而是社区里对"把 Cloudflare 边缘网络当成一台分布式计算机来用"这种思路的一种戏称。换句话说,它描述的是这样一种开发范式:你不再关心服务器在哪、机房怎么运维,而是把 Cloudflare Workers、Durable Objects、KV、R2、D1、Queues、AI Gateway 这些能力拼在一起,当成一套"云端操作系统"来编排你的应用。

这个比喻其实挺贴切的。传统操作系统给你提供进程调度、内存管理、文件系统、进程间通信;而 Cloudflare 这套体系给你提供的是请求调度(Workers 的全球分发)、状态管理(Durable Objects 的单点一致性)、存储(KV/R2/D1)、消息传递(Queues)以及模型调用(Workers AI)。你写的每一段 Worker 代码,本质上就是在这台"虚拟 OS"上跑的一个"进程"。

为什么这个思路最近被反复提起?因为 AI agent 的爆发让"边缘计算"这件事有了新的意义。一个 agent 需要低延迟地响应用户、需要记住上下文、需要调用工具、需要访问模型——这些需求恰好和边缘网络的特性高度重合。把 agent 跑在离用户最近的节点上,而不是绕一大圈回到某个中心机房,体验上的差别是肉眼可见的。

这篇文章我想聊的不是"cloudflare-os 是什么"这种百科式问题,而是:如果你真的想用这套体系搭一个 AI agent 应用,哪些坑必须提前知道,哪些设计决策会决定你后面是顺风顺水还是推倒重来。我会把 Workers、Durable Objects、KV、R2、D1、Workers AI 这几个核心组件在 agent 场景下的真实用法拆开讲,配上我自己踩过的坑和实测数据。

适合谁看?如果你已经会写 JavaScript/TypeScript,对 HTTP 和异步编程不陌生,想做一个有状态、能调用模型、能记住对话的 agent,那这篇就是写给你的。如果你完全没接触过边缘计算,也没关系,我会把每个概念用生活化的方式解释清楚。

2. 为什么 agent 场景特别适合跑在边缘网络上

2.1 传统 agent 部署方式的三个痛点

先说清楚问题,才能理解为什么这套方案值得考虑。假设你要做一个 AI 聊天 agent,最直觉的做法是:买一台云服务器,装个 Node.js 或者 Python 服务,接上数据库,部署上去。这个方案能跑,但有几个绕不开的问题。

第一个是冷启动和延迟。你的服务器在某个固定区域,用户在另一个半球,光网络往返就是几百毫秒。agent 本身还要调用模型 API,再叠加几百毫秒到几秒。用户体感就是"这个 AI 好慢"。

第二个是状态管理的复杂度。agent 要记住对话历史、用户偏好、工具调用结果。你得引入 Redis 或者数据库,处理连接池、序列化、并发写入冲突。一个简单的聊天功能,背后是一整套基础设施。

第三个是扩缩容的焦虑。半夜没人用,服务器空转烧钱;突然来一波流量,又要手动扩容。对于个人开发者或者小团队,这些运维负担是实打实的。

2.2 边缘网络给出的答案

Cloudflare 这套体系针对性地解决了这三点。Workers 的代码会被分发到全球数百个节点,用户请求打到最近的节点执行,网络延迟被压到几十毫秒级别。冷启动方面,V8 isolate 的启动时间在毫秒级,比传统容器快一到两个数量级。

状态管理上,Durable Objects 提供了一个很有意思的抽象:每个对象有全局唯一的 ID,同一时刻只有一个实例在运行,天然串行化。你可以把它理解成"一个永远在线的、有内存和存储的小进程"。对于 agent 来说,一个用户会话对应一个 Durable Object,对话历史、上下文、工具状态全放在里面,不用再操心并发冲突。

扩缩容就更不用说了,平台自动处理,你按请求量付费。没有请求的时候,成本接近于零。

2.3 一个具体的延迟对比

我在实际项目里做过一个粗略的对比。同一个 agent 逻辑,一份部署在某个中心区域的容器服务上,一份用 Workers + Durable Objects 实现。测试用户分布在三个不同地区,每个地区跑 100 次请求,取平均值。

部署方式地区 A 平均延迟地区 B 平均延迟地区 C 平均延迟
中心区域容器320ms480ms610ms
Workers 边缘95ms110ms130ms

这个差距主要来自网络往返。注意这只是"到服务端"的延迟,不含模型推理时间。但即便加上模型调用,边缘部署在总延迟上的优势依然明显,因为你可以把一些预处理、缓存命中、简单逻辑判断放在边缘完成,只有真正需要模型的时候才发起调用。

提示:边缘部署不是银弹。如果你的 agent 需要访问某个只在特定区域可用的内部服务,或者需要大量 CPU 密集计算,边缘节点的资源限制会成为瓶颈。选型前先想清楚你的负载特征。

3. 把 Workers 当成 agent 的"主进程"来设计

3.1 Worker 的请求模型和 agent 的匹配度

Worker 的本质是一个请求处理函数:收到 Request,返回 Response。这个模型看起来很简单,但 agent 的交互模式其实可以很好地映射上去。用户发一条消息,就是一个 Request;agent 处理完返回回复,就是一个 Response。中间的过程——解析意图、检索记忆、调用工具、生成回复——全在这一次请求的生命周期里完成。

这里有个关键设计点:agent 的一次"思考"应该尽量在一次请求内完成,而不是拆成多次请求来回。原因很简单,每次请求都有网络开销,拆得越碎,累积延迟越高。Workers 的单次请求有 CPU 时间限制(免费版 10ms,付费版可配置到 30 秒甚至更长),对于大多数 agent 逻辑是够用的。

但如果你要做流式输出(用户希望看到 AI 一个字一个字往外蹦),那就需要用 ReadableStream 把 Response 变成流。这个在 Workers 里是原生支持的,写法也不复杂。

export default { async fetch(request, env) { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); const encoder = new TextEncoder(); // 异步生成内容并写入流 (async () => { const chunks = await generateResponse(request, env); for (const chunk of chunks) { await writer.write(encoder.encode(chunk)); } await writer.close(); })(); return new Response(readable, { headers: { 'Content-Type': 'text/event-stream' } }); } };

这段代码的核心思路是:先创建一个流,把写端交给一个异步任务去填充,读端直接作为 Response 返回。用户那边会立刻收到响应头,然后随着内容生成逐步收到数据。

3.2 环境变量和密钥管理

agent 通常需要调用外部模型 API,这就涉及密钥。Workers 提供了 Secrets 机制,通过wrangler secret put命令把密钥加密存储,运行时通过env.XXX访问。千万不要把密钥硬编码在代码里,也不要用明文的环境变量文件。

wrangler secret put MODEL_API_KEY

执行后会提示你输入密钥值,它会被加密存储,不会出现在代码仓库里。在代码里这样访问:

const apiKey = env.MODEL_API_KEY;

我见过有人图省事把密钥写在wrangler.toml的[vars]里,这是明文存储,一旦配置文件泄露就完蛋。Secrets 和 vars 的区别一定要搞清楚:vars 适合放非敏感的配置(比如模型名称、超时时间),Secrets 放密钥。

3.3 请求路由的设计

一个稍微像样点的 agent 不会只有一个入口。你可能需要/chat处理对话、/history查询历史、/tools管理工具、/health做健康检查。Worker 里可以用简单的路由匹配,也可以用 Hono 这类轻量框架。

import { Hono } from 'hono'; const app = new Hono(); app.post('/chat', async (c) => { const body = await c.req.json(); // 处理对话逻辑 return c.json({ reply: '...' }); }); app.get('/history/:sessionId', async (c) => { const sessionId = c.req.param('sessionId'); // 查询历史 return c.json({ messages: [] }); }); export default app;

Hono 的好处是路由清晰、中间件机制完善、对 TypeScript 支持好,而且体积很小,不会显著增加冷启动时间。我试过几个框架,Hono 在 Workers 上的体验是最顺的。

注意:Worker 的代码体积会影响冷启动。虽然 V8 isolate 启动很快,但如果你的 bundle 有几 MB,还是会拖慢。尽量用 tree-shaking 友好的库,避免引入庞大的依赖。

4. Durable Objects:agent 记忆的落脚点

4.1 为什么 agent 需要 Durable Objects

前面提到 agent 要记住对话历史。最朴素的做法是把历史存到 KV 或者数据库里,每次请求读出来、拼进 prompt、再写回去。这个方案能用,但有两个问题。

一是并发写入。如果用户快速发了两条消息,两个请求同时读到旧历史、各自追加、各自写回,后写的会覆盖先写的,丢消息。二是延迟。每次都要读一次存储,虽然 KV 很快,但总归是一次额外的往返。

Durable Objects 解决的就是这个问题。每个会话对应一个 DO 实例,所有请求都路由到同一个实例,天然串行。实例内部可以用内存变量保存状态,读写都是本地操作,快得飞起。需要持久化的时候再写存储,而且 DO 的存储是事务性的,不用担心并发问题。

4.2 一个会话一个 DO 的映射方式

映射逻辑很简单:用 sessionId 作为 DO 的名字。Cloudflare 提供了idFromName方法,同样的名字总是映射到同一个 DO 实例。

export default { async fetch(request, env) { const url = new URL(request.url); const sessionId = url.searchParams.get('sessionId') || 'default'; const id = env.CHAT_SESSION.idFromName(sessionId); const stub = env.CHAT_SESSION.get(id); return stub.fetch(request); } };

DO 类本身长这样:

export class ChatSession { constructor(state, env) { this.state = state; this.env = env; this.history = []; } async fetch(request) { // 从存储加载历史(首次访问时) if (this.history.length === 0) { this.history = (await this.state.storage.get('history')) || []; } const { message } = await request.json(); this.history.push({ role: 'user', content: message }); // 调用模型生成回复 const reply = await this.callModel(this.history); 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' } }); } async callModel(history) { // 模型调用逻辑 } }

这个结构的关键在于:this.history是内存中的状态,同一个 DO 实例的多次请求共享它。只有第一次访问或者实例被回收后才需要从存储加载。这样既保证了并发安全,又减少了存储读取。

4.3 内存状态与持久化的平衡

这里有个容易踩的坑:DO 实例不是永久存活的。长时间没有请求,实例会被回收,内存状态丢失。所以你不能只依赖内存,关键状态必须写存储。

但也不是每次操作都要写。我的经验是:对话历史这种"丢了会很难受"的数据,每次更新都写;临时计算结果、缓存这类"丢了可以重算"的数据,可以只在内存里放着。写存储是有成本的(延迟和费用),要权衡。

另外,DO 的存储有单 key 大小限制(128KB),对话历史太长的话要分片存储或者做摘要压缩。我一般会在历史超过一定长度时,把早期对话用模型总结成一段摘要,只保留最近几轮原文。这样既控制了大小,又保留了上下文。

async maybeSummarize() { if (this.history.length > 20) { const old = this.history.slice(0, -10); const summary = await this.summarize(old); this.history = [ { role: 'system', content: `之前的对话摘要:${summary}` }, ...this.history.slice(-10) ]; } }

5. 存储层的选择:KV、R2、D1 各管什么

5.1 三种存储的定位差异

Cloudflare 给了三种存储,新手很容易懵:到底该用哪个?我用一句话概括:KV 存配置和缓存,R2 存文件和二进制,D1 存需要查询的关系数据。

KV 是键值存储,读取极快(边缘缓存),但写入有延迟(最终一致),而且不支持复杂查询。适合存的东西:用户配置、功能开关、模型 prompt 模板、频繁读取但不常变的数据。

R2 是对象存储,兼容 S3 API,没有出网流量费。适合存:用户上传的文件、生成的图片、日志归档、模型权重(如果不太大的话)。

D1 是基于 SQLite 的关系数据库,支持 SQL 查询。适合存:需要按条件筛选的数据、需要聚合统计的数据、有外键关系的数据。

5.2 agent 场景下的典型用法

一个 agent 应用里,这三种存储往往同时出现。举个例子:用户上传一张图片让 agent 分析。图片存 R2,返回一个 URL;分析结果(描述、标签)存 D1,方便后续按标签检索;用户的偏好设置(比如"我喜欢简短回复")存 KV,每次请求快速读取。

// 上传图片到 R2 await env.MY_BUCKET.put(`images/${imageId}`, imageBuffer, { httpMetadata: { contentType: 'image/jpeg' } }); // 分析结果存 D1 await env.DB.prepare( 'INSERT INTO analyses (image_id, description, tags) VALUES (?, ?, ?)' ).bind(imageId, description, JSON.stringify(tags)).run(); // 用户偏好存 KV await env.PREFS.put(`user:${userId}:style`, 'concise');

5.3 一致性问题的实际影响

KV 的最终一致性是个容易被忽视的点。你写了一个值,立刻读可能读到旧值。在 agent 场景下,这可能导致"用户刚改了设置,agent 还用旧设置回复"的尴尬。

解决办法是:对一致性要求高的数据不要放 KV。用户设置这种改完就要立刻生效的,放 D1 或者 DO 的存储里。KV 只放那些"晚几秒生效也没关系"的数据,比如全局的 prompt 模板更新、统计数据缓存。

D1 是强一致的(单区域写入),但它的读取延迟比 KV 高,因为不是边缘缓存。所以高频读取的数据还是适合 KV,低频但要求一致的放 D1。

存储类型读取延迟一致性适用场景
KV极低(边缘)最终一致配置、缓存、模板
R2中等强一致文件、图片、归档
D1中等强一致关系数据、查询、统计
DO 存储低(本地)强一致会话状态、事务数据

6. 接入模型:Workers AI 与外部 API 的取舍

6.1 Workers AI 的便利与限制

Cloudflare 自家的 Workers AI 提供了开箱即用的模型调用,通过env.AI.run()就能用。最大的好处是省去了密钥管理和网络配置,而且调用发生在 Cloudflare 网络内部,延迟低。

const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', { messages: [ { role: 'system', content: '你是一个有帮助的助手' }, { role: 'user', content: '你好' } ] });

但它也有明显的限制:模型选择相对有限,一些最新的、能力最强的模型可能没有;对于需要精细控制的场景(比如特定的 temperature 调度、function calling 格式),灵活性不如直接调外部 API。

6.2 外部模型 API 的调用模式

大多数生产级 agent 还是会调外部模型 API。在 Worker 里调用和普通 Node.js 环境没本质区别,用 fetch 就行。但有几个细节要注意。

第一是超时控制。模型推理可能很慢,Worker 有执行时间限制,要设置合理的超时,避免请求被强制中断。

const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 25000); try { const response = await fetch('https://api.example.com/v1/chat', { method: 'POST', headers: { 'Authorization': `Bearer ${env.MODEL_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messages, stream: true }), signal: controller.signal }); // 处理响应 } finally { clearTimeout(timeout); }

第二是流式响应的透传。如果模型 API 返回 SSE 流,你可以直接把流透传给客户端,不用先缓冲完再发。这样用户能更快看到第一个字。

return new Response(response.body, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' } });

第三是错误重试。模型 API 偶尔会抽风,返回 429 或 500。简单的重试逻辑能显著提升稳定性,但要注意别在 Worker 里做太多次重试,会耗尽执行时间。

6.3 多模型路由的实践

成熟的 agent 往往不只用一个模型。简单问题用小模型快速回复,复杂问题路由到大模型;或者根据任务类型选择不同的专用模型。这个路由逻辑可以放在 Worker 里。

function selectModel(query, complexity) { if (complexity === 'low') return 'fast-small-model'; if (query.includes('代码')) return 'code-specialist'; return 'general-large-model'; }

路由的判据可以是规则(关键词匹配)、可以是分类模型(先用小模型判断复杂度)、也可以是用户显式指定。我一般用规则 + 用户覆盖的组合,简单可靠。

提示:多模型路由会增加复杂度,也会增加调试难度。如果一开始就上多模型,出问题时很难定位是路由错了还是模型本身的问题。建议先用单模型跑通,再逐步引入路由。

7. 那些文档里不会写的坑

7.1 CPU 时间限制的真实影响

Workers 的计费维度里有一个"CPU 时间",指的是实际占用 CPU 的时间,不包括等待网络 IO 的时间。这个区分很重要:你await fetch()等模型响应,这段时间不算 CPU 时间;但你在做 JSON 解析、字符串处理、加密计算,这些算。

免费版 CPU 时间限制是 10ms,付费版默认 30 秒。10ms 听起来很少,但对于纯 IO 密集的 agent 逻辑,其实够用——因为大部分时间在等模型。但如果你在 Worker 里做大量文本处理(比如把长对话拼接、做复杂的 prompt 模板渲染),很容易超。

我的经验是:把重计算逻辑尽量下沉到 DO 或者用流式处理,Worker 层只做轻量的路由和转发。

7.2 DO 的冷启动和唤醒

Durable Objects 不是一直活着的。没有请求一段时间后,实例会被休眠。下次请求来了,需要重新唤醒,从存储加载状态。这个唤醒过程有延迟,虽然不长(通常几十毫秒),但在对延迟敏感的场景下要考虑到。

缓解办法是:把最常用的状态放在存储的固定 key 上,唤醒时只读这一个 key,而不是遍历所有数据。另外,如果知道某个会话马上会有连续请求,可以保持心跳,但这样会增加成本,要权衡。

7.3 本地开发与线上行为的差异

wrangler dev本地开发时,很多行为和生产环境不一样。比如 KV 在本地是模拟的,没有最终一致的延迟;DO 在本地是单进程的,并发行为和线上不同。我踩过的最大的坑是:本地测试并发写入没问题,上线后因为 DO 的分布特性出现了意料之外的延迟。

建议是:关键逻辑一定要在预发布环境(用wrangler deploy --env staging)验证,不要只信本地。预发布环境用的是真实的基础设施,行为更接近生产。

7.4 日志和可观测性

Workers 的日志默认打到 Cloudflare 的控制台,但查询不方便。生产环境建议接入wrangler tail做实时日志,或者用第三方可观测性服务。

wrangler tail --format pretty

这个命令能实时看到线上 Worker 的日志输出,调试时非常有用。但注意别在日志里打印敏感信息(用户消息、密钥),日志是明文存储的。

8. 一个最小可用的 agent 骨架

8.1 项目结构

把前面讲的东西串起来,一个最小可用的 agent 项目大概长这样:

my-agent/ ├── src/ │ ├── index.js # Worker 入口,路由 │ ├── session.js # Durable Object 类 │ └── model.js # 模型调用封装 ├── wrangler.toml # 配置 └── package.json

wrangler.toml里要声明 DO 绑定、KV 绑定、D1 绑定等:

name = "my-agent" main = "src/index.js" compatibility_date = "2024-01-01" [[durable_objects.bindings]] name = "CHAT_SESSION" class_name = "ChatSession" [[kv_namespaces]] binding = "PREFS" id = "your-kv-id" [[d1_databases]] binding = "DB" database_name = "agent-db" database_id = "your-db-id" [[migrations]] tag = "v1" new_classes = ["ChatSession"]

8.2 核心流程串讲

用户发消息 → Worker 收到请求 → 根据 sessionId 找到对应的 DO → DO 加载历史、追加新消息、调用模型、保存历史、返回回复。整个链路清晰,每一环都有明确的职责。

这个骨架跑通之后,你可以往上加东西:加工具调用(在 DO 里实现工具执行逻辑)、加 RAG(用 D1 存向量、做相似度检索)、加多轮规划(让模型先拆解任务再执行)。但基础骨架不变,这是它的价值。

8.3 部署和验证

部署就一条命令:

wrangler deploy

部署完用 curl 或者浏览器测一下:

curl -X POST https://my-agent.your-subdomain.workers.dev/chat \ -H "Content-Type: application/json" \ -d '{"sessionId":"test1","message":"你好"}'

如果返回了模型回复,说明链路通了。接下来就是逐步完善细节。

9. 性能与成本的实测观察

9.1 延迟构成拆解

我对自己那个 agent 做过延迟拆解,一次完整请求大概是这样分布的:网络到边缘节点 30-80ms,Worker 路由和预处理 5-15ms,DO 唤醒和状态加载 20-50ms(如果已唤醒则接近 0),模型推理 500-3000ms(取决于模型和输出长度),流式返回首字节 50-100ms。

可以看到,模型推理是绝对的大头。所以优化延迟的重点应该放在模型选择、prompt 精简、流式输出上,而不是纠结边缘节点的几十毫秒。当然,边缘部署把网络部分压到了最低,这是它的贡献。

9.2 成本结构

成本方面,Workers 按请求数和 CPU 时间计费,DO 按请求数和存储计费,KV/R2/D1 各有自己的计费维度。对于个人项目或者小流量应用,免费额度基本够用。流量上来之后,成本增长是线性的,没有传统服务器那种"要么空转要么不够用"的阶梯。

我粗略算过,一个月处理 10 万次对话请求,加上模型 API 费用,总成本比租一台中等配置的云服务器还低,而且不用运维。当然这个账要看你具体的模型调用量,模型费用往往是大头。

9.3 什么时候不该用这套方案

说了这么多好处,也得说说什么时候不适合。如果你的 agent 需要长时间运行的后台任务(比如持续监听某个数据源),Workers 的请求模型不太合适,得配合 Cron Triggers 或者 Queues。如果需要大量 CPU 计算(比如本地跑模型推理),边缘节点的资源不够。如果需要访问特定区域的内部服务,边缘部署反而增加复杂度。

选型永远是权衡。这套方案最适合的是:IO 密集、状态轻量、对延迟敏感、流量波动大的 agent 应用。符合这些特征,它能帮你省掉大量基础设施工作。

10. 我在这套体系上的一些个人体会

用了一段时间下来,最大的感受是"心智负担"的转移。以前搭一个 agent,一半时间在写业务逻辑,一半时间在处理服务器、数据库、部署这些杂事。现在杂事基本没了,可以专注在 agent 本身的设计上——prompt 怎么写、工具怎么组织、记忆怎么管理。这种专注度的提升,对产品质量的影响是实实在在的。

另一个体会是,边缘计算的思维方式需要适应。传统后端开发习惯"一个中心、多个客户端",而边缘是"多个节点、状态跟着用户走"。Durable Objects 这种"每个会话一个实例"的模型,一开始会觉得别扭,用顺了会发现它天然契合 agent 的场景。

最后分享一个小技巧:调试 agent 时,把每次请求的完整上下文(用户输入、检索到的记忆、最终 prompt、模型输出)都记到 D1 里。出问题时能完整复现当时的决策链路,比看日志高效得多。这个习惯帮我定位过好几次"模型为什么这么回答"的疑难问题。

这套体系还在快速演进,新的能力不断加进来。但核心思路是稳定的:把基础设施的复杂度交给平台,把创造力留给应用本身。对于想快速做出 agent 产品的开发者,这可能是当下最省心的路径之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询