它们都在解决的那个难题
大语言模型的本质输出是 token 流——一段纯文本。但真实产品需要的是按钮、表单、图表、可点击的卡片,还要能把用户的点击回传给模型、把模型的进度实时反映到屏幕上。
纯文本和富交互界面之间,横着三道鸿沟。任何一个"Agent UI 协议",本质上都是在补这三道沟里的某一道(或几道):
- 结构鸿沟:文本没有"这是一个按钮、那是一个输入框"的结构信息。需要一种方式让模型描述界面。
- 交互鸿沟:界面被点击后,事件必须安全地回传给模型/服务端,再触发下一步动作。
- 状态鸿沟:模型在流式生成、在调工具、状态在变化——前端要实时跟上,而不是等全部算完才刷新一次。
先把三者放进同一张地图
从一次请求到屏幕上出现界面,数据要穿过两个层。理解每个协议"管哪一层",比记住它们的名字更重要:
一句话AG-UI 是"管道",MCP Apps 和 A2UI 是"管道里流的两种界面包裹"。同一个应用完全可以用 AG-UI 当管道,里面包一段 A2UI 或 MCP-UI 的界面。
MCP Apps:发一段"网页",沙箱里跑
核心思路:把界面当成 MCP 的一种资源。工具不再只返回文本,而是返回一段预先声明好的 HTML,宿主在双层 iframe 沙箱里渲染它,界面再通过 JSON-RPC over postMessage 反过来调用工具。
实现原理:六步生命周期
这是它最值得理解的部分。注意"声明"和"数据"是分开的——这是它做缓存和安全审查的根基。
- 预声明 UI 资源:服务端用
ui://协议把 HTML 模板注册成静态资源,工具定义里用_meta.ui.resourceUri指向它。结构(模板)和动态数据(工具结果)就此解耦。 - 宿主预取与审查:因为模板是静态的,宿主可以在工具真正执行之前就拉取、缓存、并对 HTML 做安全审查。
- 沙箱渲染:工具执行后,宿主把 HTML 放进一个受限权限的 iframe 渲染。采用双层 iframe:外层(sandbox proxy)把内层(guest UI)和宿主页面隔离开。
- 初始化握手:内层 UI 用标准
@modelcontextprotocol/sdk与宿主建立双向通道(ui/initialize)。 - 回调工具:UI 里点了按钮要干活时,通过
postMessage发一条 JSON-RPC 2.0 消息给宿主请求调用工具——所有交互都结构化、可审计、可记录。 - 用户授权:宿主可以要求对"UI 发起的工具调用"做显式批准,敏感操作执行前留有人类监督这道闸。
关键点:guest UI 永远碰不到宿主页面,消息只能以 JSON-RPC 形式经外层代理转发——所以可被记录、可被审查、可被授权拦截。
代码长什么样
// 1) 把 HTML 注册成 ui:// 资源(静态、可预取审查) server.registerResource("ui://charts/weather", { mimeType: "text/html+skybridge", text: "<div id=app>…</div><script>…</script>" }); // 2) 工具用 _meta 指向那段 UI server.registerTool("get_weather", { inputSchema: { city: z.string() }, _meta: { "ui.resourceUri": "ui://charts/weather" } // ← 关键 }, async ({ city }) => ({ content: [{ type: "text", text: `${city} 24°C 晴` }] // 不支持 UI 的宿主仍看到纯文本(优雅降级) }));
它的取舍
- 优点:能塞任意前端代码(图表、动画、复杂逻辑),可复用现成 Web 组件;天然活在 MCP 生态里,工具调用基础设施直接复用;不支持的宿主优雅降级为文本。
- 代价:本质是 Web,靠 iframe 沙箱兜底安全;跨原生平台(iOS/Android 原生控件)需要 WebView 承载,不是真原生。
A2UI:发一份 JSON"蓝图",客户端原生渲染
核心思路与 MCP Apps 正相反:绝不传可执行代码。Agent 只发一份纯 JSON(MIME 类型 application/json+a2ui),描述"界面由哪些组件构成、数据是什么、怎么绑定"。客户端拿自己本地的组件库(Widget Registry)把它渲染成原生控件。
实现原理:三块拼图
① 组件树是一张"扁平邻接表",不是嵌套结构
这是 A2UI 一个反直觉但精妙的设计。组件不是嵌套的 JSON,而是一张扁平列表 + ID 引用来表达父子关系。为什么?
- LLM 生成扁平列表比生成深层嵌套更可靠——少出括号不匹配的错。
- 支持渐进式渲染:组件可以乱序、分批到达,先到先渲染。
② 数据与界面分离,用 JSON Pointer 绑定
组件不写死内容,而是绑定到数据模型里的路径,比如 /user/profile/name。数据模型一变,绑定的组件自动刷新——不用重发整棵组件树。这就是它高效的来源。
③ 输入组件是双向绑定
展示型组件是单向的;而输入框这类组件与数据模型建立双向绑定——用户一输入,客户端数据模型立刻更新,无需往返服务端。
点赞计数器
点"+":只有数据模型 /count 变了,组件树(结构)一行没动 → 这就是"数据驱动、无需重发 UI"。在右边输入框打字:看左边 /name 实时回写 → 这就是双向绑定。
代码长什么样
{
"components": [ // 扁平邻接表,靠 id 连父子
{ "id":"root", "type":"Column", "children":["title","count"] },
{ "id":"title", "type":"Text", "text":"点赞计数器" },
{ "id":"count", "type":"Text", "text":{ "$bind":"/count" } } // ← JSON Pointer 绑定
],
"dataModel": { "count": 0, "name": "" } // 数据与界面分离
}
它的取舍
- 优点:界面即数据,不执行任何 agent 生成的代码,安全性天生高;一份蓝图可同时渲染成 Web / iOS / Android / 桌面的真·原生控件;和你已有的设计系统无缝对齐。
- 代价:受限于客户端预定义的组件集合,无法任意自定义渲染;需要客户端预先实现 Widget Registry。
AG-UI:把 Agent 的一举一动变成事件流
前两个解决"渲染什么",AG-UI 解决另一个维度:前端怎么和 Agent 后端实时通信。它把一次 Agent 运行拆成一连串 JSON 事件,通过 SSE / WebSocket / HTTP 流式推给前端。前端对每个事件做出反应,UI 就和 Agent 的进度逐帧同步。
实现原理:~17 种事件 + 快照-增量模式
整个协议就是一套带 type 字段的事件枚举。前端是一台状态机,见到不同 type 就做不同的事:
- 生命周期:
RunStarted/RunFinished/RunError/StepStarted/StepFinished— 圈定一次运行的起止。 - 文本消息:
TextMessageStart→ 多个TextMessageContent(每个带一小段delta,前端追加到已有文本)→TextMessageEnd。这就是"打字机式"流式输出的底层。 - 工具调用:
ToolCallStart→ToolCallArgs(参数也能流式)→ToolCallEnd→ToolCallResult。前端据此显示"正在调用…→ 完成"的卡片。 - 状态同步:
StateSnapshot发完整状态快照;之后只发StateDelta——用 RFC 6902 JSON Patch 描述增量改动。不必反复传整个状态对象,这是它高效的关键。 - 兜底:
RawEvent/CustomEvent透传外部或自定义事件。
{ }
注意左边每来一个事件,右边对应区域就立刻变——文本逐字追加来自 TextMessageContent,工具卡来自 ToolCall*,状态框那次高亮来自一条 StateDelta(JSON Patch)。前端从不"等全部算完"。
代码长什么样
data: {"type":"RUN_STARTED", "runId":"r_01"}
data: {"type":"TEXT_MESSAGE_START", "messageId":"m1", "role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT", "messageId":"m1", "delta":"正在"} // 前端 append
data: {"type":"TEXT_MESSAGE_CONTENT", "messageId":"m1", "delta":"查询…"}
data: {"type":"TOOL_CALL_START", "toolName":"search"}
data: {"type":"TOOL_CALL_RESULT", "result":"找到 3 条"}
data: {"type":"STATE_DELTA", "patch":[{"op":"add","path":"/results","value":3}]} // RFC 6902
data: {"type":"RUN_FINISHED", "runId":"r_01"}
它的取舍
- 优点:传输无关(SSE/WS/HTTP 都行);流式 + 增量同步,体验实时;标准化了"前端 ↔ Agent"这层一直没人统一的接口。
- 代价:它不规定界面长什么样——渲染还得靠你自己(或叠加 A2UI / MCP-UI)。它解决的是"怎么传",不是"渲染什么"。
并排对比
| 维度 | MCP Apps / MCP-UI | A2UI | AG-UI |
|---|---|---|---|
| 主导方 | Anthropic + OpenAI + MCP-UI 社区 | Google(Apache-2.0) | CopilotKit |
| 解决的层 | 载荷层:渲染什么 | 载荷层:渲染什么 | 传输层:怎么实时通信 |
| 传输内容 | 预构建的 HTML/CSS/JS | 纯声明式 JSON 蓝图 | 带 type 的 JSON 事件流 |
| 渲染方式 | 双层 iframe 沙箱里执行 | 客户端组件库渲染成原生控件 | 不渲染——只送达,前端自行处理 |
| 核心机制 | ui:// 资源 + JSON-RPC over postMessage | 扁平邻接表 + JSON Pointer 数据绑定 | ~17 种事件 + 快照/JSON Patch 增量 |
| 安全模型 | iframe 沙箱隔离 + 用户授权门 | 不传可执行代码,天然无注入风险 | 取决于传输层本身 |
| 跨平台 | Web 为主(原生需 WebView) | 真·原生跨平台 | 传输无关,平台无关 |
| 最适合 | 复杂自定义界面、复用 Web 代码、MCP 生态内 | 原生多端、高安全、标准化 UI | 实时流式体验、状态同步的 Agent 应用 |
那我该用哪个?
它们不互斥。先按"你缺的是哪一层"来定:
原始资料
本页技术细节均来自以下权威来源,建议深入阅读规范原文: