三大 Agent-UI 协议深度剖析:AG-UI、A2UI 与 MCP-UI 的设计哲学与工程实践

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

占位符:文章封面图 - 三大协议的技术架构全景图


目录

  1. 引言:为什么需要 Agent-UI 协议?
  2. AG-UI:事件驱动的智能体交互协议
  3. A2UI:声明式 UI 的零信任渲染引擎
  4. MCP-UI:MCP 协议的可视化扩展层
  5. 协议组合:AG-UI + A2UI 的协同架构
  6. 技术选型决策框架
  7. Demo 项目实战解析
  8. 总结与展望

1. 引言:为什么需要 Agent-UI 协议?

1.1 传统 Chatbot 的局限性

传统的 AI 聊天机器人采用简单的 Request-Response 模式:用户输入文本,模型返回文本。这种模式在面对复杂业务场景时暴露出严重不足:

用户 → "帮我订一家北京的川菜馆"
传统 Bot → "好的,我找到了以下餐厅:1. 川办餐厅... 2. 眉州东坡..."

问题

  • ❌ 无法展示餐厅图片、评分、价格等结构化信息
  • ❌ 用户需要手动复制餐厅名称再去搜索
  • ❌ 无法直接在对话中完成预订操作

1.2 Agent 时代的新需求

当 AI 从 Chatbot 升级为 Agent 后,交互模式发生了本质变化:

维度 Chatbot Agent
运行时间 短(毫秒级) 长(秒/分钟级)
输出类型 纯文本 文本 + 结构化数据 + UI 控制
状态管理 无状态 复杂状态机
交互模式 单轮 Q&A 多轮工具调用 + 人机协作
flowchart TB subgraph Chatbot["🤖 传统 Chatbot"] direction TB U1["👤 用户"] -->|"文本输入"| B1["💬 Bot"] B1 -->|"文本输出"| U1 end subgraph Agent["🦾 智能体 Agent"] direction TB U2["👤 用户"] -->|"多模态输入"| A["🧠 Agent"] A -->|"状态事件"| SM["📊 状态机"] SM -->|"UI 更新"| UI["🖥️ 富交互 UI"] A <-->|"工具调用"| T["🔧 Tools"] UI -->|"用户操作"| A end Chatbot -.->|"演进"| Agent

1.3 三种协议的定位

业界给出了三种截然不同的解决方案:

协议 来源 核心定位 一句话概括
AG-UI CopilotKit 事件驱动的状态同步协议 "让前端实时感知 Agent 的每一步思考"
A2UI Google 声明式 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"。至于如何渲染这些信息,完全由前端决定。

这种设计带来了几个核心优势:

  1. 前端自由度最大化:同一个 Agent 可以对接 Web、Mobile、CLI 等不同客户端
  2. 实时性极强:基于流式事件,用户能看到 Agent 思考的每一步
  3. 与现有应用深度集成:Agent 可以驱动现有 UI 的状态变化
flowchart LR subgraph Backend["🔙 Agent Backend"] LLM["🧠 LLM"] --> EP["Event Producer"] Tools["🔧 Tools"] --> EP end subgraph Transport["📡 传输层"] EP -->|"SSE Stream"| SSE["text/event-stream"] end subgraph Frontend["🖥️ Frontend"] SSE --> Parser["Event Parser"] Parser --> SM["State Machine"] SM --> |"TEXT_MESSAGE"| Chat["💬 聊天区"] SM --> |"TOOL_CALL"| TC["🔧 工具卡片"] SM --> |"STATE_DELTA"| App["📊 应用状态"] end

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;
      }
    }
  }
};
stateDiagram-v2 [*] --> Idle: 初始化 Idle --> Running: RUN_STARTED state Running { [*] --> Streaming Streaming --> ToolCalling: TOOL_CALL_START ToolCalling --> Streaming: TOOL_CALL_RESULT Streaming --> Streaming: TEXT_MESSAGE_CONTENT } Running --> Idle: RUN_FINISHED Running --> Error: RUN_ERROR Error --> Idle: 重试

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
flowchart TB subgraph Agent["🧠 Agent"] LLM["LLM"] --> Gen["A2UI Generator"] end subgraph Protocol["📦 A2UI JSON"] Gen --> JSON["{“updateComponents”: ...}"] end subgraph Renderers["🌐 各平台渲染器"] JSON --> Web["🌐 Web\nLit/React"] JSON --> iOS["🍎 iOS\nSwiftUI"] JSON --> Android["🤖 Android\nCompose"] JSON --> Flutter["🐦 Flutter\nWidget"] end subgraph Output["📱 原生 UI"] Web --> O1["🖥️ DOM"] iOS --> O2["📱 UIKit View"] Android --> O3["📱 Compose UI"] Flutter --> O4["📱 Widget Tree"] end

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,
};

渲染流程:

  1. 解析消息:将 JSONL 解析为消息对象
  2. 构建组件树:根据邻接表重建树结构
  3. 数据绑定:将 dataModel 注入组件
  4. 原生渲染:调用平台原生组件库
flowchart LR subgraph Input["📥 输入"] JSONL["JSONL Stream"] end subgraph Process["⚙️ 处理流程"] JSONL --> Parse["1️⃣ 解析消息"] Parse --> Build["2️⃣ 构建组件树"] Build --> Bind["3️⃣ 数据绑定"] Bind --> Render["4️⃣ 原生渲染"] end subgraph Output["📱 输出"] Render --> UI["原生 UI 组件"] end subgraph DataFlow["📊 数据流"] DM[("DataModel")] -.->|"/path/to/data"| Bind end

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);
  `
}
flowchart TB subgraph Server["🔧 MCP Server"] Tool["Tool Result"] --> UIRes["UIResource"] end UIRes --> Type{"mimeType?"} subgraph Mode1["📄 Raw HTML"] Type -->|"text/html"| HTML["iframe srcDoc"] HTML --> Sandbox1["🔒 沙箱渲染"] end subgraph Mode2["🌐 External URL"] Type -->|"text/uri-list"| URL["iframe src"] URL --> Sandbox2["🔒 外部页面"] end subgraph Mode3["🖥️ Remote DOM"] Type -->|"remote-dom"| Script["JS Script"] Script --> Worker["🔒 沙箱执行"] Worker -->|"JSON Patch"| Host["🏠 宿主渲染"] end Sandbox1 & Sandbox2 & Host --> Actions["📤 postMessage"] Actions --> Client["📱 客户端处理"]

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."

A2UI Transports 文档

5.2 AG-UI 作为 A2UI 的传输层

在这种架构下:

  • A2UI 负责:UI 结构定义、组件规范、数据模型
  • AG-UI 负责:消息传输、状态同步、事件路由
flowchart TB subgraph Frontend["🖥️ Frontend"] Client["AG-UI Client\n(Events)"] --> A2UIMsg["A2UI Messages\n(JSON)"] A2UIMsg --> Renderer["A2UI Renderer\n(Components)"] end subgraph Transport["📡 Transport"] Client <-.->|"SSE Events"| Server end subgraph Agent["🤖 Agent"] Server["AG-UI Server\n(SSE Stream)"] --> Generator["A2UI Generator\n(JSONL)"] Generator --> LLM["LLM / Tools"] end style Frontend fill:#e3f2fd style Transport fill:#fff3e0 style Agent fill:#f3e5f5

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 三协议融合架构

在复杂场景下,三种协议可以同时使用:

flowchart TB subgraph Frontend["🖥️ Frontend"] subgraph Router["AG-UI Event Router"] TextR["📝 Text/Tool\nRenderer"] A2UIR["📱 A2UI Renderer\n(a2ui type)"] MCPR["🔧 MCP-UI Renderer\n(mcp-apps type)"] end end Router <-.->|"SSE Events"| Server subgraph Agent["🤖 Agent"] subgraph ServerLayer["AG-UI Server + Middlewares"] LLMAdapter["LLM Adapter"] A2AMW["A2A Middleware"] MCPMW["MCP-Apps Middleware"] end LLMAdapter --> LLM[("🧠 LLM\n(OpenAI)")] A2AMW --> SubAgents[("🤝 A2A Agents\n(Sub-agents)")] MCPMW --> MCPServers[("🔌 MCP Servers\n(Tools+UI)")] end style Frontend fill:#e3f2fd style Agent fill:#f3e5f5 style Router fill:#e8f5e9 style ServerLayer fill:#fff3e0

各协议职责

  • 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 决策流程图

flowchart TB Start(["🚀 开始选型"]) --> Q1{"是否需要<br/>跨平台原生渲染?"} Q1 -->|"✅ Yes"| A2UI["📱 A2UI 为主<br/><i>统一定义,多端原生</i>"] Q1 -->|"❌ No"| Q2{"是否需要<br/>实时状态同步?"} Q2 -->|"✅ Yes"| AGUI["⚡ AG-UI 为主<br/><i>事件驱动,状态透明</i>"] Q2 -->|"❌ No"| Q3{"是否复用<br/>MCP 生态?"} Q3 -->|"✅ Yes"| MCPUI["🔧 MCP-UI 为主<br/><i>工具即 UI,渐进增强</i>"] Q3 -->|"❌ No"| Custom["🎨 自定义方案<br/><i>根据需求定制</i>"] subgraph Combinations["💡 组合方案"] AGUI --> Combo1["AG-UI + A2UI<br/><i>事件传输 + 声明式UI</i>"] AGUI --> Combo2["AG-UI + MCP-UI<br/><i>事件传输 + 工具UI</i>"] A2UI --> Combo1 end style Start fill:#e1f5fe style A2UI fill:#c8e6c9 style AGUI fill:#fff3e0 style MCPUI fill:#f3e5f5 style Custom fill:#ffecb3

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

AG-UI
A2UI
MCP-UI


8. 总结与展望

8.1 核心观点回顾

  1. AG-UI 是"事件总线":它不关心 UI 长什么样,只负责把 Agent 的状态变化广播出去。适合需要深度集成实时反馈的场景。

  2. A2UI 是"UI 契约":它定义了一套抽象的组件语言,让 Agent 能够"描述 UI 意图"而不是"生成 UI 代码"。适合跨平台高安全性场景。

  3. MCP-UI 是"UI 插件":它让 MCP 工具能够直接返回可视化结果,无需修改宿主应用。适合快速扩展插件生态场景。

  4. 组合使用是最佳实践: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 项目源码分析,建议读者下载运行体验。如有问题或建议,欢迎交流讨论。

https://github.com/MadLongTom/demo-agent-ui-protocols

posted @ 2026-01-07 14:36  MadLongTom  阅读(172)  评论(0)    收藏  举报