page-agent 自定义工具开发最佳实践:Zod Schema 与 AbortSignal 协作式取消
【免费下载链接】page-agentJavaScript in-page GUI agent. Control web interfaces with natural language.项目地址: https://gitcode.com/GitHub_Trending/pa/page-agent
page-agent 是一个运行在网页内的 JavaScript GUI Agent,用自然语言操控网页界面。本文面向新手,讲透两个核心问题:如何用 Zod Schema 定义自定义工具的输入参数,以及如何用 AbortSignal 实现协作式取消(cooperative cancellation),让你的工具在用户点击"停止"时能立即、干净地退出。
内置工具不够用?先认识 customTools
page-agent 自带一组内置工具(点击、输入文本、下拉选择、滚动、等待、执行 JS 等),定义在 tools/index.ts 中。当你的业务需要"加入购物车""查知识库""提交审批单"这类专属动作时,就通过customTools配置项扩展能力。
注册方式只需一步:在创建 Agent 时传入customTools,每个工具由tool()辅助函数包裹,包含三个要素:
| 要素 | 作用 | 关键点 |
|---|---|---|
description | 写给大模型看的"说明书",决定 AI 何时调用该工具 | 要具体、可操作 |
inputSchema | 用 Zod 定义的输入参数结构 | 必须从zod/v4子路径导入 |
execute | 实际业务逻辑,返回字符串结果 | 异步工具必须响应ctx.signal |
一个最小示例(完整示例见 types.ts 中的注释文档):
import { z } from 'zod/v4' import { tool } from 'page-agent' add_to_cart: tool({ description: 'Add a product to the shopping cart by its product ID.', inputSchema: z.object({ productId: z.string(), quantity: z.number().min(1).default(1), }), execute: async function (input, { signal }) { await fetch('/api/cart', { method: 'POST', body: JSON.stringify(input), signal, // 关键:把取消信号传给网络请求 }) return `Added ${input.quantity}x ${input.productId} to cart.` }, })Zod Schema 定义的 3 个最佳实践
1. 一律从zod/v4子路径导入
page-agent 的 LLM 客户端使用 Zod 4 的z.toJSONSchema()把你的 Schema 转成大模型能理解的 OpenAPI 参数格式(见 utils.ts 中的zodToOpenAITool)。官方文档明确:支持 Zod 3(>=3.25.0)与 Zod 4,但必须写import { z } from 'zod/v4',不支持 Zod Mini。
2. 用约束代替提示词
大模型传参不可靠,把"规矩"写进 Schema 比写进 description 更稳:
z.number().min(1).max(10)限制取值范围(内置wait工具就是min(1).max(10),见 tools/index.ts).default(1)提供缺省值,AI 少传一个参数也不会崩.optional()标记可选参数- 字段命名用简短的 camelCase,降低模型出错率
3. description 是"AI 的行为开关"
Schema 决定"参数长什么样",description 决定"什么时候调用"。参考内置scroll工具的描述写法(tools/index.ts):先说做什么,再说明有无参数的不同行为,最后给出使用建议。模糊的 description 会导致 AI 在错误步骤调用工具。
AbortSignal 协作式取消:让任务随时可控
为什么需要"协作式"取消?
用户随时可能点击"停止"。page-agent 为每个任务创建一个AbortController,其signal会同时送达三处:大模型请求、每个工具的ctx.signal、以及异步回调(见 PageAgentCore.ts 中的设计注释)。
"协作式"的含义是:框架不会暴力杀掉你的工具,而是把signal交给你,由你的代码在合适的检查点"自愿"退出——这正是浏览器fetch取消机制的标准玩法。
工具内响应 signal 的 3 种姿势
execute: async function (input, { signal }) { // ① 网络请求:直接传给 fetch const res = await fetch(url, { signal }) // ② 长循环:每轮检查一次 for (const item of items) { signal.throwIfAborted() await process(item) } // ③ 纯等待:用内置的 waitFor(seconds, signal) await waitFor(3, signal) }其中waitFor是项目内置的可取消等待工具(utils/index.ts):传入 signal 后,一旦取消会立刻以标准AbortError拒绝 Promise,而不是傻等到时间结束。
内置wait工具就是标准示范(tools/index.ts);实验性的execute_javascript工具甚至会把signal注入到 AI 生成的脚本作用域里,并要求生成代码遵守它(tools/index.ts)。
双保险:即使你忘了,框架也会兜底
框架在工具执行完毕后会强制再检查一次:signal.throwIfAborted()(PageAgentCore.ts)。也就是说,即使你的工具忽略了 signal 并正常返回,只要期间任务被停止,框架仍会中断任务。
当AbortError被捕获时,任务状态优雅地变为stopped,历史记录里只留一条简短的 "Task aborted",不会污染 Agent 的记忆(PageAgentCore.ts)。
💡 同理:自定义
onAskUser回调(询问用户)也必须响应signal,在取消时 reject,否则"停止"会卡死在等待用户回答上(见 PageAgentCore.ts 的接口注释)。
进阶:覆盖与移除内置工具
customTools的值支持两种"元操作"(types.ts):
- 同名覆盖:用内置工具名(如
ask_user)注册新工具,直接替换其行为——例如把"问用户"改成"问你的后端模型" - 设为
null移除:如scroll: null让 Agent 永远无法滚动页面,适合做安全围栏
常见踩坑清单 ✅
| 坑 | 后果 | 正确做法 |
|---|---|---|
写成import { z } from 'zod' | Schema 转换报错或行为异常 | 统一用zod/v4子路径 |
异步工具不传signal | 点"停止"后要干等请求超时 | fetch必传{ signal },长循环加throwIfAborted() |
execute返回 undefined | 步骤输出为空,AI 失去反馈 | 永远返回字符串(可含 emoji 状态标记) |
| description 只写一句话 | AI 在不该调用的步骤乱调用 | 说明参数语义 + 适用场景 + 注意事项 |
用setTimeout裸等待 | 无法被取消 | 改用内置waitFor(seconds, signal) |
获取源码与延伸阅读
克隆仓库即可本地研读:
git clone https://gitcode.com/GitHub_Trending/pa/page-agent- 工具定义与内置工具全集:packages/core/src/tools/index.ts
customTools配置说明:packages/core/src/types.ts- 取消机制主流程:packages/core/src/PageAgentCore.ts
- 官方文档页(含完整示例):custom-tools 文档
小结:Zod Schema 负责"AI 能不能传对参数",AbortSignal 负责"任务能不能随时停下"。两者配合,才能写出既聪明又可控的自定义工具。
【免费下载链接】page-agentJavaScript in-page GUI agent. Control web interfaces with natural language.项目地址: https://gitcode.com/GitHub_Trending/pa/page-agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考