1. 引言

AI Agent 正在从独立应用走向网页端嵌入式交互。前端团队面临的核心挑战是:如何在浏览器环境中安全、高效地集成服务端的大语言模型推理能力与客户端的特有操作(如 GPS、剪贴板、DOM 操作)。本文以 React(基于 AG-UI 框架)和 Vue3 两种主流前端框架为例,说明网页端嵌入 AI Agent 前端方案的具体实现路径。

内容涵盖客户端-服务端协同原理、前端工具函数定义与执行、流式输出对接、安全通信以及多 Agent 隔离策略。阅读本文后,你将掌握:

  • React AG-UI 智能体组件集成的关键步骤与配置要点
  • Vue3 SSE 流式对接 Agent 的完整代码实现
  • 前端工具函数的定义规范与安全执行机制
  • 使用 AbortController 管理流式请求的用户中止
  • 多 Agent 上下文隔离的最佳实践

2. 核心概念:网页端嵌入 AI Agent 的前端协作模式

2.1 客户端-服务端协同原理

网页端嵌入 AI Agent 前端方案的本质,是客户端与服务器之间的职责分工与协同工作。Agent 实例运行在服务端,负责调用 LLM 进行推理和决策;前端主要负责 UI 交互和浏览器环境特有的操作。

具体协作流程如下:

  1. 服务端主导推理:用户在前端输入消息,通过 API 请求发送至服务端。服务端维护 Agent 的对话上下文,并负责调用 LLM 生成回复或决策。

  2. 工具调用需要客户端能力时:当 LLM 的决策涉及浏览器特有操作(如获取位置、访问剪贴板、操作 DOM 元素),服务端不下发最终答案,而是发送一条“工具调用请求”给前端。

  3. 前端执行并返回结果:前端收到工具调用请求后,执行对应的前端函数,将结果通过协议(如 JSON-RPC)回传给服务端。

  4. 服务端继续推理:服务端将工具执行结果作为附加上下文,继续调用 LLM 生成最终回复。

以微软 AG-UI 框架为例,Agent 实例在服务端创建,但工具函数可通过 AIFunctionFactory.Create 标记为“前端执行”。当 LLM 决策需要客户端能力时,服务端向客户端下发工具调用 ID 和参数,前端执行后返回结果。这种分工既利用了服务端的计算资源、保护了 API 密钥,又保留了前端对浏览器环境的控制权限。

实际工程中,服务端与前端通过标准协议(如 JSON-RPC)交换数据,前端不直接暴露 API 密钥或 LLM 配置,从而保障了安全性。

注意:该模式要求前端与后端约定统一的通信协议,包括工具调用请求的格式、参数传递方式、结果返回格式以及错误处理。推荐在项目初期就定义好接口规范,避免后期反复调整。

2.2 前端工具函数的定义与执行机制

前端工具函数是 Agent 与浏览器环境交互的桥梁。与后端工具不同,前端工具直接操作 DOM、调用浏览器 API、读取本地状态,因此需要更严格的定义规范。在 AG-UI 示例中,前端工具的定义需遵循以下原则:

  1. 明确描述与签名:每个工具函数需附带 [Description] 属性,向 LLM 说明该工具的用途、参数及触发条件。例如,GetUserLocation() 的说明为“从当前浏览器获取用户的地理位置”。LLM 根据这个描述决定何时调用工具。如果描述模糊或缺失,LLM 可能不会正确调用该工具。

  2. 无副作用:前端工具应避免修改全局状态或执行无法撤销的操作(如弹窗)。如果必须包含副作用,应通过异步设计并提供取消机制(如 AbortController),确保用户可以中断执行。

  3. 权限分级:根据操作敏感度,将工具分为不同权限级别。例如,读取屏幕亮度为低风险操作,可直接执行;访问剪贴板或发送邮件为中风险操作,需用户二次确认;操作 DOM 结构为高风险操作,需服务端签名授权。

在 AG-UI 中,创建前端工具的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义前端工具函数
[Description("Get the user's current location from GPS.")]
static string GetUserLocation()
{
// 实际由前端 JavaScript 调用浏览器 API 完成
return "Amsterdam, Netherlands (52.37°N, 4.90°E)";
}

// 注册为前端工具
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)];

// 创建 Agent 时传入前端工具
AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
description: "AG-UI Client Agent",
tools: frontendTools);

前端收到工具调用请求时,需安全执行对应函数,并将结果返回给服务端。执行过程中,应捕获所有异常,确保即使调用失败也不影响 Agent 的后续运行。

提示:工具函数的执行结果会影响 LLM 的后续推理。如果工具返回空值或错误信息,LLM 可能重复调用同一工具。建议在前端工具中设计兜底返回数据,例如获取位置失败时返回“用户拒绝了位置权限”,而非返回 null

3. 实战:React AG-UI 集成 Agent 对话与前端工具

3.1 搭建 AG-UI 客户端并注册前端工具

React 项目集成 AG-UI 客户端需完成以下步骤:

  1. 安装依赖:在项目中引入 AG-UI 的 npm 包和相关类型定义。假设使用 AG-UI React 组件库,需安装 @agentuity/react@agentuity/sdk。执行 npm install @agentuity/react @agentuity/sdk

  2. 初始化客户端:在应用入口处创建 AG-UI 客户端实例,配置服务端地址和通信协议。通常单例模式管理客户端,避免重复实例化。

  3. 注册前端工具:为每个前端工具定义一个处理函数,并注册到客户端。AG-UI 的 React 钩子 useAgent 会自动处理工具调用的分发。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useAgent } from '@agentuity/react';
import { useState, useCallback } from 'react';

// 定义前端工具处理逻辑
const frontendTools = {
get_user_location: async () => {
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve({ latitude: pos.coords.latitude, longitude: pos.coords.longitude }),
() => resolve({ error: 'Location permission denied' })
);
});
},
// 其他工具...
};

function AgentChat() {
const [messages, setMessages] = useState([]);
const { sendMessage, isStreaming, abort } = useAgent({
agentId: 'my-agent',
tools: frontendTools,
});

// ...
}

关键说明

  • 工具处理函数必须是异步函数,返回 Promise。AG-UI 内部等待 Promise 解析后,自动将结果返回服务端。
  • 工具名称(如上例中的 get_user_location)必须与服务端注册的 AIFunctionFactory.Create 中的函数名称一致。大小写敏感,建议统一使用小写蛇形命名。

3.2 对接流式输出与用户交互

Agent 的回复通常以流式方式返回,前端逐段渲染,以提升用户体验。AG-UI 的 useAgent 钩子内部封装了 SSE 连接,自动处理流式文本和工具调用请求。

  1. 流式渲染useAgent 返回的 messages 状态会随服务器推送实时更新。每条消息包含 role(user/assistant/tool)和 content。渲染时只需迭代 messages 数组,对 assistant 类型的消息使用 Markdown 渲染组件。

  2. 用户中止:调用 useAgent 返回的 abort 函数即可中断当前流式请求。内部使用 AbortController 实现。当用户点击“停止”按钮时,调用 abort(),前端断开 SSE 并通知服务端取消推理。

  3. 工具调用 UI 反馈:当 Agent 调用前端工具时,messages 中会出现 role: 'tool' 的消息。建议在 UI 上展示工具调用的状态,例如显示“正在获取您的当前位置…”这样的中间提示,让用户知道 Agent 正在执行操作。

对应资源或网络请求的处理机制

1
2
3
4
5
6
7
8
9
const { sendMessage, isStreaming, abort } = useAgent({
agentId: 'my-agent',
tools: frontendTools,
onToolStart: (toolName) => console.log(`Tool ${toolName} started`),
onToolEnd: (toolName, result) => console.log(`Tool ${toolName} ended with`, result),
});

// 实现中止按钮
<button onClick={abort} disabled={!isStreaming}>停止回复</button>

注意AbortController 中断的是前端侧的请求。如果服务端未实现取消机制,服务端仍可能继续推理并浪费计算资源。建议后端也监听请求取消事件,主动终止 LLM 接口调用。

4. 实战:Vue3 SSE 对接 Agent 流式输出

4.1 搭建 Vue3 项目并实现 SSE 长连接

在 Vue3 项目中,由于 AG-UI 目前主要面向 React,通常采用手动构建 SSE 或 fetch + ReadableStream 的方式对接服务端。推荐使用 fetch + ReadableStream,因为传统 EventSource 仅支持 GET 请求且无法自定义 headers,而 Agent 对话通常是 POST 请求。

以下是一个完整的 Vue3 composable 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// useAgentStream.ts
import { ref, onUnmounted } from 'vue';
import { AbortController as PolyfillAbortController } from 'abortcontroller-polyfill';

interface StreamMessage {
type: 'thinking' | 'tool_call' | 'tool_result' | 'message' | 'done';
content?: string;
tool?: string;
arguments?: Record<string, any>;
result?: any;
}

export function useAgentStream() {
const isStreaming = ref(false);
const messages = ref<StreamMessage[]>([]);
let abortController: AbortController | null = null;

const sendMessage = async (userMessage: string) => {
if (isStreaming.value) return;

abortController = new (window.AbortController || PolyfillAbortController)();
isStreaming.value = true;

try {
const response = await fetch('/api/agent/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage }),
signal: abortController.signal,
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';

for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') {
messages.value.push({ type: 'done' });
break;
}
try {
const parsed: StreamMessage = JSON.parse(data);
messages.value.push(parsed);
} catch (e) {
console.warn('Failed to parse SSE data:', data);
}
}
}
}
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('Stream aborted by user');
} else {
console.error('SSE stream error:', error);
}
} finally {
isStreaming.value = false;
abortController = null;
}
};

const stopStream = () => {
abortController?.abort();
};

onUnmounted(() => {
abortController?.abort();
});

return { isStreaming, messages, sendMessage, stopStream };
}

关键说明

  • 使用 fetch + ReadStream 的主要优势是支持 POST 请求和自定义 headers(如认证 token)。EventSource 只支持 GET 请求,无法满足 Agent 对话场景。

  • 缓冲区 buffer 处理分块数据。服务端推送的 SSE 消息可能跨多个数据块,buffer 确保拼接完整后再解析。

  • 响应体流读取后需手动解码,否则无法正确处理中文。decoder.decode(value, { stream: true }) 确保多字节字符不被截断。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<input v-model="input" @keyup.enter="send" />
<div v-for="msg in messages" :key="msg.type">
<div v-if="msg.type === 'message'">{{ msg.content }}</div>
<div v-else-if="msg.type === 'tool_call'">调用工具:{{ msg.tool }}</div>
</div>
<button @click="stopStream" v-if="isStreaming">停止</button>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useAgentStream } from './useAgentStream';

const input = ref('');
const { isStreaming, messages, sendMessage, stopStream } = useAgentStream();
const send = () => {
if (input.value.trim()) {
sendMessage(input.value);
input.value = '';
}
};
</script>

4.2 前端工具回调与安全通信

当服务端推送 tool_call 类型的消息时,前端需执行对应的工具函数,并将结果通过另一个 API 端点回传给服务端。这是 SSE 对接中最关键的环节,涉及跨域通信的安全问题。

工具回调的流程

  1. 解析 tool_call 消息,获取 tool 名称和 arguments
  2. 根据工具名称调用对应函数。建议将工具函数定义在一个对象映射中,便于维护和权限控制。
  3. 将工具执行结果通过 PUT /agent/tool-result 或类似接口回传给服务端。
  4. 服务端收到结果后,继续 SSE 推送后续消息。

安全通信的三个维度

  1. 工具白名单:前端仅执行白名单中的工具函数,杜绝执行未知函数的风险。白名单可在项目初始化时由服务端下发,前端验证后启用。

  2. HTTPS + 签名:所有通信使用 HTTPS 加密。服务端在推送 tool_call 消息时,在消息体中包含签名(如使用 HMAC-SHA256 对 tool 标识和 agentId 签名),前端验证签名通过后再执行。这防止了服务端模拟或中间人攻击。

  3. 用户二次确认:对高风险操作(如修改 DOM、读取敏感数据),在工具执行前弹窗让用户确认。可在 tool_call 处理函数中添加权限级别判断,根据级别决定是否弹窗。例如,读取位置只需用户确认一次,但发送剪贴板数据每次都需要确认。

Vue3 工具回调示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const toolHandlers: Record<string, (args: any) => Promise<any>> = {
get_user_location: async () => {
// 低风险,直接执行
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve({ latitude: pos.coords.latitude, longitude: pos.coords.longitude }),
() => resolve({ error: 'Location permission denied' })
);
});
},
get_clipboard: async () => {
// 中风险,需用户确认
const confirmed = await userConfirm('允许Agent读取剪贴板内容?

');
if (!confirmed) throw new Error('User cancelled clipboard access');
return navigator.clipboard.readText();
},
};

const handleToolCall = async (toolName: string, args: any) => {
const handler = toolHandlers[toolName];
if (!handler) {
console.warn(`Unknown tool: ${toolName}`);
return { error: 'Tool not found' };
}
try {
const result = await handler(args);
// 将结果通过 API 回传给服务端
await fetch('/api/agent/tool-result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'demo-agent', toolCallId: args.toolCallId, result }),
});
} catch (e) {
// 回传错误信息
await fetch('/api/agent/tool-result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'demo-agent', toolCallId: args.toolCallId, error: String(e) }),
});
}
};

提示:每次工具调用都需携带 toolCallId,用于服务端关联工具执行结果与 LLM 推理上下文。如果前端未能正确将 toolCallId 匹配到服务端请求,Agent 可能会错误地将结果关联到其他对话上下文中。建议在开发阶段记录工具调用的全链路日志,方便排查。

5. 进阶技巧与踩坑记录

5.1 多 Agent 协同中的上下文隔离

在同一个页面中嵌入多个 Agent(如助手、数据分析师)时,必须通过 agentId 隔离上下文,避免状态串扰。每个 Agent 实例拥有独立的对话历史、工具列表和会话状态。

设计要点

  1. 独立实例:每个 Agent 使用独立的客户端实例或上下文对象。在 React 中,可使用多个 useAgent 调用或嵌套 Provider;在 Vue3 中,为每个 Agent 创建独立的 useAgentStream 实例。

  2. 通信隔离:后端必须有 agentId 路由,前端发送请求时携带所属 Agent 的唯一标识。服务端据此区分并维护各自的上下文。

  3. 前端路由代理:如果多个 Agent 使用同一个后端入口,前端需在请求路径中注入 agentId,服务端根据 agentId 路由到对应的 Agent 实例。例如,使用 /api/agent/:agentId/stream 这样的路径。

  4. 命名空间工具:不同 Agent 可能同时调用同名工具(如 get_user_location),但执行结果应归属于各自上下文。前端需确保每个 Agent 的工具回调携带正确的 agentId

常见问题:如果未做上下文隔离,Agent A 的工具结果可能被 Agent B 使用,导致 B 的推理出现混乱。这在大并发或长时间运行场景下尤为明显。建议在开发时就采用严格隔离方案,避免后期重构。

5.2 常见踩坑:工具函数副作用与超时处理

前端工具执行中的常见问题及应对策略:

  1. 副作用未撤销:工具函数可能触发 UI 操作(如弹出确认框),若用户未响应或取消操作,工具状态将被挂起。解决方案:

    • 所有工具函数应设计为可取消,使用 AbortControllerCancelToken 模式。
    • 超时自动拒绝:为每个工具调用设置硬性超时时间(如 30 秒),超时后前端自动返回 timeout 结果。
  2. AbortController 的正确取消方式

    • 用户点击“停止回复”时,应同时中断 SSE 流和正在执行的工具函数。
    • tool_call 处理函数中,为每个工具调用创建一个局部的 AbortController,并与主请求的 signal 关联。当主请求被中断时,所有子工具任务也应被取消。
  • 确保前端在工具调用完成后,不再尝试向已中断的 SSE 连接发送结果,否则会触发 TypeError: Failed to fetch
  1. 服务端超时后客户端状态恢复:当服务端因长时间推理或 LLM API 超时导致断开连接,客户端应自动恢复到可交互状态。具体做法:
    • useAgentStream 的异常处理分支中,设置 isStreaming.value = false 并清空部分缓存。
    • 为用户提供“重试”按钮,重新发送上一条消息。

建议在 messages 数组的末尾记录最后一条用户消息的用户输入内容,方便重试。

  • 服务端应返回最后一条已处理的消息偏移,客户端据此恢复上下文。前端在重试时带上该偏移,服务端从断点处继续推理。

注意事项:不要在前端工具函数中直接操作服务端状态。前端工具只负责获取或修改浏览器环境的数据,服务端状态(如用户登录状态、购物车)应由 Agent 通过后端工具操作。前端工具如果错误地修改了服务端数据,可能导致数据不一致。

6. 总结与拓展

6.1 方案总结

本文详细说明了网页端嵌入 AI Agent 前端方案的实现,核心模式为“服务端推理 + 前端工具执行 + 安全通信”。两个主流框架的具体方案对比如下:

维度 React AG-UI 集成 Vue3 SSE 对接
框架封装度 高,AG-UI 提供完整 React 组件和钩子 低,需手动构建 SSE 协议和工具回调
适用场景 新项目重建或可引入 AG-UI 体系 现有 Vue3 项目,或需定制流式通信
工具执行方式 AG-UI 自动调度工具,前端注册即可 需手动解析 tool_call 消息并回传结果
流式输出 自动处理,useAgent 返回增量 messages 手动构建 ReadableStream 并维护消息状态
安全通信 框架内置签名和权限机制 需自行实现白名单 + HTTPS + 签名

选型建议:如果团队使用 React 且项目从零开始,优先选择 AG-UI;如果项目基于 Vue3 或需要高度定制通信协议,采用手写 SSE 方案更灵活。

实践落地建议

  • 前端工具函数务必标明描述、无副作用、权限分级,这是 Agent 正确调用工具的基础。

  • SSE 是前端对接 Agent 流式输出的推荐通信协议,配套 AbortController 实现用户中止。

  • 安全通信需从工具白名单、HTTPS + 签名、用户二次确认三个维度保障,缺一不可。

  • 多 Agent 协同必须用 agentId 隔离上下文,避免串扰。

  • 建议在开发阶段记录工具调用日志和 SSE 消息流,便于排查问题。

6.2 拓展方向

基于当前方案,未来可探索以下方向:

  1. WebWorker 内执行工具以提升性能:将前端工具的执行逻辑移至 WebWorker,避免工具调用阻塞 UI 线程,提升主流程的响应流畅度,尤其在多工具并行调用场景下效果明显。

  2. 多模态 Agent 前端集成:支持 Agent 生成或操作画布(如拖拽组件库、流程图编辑器),前端提供可交互的画布区域,Agent 通过工具函数读取和修改画布状态,实现更丰富的交互能力。

  3. 基于 LangChain 的通用前端 Agent 对接模式:LangChain 已提供前端工具注册和回调接口,可将其作为中间层统一管理前端 Agent。这有助于跨框架(React、Vue3、Angular)复用工具逻辑,降低后续维护成本。

  4. 状态持久化与离线能力:将 Agent 对话上下文存储到 IndexedDB 或 LocalStorage,用户刷新页面后自动恢复。配合 Service Worker,可实现有限离线场景下的 Agent 交互。

以上方向可作为下一阶段的技术探索与落地方案选型的参考。

总结

通过本文的学习,相信你已经对「网页嵌入AI」有了更深入的理解。建议结合实际项目多加练习。如有疑问,欢迎交流!