Google A2UI 入门:让 Agent "说 UI",用声明式 JSON 安全渲染到原生界面(配合 AG-UI 把交互跑起来)
生成式 AI 很会输出文本/代码,但一旦要把用户带着"点选、填表、确认、提交"走完一个流程,纯聊天就会变得又慢又别扭。Google 的 A2UI(Agent-to-User Interface)就是为了解决这件事:让 Agent 用一份安全的、声明式的 UI 描述把界面"说出来",客户端再用自己的原生组件库渲染出来。
1. 为什么要 A2UI:Agent 需要"可交互的 UI",而不是只会聊天
Google 在介绍 A2UI 时给的背景很直白:生成式 AI 擅长生成文本/图片/代码,但 Agent 在"跨信任边界、远程执行"的情况下,很难把结果呈现成丰富且可交互的界面;A2UI 的目标就是让 Agent 生成上下文相关的界面并发送给前端应用渲染。
你可以把它类比成:
- OpenAPI:后端用"接口规范"把能力说清楚
- A2UI:Agent 用"UI 规范"把界面说清楚
重点:A2UI 面向的是"落地到真实产品"的 UI,不是 Demo 里随便生成一段 HTML/JS。
2. A2UI 是什么:一个"让 Agent 说 UI"的开放标准 + 客户端渲染库
A2UI 在 GitHub README 里定义得很明确:
- A2UI 是一个开源项目,包含一种适合表示"可更新的、由 Agent 生成的 UI"的格式,以及一套初始的客户端渲染器(renderers)。
- Agent 发送声明式 JSON 来表达 UI 的"意图(intent)",客户端用自己的原生组件库渲染(Flutter/Angular/Lit 等)。
- 这个思路的关键收益:像数据一样安全、像代码一样表达力强。
一句话:A2UI = "UI Spec(规范/契约)" + "Renderer(安全渲染器)"。
3. 核心哲学:安全第一 + 适合 LLM 增量生成
3.1 安全第一:不运行 LLM 生成的任意代码
- 直接运行 LLM 生成代码会有安全风险
- A2UI 选择 声明式数据格式(declarative data format),而不是可执行代码
- 客户端维护一个可信的组件目录(catalog),Agent 只能请求渲染目录里的组件(例如 Card/Button/TextField 等)
这句话非常关键:控制权在客户端------品牌一致性、组件安全、权限边界,最终都由客户端兜住。
3.2 LLM 友好 + 支持增量更新(progressive rendering)
- UI 用"扁平组件列表 + ID 引用"的方式表示,便于 LLM 分段/增量生成
- Agent 可以在对话推进中对 UI 做增量修改,从而实现更丝滑的响应体验
这点在真实产品里很重要:你不希望等模型把整个页面 JSON 都吐完才开始渲染。
4. UI Spec 到底是什么(把概念说清楚)
你在工程里看到的 Spec,通常就是 Specification(规范/契约):
- API Spec:约定请求/响应长什么样
- UI Spec(A2UI 的 JSON):约定"界面结构/组件/交互/数据绑定"长什么样
Spec 的精髓不是"描述",而是"可校验(validate)":
- JSON Schema 校验结构合法
- 组件白名单校验安全合法
- 版本/降级策略保证老端不崩
5. A2UI 里的"组件目录(catalog)"怎么理解?
这是 A2UI 在客户端落地时最核心的工程抽象。
5.1 catalog 是一份"白名单 + 设计系统映射"
- 里面定义了:有哪些组件可用、组件有哪些属性、属性的类型与范围、默认样式
- 你的设计系统(Design System)在客户端如何落地,本质上就体现在 catalog 里
- Agent 不是"发明 UI",而是在 catalog 的约束下组合 UI
5.2 catalog 需要"版本治理"
真实产品一定会遇到:
- 新组件上线,但老版本客户端不认识
- 老组件废弃,但线上还有流量
因此 catalog 通常要带:
- minVersion / deprecated
- fallback(降级组件映射)
- "性能预算"约束(最大层级、最大节点数、图片体积、动效开关等)
A2UI 把"客户端拥有渲染权"的原则写得很明确:客户端可以把它无缝集成到自身品牌体验里。
6. A2UI 和 AG-UI 的关系:A2UI 负责"UI 长啥样",AG-UI 负责"怎么互动"
很多人会把这俩混在一起。简单分工是:
- A2UI:生成式 UI 的"规格"(让 Agent 交付 UI widget/布局描述)
- AG-UI:Agent↔用户应用的"交互协议"(双向、事件驱动、把状态/UI意图/用户交互流起来)
AG-UI 文档里也明确写到:A2UI 是 generative UI specification,而 AG-UI 是连接前端与后端 agent 的交互协议,它们"不同但配合很好"。
协议栈示意图
AG-UI Event Stream
A2UI Payload - UI Spec JSON
User Events - click / input / submit
Client App
AG-UI Client + A2UI Renderer
Agent Backend / Orchestrator
AG-UI 在工程上解决什么?
AG-UI 的定位是:开放、轻量、事件驱动,用标准事件类型把 agent 的执行过程、状态、以及 UI/交互在前后端之间同步起来;并且可以跑在 SSE/WebSocket 等多种传输之上。
7. 业内实践:A2UI 已经在哪些地方被用起来?
Google 的官方介绍里提到了一些"正在集成/使用"的方向(这对判断可落地性很有帮助):
- Google 自己的产品/团队在使用 A2UI,并把项目公开出来与社区协作,完善 specs、增加传输(transports)与更多客户端 renderer。
- Opal:用于快速构建/分享 AI mini-apps,A2UI 会让 UI 更动态、更适合每个用例。
- Gemini Enterprise:集成 A2UI,让企业 agent 在宿主应用里渲染更丰富的交互 UI,用于复杂工作流(表单、审批面板等)。
- Flutter GenUI SDK:官方提到它用 A2UI 作为"远程 server-side agents 与 app 之间的 UI 声明格式",并强调遵循品牌指南、使用 widget catalog。
结论:A2UI 不是"只存在论文里的概念",它在 Google 体系内已经进入实际集成与工程化推进阶段。
8. 最小可行落地:从"卡片化"开始,先跑通闭环
如果你要在客户端/前端做一个 MVP,建议分 5 步(非常务实):
Step 1:先定一个最小组件集(10 个以内)
Text / Image / Button / Input / Select / List / Card / Row / Column / Divider
------越少越好,先保证"能用、可控"。
Step 2:定义 A2UI 风格的 UI Spec Schema(强约束)
A2UI 的核心思想是"声明式 JSON + catalog",你可以先按这个方向把 Schema 定下来(示例仅为解释形态):
json
{
"components": [
{ "id": "c1", "type": "Card", "children": ["c2", "c3"] },
{ "id": "c2", "type": "Text", "text": "为你找到 3 家餐厅" },
{
"id": "c3",
"type": "List",
"items": ["c4"]
},
{
"id": "c4",
"type": "Row",
"children": ["c5", "c6"]
},
{ "id": "c5", "type": "Text", "text": "Trattoria A" },
{ "id": "c6", "type": "Button", "text": "预订", "action": { "type": "submit", "value": { "id": "A" } } }
]
}
这里刻意用了"扁平列表 + ID 引用"的形态,因为 A2UI 明确说它这样做是为了 LLM 更容易增量生成与更新。
Step 3:用 AG-UI 把 UI Spec 当成一种"事件载荷"推送给客户端
AG-UI 的关键是"事件流",客户端消费事件并更新 UI/状态。
Step 4:事件回传:把点击/输入/提交变成标准事件发回后端
- 用户点了 Button → 发送 submit 事件
- 用户改了 Input → 发送 input_change 事件(或类似)
- 后端 agent 收到事件继续推理/调用工具,再推送新的 UI patch
Step 5:生产必备:降级、版本、观测、回滚
A2UI 强调"安全第一、catalog 白名单"。因此你至少需要:
- Schema 校验失败 → 回退到纯文本/固定页
- 未知组件 → fallback 或隐藏
- 灰度开关/回滚 → 线上风险可控
- 可观测:首帧耗时、渲染耗时、事件丢失率、Crash/ANR、以及"为何生成这个 UI"的解释链路
9. 写在最后:用一句话总结 A2UI 的"工程价值"
A2UI 让 Agent 能跨信任边界、安全地交付可交互 UI;客户端保留渲染控制权(品牌/安全/性能);配合 AG-UI 的事件流,就能把"能点能填能跑流程"的 agent 体验做成可灰度、可回滚、可观测的生产系统。
附录 A:A2UI Component Catalog(组件目录)版本治理模板
A2UI 强调:客户端维护"catalog of trusted components",Agent 只能请求渲染 catalog 内的组件。
因此,catalog 本质是:设计系统(Design System)在协议层的映射 + 白名单 + 版本/降级策略。
A.1 最小可用目录(懂车帝客户端示例)
你要知道的三件事(站在客户端负责人视角):
- catalogVersion 是"你端侧组件白名单"的版本号,任何组件增删改都要 bump,用于灰度/回滚。
- minClientVersion 决定"低版本客户端是否接收新组件",由编排层按版本投放,保障老端不崩。
- components 是"设计系统到协议"的映射:type=组件名、propsSchema=属性强约束、fallback=老端替代。
json
{
"catalogVersion": "2026.01.0",
"minClientVersion": "7.3.0",
"components": [
{
"type": "Card",
"since": "2026.01.0",
"deprecatedSince": null,
"fallback": "Container",
"propsSchema": {
"padding": { "type": "number", "min": 0, "max": 24, "default": 12 }
},
"childrenPolicy": { "min": 0, "max": 20 }
},
{
"type": "Button",
"since": "2026.01.0",
"fallback": "TextLink",
"propsSchema": {
"text": { "type": "string", "maxLen": 16 },
"style": { "type": "enum", "values": ["primary", "secondary", "danger"] },
"action": { "$ref": "#/definitions/action" }
}
}
],
"definitions": {
"action": {
"type": "object",
"properties": {
"kind": { "type": "enum", "values": ["deeplink", "submit", "tool", "copy"] },
"payload": { "type": "object" }
},
"required": ["kind", "payload"]
}
}
}
字段解读(给"端侧 Owner"看的口径):
- catalogVersion:目录版本号,发布/回滚都以此为准(埋点带上便于定位问题)。
- minClientVersion:小于该版本的客户端不接收新目录(由后端/编排层控制投放)。
- type / since / deprecatedSince:组件生命周期(引入/废弃),便于治理与"兼容范围"判断。
- fallback:老端替代组件,保障"永不白屏"(TextLink/Container 这类简单组件常用于兜底)。
- propsSchema:属性强约束(类型/范围/默认值),防止异常值导致 UI 崩溃或体验劣化。
- childrenPolicy:子节点上下限,控制复杂度与性能预算。
- definitions.action:统一交互动作模型(deeplink/submit/tool/copy),payload 由业务自行定义。
你需要坚持的 3 条"硬规矩"
- 组件白名单:Agent 不得"发明组件"。(catalog 不存在=拒绝/降级)
- Props 强约束:每个 props 必须有类型/范围/默认值(防止"模型把 padding 写成 1000")。
- 版本与降级:每个组件都要有 since、可选 deprecatedSince、必备 fallback(老端不认识也能渲染)。
A.2 版本治理策略(推荐)
- catalogVersion:每次变更 bump(组件新增/字段变更/默认值变更都算)
- minClientVersion:后端/编排层据此决定是否投放新组件
- fallback 链:新组件 → 老组件 → 最终 Text(保证"永不白屏")
Google 在介绍 A2UI 与 Flutter GenUI SDK 时也强调:生成式 UI 需要遵循既有品牌规范并使用自己的 widget catalog,这就是 catalog 必须可控可版本化的原因。
附录 B:客户端 Renderer 分层(A2UI 渲染器)推荐架构
A2UI 的框架无关性来自一个原则:Agent 发结构与数据模型描述,客户端负责映射到原生组件(Flutter/Angular/SwiftUI/ArkUI)。
B.1 推荐分层(端侧可落地)
Spec In (JSON)
↓
- Parser(解析)
- JSON → AST/ComponentNode
- 处理扁平列表 + ID 引用的组装(LLM 友好结构)
↓
- Validator(校验)
- JSON Schema 校验
- catalog 白名单校验
- 预算校验(深度/节点数/图片体积/文本长度)
↓
- Planner(布局规划)
- 组件树 → Render Plan(测量/布局策略)
- 数据绑定占位符处理
↓
- Renderer(渲染执行)
- Render Plan → 原生组件树(View/Compose/SwiftUI/ArkUI)
- 异步图片/列表虚拟化/预取
↓
- Action Dispatcher(交互派发)
- 用户点击/输入 → 统一 Action → 事件上报/AG-UI 回传
↓
- Telemetry(观测)
- 首帧、渲染耗时、patch 次数、降级次数、崩溃/ANR
B.2 性能预算(建议默认值,可在你们体系里调)
- 最大组件节点数:200(超过就降级到"摘要卡片")
- 最大嵌套深度:10
- 图片总像素/总字节:按机型分档(低端机更严)
- 列表类组件:必须虚拟化(Recycler/LazyColumn/List)
- 增量更新:优先做 patch/diff 渲染(避免整树重建)
A2UI 的"增量可更新"设计目标,就是为了 progressive rendering 与对话推进时 UI 迭代更新。
B.3 降级兜底(上线必备)
- 校验失败:回退到"文本模式"或"固定模板页"
- 未知组件:走 fallback
- 预算超限:裁剪/分页/降级(如图片换占位、动效关)
- 网络异常:渲染上一次缓存 spec(加"可能过期"的提示)
附录 C:AG-UI 事件字段(Android 端落地口径)
AG-UI 在端侧就是一条事件流(SSE/WebSocket 皆可)。你要做的是定义一个"事件信封(Envelope)",统一承载类型、时间、追踪 ID、序号和载荷,然后按类型分发处理。
C.1 事件信封(Envelope)
kotlin
data class EventEnvelope(
val type: String, // UI_SNAPSHOT / UI_DELTA / STATE_SNAPSHOT / STATE_DELTA / TOOL_CALL_*
val timestamp: Long,
val traceId: String, // 一次 run 的全链路标识
val spanId: String? = null, // 步骤切片(便于复盘:哪一步慢/错)
val seq: Long, // 流内顺序号(乱序/重放可处理)
val payload: JsonElement // 具体事件数据
)
为什么需要 traceId / spanId / seq:
- traceId:把端上/服务端/工具调用串起来,便于端到端定位。
- spanId:把"长流程"切成可复盘的步骤(如:候选生成→表单渲染→提交)。
- seq:SSE/WS 可能乱序或重放;用 seq 保证落地侧按序消费。
C.2 UI 事件(渲染 A2UI Spec)
两类事件就够用:
- UI_SNAPSHOT:首次渲染,一次性携带整份 A2UI Spec。
- UI_DELTA:后续增量更新,携带 patch,用于局部更新。
json
{
"type": "UI_SNAPSHOT",
"payload": {
"a2uiSpec": { "components": [/* ... */] },
"specVersion": "0.8",
"catalogVersion": "2026.01.0"
}
}
在 Android 端的处理方式:
- UI_SNAPSHOT:parse → 生成组件树 → 映射到原生控件(Compose/View/ArkUI)。
- UI_DELTA:对本地 Spec 做 JSON Patch → 最小化重绘(diff/patch 而非重建整树)。
- specVersion / catalogVersion:用于兼容与降级判断(老端不认识 → fallback)。
C.3 State 事件(端侧状态机)
配合 JSON Patch(RFC 6902)做增量状态更新,避免整份状态重传。
json
{
"type": "STATE_DELTA",
"payload": {
"patch": [
{ "op": "add", "path": "/form/name", "value": "张三" },
{ "op": "replace", "path": "/step", "value": 2 }
]
}
}
端侧做法:
- 维护一个"唯一真相"的 State Store(如:ViewModel + immutable state)。
- 收到 STATE_DELTA → 应用 patch → 推送到 UI(Flow/LiveData)。
- 大量列表/表单场景:patch 能显著降低带宽与渲染成本。
C.4 Tool Call 事件(让 UI 能"边看边预填")
事件序列固定:TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END。端侧需按 callId 聚合分片。
kotlin
val argBuffers = mutableMapOf<String, StringBuilder>()
fun onToolCallArgs(callId: String, argsChunk: String) {
argBuffers.getOrPut(callId) { StringBuilder() }.append(argsChunk)
}
fun onToolCallEnd(callId: String) {
val json = argBuffers.remove(callId)?.toString() ?: return
// 解析 JSON → 预填 UI 或触发下一步
}
注意点:
- argsChunk 可能按分片到达,需按 seq 排序再拼接。
- START/END 可驱动端侧"loading/progress",提升交互感受。
- 工具调用失败/超时需有兜底(降级到固定模板或提示重试)。