A2UI 协议深度研究:让远程 Agent「安全地说 UI」

引子:远程 Agent 给你「画」了个界面

设想你在和一个 Agent 聊天,让它帮你订一桌餐。它没有跟你来回文字拉扯(几人?哪天?几点?),而是直接甩给你一个带日期选择器、时间段、确认按钮的表单——你点几下就搞定。

关键来了:这个表单,不是你本地 App 预先做好的页面,而是那个跑在别的公司服务器上的远程 Agent,「画」出来递给你的。

这立刻引出两个尖锐的问题:

  • 怎么敢渲染一段来自不可信远程 Agent 的 UI?万一它塞段恶意 JS 呢?
  • 你又怎么让这份 UI 在 Web、手机、桌面上都长得一样,还完美继承你 App 的样式?

传统答案是把 HTML/JS 塞进 iframe——又重、又割裂、又是安全噩梦。Google 给出了另一个答案:A2UI(Agent-to-User Interface),一套声明式 UI 协议。它的设计纲领一句话能说完:

传输一种 UI——像数据一样安全,又像代码一样有表现力。

这篇就讲透 A2UI:为什么需要它、它的三个反直觉设计、安全模型,以及 UI 协议「三巨头」里它和 MCP Apps、ChatKit 到底差在哪。最后落到一个实践——如何把它的思想借到一个自研 Agent 框架(以我的 Hermes 为例)。

一、第一性原理:跨信任边界的 UI

核心矛盾

理解 A2UI 要先抓住它要解决的那个矛盾。

  • 过去:Agent 和 UI 在同一个 App 进程里,Agent 想画什么直接操作 DOM 就行,因为完全可信。
  • 现在(多 Agent 时代):干活的 Agent 往往是远程的——跑在别人服务器、别的组织里。它碰不到你的 UI,只能发消息。

那远程 Agent 想给用户呈现富交互界面(表格、图表、表单),怎么办?传统答案是 iframe 嵌 HTML/JS。问题一堆:重、视觉割裂(永远不匹配宿主样式)、安全边界复杂。

破局:把「生成 UI」和「执行 UI」解耦

A2UI 的解法是解耦

  • Agent 只「描述」UI 意图——发一段声明式 JSON 数据。
  • 客户端用「自己的」原生组件渲染——完全掌控安全与样式。

因为 Agent 发出的是「不可执行的数据」,所以安全;又因为描述足够丰富,所以有表现力。这就是「safe like data, expressive like code」的来历。

本质上,这是 Server-Driven UI(SDUI)的复兴。Instagram 原生 App 约一半屏幕已是 SDUI。A2UI 把它从「自家服务端→自家客户端」(信任的)升级为「别人家的 Agent→我家客户端」(不信任的)这个更难的场景。

二、三层架构:把 UI 拆成三个正交层

A2UI 把 UI 严格拆成三个正交层,这是理解它一切机制的基座:

关心的问题 控制权
① UI 结构层 页面由哪些组件、怎么组合 Agent
② 应用状态层 每个数据字段的当前值(Data Model) 数据绑定(双向)
③ 渲染执行层 抽象组件如何映射到原生 widget 客户端

为什么要这么拆?因为这三件事的变化频率和归属方完全不同:结构是 Agent 临场生成(频繁变)、数据是用户交互产生(实时变)、渲染是客户端固有能力(基本不变)。

把「渲染」完全交给客户端,就实现了「一次生成、到处渲染」的跨平台——同一份 JSON 能在 Web(Lit/Angular/React)、移动(Flutter/SwiftUI/Compose)、桌面渲染。

flowchart TB
    L1["① UI 结构层 Structure<br/>组件树 → Agent 生成"] -->|通过 path 绑定| L2["② 应用状态层 State<br/>Data Model → 双向绑定"]
    L2 -->|驱动更新| L3["③ 渲染执行层 Rendering<br/>原生 widget → 客户端掌控"]
    L1 -->|映射| L3

三、为 LLM 量身的设计:扁平列表 + ID 引用

这是 A2UI 最反直觉、也最精妙的设计。

组织组件有两种方式:传统的嵌套树(像 HTML),和 A2UI 的扁平列表 + ID 引用。看个真实例子(v0.9.1):

1
2
3
4
5
{"version":"v0.9.1","updateComponents":{"surfaceId":"booking","components":[
{"id":"header","component":"Text","text":"# 订餐","variant":"h1"},
{"id":"date-picker","component":"DateTimeInput","value":{"path":"/booking/date"},"enableDate":true},
{"id":"submit-btn","component":"Button","child":"submit-text","action":{"event":{"name":"confirm_booking"}}}
]}}

注意 submit-btnchild: "submit-text"——它不物理嵌套一个文本节点,而是引用另一个组件的 id。整个 UI 是一个扁平的组件列表,靠 id 互相引用组装。

为什么不用更「自然」的嵌套树?因为要顺着 LLM 的脾性,而不是跟它较劲:

维度 嵌套树 扁平列表 + ID
闭合性 必须精确匹配括号,漏一个全错 每条独立,无嵌套闭合压力
流式生成 难(要等完整树) 易(逐条吐)
纠错 牵一发动全身 补一条覆盖某 id

LLM 是 token-by-token 自回归生成,深度嵌套 JSON 要它「一次写对全部括号」违反生成特性——越深越容易错。扁平列表把「结构关系」从语法嵌套变成语义引用,让 LLM 可以边想边吐、错了就补

这是「LLM-friendly」从口号落到协议细节的典范。好的协议设计要顺着生成模型的脾性,而不是逼模型适应协议。

配合数据绑定(组件用 path 绑定 Data Model,而非直接持有值),组件成了无状态纯渲染器——状态全在 Data Model,事件全回 Agent。这一进一出(value.path 读数据、action.event 发事件)构成组件的 I/O 契约,是跨平台渲染能成立的前提。

四、安全模型:Catalog 白名单与「消灭执行面」

A2UI 的安全核心是一句话:Agent 只能从客户端的「菜单」里点菜,不能冲进厨房。

这个「菜单」叫 Catalog——客户端预定义的可信组件白名单。Agent 说「我要一个 Button」,但 Button 的实际实现是客户端自己的。Agent 无法注入代码、无法访问 DOM、无法做任何超出 Catalog 的事。

flowchart TB
    AG["Agent 想渲染某组件"] --> CHECK{"在 Catalog 白名单?"}
    CHECK -->|是 预批准| OK["允许, 用客户端原生实现"]
    CHECK -->|否 未批准| BLOCK["拒绝, 防 UI 注入"]

那如果客户端想要更复杂的能力(嵌入一个图表、甚至受控的 iframe)怎么办?用 Smart Wrapper(开放注册表):开发者把任意已有组件注册进 A2UI,把安全策略(沙箱、「信任阶梯」)写在自己的组件逻辑里,而非依赖协议核心兜底。

进阶:双重 iframe——嵌入不可信 HTML 的极致隔离

A2UI 甚至能安全嵌入一段不可信的第三方 HTML(比如一个 MCP App)。它用了一个反直觉的「双重 iframe」:

  • 外层 Sandbox Proxy(同源、不沙箱、验证来源)
  • 内层(allow-scripts 但严禁 allow-same-origin

为什么要双层?因为单 iframe 若同时 allow-scripts + allow-same-origin,可编程操作父 DOM 甚至移除自身 sandbox 逃逸。A2UI 内层严禁 allow-same-origin,使第三方 HTML 因独特 origin 失去 localStorage/cookie/IndexedDB,且无法触碰父 DOM。所有跨信任边界的数据流,都必须经 Agent 这道「中介」——零信任在 UI 层的极致体现。

为什么不直接用 HTML/iframe?

这是社区最大的质疑。A2UI 的回答很硬:浏览器 HTML 天然绑定 CSS+JS,本质是「远程代码执行引擎」。企业多 Agent 场景下(让第三方 SaaS 的 Agent 跟自家 Agent 并存),接受任意 HTML 就是接受代码注入。把 UI 抽象成声明式 JSON,从根上消灭了「执行」这个攻击面。

五、UI 协议三巨头:信任放哪,决定了你是谁

A2UI 不是唯一解。当下 Agent UI 协议有「三巨头」,它们的本质差异就一句话——信任放哪

维度 A2UI(Google) MCP Apps(MCP 官方) ChatKit(OpenAI)
信任放哪 客户端 catalog(无执行面) iframe 沙箱(容忍执行但隔离) OpenAI 平台(全栈托管)
渲染方 客户端原生组件 iframe OpenAI 托管 widget
自由度 中(catalog 约束) 高(任意 HTML/JS) 低(widget 类型约束)
样式一致性 完美继承宿主 iframe 隔离(割裂) OpenAI 主题 + 有限定制
跨平台 声明式天生跨平台 绑 Web/iframe 绑 OpenAI SDK
生态 开放(A2A/AG-UI) MCP 生态 OpenAI 闭环

选型 = 回答「你愿意把 UI 安全托付给谁」:

  • 企业多 Agent Mesh、要跨平台原生感 → A2UI
  • MCP 工具、要高自由度富交互 → MCP Apps
  • OpenAI 闭环、要快速上线 → ChatKit

三者分层共存,非一统天下。而且 A2UI 和 MCP Apps 互补——A2UI 的 custom 节点能嵌入 MCP App(双重 iframe),Google 官方称「两者结合最佳」。

一个判断:现在是「汽车工业早期 80 家厂商」阶段。真正能赢的标准,会是「无聊到产品团队能忍受 5-10 年」的那个,而非 demo 最酷的。A2UI 的「声明式 + 无执行面」最符合长期标准化特征,前提是 v1.0 稳定 + 渲染器生态补齐。

六、与 A2A:传输底座

A2UI 反复强调「传输可插拔(A2A / AG-UI / SSE / WebSocket)」,它不绑定传输。其中 A2A(Agent2Agent) 是最重量级的传输底座——A2UI 的 JSONL 消息,正是作为 A2A 的 data 类型 Part,在 Agent 间流式传输的。

A2A 提供的「有状态、可流式、可中断恢复」传输,恰好是 agent-driven UI 在多轮对话里的刚需。A2A 的 Task 生命周期承载 A2UI 的 Surface 生命周期,SSE 承载渐进渲染。

一句话:A2UI 定义「UI 长什么样」,A2A 定义「UI 怎么传过来」——一个管表现层,一个管通信层。 A2A 的深度解读见本系列另一篇。

七、落地:从 A2UI 借鉴,给 Hermes 设计结构化输出

理论讲完,落到一个实际问题:如果不直接采用 A2UI 协议(标准尚早期),怎么把它的思想用一个自研 Agent 框架? 以我的 Hermes Agent 为例。

Hermes 当前的输出主要是自然语言散文(给人看的),下游消费方是 CLI 和 Telegram。直接上 A2UI/MCP Apps 的 iframe/widget 模型过重。但 A2UI 的三个核心思想可以轻量迁移:

A2UI 思想 Hermes 落地
① 声明式契约 schema 描述意图,非执行命令
② 状态/渲染分离 schema 只描述数据,CLI/Web/Telegram 各自渲染
③ 扁平 + 版本化 + 增量 扁平 JSON + schema_version + Task 状态流

一个意外收获:Hermes 自己的记忆里已经记录了「结构化生成成为标准实践」(Outlines/Guidance/Instructor,Pydantic→JSON Schema→FSM)。所以这套 schema 不是引入新依赖,而是把 Hermes 已学的技术用到它自己的输出上——学以致用的闭环。

三个 schema(对齐 Hermes 真实接口)

我把 Hermes 的 cron/jobs.json、messages_send、任务状态这些真实接口,抽象成三个 Pydantic schema。每个都带 schema_version(对应 A2UI 的 version 字段),字段扁平(对应 A2UI 的扁平列表)。

HermesCronJob(对齐 jobs.json)——让 LLM 直接产出可被 hermes cron create 消费的结构化意图,而非自然语言:

1
2
3
4
5
6
7
8
9
class HermesCronJob(BaseModel):
schema_version: str = "1.0"
kind: str = "cron_job"
name: str
schedule: CronSchedule # kind: cron/interval, expr
prompt: str
deliver: str = "telegram:xxx"
repeat_times: int | None = None # None = 永久
model: str | None = None

HermesNotify(对齐 messages_send + 4 个通知模板)——把散文模板升级为结构化消息,多端按 category 差异化渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NotifyCategory(str, Enum):
TASK_COMPLETE = "task_complete"
ERROR = "error"
SUMMARY = "summary"
CODE_REVIEW = "code_review"
CUSTOM = "custom" # 预留扩展位

class HermesNotify(BaseModel):
schema_version: str = "1.0"
target: str
category: NotifyCategory
custom_kind: str | None = None # category=CUSTOM 时必填
priority: NotifyPriority = NotifyPriority.NORMAL
title: str
body: NotifyBody

CUSTOM 预留位 + custom_kind 校验,是为 Hermes 自进化产生的新通知类型(技能进化报告、Dojo 结果)留口子——避免频繁改枚举,又用校验防滥用。

HermesTask(任务执行单元 + 状态流)——对应 A2UI 的增量更新(updateDataModel):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TaskState(str, Enum):
PENDING = "pending"; RUNNING = "running"
COMPLETED = "completed"; FAILED = "failed"; PAUSED = "paused"

class HermesTask(BaseModel):
schema_version: str = "1.0"
task_id: str
title: str
state: TaskState = TaskState.PENDING
progress: TaskProgress
result: Any | None = None
error: str | None = None

def transition_to(self, new_state: TaskState) -> "HermesTask":
# 合法状态转换校验, 防状态损坏
...

最有意思的是 HermesTask 和 A2A 的 Task 模型天然同构——这意味着 Hermes 未来若要接入多 Agent Mesh,HermesTask 几乎可直接映射为 A2A Task,接入阻抗极低。这是一个「先按 A2UI/A2A 思想设计自己的 schema,未来自然对接标准」的低风险路径。

落地路径

  1. Phase 1:Pydantic 定义三个 schema(复用 Hermes 已学的 Outlines/Instructor)
  2. Phase 2:接入 hermes cron create / messages_send
  3. Phase 3:Task 状态流 + WebUI 卡片渲染

八、判断与启示

把 A2UI 放回更大的图景,四点判断:

1. A2UI 是「跨信任边界 agent-driven UI」当前最正经的开放答案。 它用声明式 + Catalog 白名单,从根上消灭执行面,做到「安全如数据、表达如代码」。在企业多 Agent Mesh 场景,这套设计哲学是对的。

2. 三个反直觉设计是它的精髓。 三层解耦、扁平列表+ID 引用(为 LLM)、Catalog 守门——每一个都是对「UI 该怎么生成」的重新思考,而非堆功能。

3. 三巨头分层共存,A2UI 与 MCP Apps 互补。 别纠结「选谁」,按场景:Mesh 跨平台选 A2UI,MCP 富交互选 MCP Apps,OpenAI 闭环选 ChatKit。

4. 对自研框架,思想比协议更值得借。 标准尚早期(v1.0 候选、渲染器未齐),现在 all-in 风险高。但它的「声明式输出 + 状态/渲染分离 + 扁平版本化」思想,可以立刻用 Pydantic 落到自己的 schema 上——既改善当下输出,又为未来对接标准留好低阻抗通道。


Agent 时代正在重走互联网协议分层的路:UI 表现层(A2UI)、Agent 通信层(A2A)、工具接入层(MCP),各有一套开放标准,各司其职。A2UI 补上的,是「Agent 怎么把界面安全地递给用户」这一层。理解了它,你就能判断:当远程 Agent 要给你「画」个界面时,什么样的设计才既敢用、又好看。

信息来源

来源 URL
A2UI 官网 https://a2ui.org/
What is A2UI https://a2ui.org/introduction/what-is-a2ui/
Google 官方公告(2025-12-15) https://developers.googleblog.com/introducing-a2ui-an-open-project-for-agent-driven-interfaces/
GitHub a2ui-project/a2ui https://github.com/a2ui-project/a2ui
Hacker News 讨论 https://news.ycombinator.com/item?id=46286407
MCP Apps in A2UI(双重 iframe) https://a2ui.org/guides/mcp-apps-in-a2ui/
下一篇:A2A 协议深度解读 (本博客 Agent UI 系列)
文章作者: MichaelMao
文章链接: http://michaelmaomao.github.io/2026/06/29/A2UI%E5%8D%8F%E8%AE%AE%E6%B7%B1%E5%BA%A6%E7%A0%94%E7%A9%B6-%E8%AE%A9%E8%BF%9C%E7%A8%8BAgent%E5%AE%89%E5%85%A8%E5%9C%B0%E8%AF%B4UI/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下