前端工具注册模式:Agent 操作浏览器的安全通道
前端工具注册模式
学习目标
理解 CopilotKit 的前端工具注册与执行模式:
- 工具如何在前端注册、发现和调用
- 工具执行结果如何回传给 Agent
- 工具的生命周期管理(注册 → 执行 → 注销)
- 前端工具与后端工具的安全边界
前置知识
本章涉及工具调用协议的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 CopilotKit 的前端工具实现。
项目实践
工具注册表
CopilotKitCore 内部维护一个工具注册表,管理所有前端可用的工具:
| 操作 | 说明 |
|---|---|
registerTool() | 注册一个新工具 |
getTool(name) | 按名称查找工具 |
runTool(name, args) | 执行工具并返回结果 |
每个工具包含以下字段:
工具结构(伪代码):{ name: "工具名称", description: "工具描述(Agent 用此理解工具用途)", parameters: [参数定义], handler: (args) => Promise<result>, // 浏览器端执行函数 render: (args, status) => ReactNode, // 可选:工具调用时的 UI 展示 available: "enabled" | "disabled" // 可选:动态可用性}React Hook 封装
前端工具通过 React Hook 声明式注册:
// 基础前端工具useFrontendTool({ name: "getCurrentPage", description: "获取当前页面的 URL 和标题", parameters: [], handler: () => ({ url: window.location.href, title: document.title })});
// 带 UI 渲染的交互工具useCopilotAction({ name: "confirmBooking", description: "确认预订", parameters: [ { name: "date", type: "string" }, { name: "room", type: "string" } ], renderAndWaitForResponse: ({ args, respond, status }) => { return ( <BookingDialog date={args.date} room={args.room} onConfirm={() => respond("已确认")} onCancel={() => respond("已取消")} /> ); }});工具执行的完整流程
工具生命周期管理
React Hook 的 useEffect 自动处理工具的注册和注销:
- 挂载:组件 mount → 工具注册到 Core
- 更新:组件 update → 更新工具定义(通过
useRef保持 handler 最新) - 卸载:组件 unmount → 工具自动注销
关键设计:handler 通过 useRef 保持引用稳定,避免因 React 重新渲染导致 handler 过期。
关键代码模式(伪代码):const handlerRef = useRef(handler);useEffect(() => { handlerRef.current = handler; }, [handler]);
// 注册时使用稳定的 ref,执行时调用最新的 handler工具状态机
工具调用经历三个状态:
| 状态 | 触发事件 | UI 表现 |
|---|---|---|
InProgress | TOOL_CALL_START | 显示”正在执行…” |
Executing | handler 开始执行 | 显示参数和自定义 render UI |
Complete | handler 返回结果 | 显示结果 |
Catch-All 通配符
name: "*" 注册一个捕获所有未定义工具的 handler:
useCopilotAction({ name: "*", render: ({ name, args, status }) => { return <div>未知工具: {name}({JSON.stringify(args)})</div>; }});用途:
- 调试:查看 Agent 尝试调用哪些未注册的工具
- 降级:为未知工具提供默认 UI 而非静默忽略
类型安全参数
工具参数通过 Zod Schema 定义,自动映射为 TypeScript 类型:
Parameter[] → Zod Schema → TypeScript 类型推断{ name: "url", type: "string" } → z.string() → { url: string }{ name: "count", type: "number" } → z.number() → { count: number }{ name: "tag", type: "string", enum: ["a", "b"] } → z.enum(["a", "b"])问题与规避
| 问题 | 规避策略 |
|---|---|
| 工具执行时间过长导致 Agent 超时 | 前端设置合理的超时(如 30s);handler 内部实现 AbortSignal 支持 |
| 工具执行失败时的错误传递 | handler 抛出的异常被 Core 捕获,错误消息通过 TOOL_CALL_RESULT 回传 |
| 工具可用性动态切换时 Agent 感知延迟 | available: "disabled" 标记后,需等待 Agent 下次运行才知道工具已不可用 |
| React 组件卸载导致 handler 丢失 | 使用 useRef 保持 handler 引用;Core 中的执行通过 ref 调用最新 handler |
| 工具名称冲突 | 注册表按名称索引,后注册的覆盖先注册的;建议统一命名前缀 |
设计取舍
前端工具 vs 后端工具
| 维度 | 前端工具 | 后端工具 |
|---|---|---|
| 执行位置 | 浏览器 | 服务器 |
| 适合场景 | DOM 操作、用户交互、浏览器 API | 数据库查询、外部 API 调用、文件操作 |
| 安全性 | 用户本地执行,不涉及服务器资源 | 服务器端执行,需要沙箱隔离 |
| 延迟 | 即时(无网络往返) | 需要网络往返到后端 |
| Agent 视角 | 透明(两种工具通过同一协议调用) | 透明 |
useFrontendTool vs useCopilotAction
useFrontendTool:轻量级,仅有handler,适合纯逻辑操作useCopilotAction:重量级,支持render/renderAndWaitForResponse,适合需要 UI 交互的工具
选择策略:不需要 UI 的用 useFrontendTool,需要用户确认/输入的用 useCopilotAction。
参考来源
packages/react-core/src/hooks/use-frontend-tool.tspackages/react-core/src/hooks/use-copilot-action.tspackages/core/src/core/core.ts(工具注册表实现)