Agent UI 协议解剖
实现原理 · 知其然,知其所以然

当模型不再只吐文本
而是驱动界面

三个解决同一难题的协议——让 Agent 的输出从一串纯文本,变成可渲染、可交互、可同步状态的真实界面。它们各自是怎么做到的?

MCP Apps · Anthropic A2UI · Google AG-UI · CopilotKit
00

它们都在解决的那个难题

大语言模型的本质输出是 token 流——一段纯文本。但真实产品需要的是按钮、表单、图表、可点击的卡片,还要能把用户的点击回传给模型、把模型的进度实时反映到屏幕上。

纯文本和富交互界面之间,横着三道鸿沟。任何一个"Agent UI 协议",本质上都是在补这三道沟里的某一道(或几道):

关键分野 记住这一点,后面全部豁然开朗:MCP Apps 和 A2UI 解决的是"渲染什么"(payload 层),回答结构鸿沟;AG-UI 解决的是"前端怎么和 Agent 实时对话"(transport 层),回答交互与状态鸿沟。它们不是竞品,常常叠在一起用。
01

先把三者放进同一张地图

从一次请求到屏幕上出现界面,数据要穿过两个层。理解每个协议"管哪一层",比记住它们的名字更重要:

传输 / 同步层 — 前端 ⇄ Agent 后端怎么通信
AG-UI
一条事件流(SSE/WebSocket/HTTP),把"模型说话、调工具、状态变更"逐条推给前端。它不规定长什么样,只负责实时把消息送达
事件流
载荷层 — 到底渲染什么界面(Web 路线)
MCP Apps / MCP-UI
服务端把一段 HTML/JS 作为 UI 资源送来,宿主在沙箱里执行。灵活、能复用现成前端代码。
发"网页"
载荷层 — 到底渲染什么界面(原生路线)
A2UI
Agent 送来一份 纯 JSON 蓝图,客户端用本地组件库把它渲染成原生控件。安全、跨平台。
发"描述"

一句话AG-UI 是"管道",MCP Apps 和 A2UI 是"管道里流的两种界面包裹"。同一个应用完全可以用 AG-UI 当管道,里面包一段 A2UI 或 MCP-UI 的界面。

02
Anthropic · OpenAI · MCP-UI 社区 · SEP-1865

MCP Apps:发一段"网页",沙箱里跑

核心思路:把界面当成 MCP 的一种资源。工具不再只返回文本,而是返回一段预先声明好的 HTML,宿主在双层 iframe 沙箱里渲染它,界面再通过 JSON-RPC over postMessage 反过来调用工具。

实现原理:六步生命周期

这是它最值得理解的部分。注意"声明"和"数据"是分开的——这是它做缓存和安全审查的根基。

  1. 预声明 UI 资源:服务端用 ui:// 协议把 HTML 模板注册成静态资源,工具定义里用 _meta.ui.resourceUri 指向它。结构(模板)和动态数据(工具结果)就此解耦。
  2. 宿主预取与审查:因为模板是静态的,宿主可以在工具真正执行之前就拉取、缓存、并对 HTML 做安全审查。
  3. 沙箱渲染:工具执行后,宿主把 HTML 放进一个受限权限的 iframe 渲染。采用双层 iframe:外层(sandbox proxy)把内层(guest UI)和宿主页面隔离开。
  4. 初始化握手:内层 UI 用标准 @modelcontextprotocol/sdk 与宿主建立双向通道(ui/initialize)。
  5. 回调工具:UI 里点了按钮要干活时,通过 postMessage 发一条 JSON-RPC 2.0 消息给宿主请求调用工具——所有交互都结构化、可审计、可记录。
  6. 用户授权:宿主可以要求对"UI 发起的工具调用"做显式批准,敏感操作执行前留有人类监督这道闸。
交互演示 · 一次 UI 发起的工具调用如何穿过双层沙箱
·宿主:等待 UI 事件…
收到 JSON-RPC: tools/call
?这是敏感调用,请求用户授权
已批准,执行 get_weather
把结果回传给 guest UI
① 宿主页面 host
② 外层 sandbox proxy(隔离)
③ 内层 guest UI(你的 HTML)
天气小组件
点上面的按钮发起调用 →

关键点:guest UI 永远碰不到宿主页面,消息只能以 JSON-RPC 形式经外层代理转发——所以可被记录、可被审查、可被授权拦截。

代码长什么样

server.ts — 声明 UI 资源 + 工具绑定MCP Apps
// 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 的宿主仍看到纯文本(优雅降级)
}));

它的取舍

03
Google · 开源 Apache-2.0 · 规范 v0.9

A2UI:发一份 JSON"蓝图",客户端原生渲染

核心思路与 MCP Apps 正相反:绝不传可执行代码。Agent 只发一份纯 JSON(MIME 类型 application/json+a2ui),描述"界面由哪些组件构成、数据是什么、怎么绑定"。客户端拿自己本地的组件库(Widget Registry)把它渲染成原生控件。

实现原理:三块拼图

① 组件树是一张"扁平邻接表",不是嵌套结构

这是 A2UI 一个反直觉但精妙的设计。组件不是嵌套的 JSON,而是一张扁平列表 + ID 引用来表达父子关系。为什么?

② 数据与界面分离,用 JSON Pointer 绑定

组件不写死内容,而是绑定到数据模型里的路径,比如 /user/profile/name数据模型一变,绑定的组件自动刷新——不用重发整棵组件树。这就是它高效的来源。

③ 输入组件是双向绑定

展示型组件是单向的;而输入框这类组件与数据模型建立双向绑定——用户一输入,客户端数据模型立刻更新,无需往返服务端。

交互演示 · 左边是 Agent 发来的 JSON,右边是客户端原生渲染
协议消息(纯数据,零代码)
客户端用本地组件库渲染
点赞计数器
绑定到 /count — 改数据它就变
0
输入组件 · 双向绑定到 /name
数据模型 /name = ""

点"+":只有数据模型 /count 变了,组件树(结构)一行没动 → 这就是"数据驱动、无需重发 UI"。在右边输入框打字:看左边 /name 实时回写 → 这就是双向绑定。

代码长什么样

Agent 输出的 A2UI 消息(节选)application/json+a2ui
{
  "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": "" }   // 数据与界面分离
}

它的取舍

04
CopilotKit · 开源 · 事件协议

AG-UI:把 Agent 的一举一动变成事件流

前两个解决"渲染什么",AG-UI 解决另一个维度:前端怎么和 Agent 后端实时通信。它把一次 Agent 运行拆成一连串 JSON 事件,通过 SSE / WebSocket / HTTP 流式推给前端。前端对每个事件做出反应,UI 就和 Agent 的进度逐帧同步

实现原理:~17 种事件 + 快照-增量模式

整个协议就是一套带 type 字段的事件枚举。前端是一台状态机,见到不同 type 就做不同的事:

交互演示 · 一次 Agent 运行的事件流如何逐条点亮 UI
SSE 事件流(服务端逐条推送)
前端:每个事件就地反应
点"运行 Agent"开始…
tools/call
共享状态(snapshot + delta 同步)
{ }

注意左边每来一个事件,右边对应区域就立刻变——文本逐字追加来自 TextMessageContent,工具卡来自 ToolCall*,状态框那次高亮来自一条 StateDelta(JSON Patch)。前端从不"等全部算完"。

代码长什么样

服务端推送的 SSE 事件流(节选)text/event-stream
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"}

它的取舍

05

并排对比

维度 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 应用
06

那我该用哪个?

它们不互斥。先按"你缺的是哪一层"来定:

我的 Agent 已经在 MCP 生态里,想让某个工具顺手返回一块复杂交互界面
MCP Apps
UI 复杂、要图表/动画/自定义逻辑,客户端主要是桌面 / Web
MCP Apps
要覆盖原生移动端,且不想在客户端执行模型生成的任意代码
A2UI
界面形态相对标准(表单/卡片/列表),要对齐已有设计系统
A2UI
我要的是流式输出、工具进度、共享状态的实时同步通道
AG-UI
前端和 Agent 后端之间需要一个标准、传输无关的接口
AG-UI
组合用法最强的架构往往是叠加:用 AG-UI 当前端 ↔ Agent 的实时管道,在它的事件里一段 A2UI 的 JSON 蓝图或 MCP-UI 的界面资源来负责具体渲染。一个管"怎么传",一个管"渲染什么"——各司其职。
07

原始资料

本页技术细节均来自以下权威来源,建议深入阅读规范原文: