前端 Agent 系统的工具调用架构:Function Calling 在 UI 操作中的应用
Agent 系统的核心能力是"理解用户意图 → 选择工具 → 执行操作 → 反馈结果"。在前端场景中,工具不再是后端 API,而是 DOM 操作、表单填充、路由跳转、数据查询等 UI 层面的能力。搭建一个可靠的前端 Agent 工具调用架构,需要在工具定义、执行安全、错误处理和用户确认四个维度上做好设计。
一、前端工具的定义规范
Function Calling 协议定义了 LLM 如何调用外部函数。在前端 Agent 中,工具定义需要覆盖 UI 操作的完整语义。
// 前端工具的标准化定义 interface FrontendTool { /** 工具唯一标识 */ name: string; /** 工具描述(用于 LLM 理解工具能力) */ description: string; /** 参数 Schema(JSON Schema 格式) */ parameters: ToolParameter[]; /** 工具分类 */ category: ToolCategory; /** 是否需要用户确认 */ requiresConfirmation: boolean; /** 执行风险等级 */ riskLevel: 'safe' | 'moderate' | 'dangerous'; /** 工具执行函数 */ execute: (args: Record<string, unknown>) => Promise<ToolResult>; } 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 Record<string, 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.querySelector<HTMLInputElement>( `[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 Record<string, unknown>) || {}; const limit = (args.limit as number) || 50; try { let rows: Record<string, 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: Record<string, unknown>) { window.history.pushState(params, '', path); window.dispatchEvent(new PopStateEvent('popstate')); }, };二、工具执行引擎
执行引擎负责工具调用的全生命周期管理:参数校验 → 权限检查 → 确认拦截 → 执行 → 结果处理。
// 工具执行引擎 class ToolExecutionEngine { private tools: Map<string, FrontendTool> = new Map(); private pendingConfirmations: Map<string, 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: Record<string, unknown> ): Promise<ToolCallResult> { 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: Record<string, 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: Record<string, unknown> ): Promise<boolean> { 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: Record<string, 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: Record<string, 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): Promise<string> { // 初始化对话 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(): Promise<LLMResponse> { 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[] ): Promise<ToolCallResult[]> { 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: Record<string, 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: Map<string, 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: Record<string, unknown>): Record<string, unknown> { const sanitized: Record<string, 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),每次扩展都需要重新评估安全边界和确认策略。