Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 1 章:5 分钟跑通最小 Think agent

一句话定位:你会拿到一个能接收 HTTP 请求、调 LLM、流式返回回答的最小 agent —— 用 Cloudflare 2026 年新一代 Agents SDK(Project Think)。

想要什么

打开终端,输入:

curl -N -X POST http://localhost:8787/api/chat \
  -H 'content-type: application/json' \
  -d '{"message":"用一句话介绍 Cloudflare Workers"}'

回车后,屏幕开始逐字流式吐出对 Cloudflare Workers 的中文介绍。仅此而已,但这是后续所有功能的地基。

我们要的不是“调一次返回一坨“的函数,而是一个有身份、有持久状态、有完整生命周期 hook、可流式响应的实例。这一章只用其中三个能力(身份 + 流式 + LLM 调用),后面 9 章把剩下的逐章加上去。

为什么

最直接的方案是:写一个普通的 Cloudflare Worker,在 fetch 里调 Workers AI,返回结果。100 行代码搞定。

问题是,普通 Worker 完全无状态。下一章我们要让 agent 记住对话,Worker 就抓瞎 —— 它每次请求都是冷启动,记不住任何东西。你只能把状态外挂到 KV / D1 / R2,然后每次重新加载、序列化、写回。

第二个问题:即使我们用 Agent 基类(Cloudflare 上一代 SDK,2025 年发布)解决了状态,我们仍然要手写 onChatMessage、自己接 AI SDK、自己管消息持久化、自己处理 sub-agent 调用、自己实现工具循环。每个 agent 项目都在重复造这一圈轮子。

Project Think(@cloudflare/think,2026 年 Agents Week 发布)是 Cloudflare 给的官方答案:把“AIChatAgent + Session + 工具循环 + sub-agent + sandbox“打包成一个基类 Think,你只实现 getModel() / getTools() / configureSession() 三个钩子,其余全有默认实现。我们从一开始就用 Think,不会有“等大了再迁移“的痛苦。

图 1-1:Think 替你管的、你自己管的

Think 基类把"消息 / 流 / 工具 / 持久化 / sub-agent"包好,你只写三个钩子 extends Think 之后,你拿到了什么 Think 内置(不用你写) 消息持久化 SQLite + 树形 Session 流式响应 SSE / WebSocket 工具循环 tool calls + Code Mode Sub-agent Facets + RPC 生命周期 hook beforeTurn / afterToolCall... 沙箱集成 Sandbox / Browser / MCP 你必须写(三个钩子) getModel() 用哪个 LLM getTools() 能调哪些工具(后面章节加) configureSession() 系统提示 / context / 记忆

这一章我们只实现 getModel() —— 其它两个钩子留默认。

方案选择

方案适合什么我们用吗
普通 Worker + KV一次性的无状态 API不用,后面会重写
Agent 基类(2025 GA)你想完全控制 LLM 循环、不要 opinion不用,Think 是它的超集
Think 基类(2026 preview)标准 LLM agent,要 sandbox / 工具 / sub-agent
LangGraph + Postgres 自托管需要复杂图编排、不上 Cloudflare不用

Project Think 在 2026-04 是 experimental preview,API 已稳定但还会演进。本书面向接下来 12-18 个月的开发模式;如果你读到本书时 Think 已 GA,代码大概率仍可跑,字段名可能略有微调。

落地

创建项目

# Terminal
npm create cloudflare@latest -- agent-coder \
  --template=cloudflare/agents-starter --no-deploy
cd agent-coder

如果 starter 拉下来一堆前端文件,全部删掉,只留 src/wrangler.jsoncpackage.jsontsconfig.json。我们这本书所有验证都用 curl,不绑前端框架。

同时把 starter 自带的 ChatAgent Durable Object 清理掉:删 src/server.ts(或者其它定义 ChatAgent 的文件)、把 wrangler.jsoncdurable_objects.bindings / migrations 改成只剩我们的 CoderAgent(下一节会贴完整配置)、删掉根目录可能存在的 worker-configuration.d.ts / env.d.ts,等下一步配完 wrangler 再重跑 npx wrangler types 让 Cloudflare 重新生成类型。否则 Cloudflare.Env 里残留的 ChatAgent 字段会跟我们自己的 Env 类型打架。

装 Think

# Terminal
npm install @cloudflare/think@latest agents @cloudflare/ai-chat workers-ai-provider ai

(agents 是底层依赖,提供 routeAgentRequest;@cloudflare/ai-chat 提供 useAgentChat 给客户端;workers-ai-providergetModel() 返回的对象认得 Workers AI;ai 是 Vercel AI SDK 主包,Think 内部用它驱动 LLM。)

配 wrangler.jsonc

// wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "agent-coder",
  "main": "src/index.ts",
  "compatibility_date": "2026-04-30",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" },
  "durable_objects": {
    "bindings": [{ "class_name": "CoderAgent", "name": "CODER_AGENT" }]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["CoderAgent"] }
  ]
}

配完之后立刻重新生成类型:

# Terminal
npx wrangler types

这会重写 worker-configuration.d.ts,把全局的 Cloudflare.Env 同步成我们的 CODER_AGENT + AI 两个绑定。如果跳过这一步,Cloudflare.Env 里残留的 ChatAgent 字段会让 getAgentByName(env.CODER_AGENT, ...) 编译报错(Property 'ChatAgent' is missing in type Env)。

写 agent

// src/agent.ts
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, type UIMessage } from "ai";

export type Env = {
  AI: Ai;
  CODER_AGENT: DurableObjectNamespace<CoderAgent>;
};

export class CoderAgent extends Think<Env> {
  // 唯一你必须实现的钩子:告诉 Think 用哪个模型
  getModel() {
    const wai = createWorkersAI({ binding: this.env.AI });
    return wai("@cf/meta/llama-3.3-70b-instruct-fp8-fast");
  }

  // 可选:覆盖默认的系统提示
  getSystemPrompt() {
    return "你是一个有帮助的中文技术助手,回答简洁、准确。";
  }

  // POST /api/chat:接收 UIMessage 数组,流式返回纯文本(curl 友好)
  async onRequest(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/api/chat" && request.method === "POST") {
      const { messages } = await request.json<{ messages: UIMessage[] }>();
      const result = streamText({
        model: this.getModel(),
        system: this.getSystemPrompt(),
        // ⚠️ AI SDK v6 把 convertToModelMessages 改成了 async,必须 await
        messages: await convertToModelMessages(messages),
      });
      // 第 1 章先用纯文本流(curl 直接看字逐个出来);
      // 第 3 章接 useAgentChat 之后再换成 toUIMessageStreamResponse() 走完整 v6 协议
      return result.toTextStreamResponse();
    }
    return super.onRequest(request);
  }
}

写 worker 入口

// src/index.ts
import { routeAgentRequest, getAgentByName } from "agents";
import type { CoderAgent, Env } from "./agent";

export { CoderAgent } from "./agent";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // /api/chat 直接桥接到固定的 agent 实例(本章只用一个房间)
    if (url.pathname === "/api/chat") {
      const agent = await getAgentByName<Env, CoderAgent>(
        env.CODER_AGENT,
        "default"
      );
      return agent.fetch(request);
    }

    // /agents/{class}/{name}/... 这种标准路径走 routeAgentRequest
    return (
      (await routeAgentRequest(request, env)) ??
      new Response("Not found", { status: 404 })
    );
  },
};

注意几个细节:

  1. CoderAgent 必须从 worker 入口(src/index.ts)re-export,Cloudflare 才能找到 Durable Object 类。
  2. routeAgentRequest 处理的是 /agents/{class}/{name} 这种标准路径(也支持 WebSocket)—— 它不会自动接管 /api/chat。所以我们用 getAgentByName/api/chat 显式桥到一个名为 default 的固定实例上。第 3 章引入 WebSocket 后,前端用 useAgentChat 直接走 /agents/coder-agent/{room},这个 /api/chat 桥就只为 curl 测试服务。
  3. agent.fetch(request) 把请求转发进 Durable Object,在那里命中我们刚写的 onRequestonRequest 自己解析 body、用 AI SDK v6 的 streamText 流式返回 —— 这一章就先用最直白的方式跑通,后面章节再换成 Think 的工具循环 / Session 机制。

跑起来

# Terminal
npx wrangler dev

看到 Ready on http://localhost:8787 就行。

如果你 wrangler 账号不止一个,会看到 More than one account available but unable to select one in non-interactive mode。两个办法:在 wrangler.jsonc 里加 "account_id": "...",或者临时 CLOUDFLARE_ACCOUNT_ID=xxxx npx wrangler dev

AI 绑定永远是远程的(env.AI 跑在 Cloudflare 边缘上,本地调用要走 HTTPS 回 Cloudflare 推理集群)。如果你机器在 GFW 内或被代理拦了 Cloudflare AI 的若干 IP 段,本地 wrangler dev 调 LLM 会出 InferenceUpstreamError: Network connection lostETIMEDOUT。这种情况见下面“验证“小节最后一段的兜底方案 —— 直接 npx wrangler deploy 部署到边缘上 curl,所有调用都在云内,不依赖本地出网。

验证

另开一个终端:

# Terminal
curl -N -X POST http://localhost:8787/api/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","parts":[{"type":"text","text":"用一句话介绍 Cloudflare Workers"}]}]}'

应该看到纯文本逐字流出来(toTextStreamResponse() 用的是 transfer-encoded chunked 文本流,curl 会一段段打印),最后拼出来类似:

Cloudflare Workers 是运行在全球边缘节点上的无服务器 JavaScript 运行时,让开发者用最少的代码把应用部署到离用户最近的位置。

请求 body 是 Vercel AI SDK v6 的 UIMessage 格式(每个 message 由若干 parts 组成)。客户端用 useAgentChat 时这一切是透明的;裸 curl 时要照着写 messages: [{ role, parts: [{ type: "text", text: "..." }] }]

我们这一章的响应故意只用 toTextStreamResponse() —— 纯文本流,curl 直接看字。第 3 章接 useAgentChat 后会换成 toUIMessageStreamResponse(),走完整的 v6 chat 协议(包含 start / text-delta / tool-call / finish 等结构化事件)。

兜底:本地调 AI 不通,就部署上去 curl

如果上面那条本地 curl 死活只看到 200 OK 不见正文,wrangler 终端又冒 InferenceUpstreamError: Network connection lost / ETIMEDOUT,就是你本地出网到 Cloudflare AI 推理集群的某些 IP 不通。最快的兜底:把整个 worker 部署到边缘上,所有 LLM 调用就都在 Cloudflare 内网完成:

# Terminal
CLOUDFLARE_ACCOUNT_ID=<你的 account id> npx wrangler deploy
# 部署成功后会打出公网 URL,例如:
#   https://agent-coder.<your-subdomain>.workers.dev

然后从你机器或者任何能上网的机器 curl 部署后的地址:

# Terminal
curl -N -X POST https://agent-coder.<your-subdomain>.workers.dev/api/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","parts":[{"type":"text","text":"用一句话介绍 Cloudflare Workers"}]}]}'

应该会逐字看到中文回答。验证完别忘了 npx wrangler delete agent-coder 把 demo worker 清掉(留着也不要钱,但暴露公网总是不雅)。

如果你看到 {"errors":[{"code":7003,...}]},通常是 wrangler.jsoncclass_name 拼错或 re-export 漏了。

边界与坑

  • Project Think 是 preview(@cloudflare/think@0.4.x),API 已稳定但还会演进。生产前先把版本号锁住。
  • 本地 dev 模式默认调真 Workers AI(消耗免费额度)。完全离线开发可换更小的模型(@cf/meta/llama-3.1-8b-instruct)或 mock。
  • SQLite 后端不可改:一旦用了 new_sqlite_classes,这个 class 就永远是 SQLite 后端。Think 强依赖 SQLite,这没毛病。
  • useAgentChat 客户端依赖 @cloudflare/ai-chat/react:别引到 agents/reactuseAgent(那个是底层 hook,不带 chat 协议)。
  • convertToModelMessages 在 v6 是 async(返回 Promise<ModelMessage[]>),v5 是同步的。必须 await,否则 streamText 会拿到一个 Promise 当 messages,内部 messages.some(...)TypeError
  • wrangler.jsonc 之后必须 npx wrangler typesworker-configuration.d.ts 是 wrangler 自动生成的,里面的 Cloudflare.Env 必须跟实际绑定一致;否则 getAgentByName(env.CODER_AGENT, ...) 这种 API 会因为 Env 类型不匹配编译失败。
  • wrangler dev --remote 不是 SQLite-DO 的退路Think 强依赖 SQLite,而 --remote 模式(“worker 也跑在云上”)明确不支持 SQLite-backed Durable Object。本地调 AI 失败时,只能选“换网络“或“wrangler deploy 部署后远端 curl“,别指望 --remote 兜底。

延伸阅读

下一章预告

现在我们用的是 Workers AI 上的开源模型(Llama)。下一章把 model 切到 Anthropic Claude / OpenAI GPT,通过 AI Platform(Cloudflare 2026 GA 的统一推理层)用一行 env.AI.run("anthropic/...") 搞定 —— 不需要装 @ai-sdk/anthropic / @ai-sdk/openai 任何 provider 包。