用 Cloudflare Agents 与 GPT-4o 构建智能井字棋:Stateful Agent、可调用方法与结构化输出的完整实战
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
本指南以仓库中的 井字棋示例 为核心,剖析一个"AI Agent 与人类对战"的最小完整实现:服务端用agents包定义带状态的TicTacToeAgent,通过@callable()暴露方法;落子决策交由 GPT-4o 借助generateObject+ Zod schema 输出结构化坐标;前端用 React 的useAgent钩子实时同步棋盘并触发调用。读完本文,你将掌握在 Cloudflare Workers 上编写「有状态、可远程调用、可被 AI 驱动」的 Agent 的完整套路,并可直接复用到猜拳、五子棋、回合制策略等任何带状态交互场景。
项目定位:不止是一个游戏
examples/tictactoe是agents仓库中的一个演示型示例(完整入口见 README),它的价值不在于井字棋本身,而在于浓缩了「人类 ↔ 浏览器 ↔ Cloudflare 上的 Stateful Agent ↔ LLM」这条完整链路:
- 状态由 Agent 持有:棋盘、当前玩家、胜者都存放在服务端 Agent 的状态中,而不是前端内存;
- AI 作为玩家:轮到 AI 时,由 GPT-4o 根据当前棋局生成下一步落子;
- 调用即 RPC:前端通过
agent.call("makeMove", ...)远程调用服务端方法,方法名、参数、返回值都受 TypeScript 类型约束。
快速开始:3 步把 Agent 跑起来
按 README 的指引,本地运行只需两步准备 + 一条命令:
- 将
.env.example复制为.env,填入你的 OpenAI API key(@ai-sdk/openai默认读取OPENAI_API_KEY环境变量); - 安装依赖并启动:
npm i && npm start- 打开 http://localhost:5174 即可与 Agent 对战。
npm start实际执行的是vite dev(见 package.json 的scripts字段),由 vite.config.ts 中的agents()、react()、tailwindcss()、cloudflare()四个 Vite 插件协同完成本地开发环境。其中agents()插件负责在本地模拟 Workers 运行环境并提供 Agent 的 WebSocket 端点,因此你不需要先wrangler dev,一条命令就能同时启动前端与 Agent 后端。
示例还提供了部署脚本npm run deploy(内部为vite build && wrangler deploy),以及用于生成类型声明的npm run types(wrangler types env.d.ts --include-runtime false)。
服务端设计:把"游戏规则"写进 Agent 状态机
核心实现位于 src/server.ts。它定义了一个继承自agents包Agent基类的TicTacToe类,泛型参数Agent<Env, TicTacToeState>把状态类型绑定到 Agent 上。
状态模型:TicTacToeState
export type TicTacToeState = { board: [ [played, played, played], [played, played, played], [played, played, played] ]; currentPlayer: Player; // "X" | "O" winner: Player | null; };棋盘被建模为 3×3 的二维数组,每个格子取Player | null(X/O/ 空)。currentPlayer表示当前轮到谁,winner在游戏结束时记录胜者。state 由 Agent 在服务端持久化,每次setState都会触发保存,玩家刷新页面后棋局依然在。
initialState字段给出初始棋局(空棋盘、X先手、无胜者),它对应agents包中 Agent 基类 的initialState约定——当持久化状态缺失或读取失败时,Agent 会回退到这个初始值(状态恢复逻辑)。
@callable():把类方法变成远程可调用接口
makeMove与clearBoard都标注了@callable()装饰器。该装饰器来自agents包的 callable-decorator.ts,作用是收集被装饰方法的元数据,运行时通过 WebSocket 把前端调用路由到对应方法——这相当于为 Agent 自动生成了一套类型安全的 RPC 接口,前端不需要手写 HTTP 路由。
makeMove的完整流程(src/server.ts):
- 回合校验:如果
this.state.currentPlayer !== player,直接抛出"It's not your turn"; - 落子校验:目标格子已被占用时抛出
"Cell already played"; - 更新状态:拷贝棋盘 → 落子 → 切换
currentPlayer→ 调用checkWinner更新winner; - 判终:有胜者或棋盘已满则直接返回;
- AI 接管:否则调用
generateObject让 GPT-4o 决策下一步。
AI 落子:结构化输出的关键用法
AI 决策部分使用了 Vercel AI SDK 的generateObject(注意:这里用的是对象生成而非普通文本补全),搭配 Zod schema 把模型输出约束为坐标数组:
const { object } = await generateObject({ model: openai("gpt-4o"), prompt: `...`, schema: z.object({ move: z.array(z.number()) }) }); await this.makeMove(object.move as [number, number], player === "X" ? "O" : "X");z.object({ move: z.array(z.number()) })要求模型返回一个move数组,随后被断言为[number, number]作为坐标递归调用makeMove。用结构化输出而不是让模型返回纯文本,是让 LLM 可靠地驱动状态机落子的关键——解析成本为零,失败率大幅下降。
Prompt 中给出的策略优先级是这套逻辑的灵魂,它把井字棋的经典最优策略编码成了 7 条让模型逐级判断的规则:
- 一步可赢,直接赢;
- 对手一步可赢,必须堵;
- 中心空着,占中心;
- 能造出双重威胁(fork),就造;
- 对手下一步能造 fork,提前堵;
- 占角落;
- 最后选边。
棋盘状态以JSON.stringify(board, null, 2)注入,并用文字明确"空位是 null、坐标是[row, col]从 0-2"的约定。这种「明确坐标约定 + 优先级清单 + 返回格式约束」的组合,是让 LLM 稳定完成回合制决策的通用模板,可平移至任何棋盘类游戏。
checkWinner:纯函数的胜者判定
checkWinner是一个无副作用的纯函数:预置 8 条胜利线(3 行 + 3 列 + 2 对角线),逐条检查是否三子同号。它不依赖 AI,属于 100% 确定性的规则逻辑,保证游戏判定的正确性不受模型影响。
前端设计:useAgent钩子与实时同步
前端 src/client.tsx 用 React 实现棋盘 UI,核心连接代码只有几行:
const agent = useAgent<TicTacToeState>({ agent: "tic-tac-toe", prefix: "some/prefix" });useAgent来自agents/react(实现见 packages/agents/src/react.tsx),它建立与 Agent 的 WebSocket 连接,返回的对象包含state(服务端状态的实时镜像)、call(调用可调用方法)、setState、stub、getHttpUrl等能力。agent.state会随服务端setState自动更新并触发前端重渲染——这正是 Stateful Agent 方案的体验优势:前端无需维护任何游戏逻辑副本。
玩家落子:一行远程调用
await agent.call("makeMove", [[row, col], state.currentPlayer]);点击格子时,前端把[row, col]和当前玩家传给服务端;若服务端因"还没轮到你"或"格子已占用"抛错,前端在catch中记录错误而不破坏 UI 状态。
三个值得注意的交互细节
- 随机开局开关:
autoPlayEnabled开启时,新游戏开始 1 秒后随机走第一步,让玩家选择执 X 或执 O(对应界面上的 "Random First Move" 开关); - 自动连打:当
state.winner出现或棋盘填满时,前端累计 X 胜 / O 胜 / 平局统计,并在 3 秒后自动调用clearBoard开新局——方便快速观察 AI 在不同开局下的表现; - "New Game" 按钮:直接调用
agent.call("clearBoard"),对应服务端的clearBoard,把状态重置回initialState。
部署配置:Durable Object + SQLite 持久化
wrangler.jsonc 揭示了 Agent 运行时的底座:
{ "durable_objects": { "bindings": [ { "class_name": "TicTacToe", "name": "TicTacToe" } ] }, "migrations": [ { "new_sqlite_classes": ["TicTacToe"], "tag": "v1" } ], "main": "src/server.ts" }TicTacToeAgent 以Durable Object形式部署,每个游戏会话对应一个持久化实例,setState写入 SQLite 存储;new_sqlite_classes迁移声明为 Agent 创建 SQLite 数据库,用于保存状态与(可选)历史数据;main: "src/server.ts"把服务端入口指向 Agent 定义文件;文件末尾通过routeAgentRequest(来自 packages/agents/src/agent-routing.ts)把请求路由到 Agent,未命中则返回 404。路由前缀some/prefix与前端useAgent的prefix必须保持一致,这是前后端正确对接的隐含约束。- 生成的 env.d.ts 会为
Env注入TicTacToe: DurableObjectNamespace<...>类型,让Agent<Env, ...>的泛型在开发期即可校验。
源码级速览:理解这套模式的三个关键点
@callable()是 RPC 声明而非魔法:装饰器在模块加载时把方法元数据记录到WeakMap,运行时再由callablesFromDecorated(packages/agents/src/websockets/callables-target.ts)提取成 RPC target。理解这一点,你就知道"哪些方法能被前端调用、为什么方法的this.state始终一致"。状态持久化有回退机制:
Agent基类从存储读取状态失败时会回退到initialState(packages/agents/src/index.ts)。因此务必把initialState当作「无历史会话」时的默认棋盘,而不是每次请求都重置。确定性与非确定性分离:回合校验、落子、胜负判定全部是确定性的 TypeScript 代码;只有"AI 该走哪一步"交给模型,且用
generateObject+ Zod 强制其输出可解析的坐标。这条边界保证了无论模型发挥如何,游戏本身的正确性始终受控。
总结
examples/tictactoe虽小,却是agents仓库中最完整的「带状态 AI 交互应用」范式:
- 服务端:
Agent<Env, State>泛型 +initialState+@callable()方法,构成有状态、可调用的 Agent 单元; - AI 接入:
generateObject+ Zod schema + 分级策略 Prompt,让 LLM 稳定地以结构化输出驱动状态机; - 前端:
useAgent一行连接,agent.call一行调用,agent.state自动同步; - 部署:Durable Object + SQLite 保证状态跨请求存活。
如果想继续深入,建议阅读仓库中的 agents 核心包 中Agent基类(状态管理、调用路由)与@callable装饰器的实现,并结合 react.tsx 理解useAgent的订阅机制。把这个示例吃透后,你可以用同样的骨架快速搭建 AI 象棋、AI 猜数字、回合制卡牌等更多带状态、带 AI 决策的交互式 Agent 应用。
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考