news 2026/9/18 5:32:58

用 Cloudflare Agents 与 GPT-4o 构建智能井字棋:Stateful Agent、可调用方法与结构化输出的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Cloudflare Agents 与 GPT-4o 构建智能井字棋:Stateful Agent、可调用方法与结构化输出的完整实战

用 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/tictactoeagents仓库中的一个演示型示例(完整入口见 README),它的价值不在于井字棋本身,而在于浓缩了「人类 ↔ 浏览器 ↔ Cloudflare 上的 Stateful Agent ↔ LLM」这条完整链路:

  • 状态由 Agent 持有:棋盘、当前玩家、胜者都存放在服务端 Agent 的状态中,而不是前端内存;
  • AI 作为玩家:轮到 AI 时,由 GPT-4o 根据当前棋局生成下一步落子;
  • 调用即 RPC:前端通过agent.call("makeMove", ...)远程调用服务端方法,方法名、参数、返回值都受 TypeScript 类型约束。

快速开始:3 步把 Agent 跑起来

按 README 的指引,本地运行只需两步准备 + 一条命令:

  1. .env.example复制为.env,填入你的 OpenAI API key(@ai-sdk/openai默认读取OPENAI_API_KEY环境变量);
  2. 安装依赖并启动:
npm i && npm start
  1. 打开 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 typeswrangler types env.d.ts --include-runtime false)。

服务端设计:把"游戏规则"写进 Agent 状态机

核心实现位于 src/server.ts。它定义了一个继承自agentsAgent基类的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 | nullX/O/ 空)。currentPlayer表示当前轮到谁,winner在游戏结束时记录胜者。state 由 Agent 在服务端持久化,每次setState都会触发保存,玩家刷新页面后棋局依然在。

initialState字段给出初始棋局(空棋盘、X先手、无胜者),它对应agents包中 Agent 基类 的initialState约定——当持久化状态缺失或读取失败时,Agent 会回退到这个初始值(状态恢复逻辑)。

@callable():把类方法变成远程可调用接口

makeMoveclearBoard都标注了@callable()装饰器。该装饰器来自agents包的 callable-decorator.ts,作用是收集被装饰方法的元数据,运行时通过 WebSocket 把前端调用路由到对应方法——这相当于为 Agent 自动生成了一套类型安全的 RPC 接口,前端不需要手写 HTTP 路由。

makeMove的完整流程(src/server.ts):

  1. 回合校验:如果this.state.currentPlayer !== player,直接抛出"It's not your turn"
  2. 落子校验:目标格子已被占用时抛出"Cell already played"
  3. 更新状态:拷贝棋盘 → 落子 → 切换currentPlayer→ 调用checkWinner更新winner
  4. 判终:有胜者或棋盘已满则直接返回;
  5. 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 条让模型逐级判断的规则:

  1. 一步可赢,直接赢;
  2. 对手一步可赢,必须堵;
  3. 中心空着,占中心;
  4. 能造出双重威胁(fork),就造;
  5. 对手下一步能造 fork,提前堵;
  6. 占角落;
  7. 最后选边。

棋盘状态以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(调用可调用方法)、setStatestubgetHttpUrl等能力。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与前端useAgentprefix必须保持一致,这是前后端正确对接的隐含约束。
  • 生成的 env.d.ts 会为Env注入TicTacToe: DurableObjectNamespace<...>类型,让Agent<Env, ...>的泛型在开发期即可校验。

源码级速览:理解这套模式的三个关键点

  1. @callable()是 RPC 声明而非魔法:装饰器在模块加载时把方法元数据记录到WeakMap,运行时再由callablesFromDecorated(packages/agents/src/websockets/callables-target.ts)提取成 RPC target。理解这一点,你就知道"哪些方法能被前端调用、为什么方法的this.state始终一致"。

  2. 状态持久化有回退机制Agent基类从存储读取状态失败时会回退到initialState(packages/agents/src/index.ts)。因此务必把initialState当作「无历史会话」时的默认棋盘,而不是每次请求都重置。

  3. 确定性与非确定性分离:回合校验、落子、胜负判定全部是确定性的 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 5:31:11

本地LLM构建知识图谱:从笔记到智能关联

1. 项目概述最近在整理个人知识库时&#xff0c;发现了一个有趣的问题&#xff1a;我们积累的笔记、文档和资料越来越多&#xff0c;但它们之间缺乏有效的关联。传统的文件夹分类方式已经无法满足知识管理的需求。于是我开始尝试用本地运行的大语言模型&#xff08;LLM&#xf…

作者头像 李华
网站建设 2026/9/18 5:27:45

OpenClaw 一键安装后模型 Key 怎么填?TaoToken 给 Key 和 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 5:27:45

JS逆向实战:从Network断点到Python签名复现

1. 这不是黑客电影&#xff0c;是每个想拿真实数据的人绕不开的实操课“JS逆向”四个字&#xff0c;这两年在爬虫圈里像块试金石——有人看到就头皮发麻&#xff0c;觉得是加密学汇编语言浏览器内核的三重地狱&#xff1b;也有人点开教程两分钟就关掉&#xff0c;因为满屏eval(…

作者头像 李华