引言:Agent 时代的 UI 困境
想象这样一个场景——你对一个 AI 助手说:"帮我订一张明天晚上 7 点的两人桌。" 如果 Agent 只能回复文本,接下来将是一连串低效的对话:"请问哪一天?""什么时间?""几位?"……一个本可以用一个表单瞬间解决的事情,变成了五六个回合的文字乒乓球。
更好的方式显然是:Agent 直接生成一个表单界面,带有日期选择器、时间选择器、人数输入框和确认按钮。用户在 UI 上操作,一次提交,搞定。
但这件"显然更好"的事情,在技术上却极其棘手。Agent 可能运行在远程服务器上,甚至跨越组织的信任边界。它不能直接操控你的 UI,只能发送消息。传统的方案——在 iframe 中嵌入 Agent 返回的 HTML/JavaScript——不仅笨重、风格割裂,还引入了严重的安全隐患。
Google 发起的开源项目 A2UI(Agent-to-User Interface) 就是为了解决这个问题而生的。它定义了一种让 Agent "说 UI" 的通用语言:Agent 发送声明式的 JSON 消息来描述界面的意图,客户端应用用自己原生的组件库来渲染。安全如数据,表达如代码。
一、A2UI 是什么?一句话理解核心理念
A2UI 是一个声明式 UI 协议,而不是一个框架。它的核心思想可以拆成三层:
Agent 生成一段 JSON,描述"我想展示一个标题、一个日期选择器和一个按钮"。这段 JSON 通过任意传输通道(A2A 协议、WebSocket、SSE 等)到达客户端。客户端的 A2UI 渲染器读取 JSON,将抽象的组件描述映射为自己代码库中的原生组件——可以是 Flutter Widget、Angular Component、Lit Web Component 或 React 组件。
这意味着同一份 A2UI JSON 可以在 Web、移动端和桌面端被不同的渲染器渲染为风格统一、性能原生的界面,同时 Agent 完全无法执行任何代码——它只能从客户端预先批准的"组件目录"中选取组件来组合界面。
二、为什么需要 A2UI?三大设计支柱
安全优先。 A2UI 是一种声明式数据格式,不是可执行代码。Agent 不能注入 JavaScript,不能操纵 DOM——它只能请求渲染客户端目录中已经存在的、预先审核过的组件(如 Card、Button、TextField)。安全性由客户端完全掌控。
LLM 友好且支持增量更新。 UI 被表示为一个扁平的组件列表,组件之间通过 ID 引用来建立父子关系,而不是深层嵌套的 JSON 树。这种"邻接表"模型让 LLM 可以逐步生成组件、流式发送,客户端可以渐进式渲染——用户看到界面逐步构建,而不是盯着转圈等待。当需要更新时,只需发送带有已有 ID 的新定义即可,无需重新生成整个 UI。
框架无关且可移植。 Agent 发送的是对组件树和数据模型的抽象描述。渲染的责任完全在客户端。同一份 JSON 可以被 Lit 渲染为 Web Component,被 Flutter 渲染为原生移动控件,被 Angular 渲染为 Angular 组件,甚至未来被 SwiftUI 或 Jetpack Compose 渲染为 iOS 和 Android 原生视图。
三、核心概念详解
3.1 Surface(画布)
Surface 是 A2UI 中承载组件的容器,可以理解为一个完整的 UI 单元——一个对话框、一个侧边栏或一个主视图。每个 Surface 有唯一的 surfaceId,拥有自己的组件树和数据模型。Agent 通过创建、更新和删除 Surface 来管理界面的生命周期。
3.2 组件与邻接表模型
这是 A2UI 最独特的设计决策。传统的 UI 描述通常使用嵌套树结构,但 LLM 生成深层嵌套 JSON 时很容易出错,且难以流式传输和增量更新。A2UI 采用了扁平的邻接表模型:所有组件排成一个列表,每个组件有唯一 ID,通过 ID 引用子组件。
举个例子,一个包含标题和两个按钮的简单界面,在 v0.9 中是这样描述的:根组件 Column 声明子组件列表为 ["greeting", "buttons"],greeting 是一个 Text 组件,buttons 是一个 Row 组件再引用两个 Button。所有组件平级排列,通过 ID 建立层次关系。
A2UI 提供了一套标准组件目录,按用途分为布局类(Row、Column、List)、展示类(Text、Image、Icon、Divider)、交互类(Button、TextField、CheckBox、DateTimeInput、Slider、ChoicePicker)和容器类(Card、Tabs、Modal)。
3.3 数据绑定
A2UI 将 UI 结构与应用状态分离。每个 Surface 拥有一个 JSON 数据模型,组件通过 JSON Pointer 路径(如 /user/name、/cart/items/0/price)绑定到数据模型中的值。
这种分离带来了强大的响应式更新能力:当 Agent 更新数据模型中的某个路径时,绑定到该路径的所有组件自动更新显示内容,无需重新发送组件定义。同时,交互组件(如 TextField)支持双向绑定——用户输入立即写入本地数据模型,当用户点击提交按钮时,按钮的 Action 从数据模型中解析出最新值发送给 Agent。
动态列表是数据绑定的一个精彩应用:一个模板组件绑定到数据模型中的数组路径,数组中每增加一个元素就自动渲染一个新实例,且模板内的路径自动限定到当前数组元素的作用域。
3.4 消息类型与生命周期
以 v0.9 为例,Agent 与客户端之间通过四种核心消息通信。createSurface 创建画布并指定使用的组件目录。updateComponents 定义或更新 UI 组件。updateDataModel 更新应用状态。deleteSurface 移除一个界面。
一个典型的餐厅预订流程是这样的:Agent 先发送 createSurface 创建画布,然后通过 updateComponents 定义表单结构(标题、人数输入框、提交按钮),再通过 updateDataModel 填充初始数据(日期、人数)。用户修改人数,客户端自动更新本地数据模型。用户点击确认,客户端将按钮 Action 中引用的数据路径解析为当前值,封装为 action 消息发送给 Agent。Agent 处理后可以更新界面或删除 Surface。
3.5 Catalog(组件目录)
Catalog 是 A2UI 安全模型的关键枢纽。它是一个 JSON Schema 文件,定义了 Agent 可以使用的所有组件、函数和主题。客户端告诉 Agent 自己支持哪些 Catalog,Agent 在创建 Surface 时选择一个 Catalog 并锁定。之后 Agent 生成的所有 JSON 都会在双端被验证——Agent 端在发送前验证,客户端在接收后再验证。如果验证失败,客户端会发送 VALIDATION_FAILED 错误,Agent 可以据此自我纠正。
A2UI 团队维护了一个"Basic Catalog"作为起步用的通用组件集,但大多数生产应用会定义自己的 Catalog 来反映自己的设计系统。你可以从零开始定义,也可以导入 Basic Catalog 中的部分组件再扩展自定义组件(如图表、地图、股票行情组件)。Catalog 之间通过 URI 作为唯一标识符进行协商和版本管理。
3.6 传输层
A2UI 是传输无关的——任何能传递 JSON 的机制都可以工作。目前与 A2A 协议和 AG UI 有成熟的集成,REST、WebSocket 和 SSE 在路线图中。消息通常以 JSON Lines(JSONL)格式流式传输,每行一个完整的 JSON 对象。在 A2A 绑定中,A2UI 消息被编码为 DataPart,MIME 类型为 application/json+a2ui。
四、v0.8 → v0.9:从"结构化输出优先"到"提示词优先"
A2UI 的版本演进体现了对 LLM 生成能力更深层的理解。v0.8 被设计为通过 LLM 的"结构化输出"模式生成,依赖深层嵌套和特定的包装结构。v0.9 则做了一次哲学性的转变——为"嵌入系统提示词"而优化。
最直观的变化是组件格式从嵌套键变成了扁平判别器。v0.8 写作 "component": { "Text": { "text": { "literalString": "Hello" } } },v0.9 则简化为 "component": "Text", "text": "Hello"。数据模型更新从键值对数组变成了标准 JSON 对象。子组件列表从 {"explicitList": [...]} 变成了简单的数组。这些改动大幅减少了 token 消耗,也更符合 LLM 天然擅长生成的 JSON 模式。
v0.9 还引入了几个重要的新能力:createSurface 消息要求指定 catalogId,使目录协商变得显式;新增了 sendDataModel 标志,允许客户端在每条消息的元数据中自动附带完整数据模型,实现"无状态 Agent"模式;引入了 formatString 函数支持字符串插值;以及结构化的 VALIDATION_FAILED 错误反馈机制,让 LLM 可以在"生成-验证-纠正"循环中自我改进。
五、双向交互:Action 与数据同步
A2UI 不只是单向的 UI 推送,它支持完整的双向通信。交互组件(如 Button)可以定义 Action,分为两种:Server Event 发送到 Agent 处理(如提交表单),Local Function Call 在客户端本地执行(如打开 URL、格式化字符串、输入验证)。
Action 中的 context 是一个精心设计的机制——它允许按钮在触发时从数据模型中"摘取"特定路径的当前值,封装成一个简洁的上下文对象发送给 Agent,避免 Agent 需要解析整个数据模型。
v0.9 的 Data Model Sync 更是支持了一种优雅的"口头提交"模式:当 sendDataModel 启用时,用户甚至不需要点击按钮——只要说"好的,提交吧",客户端会将当前完整数据模型附在消息元数据中,Agent 从元数据中读取所有表单值即可完成处理。
在多 Agent 架构中,协调器(Orchestrator)需要维护 Surface 到子 Agent 的所有权映射,确保用户的 Action 被路由回正确的子 Agent,并且在转发消息时剥离其他 Agent 的数据模型,防止跨 Agent 的数据泄露。
六、生态系统与真实应用
A2UI 不是一个纸上协议——它已经在多个 Google 产品和合作伙伴项目中投入使用。
Google Opal 使用 A2UI 驱动 AI 小应用的动态生成式 UI 系统,让数十万用户可以用自然语言创建、编辑和分享 AI 应用。Flutter GenUI SDK 在底层使用 A2UI 作为服务端 Agent 与 Flutter 应用之间的通信协议,实现跨 iOS、Android、Web、桌面的原生渲染。Google ADK(Agent Development Kit)内置了 A2UI v0.8 标准目录的原生渲染支持。CopilotKit / AG UI 提供了 A2UI 的 Day-zero 兼容,AG UI 作为传输层,A2UI 作为 UI 内容格式,形成互补。
A2UI 与同类方案的定位也值得理解:MCP Apps 让远程服务器通过 iframe 提供完整的 UI 体验,适合服务器完全掌控 UI 的场景;AG UI 是一个前后端连接的传输协议;A2UI 则是 UI 负载本身的格式标准。三者可以组合使用——A2UI + AG UI 用 AG UI 做管道、A2UI 做内容,A2UI + A2A 用 A2A 协议在多 Agent 系统中传递 A2UI 消息。
七、动手试一试
体验 A2UI 最快的方式是运行仓库自带的餐厅查找器 Demo。克隆仓库、设置 Gemini API Key、进入 samples/client/lit 目录运行 npm run demo:all,就能在浏览器中看到一个由 Gemini 驱动的 Agent 实时生成交互式 UI 的完整流程。
如果你想更深入地开发,有三条路径可选:前端开发者可以将 A2UI 渲染器集成到自己的应用中(目前支持 Lit、Angular,React 在路线图中);后端开发者可以使用 Google ADK 构建生成 A2UI 响应的 Agent;或者直接使用 AG UI/CopilotKit 或 Flutter GenUI SDK 等已内置 A2UI 支持的框架。
结语
A2UI 的出现代表了 Agent 交互范式的一次重要进化——从"Agent 只能说文字"到"Agent 可以说 UI"。它用声明式数据格式解决了安全问题,用邻接表模型解决了 LLM 生成和流式渲染的问题,用框架无关的抽象解决了跨平台问题,用 Catalog 协商机制解决了可扩展性问题。
作为 Google 发起、Apache 2.0 许可的开源项目,A2UI 目前处于 v0.8(稳定)和 v0.9(草案)阶段,正在积极向 v1.0 迈进。如果你正在构建 AI Agent 驱动的应用,无论是对话式界面、企业工作流还是多 Agent 系统,A2UI 都值得关注和尝试。