Cloudflare 用自建容器重写 Browser Run,拼完六层 Agent 基础设施的最后一块

2026-05-22 18 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

Cloudflare 的 Browser Rendering 一直是开发者做网页抓取、自动化测试时的热门选择,但底层跑的是第三方托管方案,并发和延迟都有天花板。最近 Cloudflare 把整个 Browser Run 搬到了自家 Containers 平台上,并发能力翻了 4 倍、响应速度快了 50%。更重要的是,这一步把 Cloudflare 的 Agent 基础设施从五块补成了六块——一个从计算到交易的完整栈终于闭环了。

为什么必须自建容器

Browser Run 之前的架构依赖外部容器调度,问题很典型:

  • 并发瓶颈——外部平台的容器实例有硬性上限,高峰期排队严重。
  • 冷启动慢——每次启动浏览器实例都要走跨平台调度链路,延迟不可控。
  • 成本不透明——第三方计费和 Cloudflare 自身 Workers 体系割裂,难以做统一用量管理。

搬到自建 Containers 之后,浏览器实例直接跑在 Cloudflare 边缘网络的容器里,调度链路缩短到内部系统,冷启动大幅压缩,实例池可以按边缘节点弹性扩缩。4 倍并发和 50% 响应提速,本质上是"把中间商去掉"后的自然结果。

六层 Agent 基础设施全貌

Browser Run 补位之后,Cloudflare 的 Agent 平台栈变成了六层,每一层对应 Agent 运行的一个关键能力:

层级 能力 产品 作用
1 计算 Dynamic Workers Agent 主逻辑运行,按请求动态调度
2 安全沙箱 Containers / Sandboxes 隔离执行不可信代码、跑浏览器实例
3 编排 Dynamic Workflows 多步骤 Agent 流程的 DAG 编排与重试
4 记忆 Agent Memory 跨会话的状态持久化与上下文召回
5 浏览 Browser Run 真实浏览器环境,用于网页交互与数据提取
6 商务 Stripe Projects Agent 产生的交易、支付与计费闭环

这六层不是简单堆叠,而是互相衔接。一个典型 Agent 的生命周期:Workers 接收请求 → Workflows 编排多步流程 → Memory 存取上下文 → Browser Run 执行网页操作 → Sandboxes 处理生成物 → Stripe Projects 完成付费。每一层都在 Cloudflare 边缘网络内闭环,不需要跨云。

Browser Run 的实际用法

下面用一个完整示例演示:在 Cloudflare Workers 中调用 Browser Rendering API,抓取页面标题和关键文本,然后把结果写入 Agent Memory。这是五层联动的最小可行路径。

1. 项目配置

# 创建 Workers 项目
mkdir agent-browser && cd agent-browser
npm init -y
npm install @cloudflare/workers-types

wrangler.toml 配置——绑定 Browser Rendering 和 KV(用作 Agent Memory):

name = "agent-browser"
main = "src/index.ts"
compatibility_date = "2024-09-23"

# Browser Rendering 绑定
[browser]
binding = "BROWSER"

# KV 绑定,用作 Agent Memory
[[kv_namespaces]]
binding = "AGENT_MEMORY"
id = "你的KV命名空间ID"

2. Worker 代码

// src/index.ts
interface Env {
  BROWSER: Fetcher;       // Browser Rendering 绑定
  AGENT_MEMORY: KVNamespace; // Agent Memory 绑定
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const targetUrl = url.searchParams.get("url");
    if (!targetUrl) {
      return new Response("请提供 ?url= 参数", { status: 400 });
    }

    // 第一步:启动浏览器实例
    const browserResp = await env.BROWSER.fetch(
      "https://browser.cloudflare.com/new"
    );
    if (!browserResp.ok) {
      return new Response("浏览器实例启动失败", { status: 500 });
    }

    const browserId = await browserResp.text();

    try {
      // 第二步:通过 WebSocket 连接浏览器,执行 CDP 命令
      const wsUrl = `https://browser.cloudflare.com/${browserId}/ws`;
      const wsResp = await env.BROWSER.fetch(wsUrl, {
        headers: { Upgrade: "websocket" },
      });
      const ws = wsResp.webSocket!;
      ws.accept();

      // 导航到目标页面
      ws.send(JSON.stringify({
        id: 1,
        method: "Page.navigate",
        params: { url: targetUrl },
      }));

      // 等待页面加载
      await new Promise((r) => setTimeout(r, 3000));

      // 提取页面标题
      ws.send(JSON.stringify({
        id: 2,
        method: "Runtime.evaluate",
        params: { expression: "document.title" },
      }));

      // 提取页面主要文本内容
      ws.send(JSON.stringify({
        id: 3,
        method: "Runtime.evaluate",
        params: {
          expression: `
            Array.from(document.querySelectorAll('h1, h2, p'))
              .map(el => el.innerText)
              .filter(t => t.length > 20)
              .slice(0, 5)
          `,
          returnByValue: true,
        },
      }));

      // 收集结果
      const results: Record<number, any> = {};
      ws.addEventListener("message", (event: MessageEvent) => {
        const data = JSON.parse(event.data as string);
        if (data.id) results[data.id] = data.result?.value;
        if (Object.keys(results).length >= 2) ws.close();
      });

      await new Promise((r) => setTimeout(r, 2000));

      const title = results[2] || "未知标题";
      const keyTexts = results[3] || [];

      // 第三步:写入 Agent Memory(KV)
      const memoryKey = `page:${targetUrl}:${Date.now()}`;
      await env.AGENT_MEMORY.put(memoryKey, JSON.stringify({
        url: targetUrl,
        title,
        keyTexts,
        capturedAt: new Date().toISOString(),
      }));

      return Response.json({
        url: targetUrl,
        title,
        keyTexts,
        memoryKey,
      });
    } finally {
      // 关闭浏览器实例,释放资源
      await env.BROWSER.fetch(
        `https://browser.cloudflare.com/${browserId}/close`,
        { method: "POST" }
      );
    }
  },
};

3. 部署与测试

# 部署到 Cloudflare
npx wrangler deploy

# 测试调用
curl "https://agent-browser.你的子域.workers.dev/?url=https://example.com"

返回示例:

{
  "url": "https://example.com",
  "title": "Example Domain",
  "keyTexts": [
    "This domain is for use in illustrative examples..."
  ],
  "memoryKey": "page:https://example.com:1718000000000"
}

这个示例串联了 Workers(计算)、Browser Run(浏览)、Agent Memory/KV(记忆)三层。加上 Workflows 做多步编排、Sandboxes 做结果后处理、Stripe Projects 做按次计费,就是完整的六层联动。

从五层到六层,意味着什么

Browser Run 补位之前,Cloudflare 的 Agent 栈缺了"真实浏览器交互"这一环。Agent 能算、能记、能编排、能交易,但到了需要和真实网页打交道的时候——登录、填表、滚动加载、处理 JS 渲染的内容——只能外接 Puppeteer 远程服务或自建浏览器集群,链路断裂。

现在闭环之后,几个变化值得关注:

  • 延迟确定性——浏览器实例和 Worker 在同一边缘网络,不再受跨区域调度抖动影响。对需要多轮网页交互的 Agent(比如比价、监控、自动填报),延迟稳定性比峰值速度更重要。
  • 并发弹性——4 倍并发意味着同一个 Agent 可以并行开更多浏览器 tab 做批量操作,而不用排队等实例释放。
  • 计费统一——Browser Run 的用量现在纳入 Cloudflare 统一体系,和 Workers、KV 一样在 Dashboard 里看,不再有第三方账单黑洞。

落地前需要想清楚的几件事

  1. 浏览器实例生命周期管理——每次调用完必须关闭实例,否则容器资源会持续占用。上面的示例用了 finally 块确保清理,生产环境建议加超时兜底。
  2. CDP 协议熟练度——Browser Run 的底层是 Chrome DevTools Protocol,不是 Puppeteer 那套高层 API。你需要自己拼 JSON 命令、管理 id 映射、处理异步回调。团队里至少要有一个人熟悉 CDP。
  3. 冷启动仍有代价——虽然比之前快 50%,但浏览器实例启动依然在秒级。高频短任务(比如只取一个 <title>)考虑用 HTTP 抓取替代;只有需要 JS 渲染或交互操作时才值得开浏览器。
  4. Memory 选型——示例用 KV 做简单记忆,适合键值场景。如果 Agent 需要向量检索(比如"找上次类似任务的上下文"),KV 不够用,需要外接向量数据库或等 Cloudflare Vectorize 成熟后迁移。
  5. 成本模型——Browser Run 按浏览器实例运行时间计费,长时间挂机等待的 Agent(比如等页面异步加载)成本会快速累积。设计流程时尽量把等待逻辑拆成短轮询 + Workflows 重试,而不是一个浏览器实例傻等。

Cloudflare 用自建容器重写 Browser Run,表面上是性能优化,实质上是把 Agent 基础设施的最后缺口补上了。六层栈闭环之后,开发者可以在一个平台上从计算到交易完整跑通 Agent,不再需要拼凑外部服务。如果你已经在用 Cloudflare Workers 做 Agent,现在值得重新评估 Browser Run 的并发和延迟表现——和半年前已经不是同一个东西了。


相关推荐