网页端嵌入 Agent 对接前端方案
本文将深入探讨「网页端嵌入AI」的核心概念与实战技巧,帮助你快速掌握关键要点。让我们开始吧!
网页端嵌入 Agent 对接前端方案
1. 引言
当前前端项目正从被动展示走向主动交互,AI Agent 嵌入网页端可自动化 UI 操作、优化布局并辅助编码。本文聚焦如何在前端(React/Vue3)中集成 AI Agent,涵盖客户端工具定义、安全通信及流式输出等核心环节。读完本文,你将掌握:React 集成 AG-UI 组件、Vue3 对接 Agent 流式输出、前端 Agent 安全通信实现、LangChain 前端 Agent 对接的基本方法。
2. 核心概念:AI Agent 在前端的运行模式
2.1 网页端嵌入 AI Agent 前端方案的本质
网页端嵌入 AI Agent 前端方案的本质是客户端与服务器之间的协同工作模式。负责推理和决策的 LLM 运行在服务端,而前端主要负责 UI 交互和本地环境操作。Agent 在客户端与服务器间协同,由服务器协调工具调用,前端负责浏览器端特有的操作:获取 GPS 位置、访问剪贴板、操作 DOM 元素、调用浏览器 API 等。
以微软 AG-UI 框架为例,Agent 实例在服务端创建,但工具函数可能被标记为“前端执行”,当 LLM 决策需要客户端能力时,服务端会向客户端下发工具调用请求,由前端执行后将结果返回。这种分工既利用了服务端的计算资源、保护了 API 密钥,又保留了前端对浏览器环境的控制权限。在实际工程中,服务端与前端通过标准协议(如 JSON-RPC)交换工具调用ID和参数,前端无须直接暴露 API 密钥或 LLM 配置,从而保障了安全性。
2.2 React 集成 Agent UI 组件——AG-UI 的使用模式
微软开源的 @microsoft/ag-ui-react 库提供了一套开箱即用的 React 组件集合,用于快速在 React 应用中嵌入 AI Agent 界面。它包含 AIAgent、AgentConversation、AgentToolList 等组件,开发者只需在组件树中引入 <AIAgent>,传入必要属性即可使页面具备 Agent 对话与工具调用能力。
AG-UI 的核心设计思路是声明式组件 + 工具函数自动路由。开发者在项目中预先定义前端工具函数(如获取用户位置、切换主题、读取浏览器存储),并用 [Description] 属性标注。通过 AIFunctionFactory.Create 将这些函数包装为 AITool 对象,创建 Agent 实例时传入工具数组,框架会自动确定哪些工具需要在前端执行,并在对话期间接管调用与响应。
此外,AG-UI 提供了内置的 Agent 状态管理(thinking / waiting / error),结合 React Suspense 或自定义 Loading 组件,可以显著减少状态边界处理的开发成本。
2.3 前端 Agent 工具函数定义方法
前端 Agent 工具函数是 Agent 在客户端环境中的可调用能力单元,每个工具函数对应一个具体的浏览器端操作。定义方法主要有以下步骤:
- 函数签名:工具函数必须是静态方法或独立函数,返回类型为
string或可被 JSON 序列化的对象。返回信息将直接作为 LLM 理解的结果。 - 元数据标注:通过
[Description("...")]标注函数功能、参数含义、返回格式。服务端 LLM 会根据描述决定何时调用该工具。
描述应精准简练,例如 [Description("获取用户当前地理位置,返回经纬度字符串。")]。
-
包装为 AITool:使用
AIFunctionFactory.Create(GetUserLocation)将普通函数转为AITool对象,框架会提取其描述、参数类型约束等元数据。 -
注册到 Agent:创建 Agent 实例时,将
AITool[]数组传入tools属性,Agent 即可在对话中识别并调度这些工具。
重要原则:工具函数尽量避免产生副作用,或必须前置确认对话框;输出结果应在合理长度内(建议不超过 2000 字符),避免 token 浪费。对于会修改页面状态的操作(如切换主题),通常不返回复杂对象,只返回固定字符串(例如 Theme toggled successfully.)。
3. 实战:React 中使用 AG-UI 集成 Agent
3.1 项目初始化
在现有 React 项目(确保 React 版本 ≥ 17.0.2)中安装 AG-UI 库:
npm install @microsoft/ag-ui-react @microsoft/ag-client
在应用入口处引入 Provider 组件,用于提供 Agent 客户端实例:
// index.jsx
import { ChatClientProvider } from '@microsoft/ag-ui-react';
import { ChatClient } from '@microsoft/ag-client';
import App from './App';
// 需替换为真实服务端 endpoint,该 endpoint 负责连接 LLM 并协调工具调用
const chatClient = new ChatClient({ endpoint: "https://your-server.com/agent" });
ReactDOM.render(
<ChatClientProvider client={chatClient}>
<App />
</ChatClientProvider>,
document.getElementById('root')
);
3.2 定义前端工具
假设我们需要 Agent 能够获取用户 GPS 位置和切换深色模式。定义两个前端工具函数:
// 后端 C# / .NET 示例(也可以在 JS/TS 中用类似方式)
[Description("Get the user's current location from GPS.")]
static string GetUserLocation()
{
// 实际应调用浏览器 Geolocation API,此处仅做示意
// 前端通过 Bridge 层(JSInterop)从浏览器获取数据
return "Amsterdam, Netherlands (52.37°N, 4.90°E)";
}
[Description("Toggle dark/light theme in current page.")]
static string ToggleDarkMode()
{
// 切换主题逻辑(前端执行)
return "Dark mode toggled to true.";
}
// 创建前端工具数组
AITool[] frontendTools = {
AIFunctionFactory.Create(GetUserLocation),
AIFunctionFactory.Create(ToggleDarkMode)
};
在实践中,由于工具函数必须在前端浏览器环境执行,通常通过 JSInterop(例如 Blazor 场景)或直接在前端 JavaScript 中定义对应函数,然后将函数引用传递给 AG-UI 的 AIFunctionFactory。React 组件内可直接定义纯前端函数。
3.3 创建 Agent 实例并挂载
在服务端或前端初始化时,通过已配置好的 chatClient 创建 Agent 实例:
// App.jsx
import { useChatClient, AIAgent, AgentConversation } from '@microsoft/ag-ui-react';
function App() {
const chatClient = useChatClient();
// 创建 Agent 实例
const agent = chatClient.AsAIAgent({
name: "agui-client",
description: "An agent that can access client-side capabilities.",
tools: frontendTools // 上文定义的 frontendTools
});
return (
<div>
<h2>AI Agent 助手</h2>
{/* AG-UI 内置组件自动处理对话与工具调用 */}
<AIAgent agent={agent}>
<AgentConversation />
</AIAgent>
</div>
);
}
AG-UI 前端工具调用实战:当用户输入“我在哪里?”,LLM 决定调用 GetUserLocation 工具,服务端将工具调用请求(带有 tool_call_id)下发给前端。AG-UI 组件自动执行对应的前端函数,将结果(如经度纬度)回传给服务端,最终以文本形式展示给用户。整个流程对开发者透明,仅需关注工具函数的定义和描述。
4. 实战:Vue3 对接 Agent 流式输出(SSE 实现)
4.1 为什么选择 SSE
AI Agent 的响应通常以流式方式生成,前端逐块展示 token,提升用户体验。在 Vue3 项目中对接流式输出,推荐使用 SSE(Server-Sent Events)。原因如下:SSE 是浏览器原生支持的轻量文本流协议,无需像 WebSocket 那样手动管理握手、帧解析和心跳;它适用于常规的单向文本流场景——Agent 服务端向客户端流式发送生成结果。
对于多数前端对话场景,客户端的输入可通过常规 POST 请求发送,不需要全双工通信。实现复杂度与维护成本均低于 WebSocket。此外,SSE 天然支持断线重连(Last-Event-ID),这在 Agent 长回答场景中尤为重要。
4.2 前端实现:Vue3 Composition API + ReadableStream
<template>
<div>
<div v-for="msg in messages" :key="msg.id">
<strong>{{ msg.role }}:</strong> {{ msg.content }}
</div>
<button @click="cancelRequest" v-if="generating">中止</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const messages = ref([]);
const generating = ref(false);
let abortController = null;
async function sendMessage(text) {
// 将用户消息加入对话
messages.value.push({ role: 'user', content: text, id: Date.now() });
abortController = new AbortController();
generating.value = true;
try {
const response = await fetch('/api/agent/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, history: messages.value.slice(0, -1) }),
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// 使用 ReadableStream 逐块读取 SSE 数据流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let assistantContent = '';
let assistantMsgId = Date.now() + 1;
// 创建助理消息容器,先展示空内容并动态追加
messages.value.push({ role: 'assistant', content: '', id: assistantMsgId });
const msgIndex = messages.value.length - 1;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// SSE 数据格式: data: {"token": "..."} 每行
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data:')) {
try {
const json = JSON.parse(line.slice(5).trim());
if (json.token) {
assistantContent += json.token;
// 利用 Vue3 响应式特性更新消息内容
messages.value[msgIndex].content = assistantContent;
}
} catch (e) {
// 忽略解析错误(如非 JSON 行)
}
}
}
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error('SSE 数据接收错误:', err);
}
} finally {
generating.value = false;
}
}
function cancelRequest() {
if (abortController) {
abortController.abort();
}
}
</script>
要点说明:
- 使用
AbortController支持用户中止请求,避免 Agent 生成过长内容时无法停止。 - 逐块解码时设置
{ stream: true },保证 UTF-8 字符边界正确。 - SSE 数据行格式取决于后端实现,这里假设每行
data: {"token":"xxx"}后跟\n\n作为事件分隔。
前端按行解析、拼接到 ref 中触发视图更新。
- 注意:当大量 token 连续推送时,频繁触发 Vue 响应式更新可能产生性能问题;可在回调中加入节流或批处理(比如每50ms批量更新一次)。
4.3 后端配合(FastAPI 示例)
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
app = FastAPI()
async def agent_stream(message: str, history: list):
"""模拟 Agent 流式生成,实际调用 LLM 或 agent.invoke"""
# 假设 agent 是一个可流式调用的实例
# async for token in agent.astream(input={"message": message, "history": history}):
# yield f"data: {json.dumps({'token': token})}\n\n"
# await asyncio.sleep(0.01) # 控制推送速率
response = "这是模拟的流式输出内容。
"
for char in response:
yield f"data: {json.dumps({'token': char})}\n\n"
await asyncio.sleep(0.02)
@app.post("/api/agent/chat")
async def chat(request: Request):
data = await request.json()
message = data.get("message")
history = data.get("history", [])
return StreamingResponse(
agent_stream(message, history),
media_type="text/event-stream",
headers={
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
后端必须设置 Content-Type: text/event-stream,否则前端 ReadableStream 将无法按预期工作。Java Spring Boot 中可对应使用 SseEmitter。
提示:生产环境下对 SSE 流要进行连接数控制和超时管理。可在前端实现重连逻辑(指数退避,初始延迟 1s,最大 30s),浏览器原生 EventSource 可简化实现,但无法自定义 POST 请求方法。若需要 POST(传用户消息体较大),则推荐使用 fetch + ReadableStream 方案。
5. 安全通信:前端 Agent 安全通信实现
5.1 核心风险识别
前端 Agent 工具函数暴露了浏览器端能力,引入的安全风险不可忽视。主要风险包括:
-
工具函数可能被滥用读取敏感数据:如
navigator.cookie、localStorage、GPS 坐标。 -
LLM 幻觉可能导致服务器错误地调度一个工具函数,如果该函数涉及写操作(如修改 localStorage、建 cookie、发送 HTTP 请求),可能造成用户数据损坏或隐私泄漏。
-
中间人攻击:如果通信链路未加密,攻击者可能篡改服务器下发的工具调用指令,诱导前端执行恶意操作。
5.2 前端 Agent 安全通信实现方案
5.2.1 工具白名单与权限分级
所有工具函数必须注册在前端明确定义的白名单中,前端绝不能执行由服务端动态注入的任意函数名。权限分级建议:
| 等级 | 描述 | 示例工具 |
|---|---|---|
| L0 | 无敏感信息、任意调用 | 获取当前时间、温度转换 |
| L1 | 读取非私有信息,需一次授权 | 获取地理位置 |
| L2 | 读取/写入用户数据 | 写剪贴板、改主题 |
| L3 | 高危操作(写 cookie、发送网络请求) | 需每次确认 |
实现上,可在工具函数定义阶段附加 PermissionLevel 属性,框架在执行工具前检查授权状态。
5.2.2 通信使用 HTTPS + 签名
所有 Agent 与服务器间的通信必须通过 HTTPS。关键调用(如工具执行结果返回)可增加签名机制:服务器在下发 tool_call_id 时附带 HMAC 签名,前端执行后返回时携带签名,服务器验签通过才接受结果。这样可以防止结果被篡改或者重放攻击。
5.2.3 敏感操作的用户二次确认
对于需要修改用户数据或触发系统副作用的工具(L2 及以上),前端必须在执行前弹出确认对话框(如弹窗提示“助手想要修改您的主题,是否允许?”)。用户授权结果应短时缓存(例如 5 分钟内同一工具不再重复确认),避免打断交互流程。
5.2.4 示例:AG-UI 中的权限控制
在 AG-UI 中可以通过 ToolPolicy 扩展实现权限控制:
// 伪代码示意:工具权限策略
const toolPolicy = new ToolPolicy({
onBeforeExecute: async (toolName, args, permissionLevel) => {
if (permissionLevel >= 2) {
const confirmed = await showConfirmDialog(`允许执行 ${toolName} 吗?
`);
return confirmed;
}
return true;
}
});
createAgent({
tools: frontendTools.map(t => ({ tool: t, policy: toolPolicy })),
});
6. 进阶:LangChain 前端 Agent 对接教程与多 Agent 协同
6.1 LangChain 前端 Agent 对接的基本方法
当项目需要更灵活的前端 Agent 逻辑时(例如自定义 LLM 调用、状态机管理),可使用 LangChain 的 JavaScript SDK 创建前端 Agent。基本模式如下:
import { ChatOpenAI } from "@langchain/openai";
import { AgentExecutor, createReactAgent } from "@langchain/langgraph";
// 定义前端工具:使用 @langchain/core 的 Tool 类
const getLocationTool = new Tool({
name: "get_location",
description: "获取用户当前地理位置。",
func: async () => {
// 调用浏览器 Geolocation API
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve(`${pos.coords.latitude},${pos.coords.longitude}`),
(err) => reject("无法获取位置")
);
});
}
});
// 创建 Agent 实例(前端可执行模式)
const agent = await createReactAgent({
llm: new ChatOpenAI({ model: "gpt-4o", temperature: 0 }),
tools: [getLocationTool],
});
const agentExecutor = new AgentExecutor({ agent });
// 调用
const result = await agentExecutor.invoke({ input: "我在哪里?" });
但需要注意,前端直接使用 LLM 的 API key 有暴露风险。LangChain 前端 Agent 对接时,建议将 ChatOpenAI 的调用代理到自己后端,或者使用 callbacks 将函数调用请求发送到后端进行推理。LangChain 新增的 RemoteRun 模式支持这种模式:前端只定义工具执行逻辑,LLM 推理全部通过后端 RemoteRunnable 完成。
6.2 多 Agent 协同与路由
在天猫行业中后台研发场景中(参考搜索资料),复杂的前端任务通常需要多个 Agent 协同:一个 Agent 负责 UI 扫描与实际分析,一个负责布局优化,一个负责组件重构。在多 Agent 架构下,前端需要管理多个 Agent 实例及其工具集。
实现方案:
- 服务端路由层:前端仅维护一个统一的 Agent 入口,通过请求中的
requestType字段让服务端路由到对应下游 Agent。服务端返回的流式数据中包含agentId标识来当前响应的 Agent 身份。 - 前端命名空间隔离:每个 Agent 对应一个工具命名空间,工具函数名称加上
agentId前缀以避免冲突。
每个 Agent 实例独立管理对话历史和状态,利用 React/Vue 的 Context API 或状态管理库(Pinia/Zustand)按 agentId 创建独立 store。
3. 踩坑点:上下文串扰:同一页面存在多个 Agent 实例时,浏览器内存中的会话历史可能被重写。
根本原因在于使用了全局共享的变量或数组存储历史。解决方案:为每个 Agent 创建唯一 agentId,历史记录、AbortController、重连定时器等全部存储在该 ID 为 key 的 Map 中,避免交叉影响。
延伸阅读
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)