第 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 替你管的、你自己管的
这一章我们只实现 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.jsonc、package.json、tsconfig.json。我们这本书所有验证都用 curl,不绑前端框架。同时把 starter 自带的
ChatAgentDurable Object 清理掉:删src/server.ts(或者其它定义ChatAgent的文件)、把wrangler.jsonc里durable_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-provider 让 getModel() 返回的对象认得 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 })
);
},
};
注意几个细节:
CoderAgent必须从 worker 入口(src/index.ts)re-export,Cloudflare 才能找到 Durable Object 类。routeAgentRequest处理的是/agents/{class}/{name}这种标准路径(也支持 WebSocket)—— 它不会自动接管/api/chat。所以我们用getAgentByName把/api/chat显式桥到一个名为default的固定实例上。第 3 章引入 WebSocket 后,前端用useAgentChat直接走/agents/coder-agent/{room},这个/api/chat桥就只为 curl 测试服务。agent.fetch(request)把请求转发进 Durable Object,在那里命中我们刚写的onRequest。onRequest自己解析 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 lost或ETIMEDOUT。这种情况见下面“验证“小节最后一段的兜底方案 —— 直接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.jsonc 的 class_name 拼错或 re-export 漏了。
边界与坑
- Project Think 是 preview(
@cloudflare/[email protected]),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/react的useAgent(那个是底层 hook,不带 chat 协议)。convertToModelMessages在 v6 是 async(返回Promise<ModelMessage[]>),v5 是同步的。必须await,否则streamText会拿到一个 Promise 当 messages,内部messages.some(...)抛TypeError。- 改
wrangler.jsonc之后必须npx wrangler types。worker-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兜底。
延伸阅读
- Project Think 官方公告 — Cloudflare 介绍它为什么造 Think
- Agents API —
Agent基类签名(Think 继承自它) - Chat Agents —
AIChatAgent(Think 也继承自它)与流协议 - Routing —
routeAgentRequest与getAgentByName
下一章预告
现在我们用的是 Workers AI 上的开源模型(Llama)。下一章把 model 切到 Anthropic Claude / OpenAI GPT,通过 AI Platform(Cloudflare 2026 GA 的统一推理层)用一行 env.AI.run("anthropic/...") 搞定 —— 不需要装 @ai-sdk/anthropic / @ai-sdk/openai 任何 provider 包。