三大 Agent-UI 协议深度剖析:AG-UI、A2UI 与 MCP-UI 的设计哲学与工程实践
摘要:随着大模型从"对话框"演进为"自主智能体",如何让 Agent 具备富交互能力成为关键挑战。本文基于项目实战,结合 AG-UI、A2UI、MCP-UI 三大协议的源码深度分析,系统阐述它们的设计哲学、核心机制、实现方案及对接方式,并探讨协议组合使用的最佳实践。

目录
- 引言:为什么需要 Agent-UI 协议?
- AG-UI:事件驱动的智能体交互协议
- A2UI:声明式 UI 的零信任渲染引擎
- MCP-UI:MCP 协议的可视化扩展层
- 协议组合:AG-UI + A2UI 的协同架构
- 技术选型决策框架
- Demo 项目实战解析
- 总结与展望
1. 引言:为什么需要 Agent-UI 协议?
1.1 传统 Chatbot 的局限性
传统的 AI 聊天机器人采用简单的 Request-Response 模式:用户输入文本,模型返回文本。这种模式在面对复杂业务场景时暴露出严重不足:
用户 → "帮我订一家北京的川菜馆"
传统 Bot → "好的,我找到了以下餐厅:1. 川办餐厅... 2. 眉州东坡..."
问题:
- ❌ 无法展示餐厅图片、评分、价格等结构化信息
- ❌ 用户需要手动复制餐厅名称再去搜索
- ❌ 无法直接在对话中完成预订操作
1.2 Agent 时代的新需求
当 AI 从 Chatbot 升级为 Agent 后,交互模式发生了本质变化:
| 维度 | Chatbot | Agent |
|---|---|---|
| 运行时间 | 短(毫秒级) | 长(秒/分钟级) |
| 输出类型 | 纯文本 | 文本 + 结构化数据 + UI 控制 |
| 状态管理 | 无状态 | 复杂状态机 |
| 交互模式 | 单轮 Q&A | 多轮工具调用 + 人机协作 |
1.3 三种协议的定位
业界给出了三种截然不同的解决方案:
| 协议 | 来源 | 核心定位 | 一句话概括 |
|---|---|---|---|
| AG-UI | CopilotKit | 事件驱动的状态同步协议 | "让前端实时感知 Agent 的每一步思考" |
| A2UI | 声明式 UI 组件规范 | "Agent 描述意图,客户端负责渲染" | |
| MCP-UI | 社区 | MCP 工具的可视化扩展 | "让工具调用结果具备可视化能力" |
2. AG-UI:事件驱动的智能体交互协议
2.1 设计哲学
AG-UI(Agent-User Interaction Protocol)的核心理念可以概括为:
"UI 是前端的领域,Agent 只负责广播状态变化"
AG-UI 认为:Agent 不应该知道"按钮是圆的还是方的",不应该知道"当前是 React 还是 Vue"。Agent 只需要告诉前端:"我正在调用搜索工具"、"搜索参数是 XXX"、"搜索结果是 YYY"。至于如何渲染这些信息,完全由前端决定。
这种设计带来了几个核心优势:
- 前端自由度最大化:同一个 Agent 可以对接 Web、Mobile、CLI 等不同客户端
- 实时性极强:基于流式事件,用户能看到 Agent 思考的每一步
- 与现有应用深度集成:Agent 可以驱动现有 UI 的状态变化
2.2 核心机制:事件类型系统
AG-UI 定义了一套完整的事件类型体系(约 20+ 种),按功能可分为四大类:
2.2.1 生命周期事件
// 源码位置:ag-ui/sdks/typescript/packages/core/src/events.ts
enum EventType {
// 运行生命周期
RUN_STARTED = "RUN_STARTED", // Agent 开始执行
RUN_FINISHED = "RUN_FINISHED", // Agent 执行完成
RUN_ERROR = "RUN_ERROR", // Agent 执行出错
// 步骤生命周期
STEP_STARTED = "STEP_STARTED", // 开始执行某个步骤
STEP_FINISHED = "STEP_FINISHED", // 步骤执行完成
}
2.2.2 消息流事件
enum EventType {
// 文本消息(流式)
TEXT_MESSAGE_START = "TEXT_MESSAGE_START",
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT", // delta: 增量文本
TEXT_MESSAGE_END = "TEXT_MESSAGE_END",
TEXT_MESSAGE_CHUNK = "TEXT_MESSAGE_CHUNK", // 批量模式
// 思考过程(可选暴露)
THINKING_TEXT_MESSAGE_START = "THINKING_TEXT_MESSAGE_START",
THINKING_TEXT_MESSAGE_CONTENT = "THINKING_TEXT_MESSAGE_CONTENT",
THINKING_TEXT_MESSAGE_END = "THINKING_TEXT_MESSAGE_END",
}
2.2.3 工具调用事件
enum EventType {
// 工具调用生命周期
TOOL_CALL_START = "TOOL_CALL_START", // 包含 toolCallId, toolCallName
TOOL_CALL_ARGS = "TOOL_CALL_ARGS", // 流式参数:delta 字段
TOOL_CALL_END = "TOOL_CALL_END",
TOOL_CALL_RESULT = "TOOL_CALL_RESULT", // 工具执行结果
TOOL_CALL_CHUNK = "TOOL_CALL_CHUNK", // 批量模式
}
2.2.4 状态同步事件
enum EventType {
// 状态管理
STATE_SNAPSHOT = "STATE_SNAPSHOT", // 完整状态快照
STATE_DELTA = "STATE_DELTA", // 增量状态(JSON Patch RFC 6902)
MESSAGES_SNAPSHOT = "MESSAGES_SNAPSHOT", // 完整消息历史
// 活动状态(用于 UI 展示)
ACTIVITY_SNAPSHOT = "ACTIVITY_SNAPSHOT",
ACTIVITY_DELTA = "ACTIVITY_DELTA",
}
2.3 实现方案:传输层与客户端
2.3.1 传输层:SSE + HTTP Binary
AG-UI 支持多种传输方式,其中 SSE(Server-Sent Events)是最常用的:
// 源码位置:ag-ui/sdks/typescript/packages/client/src/agent/http.ts
export class HttpAgent extends AbstractAgent {
protected requestInit(input: RunAgentInput): RequestInit {
return {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream", // 关键:请求 SSE 格式
},
body: JSON.stringify(input),
};
}
run(input: RunAgentInput): Observable<BaseEvent> {
// 1. 发起 HTTP 请求获取 SSE 流
const httpEvents = runHttpRequest(this.url, this.requestInit(input));
// 2. 转换为 AG-UI 事件流
return transformHttpEventStream(httpEvents);
}
}
传输层数据格式示例:
event: TEXT_MESSAGE_START
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"我来帮您"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"搜索餐厅..."}
event: TOOL_CALL_START
data: {"type":"TOOL_CALL_START","toolCallId":"call_001","toolCallName":"search_restaurants"}
event: TOOL_CALL_ARGS
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_001","delta":"{\"cuisine\":\"川菜\"}"}
2.3.2 客户端:Observable + 中间件架构
AG-UI 客户端采用 RxJS Observable 模式处理事件流,并支持中间件扩展:
// 源码位置:ag-ui/sdks/typescript/packages/client/src/agent/agent.ts
export abstract class AbstractAgent {
private middlewares: Middleware[] = [];
// 订阅者模式:支持多个消费者
public subscribe(subscriber: AgentSubscriber) {
this.subscribers.push(subscriber);
return { unsubscribe: () => { /* ... */ } };
}
// 中间件注册
public use(...middlewares: (Middleware | MiddlewareFunction)[]): this {
this.middlewares.push(...normalizedMiddlewares);
return this;
}
// 抽象方法:具体 Agent 实现事件流
abstract run(input: RunAgentInput): Observable<BaseEvent>;
}
2.4 对接方式
2.4.1 服务端对接(Python 示例)
# Demo 项目:demo-agent-ui-protocols/agents/ag-ui-agent/server.py
from sse_starlette.sse import EventSourceResponse
async def generate_events() -> AsyncGenerator[str, None]:
# 1. 发送 RUN_STARTED
yield create_event(EventType.RUN_STARTED, {
"threadId": thread_id,
"runId": run_id
})
# 2. 流式调用 LLM
async for chunk in llm_stream:
if chunk.choices[0].delta.content:
yield create_event(EventType.TEXT_MESSAGE_CONTENT, {
"messageId": msg_id,
"delta": chunk.choices[0].delta.content
})
if chunk.choices[0].delta.tool_calls:
# 处理工具调用...
yield create_event(EventType.TOOL_CALL_START, {...})
# 3. 发送 RUN_FINISHED
yield create_event(EventType.RUN_FINISHED, {
"threadId": thread_id,
"runId": run_id
})
@app.post("/run")
async def run(request: RunRequest):
return EventSourceResponse(generate_events())
2.4.2 前端对接(React 示例)
// Demo 项目:demo-agent-ui-protocols/apps/web/src/app/ag-ui-demo/page.tsx
const handleSendMessage = async (content: string) => {
const response = await fetch('http://localhost:8001/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [{ role: 'user', content }] }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解析 SSE 事件
const events = parseSSE(decoder.decode(value));
for (const event of events) {
switch (event.type) {
case 'TEXT_MESSAGE_CONTENT':
// 更新消息内容(流式)
setMessages(prev => updateMessageContent(prev, event));
break;
case 'TOOL_CALL_START':
// 显示工具调用 UI
setMessages(prev => addToolCall(prev, event));
break;
case 'TOOL_CALL_RESULT':
// 渲染工具结果
setMessages(prev => updateToolResult(prev, event));
break;
}
}
}
};
3. A2UI:声明式 UI 的零信任渲染引擎
3.1 设计哲学
A2UI(Agent-to-User Interface)由 Google 推出,其核心理念是:
"Safe like data, expressive like code"(像数据一样安全,像代码一样有表现力)
与 AG-UI 的"前端主导"不同,A2UI 采用"后端主导"的思路:Agent 不仅发送数据,还发送 UI 结构描述。但为了安全,A2UI 绝对不允许 Agent 发送可执行代码(HTML/JS),而是发送一种声明式的组件描述 JSON。
3.1.1 安全性设计
A2UI 的安全模型基于"白名单组件库"(Catalog)机制:
Agent 只能说:"我要渲染一个 Card 组件,ID 是 123,标题是 XXX"
Agent 不能说:"<script>alert('XSS')</script>"
这种设计完全杜绝了 LLM 生成恶意代码的风险。
3.1.2 跨平台设计
由于 A2UI 发送的是抽象组件描述而非具体实现,同一套协议可以:
- 在 Web 端渲染为 DOM 元素
- 在 iOS 端渲染为 SwiftUI View
- 在 Android 端渲染为 Compose 组件
- 在 Flutter 中渲染为 Widget
3.2 核心机制:邻接表组件模型
3.2.1 为什么不用嵌套 JSON?
传统的 UI 描述通常采用嵌套结构:
// ❌ 传统嵌套结构 - 对 LLM 不友好
{
"type": "Column",
"children": [
{
"type": "Text",
"text": "Hello"
},
{
"type": "Button",
"children": [{ "type": "Text", "text": "Click" }]
}
]
}
问题:
- LLM 必须一次性生成完美嵌套,容易出错
- 难以增量更新(需要重新发送整个树)
- 深层嵌套难以流式生成
3.2.2 邻接表模型
A2UI 采用邻接表(Adjacency List)结构,将组件树"拍平"为列表:
// 源码位置:A2UI/docs/concepts/components.md
// ✅ A2UI 邻接表结构 - LLM 友好
{
"surfaceUpdate": {
"surfaceId": "main",
"components": [
{"id": "root", "component": {"Column": {"children": {"explicitList": ["greeting", "buttons"]}}}},
{"id": "greeting", "component": {"Text": {"text": {"literalString": "Hello"}}}},
{"id": "buttons", "component": {"Row": {"children": {"explicitList": ["cancel-btn", "ok-btn"]}}}},
{"id": "cancel-btn", "component": {"Button": {"child": "cancel-text", "action": {"name": "cancel"}}}},
{"id": "cancel-text", "component": {"Text": {"text": {"literalString": "Cancel"}}}},
{"id": "ok-btn", "component": {"Button": {"child": "ok-text", "action": {"name": "ok"}}}},
{"id": "ok-text", "component": {"Text": {"text": {"literalString": "OK"}}}}
]
}
}
优势:
- ✅ LLM 可以逐个生成组件,无需考虑嵌套
- ✅ 增量更新:只发送变化的组件
- ✅ 天然支持流式传输(JSONL 格式)
3.3 消息类型体系
A2UI 定义了四种核心消息类型:
// 源码位置:A2UI/specification/0.9/json/server_to_client.json
{
"oneOf": [
{ "$ref": "#/$defs/CreateSurfaceMessage" }, // 创建 UI 表面
{ "$ref": "#/$defs/UpdateComponentsMessage" }, // 更新组件
{ "$ref": "#/$defs/UpdateDataModelMessage" }, // 更新数据模型
{ "$ref": "#/$defs/DeleteSurfaceMessage" } // 删除 UI 表面
]
}
3.3.1 createSurface:初始化 UI 表面
{
"createSurface": {
"surfaceId": "restaurant-list",
"catalogId": "a2ui.dev:standard" // 声明使用的组件库
}
}
3.3.2 updateComponents:发送组件定义
{
"updateComponents": {
"surfaceId": "restaurant-list",
"components": [
{
"id": "root",
"component": {
"Column": {
"children": {"explicitList": ["header", "list"]}
}
}
},
{
"id": "header",
"component": {
"Text": {
"text": {"literalString": "推荐餐厅"},
"usageHint": "h1"
}
}
}
// ... 更多组件
]
}
}
3.3.3 updateDataModel:数据与 UI 分离
A2UI 的一个重要设计是数据模型与组件结构分离。组件可以通过 path 绑定数据:
// 1. 组件定义(结构)
{
"updateComponents": {
"surfaceId": "restaurant-list",
"components": [{
"id": "restaurant-name",
"component": {
"Text": {
"text": {"path": "/restaurants/0/name"} // 数据绑定
}
}
}]
}
}
// 2. 数据更新(内容)
{
"updateDataModel": {
"surfaceId": "restaurant-list",
"path": "/restaurants/0",
"op": "replace",
"value": {
"name": "川办餐厅",
"rating": 4.8,
"price": "$$"
}
}
}
优势:
- 更新数据无需重新发送组件结构
- 多个组件可以绑定同一数据路径
- LLM 可以分步生成结构和数据
3.4 标准组件库(Catalog)
A2UI 定义了一套标准组件库,涵盖常见 UI 需求:
// 源码位置:A2UI/specification/0.9/json/standard_catalog_definition.json
{
"$defs": {
"anyComponent": {
"oneOf": [
// 展示类
{ "$ref": "#/$defs/Text" },
{ "$ref": "#/$defs/Image" },
{ "$ref": "#/$defs/Icon" },
{ "$ref": "#/$defs/Video" },
{ "$ref": "#/$defs/AudioPlayer" },
// 布局类
{ "$ref": "#/$defs/Row" },
{ "$ref": "#/$defs/Column" },
{ "$ref": "#/$defs/List" },
// 容器类
{ "$ref": "#/$defs/Card" },
{ "$ref": "#/$defs/Tabs" },
{ "$ref": "#/$defs/Modal" },
{ "$ref": "#/$defs/Divider" },
// 交互类
{ "$ref": "#/$defs/Button" },
{ "$ref": "#/$defs/CheckBox" },
{ "$ref": "#/$defs/TextField" },
{ "$ref": "#/$defs/DateTimeInput" },
{ "$ref": "#/$defs/ChoicePicker" },
{ "$ref": "#/$defs/Slider" }
]
}
}
}
3.5 客户端渲染器(Renderer)
A2UI 提供了多种渲染器实现:
// 源码位置:A2UI/renderers/lit/src/0.8/core.ts
export * as Events from "./events/events.js";
export * as Types from "./types/types.js";
import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js";
import { A2uiMessageProcessor } from "./data/model-processor.js";
export const Data = {
createSignalA2uiMessageProcessor, // 响应式数据处理
A2uiMessageProcessor, // 消息处理器
Guards,
};
渲染流程:
- 解析消息:将 JSONL 解析为消息对象
- 构建组件树:根据邻接表重建树结构
- 数据绑定:将 dataModel 注入组件
- 原生渲染:调用平台原生组件库
3.6 用户交互:Action 回传
当用户点击按钮等交互时,客户端发送 userAction 消息:
// 源码位置:A2UI/specification/0.9/json/client_to_server.json
{
"userAction": {
"name": "book_restaurant", // action 名称
"surfaceId": "restaurant-list",
"sourceComponentId": "book-btn",
"timestamp": "2024-01-07T10:30:00Z",
"context": { // 上下文数据
"restaurantId": "rest_001",
"restaurantName": "川办餐厅"
}
}
}
3.7 对接方式
3.7.1 服务端对接(Python 示例)
# Demo 项目:demo-agent-ui-protocols/agents/a2ui-agent/server.py
class A2UIGenerator:
@staticmethod
def surface_update(surface_id: str, components: list) -> dict:
return {"surfaceUpdate": {"surfaceId": surface_id, "components": components}}
@staticmethod
def data_model_update(surface_id: str, path: str, value: any) -> dict:
return {
"updateDataModel": {
"surfaceId": surface_id,
"path": path,
"op": "replace",
"value": value
}
}
async def generate_ui(restaurants: list):
# 1. 创建 Surface
yield json.dumps({"createSurface": {"surfaceId": "main", "catalogId": "standard"}})
# 2. 发送组件结构
components = create_restaurant_list_components()
yield json.dumps(A2UIGenerator.surface_update("main", components))
# 3. 发送数据
for i, restaurant in enumerate(restaurants):
yield json.dumps(A2UIGenerator.data_model_update(
"main",
f"/restaurants/{i}",
restaurant
))
3.7.2 前端对接(React 示例)
// Demo 项目:demo-agent-ui-protocols/apps/web/src/app/a2ui-demo/A2UIRenderer.tsx
const A2UIRenderer = ({ messages }: { messages: A2UIMessage[] }) => {
const [components, setComponents] = useState<Map<string, ComponentDef>>();
const [dataModel, setDataModel] = useState<Record<string, any>>({});
useEffect(() => {
for (const msg of messages) {
if (msg.updateComponents) {
// 更新组件 Map
msg.updateComponents.components.forEach(comp => {
setComponents(prev => new Map(prev).set(comp.id, comp));
});
}
if (msg.updateDataModel) {
// 更新数据模型
setDataModel(prev => ({
...prev,
[msg.updateDataModel.path]: msg.updateDataModel.value
}));
}
}
}, [messages]);
// 递归渲染组件树
const renderComponent = (id: string) => {
const comp = components.get(id);
if (!comp) return null;
// 根据组件类型映射到 React 组件
switch (Object.keys(comp.component)[0]) {
case 'Text':
const textValue = resolveValue(comp.component.Text.text, dataModel);
return <span key={id}>{textValue}</span>;
case 'Column':
return (
<div key={id} className="flex flex-col">
{comp.component.Column.children.explicitList.map(renderComponent)}
</div>
);
// ... 其他组件
}
};
return renderComponent('root');
};
4. MCP-UI:MCP 协议的可视化扩展层
4.1 设计哲学
MCP-UI 是社区基于 Anthropic 的 Model Context Protocol (MCP) 开发的 UI 扩展。其核心理念是:
"让工具调用结果具备可视化能力"
与 AG-UI、A2UI 不同,MCP-UI 不试图定义新的协议,而是复用现有的 MCP 协议,在工具返回值中添加 UIResource 字段。
4.1.1 与 MCP 的关系
MCP 协议:
- Tool Definition(工具定义)
- Tool Call(工具调用)
- Tool Result(工具结果) ← MCP-UI 在这里扩展
MCP-UI 的创新在于:工具不仅可以返回文本/JSON 数据,还可以返回可交互的 UI 片段。
4.2 核心机制:UIResource
4.2.1 UIResource 数据结构
// 源码位置:mcp-ui/sdks/typescript/server/src/types.ts
interface UIResource {
type: 'resource';
resource: {
uri: string; // 唯一标识,如 ui://component/booking-form
mimeType: MimeType; // 内容类型
text?: string; // 内联内容
blob?: string; // Base64 编码内容
_meta?: Record<string, unknown>;
};
}
type MimeType =
| 'text/html' // 内联 HTML
| 'text/uri-list' // 外部 URL
| 'application/vnd.mcp-ui.remote-dom+javascript; framework=react'
| 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents';
4.2.2 三种渲染模式
MCP-UI 支持三种不同的 UI 资源类型:
1. Raw HTML(内联 HTML)
{
uri: "ui://restaurant/card",
mimeType: "text/html",
text: `
<div class="restaurant-card">
<h2>川办餐厅</h2>
<button onclick="window.parent.postMessage({type:'tool',payload:{toolName:'book'}},'*')">
预订
</button>
</div>
`
}
2. External URL(外部页面)
{
uri: "ui://restaurant/detail",
mimeType: "text/uri-list",
text: "https://restaurant.example.com/embed/123"
}
3. Remote DOM(远程 DOM)
这是 MCP-UI 最强大的模式,基于 Shopify 的 remote-dom 技术:
{
uri: "ui://restaurant/form",
mimeType: "application/vnd.mcp-ui.remote-dom+javascript; framework=react",
text: `
// 这段 JS 在沙箱中执行,通过 JSON 消息与宿主通信
const form = document.createElement('ui-form');
form.addEventListener('submit', (e) => {
window.postMessage({ type: 'tool', payload: { toolName: 'submit_booking', params: e.detail } });
});
document.body.appendChild(form);
`
}
4.3 客户端渲染器
4.3.1 UIResourceRenderer 组件
// 源码位置:mcp-ui/sdks/typescript/client/src/components/UIResourceRenderer.tsx
export const UIResourceRenderer = (props: UIResourceRendererProps) => {
const { resource, onUIAction, supportedContentTypes } = props;
const contentType = getContentType(resource);
switch (contentType) {
case 'rawHtml':
case 'externalUrl':
// 使用 iframe 沙箱渲染
return <HTMLResourceRenderer resource={resource} onUIAction={onUIAction} />;
case 'remoteDom':
// 使用 Remote DOM 渲染(更安全、更灵活)
return <RemoteDOMResourceRenderer resource={resource} onUIAction={onUIAction} />;
default:
return <p>Unsupported resource type.</p>;
}
};
4.3.2 Remote DOM 渲染器
Remote DOM 模式下,UI 逻辑在 iframe 沙箱中执行,但 DOM 变化通过 JSON 消息同步到宿主:
// 源码位置:mcp-ui/sdks/typescript/client/src/components/RemoteDOMResourceRenderer.tsx
const RemoteDOMResourceRenderer: React.FC<RemoteDOMResourceProps> = ({
resource, library, onUIAction
}) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
// 1. 创建 Remote Receiver(接收 DOM 变化)
const { receiver, components } = useMemo(() => {
const reactReceiver = new RemoteReceiver();
// 将组件库映射为 Remote Components
// ...
return { receiver: reactReceiver, components: componentMap };
}, [library]);
// 2. 监听 iframe 消息(UI Action)
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.source === iframeRef.current?.contentWindow) {
onUIAction?.(event.data as UIActionResult);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onUIAction]);
// 3. iframe 加载后注入代码
const handleIframeLoad = () => {
const thread = new ThreadIframe<SandboxAPI>(iframeRef.current);
thread.imports.render({ code: resource.text, ... }, receiver.connection);
};
return (
<>
<iframe ref={iframeRef} srcDoc={IFRAME_SRC_DOC} onLoad={handleIframeLoad} />
{/* Remote DOM 渲染结果 */}
<RemoteRootRenderer receiver={receiver} components={components} />
</>
);
};
4.4 UI Action 系统
MCP-UI 定义了五种 UI 交互类型:
// 源码位置:mcp-ui/sdks/typescript/client/src/types.ts
export type UIActionResult =
| { type: 'tool', payload: { toolName: string, params: Record<string, unknown> } }
| { type: 'prompt', payload: { prompt: string } }
| { type: 'link', payload: { url: string } }
| { type: 'intent', payload: { intent: string, params: Record<string, unknown> } }
| { type: 'notify', payload: { message: string } };
使用场景:
tool:触发工具调用(如"预订"按钮)prompt:发送新的用户消息link:打开外部链接intent:触发应用内意图notify:显示通知消息
4.5 对接方式
4.5.1 服务端对接(Python 示例)
# Demo 项目:demo-agent-ui-protocols/agents/mcp-ui-agent/server.py
def create_restaurant_card_ui(restaurants: list) -> dict:
html = f"""
<div class="restaurant-list">
{''.join([f'''
<div class="restaurant-card" data-id="{r['id']}">
<img src="{r['image']}" />
<h3>{r['name']}</h3>
<p>评分: {r['rating']} | 价格: {r['price']}</p>
<button onclick="window.parent.postMessage({{
type: 'tool',
payload: {{
toolName: 'show_booking_form',
params: {{ restaurant_name: '{r['name']}' }}
}}
}}, '*')">预订</button>
</div>
''' for r in restaurants])}
</div>
"""
return {
"type": "ui_resource",
"resource": {
"uri": "ui://restaurant/list",
"mimeType": "text/html",
"text": html
}
}
@app.post("/run")
async def run(request: RunRequest):
if request.tool_call:
# 直接执行工具调用
result = execute_tool(request.tool_call.name, request.tool_call.params)
return result
else:
# 让 LLM 决定调用哪个工具
response = await call_llm_with_tools(request.messages)
return response
4.5.2 前端对接(React 示例)
// Demo 项目:demo-agent-ui-protocols/apps/web/src/app/mcp-ui-demo/page.tsx
const MCPUIDemo = () => {
const [currentUI, setCurrentUI] = useState<UIResource | null>(null);
// 处理来自 UI 的 Action
useEffect(() => {
const handleMessage = async (event: MessageEvent) => {
const { type, payload } = event.data;
if (type === 'tool') {
// 调用后端工具
const response = await fetch('http://localhost:8003/run', {
method: 'POST',
body: JSON.stringify({ tool_call: payload }),
});
const result = await response.json();
if (result.type === 'ui_resource') {
setCurrentUI(result.resource);
}
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
return (
<div>
{currentUI && (
<UIResourceRenderer
resource={currentUI.resource}
onUIAction={handleToolCallback}
/>
)}
</div>
);
};
5. 协议组合:AG-UI + A2UI 的协同架构
5.1 为什么要组合使用?
三种协议并非互斥关系,它们可以协同工作,发挥各自优势。特别是 AG-UI + A2UI 的组合,在 A2UI 官方文档中被明确提及:
"AG UI translates from A2UI messages to AG UI messages, and handles transport and state sync automatically."
5.2 AG-UI 作为 A2UI 的传输层
在这种架构下:
- A2UI 负责:UI 结构定义、组件规范、数据模型
- AG-UI 负责:消息传输、状态同步、事件路由
5.3 实现方式:将 A2UI 消息包装为 AG-UI 事件
// 伪代码:AG-UI + A2UI 集成
// 1. Agent 生成 A2UI 消息
const a2uiMessages = generateA2UIComponents(restaurants);
// 2. 包装为 AG-UI 的 CUSTOM 或 ACTIVITY_SNAPSHOT 事件
for (const msg of a2uiMessages) {
yield {
type: EventType.ACTIVITY_SNAPSHOT,
messageId: `a2ui-${Date.now()}`,
activityType: "a2ui", // 标记为 A2UI 消息
content: msg, // A2UI 原始消息
};
}
// 3. 前端根据 activityType 路由到 A2UI 渲染器
agent.subscribe({
onActivitySnapshot: (event) => {
if (event.activityType === "a2ui") {
a2uiRenderer.processMessage(event.content);
}
}
});
5.4 AG-UI + MCP-UI 的组合
AG-UI 官方提供了 @ag-ui/mcp-apps-middleware,可以将 MCP-UI 的 UI 资源集成到 AG-UI 事件流中:
// 源码位置:ag-ui/middlewares/mcp-apps-middleware/README.md
import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware";
const agent = new YourAgent().use(
new MCPAppsMiddleware({
mcpServers: [
{ type: "http", url: "http://localhost:3001/mcp" }
],
})
);
// 中间件自动:
// 1. 发现 MCP Server 的 UI-enabled Tools
// 2. 将工具注入 Agent 的工具列表
// 3. 执行工具调用并获取 UIResource
// 4. 发射 ACTIVITY_SNAPSHOT 事件(activityType: "mcp-apps")
5.5 三协议融合架构
在复杂场景下,三种协议可以同时使用:
各协议职责:
- AG-UI:作为"总线",负责事件路由和状态同步
- A2UI:负责复杂的、跨平台的声明式 UI
- MCP-UI:负责工具级别的快速 UI 扩展
6. 技术选型决策框架
6.1 维度对比表
| 维度 | AG-UI | A2UI | MCP-UI |
|---|---|---|---|
| 核心理念 | 事件驱动的状态同步 | 声明式 UI 组件规范 | MCP 工具的 UI 扩展 |
| UI 控制权 | 前端主导 | 后端主导(结构) | 后端主导(内容) |
| 安全模型 | 依赖前端实现 | 最高(白名单组件) | 中等(iframe 沙箱) |
| 跨平台能力 | 弱(需各端适配) | 最强(抽象组件) | 中等(Web 优先) |
| 实时性 | 最强(流式事件) | 中等(JSONL 流) | 弱(Request-Response) |
| 开发复杂度 | 前端复杂 | 架构复杂 | 最简单 |
| 生态兼容性 | CopilotKit 生态 | Google 生态 | MCP 生态 |
6.2 场景-协议匹配指南
场景 A:企业级 Copilot 系统
需求特征:
- 需要与现有复杂业务系统深度集成
- Agent 需要操作现有 UI 状态(如高亮表格行、填写表单)
- 需要实时展示 Agent 思考过程
推荐方案:AG-UI 为主
// AG-UI 可以驱动现有 UI 状态
agent.subscribe({
onStateDelta: (event) => {
// 增量更新应用状态
applyJsonPatch(appState, event.delta);
},
onToolCallStart: (event) => {
// 高亮相关 UI 区域
highlightUIRegion(event.toolCallName);
}
});
场景 B:跨平台消费级 App
需求特征:
- 同时支持 Web、iOS、Android
- 对安全性要求极高(防止 XSS、幻觉输出)
- 需要统一的设计语言
推荐方案:A2UI 为主
// A2UI 一次定义,多端渲染
{
"updateComponents": {
"components": [
{"id": "card", "component": {"Card": {...}}}
]
}
}
// Web 端:渲染为 <div class="card">
// iOS 端:渲染为 SwiftUI Card
// Android 端:渲染为 Compose Card
场景 C:开发者工具 / IDE 插件
需求特征:
- 需要快速为现有工具添加 UI
- 希望第三方开发者能贡献 UI 插件
- 不需要复杂的跨平台支持
推荐方案:MCP-UI 为主
// MCP Server 返回 UI
server.tool("show_code_diff", () => ({
type: "ui_resource",
resource: {
uri: "ui://diff/viewer",
mimeType: "text/html",
text: generateDiffHTML(changes)
}
}));
场景 D:复杂多 Agent 系统
需求特征:
- 多个 Agent 协作
- 既需要实时状态同步,又需要丰富 UI
- 需要调用外部 MCP 工具
推荐方案:AG-UI + A2UI + MCP-UI 组合
const agent = new OrchestrationAgent()
.use(new A2AMiddleware({ agentUrls: [...] })) // 连接子 Agent
.use(new MCPAppsMiddleware({ mcpServers: [...] })) // 连接 MCP 工具
.use(new A2UIMiddleware({ catalog: 'standard' })); // 支持 A2UI 渲染
6.3 决策流程图
7. Demo 项目实战解析
7.1 项目结构
demo-agent-ui-protocols 项目通过一个统一的"餐厅搜索"场景,同时演示三种协议:
demo-agent-ui-protocols/
├── apps/web/ # Next.js 前端
│ └── src/app/
│ ├── ag-ui-demo/ # AG-UI 演示页面
│ ├── a2ui-demo/ # A2UI 演示页面
│ └── mcp-ui-demo/ # MCP-UI 演示页面
│
├── agents/
│ ├── ag-ui-agent/ # AG-UI Python Agent (port 8001)
│ ├── a2ui-agent/ # A2UI Python Agent (port 8002)
│ └── mcp-ui-agent/ # MCP-UI Python Agent (port 8003)
│
└── packages/shared/ # 共享类型定义
7.2 同一场景的三种实现对比
7.2.1 用户输入
"帮我找一家北京的川菜馆"
7.2.2 AG-UI 实现
[SSE Stream]
event: RUN_STARTED
data: {"runId":"run_001","threadId":"thread_001"}
event: TEXT_MESSAGE_START
data: {"messageId":"msg_001","role":"assistant"}
event: TEXT_MESSAGE_CONTENT
data: {"messageId":"msg_001","delta":"好的,我来帮您"}
event: TEXT_MESSAGE_CONTENT
data: {"messageId":"msg_001","delta":"搜索北京的川菜馆..."}
event: TOOL_CALL_START
data: {"toolCallId":"call_001","toolCallName":"search_restaurants"}
event: TOOL_CALL_ARGS
data: {"toolCallId":"call_001","delta":"{\"cuisine\":\"川菜\",\"location\":\"北京\"}"}
event: TOOL_CALL_RESULT
data: {"toolCallId":"call_001","result":"[{\"name\":\"川办餐厅\",...}]"}
event: TEXT_MESSAGE_CONTENT
data: {"messageId":"msg_001","delta":"为您找到以下餐厅:"}
event: RUN_FINISHED
data: {"runId":"run_001"}
前端效果:实时展示打字效果 + 工具调用卡片
7.2.3 A2UI 实现
[JSONL Stream]
{"createSurface":{"surfaceId":"results","catalogId":"standard"}}
{"updateComponents":{"surfaceId":"results","components":[
{"id":"root","component":{"Column":{"children":{"explicitList":["header","list"]}}}},
{"id":"header","component":{"Text":{"text":{"literalString":"推荐餐厅"},"usageHint":"h1"}}},
{"id":"list","component":{"List":{"children":{"template":{"dataBinding":"/restaurants","componentId":"card-template"}}}}}
]}}
{"updateDataModel":{"surfaceId":"results","path":"/restaurants","value":[
{"name":"川办餐厅","rating":4.8,"price":"$$","image":"..."},
{"name":"眉州东坡","rating":4.5,"price":"$$$","image":"..."}
]}}
前端效果:原生组件渲染的餐厅卡片列表
7.2.4 MCP-UI 实现
// Request
{ "messages": [{ "role": "user", "content": "帮我找一家北京的川菜馆" }] }
// Response
{
"role": "assistant",
"content": "好的,这是搜索结果:",
"ui_resource": {
"type": "resource",
"resource": {
"uri": "ui://restaurant/list",
"mimeType": "text/html",
"text": "<div class='restaurant-list'>..."
}
}
}
前端效果:iframe 内嵌的 HTML 卡片
7.3 运行 Demo
# 1. 克隆项目
git clone https://github.com/MadLongTom/demo-agent-ui-protocols
cd demo-agent-ui-protocols
# 2. 配置环境变量
cp .env.example .env
# 编辑 .env 填入 OPENAI_API_KEY 等
# 3. 安装依赖
./install.sh
# 4. 启动所有服务
./run.sh
# 5. 访问 Demo
# AG-UI: http://localhost:3000/ag-ui-demo
# A2UI: http://localhost:3000/a2ui-demo
# MCP-UI: http://localhost:3000/mcp-ui-demo



8. 总结与展望
8.1 核心观点回顾
-
AG-UI 是"事件总线":它不关心 UI 长什么样,只负责把 Agent 的状态变化广播出去。适合需要深度集成和实时反馈的场景。
-
A2UI 是"UI 契约":它定义了一套抽象的组件语言,让 Agent 能够"描述 UI 意图"而不是"生成 UI 代码"。适合跨平台和高安全性场景。
-
MCP-UI 是"UI 插件":它让 MCP 工具能够直接返回可视化结果,无需修改宿主应用。适合快速扩展和插件生态场景。
-
组合使用是最佳实践:AG-UI 可以作为 A2UI 的传输层,MCP-UI 可以通过中间件集成到 AG-UI。
8.2 未来展望
| 趋势 | 预期发展 |
|---|---|
| 协议融合 | AG-UI 和 A2UI 可能会进一步整合,形成统一的 Agent-UI 标准 |
| 语音交互 | 多模态支持(语音输入、语音输出)将成为标配 |
| 边缘计算 | 轻量级协议支持设备端 Agent(手机、IoT) |
| 安全增强 | 更完善的沙箱隔离、权限控制机制 |
8.3 参考资源
| 协议 | 官方仓库 | 文档 |
|---|---|---|
| AG-UI | github.com/ag-ui-protocol/ag-ui | docs.ag-ui.com |
| A2UI | github.com/google/A2UI | a2ui.org |
| MCP-UI | github.com/idosal/mcp-ui | [mcpui.dev] |
本文基于 demo-agent-ui-protocols 项目源码分析,建议读者下载运行体验。如有问题或建议,欢迎交流讨论。
本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19452209
浙公网安备 33010602011771号