前端 Agent 系统的工具调用架构:Function Calling 在 UI 操作中的应用

📅 2026/7/17 16:51:27
前端 Agent 系统的工具调用架构:Function Calling 在 UI 操作中的应用
前端 Agent 系统的工具调用架构Function Calling 在 UI 操作中的应用Agent 系统的核心能力是理解用户意图 → 选择工具 → 执行操作 → 反馈结果。在前端场景中工具不再是后端 API而是 DOM 操作、表单填充、路由跳转、数据查询等 UI 层面的能力。搭建一个可靠的前端 Agent 工具调用架构需要在工具定义、执行安全、错误处理和用户确认四个维度上做好设计。一、前端工具的定义规范Function Calling 协议定义了 LLM 如何调用外部函数。在前端 Agent 中工具定义需要覆盖 UI 操作的完整语义。// 前端工具的标准化定义 interface FrontendTool { /** 工具唯一标识 */ name: string; /** 工具描述用于 LLM 理解工具能力 */ description: string; /** 参数 SchemaJSON Schema 格式 */ parameters: ToolParameter[]; /** 工具分类 */ category: ToolCategory; /** 是否需要用户确认 */ requiresConfirmation: boolean; /** 执行风险等级 */ riskLevel: safe | moderate | dangerous; /** 工具执行函数 */ execute: (args: Recordstring, unknown) PromiseToolResult; } type ToolCategory | navigation // 路由跳转、页面切换 | form // 表单操作填充、提交、验证 | data // 数据查询、过滤、排序 | ui // UI 操作显示/隐藏、滚动、动画 | storage // 本地存储读写 | network; // HTTP 请求 interface ToolParameter { /** 参数名 */ name: string; /** 参数类型 */ type: string | number | boolean | object | array; /** 参数描述 */ description: string; /** 是否必需 */ required: boolean; /** 枚举值可选 */ enum?: string[]; } interface ToolResult { /** 是否成功 */ success: boolean; /** 返回数据 */ data?: unknown; /** 错误信息 */ error?: string; /** 人类可读的操作摘要 */ summary: string; }核心工具定义示例// 前端 Agent 工具注册表 const frontendTools: FrontendTool[] [ // 导航类工具 { name: navigate_to, description: 导航到指定路由页面, category: navigation, riskLevel: safe, requiresConfirmation: false, parameters: [ { name: path, type: string, description: 目标路由路径如 /users/123, required: true, }, { name: params, type: object, description: 路由参数如 { userId: 123 }, required: false, }, ], async execute(args) { const path args.path as string; const params (args.params as Recordstring, unknown) || {}; try { // 使用 React Router 或 Vue Router 的导航 API window.__agent_router?.push(path, params); return { success: true, summary: 已导航到 ${path}, }; } catch (error) { return { success: false, error: (error as Error).message, summary: 导航到 ${path} 失败, }; } }, }, // 表单填充工具 { name: fill_form_field, description: 填充表单中的指定字段。可一次填充多个字段, category: form, riskLevel: moderate, requiresConfirmation: true, parameters: [ { name: fields, type: array, description: 要填充的字段列表。每个字段包含 name字段标识和 value填充值, required: true, }, { name: formId, type: string, description: 目标表单的 ID未指定则填充当前页面上第一个表单, required: false, }, ], async execute(args) { const fields args.fields as Array{ name: string; value: unknown }; const formId args.formId as string | undefined; try { const form formId ? document.getElementById(formId) : document.querySelector(form); if (!form) { return { success: false, error: 未找到目标表单, summary: 表单填充失败 }; } const filled: string[] []; for (const { name, value } of fields) { const input form.querySelectorHTMLInputElement( [name${name}], [data-field${name}] ); if (input) { this.simulateInput(input, String(value)); filled.push(name); } } return { success: true, data: { filled }, summary: 已填充表单字段: ${filled.join(、)}, }; } catch (error) { return { success: false, error: (error as Error).message, summary: 表单填充过程中发生错误, }; } }, }, // 数据查询工具 { name: query_data, description: 查询页面上的表格或列表数据支持过滤和排序, category: data, riskLevel: safe, requiresConfirmation: false, parameters: [ { name: target, type: string, description: 数据源标识table表格或 list列表, required: true, enum: [table, list], }, { name: filters, type: object, description: 过滤条件。格式{ column: value }如 { status: active }, required: false, }, { name: sortBy, type: string, description: 排序字段名, required: false, }, { name: sortOrder, type: string, description: 排序方向, required: false, enum: [asc, desc], }, { name: limit, type: number, description: 返回结果数量上限默认 50, required: false, }, ], async execute(args) { const target args.target as string; const filters (args.filters as Recordstring, unknown) || {}; const limit (args.limit as number) || 50; try { let rows: Recordstring, unknown[] []; if (target table) { rows this.extractTableData(); } else { rows this.extractListData(); } // 应用过滤 if (Object.keys(filters).length 0) { rows rows.filter((row) Object.entries(filters).every(([key, value]) String(row[key]).toLowerCase().includes(String(value).toLowerCase()) ) ); } // 限制数量 rows rows.slice(0, limit); return { success: true, data: { rows, total: rows.length }, summary: 查询到 ${rows.length} 条数据, }; } catch (error) { return { success: false, error: (error as Error).message, summary: 数据查询失败, }; } }, }, // UI 操作工具 { name: scroll_to, description: 滚动页面到指定元素或位置, category: ui, riskLevel: safe, requiresConfirmation: false, parameters: [ { name: selector, type: string, description: 目标元素的 CSS 选择器。如未指定则滚动到顶部, required: false, }, { name: behavior, type: string, description: 滚动行为, required: false, enum: [smooth, auto], }, ], async execute(args) { const selector args.selector as string | undefined; const behavior ((args.behavior as string) || smooth) as ScrollBehavior; try { if (selector) { const element document.querySelector(selector); if (!element) { return { success: false, error: 未找到元素: ${selector}, summary: 滚动失败 }; } element.scrollIntoView({ behavior, block: start }); return { success: true, summary: 已滚动到元素: ${selector} }; } window.scrollTo({ top: 0, behavior }); return { success: true, summary: 已滚动到页面顶部 }; } catch (error) { return { success: false, error: (error as Error).message, summary: 滚动操作失败, }; } }, }, // 本地存储工具 { name: read_storage, description: 读取浏览器本地存储的数据, category: storage, riskLevel: moderate, requiresConfirmation: true, parameters: [ { name: key, type: string, description: 存储键名, required: true, }, { name: storageType, type: string, description: 存储类型, required: false, enum: [localStorage, sessionStorage], }, ], async execute(args) { const key args.key as string; const storageType (args.storageType as string) || localStorage; try { const storage storageType sessionStorage ? sessionStorage : localStorage; const raw storage.getItem(key); if (raw null) { return { success: true, data: null, summary: 键 ${key} 不存在 }; } let data: unknown; try { data JSON.parse(raw); } catch { data raw; } return { success: true, data, summary: 已读取存储键: ${key} }; } catch (error) { return { success: false, error: (error as Error).message, summary: 读取存储键 ${key} 失败, }; } }, }, ]; // 注入到全局以供工具调用 window.__agent_router { push(path: string, params: Recordstring, unknown) { window.history.pushState(params, , path); window.dispatchEvent(new PopStateEvent(popstate)); }, };二、工具执行引擎执行引擎负责工具调用的全生命周期管理参数校验 → 权限检查 → 确认拦截 → 执行 → 结果处理。// 工具执行引擎 class ToolExecutionEngine { private tools: Mapstring, FrontendTool new Map(); private pendingConfirmations: Mapstring, ConfirmationRequest new Map(); private executionLog: ExecutionRecord[] []; constructor() { this.registerTools(frontendTools); } /** * 注册工具 */ registerTools(tools: FrontendTool[]): void { for (const tool of tools) { if (this.tools.has(tool.name)) { console.warn(工具 ${tool.name} 已注册将被覆盖); } this.tools.set(tool.name, tool); } } /** * 执行单个工具调用 */ async execute( toolName: string, args: Recordstring, unknown ): PromiseToolCallResult { const tool this.tools.get(toolName); // 1. 工具存在性检查 if (!tool) { return { success: false, error: 未注册的工具: ${toolName}, toolName, }; } // 2. 参数校验 const validation this.validateArgs(tool, args); if (!validation.valid) { return { success: false, error: 参数校验失败: ${validation.errors.join(; )}, toolName, }; } // 3. 风险拦截危险操作需要用户确认 if (tool.requiresConfirmation) { const confirmed await this.requestConfirmation(tool, args); if (!confirmed) { return { success: false, error: 用户取消了操作, toolName, }; } } // 4. 执行工具 const startTime performance.now(); let result: ToolResult; try { result await tool.execute(args); } catch (error) { result { success: false, error: (error as Error).message, summary: 工具 ${toolName} 执行异常, }; } const duration performance.now() - startTime; // 5. 记录执行日志 this.executionLog.push({ toolName, args, result, duration, timestamp: Date.now(), }); return { success: result.success, data: result.data, error: result.error, toolName, summary: result.summary, duration, }; } /** * 参数校验 */ private validateArgs( tool: FrontendTool, args: Recordstring, unknown ): { valid: boolean; errors: string[] } { const errors: string[] []; for (const param of tool.parameters) { if (param.required (args[param.name] undefined || args[param.name] null)) { errors.push(缺少必需参数: ${param.name}); continue; } // 类型校验 const value args[param.name]; if (value ! undefined) { const actualType Array.isArray(value) ? array : typeof value; if (actualType ! param.type) { errors.push( 参数 ${param.name} 类型错误: 期望 ${param.type}实际 ${actualType} ); } } // 枚举值校验 if (param.enum value ! undefined) { if (!param.enum.includes(String(value))) { errors.push( 参数 ${param.name} 值非法: ${value}允许的值: ${param.enum.join(, )} ); } } } return { valid: errors.length 0, errors }; } /** * 请求用户确认 */ private async requestConfirmation( tool: FrontendTool, args: Recordstring, unknown ): Promiseboolean { const requestId crypto.randomUUID(); // 生成人类可读的确认描述 const description this.generateConfirmationDescription(tool, args); const request: ConfirmationRequest { id: requestId, toolName: tool.name, description, }; this.pendingConfirmations.set(requestId, request); // 触发 UI 确认弹窗 const event new CustomEvent(agent-confirmation-required, { detail: request, }); window.dispatchEvent(event); // 等待用户响应实际实现需结合 UI 组件 return new Promise((resolve) { // 通过事件监听获取用户选择 const handler (e: Event) { const { id, confirmed } (e as CustomEvent).detail; if (id requestId) { window.removeEventListener(agent-confirmation-response, handler); this.pendingConfirmations.delete(requestId); resolve(confirmed); } }; window.addEventListener(agent-confirmation-response, handler); // 30 秒超时自动拒绝 setTimeout(() { if (this.pendingConfirmations.has(requestId)) { window.removeEventListener(agent-confirmation-response, handler); this.pendingConfirmations.delete(requestId); resolve(false); } }, 30000); }); } /** * 生成确认描述 */ private generateConfirmationDescription( tool: FrontendTool, args: Recordstring, unknown ): string { switch (tool.name) { case fill_form_field: const fields args.fields as Array{ name: string; value: unknown }; return 将填充表单的 ${fields.length} 个字段${fields.map((f) f.name).join(、)}; case read_storage: return 将读取本地存储中的 ${args.key} 数据; default: return 将执行 ${tool.name} 操作; } } /** * 获取工具列表用于生成 LLM 的 tools 参数 */ getToolDefinitions(): Array{ type: function; function: { name: string; description: string; parameters: object; }; } { return [...this.tools.values()].map((tool) ({ type: function as const, function: { name: tool.name, description: tool.description, parameters: { type: object, properties: Object.fromEntries( tool.parameters.map((p) [ p.name, { type: p.type, description: p.description, ...(p.enum ? { enum: p.enum } : {}), }, ]) ), required: tool.parameters.filter((p) p.required).map((p) p.name), }, }, })); } /** * 获取执行日志 */ getExecutionLog(limit 50): ExecutionRecord[] { return this.executionLog.slice(-limit); } } interface ToolCallResult { success: boolean; data?: unknown; error?: string; toolName: string; summary: string; duration?: number; } interface ConfirmationRequest { id: string; toolName: string; description: string; } interface ExecutionRecord { toolName: string; args: Recordstring, unknown; result: ToolResult; duration: number; timestamp: number; }三、Agent 主循环// Agent 主循环 class FrontendAgent { private engine: ToolExecutionEngine; private apiKey: string; private messages: ChatMessage[] []; constructor(apiKey: string) { this.engine new ToolExecutionEngine(); this.apiKey apiKey; } /** * Agent 主循环用户输入 → LLM 推理 → 工具调用 → 反馈 */ async run(userInput: string): Promisestring { // 初始化对话 this.messages.push({ role: system, content: this.buildSystemPrompt(), }); this.messages.push({ role: user, content: userInput, }); let maxIterations 5; // 防止无限循环 let finalResponse ; while (maxIterations 0) { const response await this.callLLM(); // 检查是否有工具调用 const toolCalls this.extractToolCalls(response); if (toolCalls.length 0) { // 无工具调用返回文本响应 finalResponse response.content; break; } // 并行执行所有工具调用 const toolResults await this.executeToolsInParallel(toolCalls); // 将工具结果追加到消息历史 this.messages.push({ role: assistant, content: response.content, tool_calls: toolCalls, }); for (const result of toolResults) { this.messages.push({ role: tool, content: JSON.stringify(result), tool_call_id: result.toolName, }); } maxIterations--; } return finalResponse; } /** * 调用 LLM */ private async callLLM(): PromiseLLMResponse { const response await fetch(https://api.openai.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${this.apiKey}, }, body: JSON.stringify({ model: gpt-4, messages: this.messages, tools: this.engine.getToolDefinitions(), tool_choice: auto, temperature: 0.1, }), }); if (!response.ok) { throw new Error(LLM 调用失败: ${response.statusText}); } const data await response.json(); const choice data.choices[0]; const message choice.message; return { content: message.content || , tool_calls: message.tool_calls || [], }; } /** * 提取工具调用 */ private extractToolCalls(response: LLMResponse): ToolCall[] { return (response.tool_calls || []).map((tc: { function: { name: string; arguments: string } }) ({ name: tc.function.name, arguments: JSON.parse(tc.function.arguments), })); } /** * 并行执行工具调用 */ private async executeToolsInParallel( toolCalls: ToolCall[] ): PromiseToolCallResult[] { const promises toolCalls.map((call) this.engine.execute(call.name, call.arguments) ); return Promise.allSettled(promises).then((results) results.map((r) r.status fulfilled ? r.value : { success: false, error: 工具执行异常, toolName: unknown, summary: 执行失败 } ) ); } /** * 构建系统提示 */ private buildSystemPrompt(): string { return 你是一个前端操作助手可以帮助用户在页面上执行各种操作。 你的能力包括 - 导航到不同页面 - 填充表单字段 - 查询表格或列表中的数据 - 滚动到特定位置 - 读取本地存储的数据 操作规则 1. 在执行表单填充、存储读取等操作前应向用户说明即将执行的操作 2. 如果用户意图不明确应先询问确认再执行 3. 每次操作后简要告知用户操作结果 4. 不要在单次响应中执行超过 3 个工具调用; } } interface ChatMessage { role: system | user | assistant | tool; content: string; tool_calls?: ToolCall[]; tool_call_id?: string; } interface ToolCall { name: string; arguments: Recordstring, unknown; } interface LLMResponse { content: string; tool_calls: Array{ function: { name: string; arguments: string }; }; }使用示例// 初始化 Agent const agent new FrontendAgent(sk-xxx); // 执行用户指令 const result await agent.run(帮我把表单里的用户名填成张三然后查询表格中状态为待审核的记录); // 输出 // 1. 工具调用: fill_form_field({ name: username, value: 张三 }) → 已填充 // 2. 工具调用: query_data({ filters: { status: 待审核 } }) → 查到 3 条记录 // 最终返回已为你处理完成查询到 3 条待审核记录。四、安全与防御// 工具调用安全层 class ToolSecurityGuard { /** 每个时间窗口的最大调用次数 */ private rateLimits: Mapstring, number[] new Map(); private maxCallsPerMinute 10; /** * 速率限制检查 */ checkRateLimit(toolName: string): boolean { const now Date.now(); const history this.rateLimits.get(toolName) || []; // 过滤出最近 1 分钟内的调用 const recent history.filter((ts) now - ts 60000); if (recent.length this.maxCallsPerMinute) { console.warn([安全] 工具 ${toolName} 超过速率限制); return false; } recent.push(now); this.rateLimits.set(toolName, recent); return true; } /** * 参数注入检测 */ sanitizeArgs(args: Recordstring, unknown): Recordstring, unknown { const sanitized: Recordstring, unknown {}; for (const [key, value] of Object.entries(args)) { if (typeof value string) { // 移除潜在的 XSS 向量 sanitized[key] value .replace(/script\b[^]*(?:(?!\/script)[^]*)*\/script/gi, ) .replace(/javascript:/gi, ) .replace(/on\w\s*/gi, ); } else { sanitized[key] value; } } return sanitized; } /** * 路径遍历检测 */ validatePath(input: string): boolean { // 防止 ../ 路径遍历 return !/(?:\.{2}\/)/.test(input); } }五、总结前端 Agent 的工具调用架构设计需要关注四个层面工具定义标准化通过统一的 FrontendTool 接口约束工具的能力描述、参数 Schema、风险等级和确认需求执行引擎健壮性参数校验 → 风险拦截 → 用户确认 → 执行 → 日志记录每个环节都不可缺失Agent 主循环设计用户输入 → LLM 推理 → 工具并行执行 → 结果反馈 → 再次推理形成自主决策闭环安全防御多层化速率限制防止滥用、参数注入检测防 XSS、路径校验防目录遍历实际落地方案中建议从低风险的只读工具data query、scroll_to开始验证逐步扩展到写入类工具fill_form、write_storage每次扩展都需要重新评估安全边界和确认策略。