一、A2UI 是什么?
A2UI (Agent-to-User Interface) 是 Google 于 2025 年开源的声明式 UI 协议。它解决了一个核心问题:
如何让 AI Agent 安全地跨信任边界发送富交互 UI?
A2UI 是 Google 开源的声明式 UI 协议,旨在解决 AI Agent 跨信任边界安全生成富交互 UI 的问题。它采用三层解耦架构,将组件树、数据模型与组件目录分离,确保 Agent 仅能使用客户端预定义组件,防止代码注入。协议基于扁平邻接表模型,优化了 LLM 生成难度并支持增量更新与流式传输。通过 JSON Schema 约束与目录协商机制,实现了安全性与表达力的平衡。A2UI 负责 UI 层描述与交互传递,业务逻辑由 Agent 自行实现,代表了 Agent UI 领域的最佳实践。
A2UI (Agent-to-User Interface) 是 Google 于 2025 年开源的声明式 UI 协议。它解决了一个核心问题:
如何让 AI Agent 安全地跨信任边界发送富交互 UI?
传统的 Agent 交互往往是纯文本对话,效率低下。而直接让 LLM 生成 HTML/JS 代码又存在严重的安全风险。A2UI 提供了一个中间方案:Agent 发送声明式 JSON 描述 UI 意图,客户端使用自己的原生组件渲染。
安全性:像数据一样安全 表达力:像代码一样丰富
A2UI 的核心哲学是将三个关键元素解耦:
┌─────────────────────────────────────────────────────────┐
│ A2UI 三层架构 │
├─────────────────────────────────────────────────────────┤
│ 1. 组件树 (Structure) - Agent 提供的抽象 UI 结构 │
│ 2. 数据模型 (State) - 动态填充 UI 的应用状态 │
│ 3. 组件目录 (Catalog) - 客户端定义的可信组件映射 │
└─────────────────────────────────────────────────────────┘
这种设计带来的好处:
这是 A2UI 最精妙的设计之一。传统 UI 描述使用嵌套 JSON 树:
//传统嵌套结构 - LLM 难以一次性生成正确
{
"type":"Column",
"children":[
{
"type":"Text",
"text":"Hello"
},
{
"type":"Row",
"children":[
{
"type":"Button",
"child":{
"type":"Text",
"text":"Cancel"
}
},
{
"type":"Button",
"child":{
"type":"Text",
"text":"OK"
}
}
]
}
]
}
A2UI 采用扁平的邻接表:
//A2UI 邻接表结构 - LLM 友好,支持增量生成
{
"surfaceUpdate":{
"components":[
{
"id":"root",
"component":{
"Column":{
"children":{
"explicitList":["greeting","buttons"]
}
}
}
},
{
"id":"greeting",
"component":{
"Text":{
"text":{
"literalString":"Hello"
}
}
}
},
{
"id":"buttons",
"component":{
"Row":{
"children":{
"explicitList":["cancel-btn","ok-btn"]
}
}
}
},
{
"id":"cancel-btn",
"component":{
"Button":{
"child":"cancel-text",
"action":{
"name":"cancel"
}
}
}
},
{
"id":"cancel-text",
"component":{
"Text":{
"text":{
"literalString":"Cancel"
}
}
}
},
{
"id":"ok-btn",
"component":{
"Button":{
"child":"ok-text",
"action":{
"name":"ok"
}
}
}
},
{
"id":"ok-text",
"component":{
"Text":{
"text":{
"literalString":"OK"
}
}
}
}
]
}
}
邻接表的优势:
| 特性 | 嵌套树 | 邻接表 |
|---|---|---|
| LLM 生成难度 | 高(需一次性正确嵌套) | 低(逐个组件生成) |
| 增量更新 | 困难 | 简单(按 ID 更新) |
| 流式传输 | 不支持 | 原生支持 |
| 错误恢复 | 整体失败 | 单组件失败不影响其他 |
A2UI 定义了 4 种服务端到客户端的消息类型:
{
"surfaceUpdate":{
"surfaceId":"booking-form",
"components":[
{
"id":"title",
"component":{
"Text":{
"text":{
"literalString":"预订餐厅"
},
"usageHint":"h1"
}
}
},
{
"id":"date-picker",
"component":{
"DateTimeInput":{
"value":{
"path":"/reservation/date"
},
"enableDate":true,
"enableTime":true
}
}
}
]
}
}
关键点:
surfaceId:标识 UI 区域,支持多个独立 Surfacecomponents:扁平组件列表,通过 ID 引用建立父子关系{
"dataModelUpdate":{
"surfaceId":"booking-form",
"path":"/reservation",
"contents":[
{
"key":"date",
"valueString":"2025-12-20T19:00:00Z"
},
{
"key":"guests",
"valueInt":2
},
{
"key":"restaurant",
"valueMap":[
{
"key":"name",
"valueString":"川味轩"
},
{
"key":"rating",
"valueNumber":4.8
}
]
}
]
}
}
设计亮点:
{
"beginRendering":{
"surfaceId":"booking-form",
"root":"title",
"catalogId":"https://github.com/google/A2UI/.../standard_catalog_definition.json"
}
}
为什么需要这个消息?
{
"deleteSurface":{
"surfaceId":"booking-form"
}
}
下面是一个完整的餐厅预订场景数据流:
┌──────────────────────────────────────────────────────────────────┐
│ A2UI 数据流 │
└──────────────────────────────────────────────────────────────────┘
用户:"帮我预订明晚 7 点的餐厅,2 人"
▼
┌─────────────────┐
│ AI Agent │ ← 接收用户请求,调用 LLM
│ (Python/Java) │
└────────┬────────┘
│ 生成 A2UI JSON (JSONL 流)
▼
┌─────────────────────────────────────────────────────────────────┐
│ {"surfaceUpdate": {"surfaceId": "booking", "components": [...]}}│
│ {"dataModelUpdate": {"surfaceId": "booking", "contents": [...]}}│
│ {"beginRendering": {"surfaceId": "booking", "root": "form"}} │
└─────────────────────────────────────────────────────────────────┘
│ 通过 SSE/WebSocket/A2A 传输
▼
┌─────────────────┐
│ Client App │ ← 解析 JSONL,构建组件缓冲
│ (Angular/Lit) │
└────────┬────────┘
│ 收到 beginRendering 后
▼
┌─────────────────┐
│ A2UI Renderer │ ← 从 root 开始递归构建组件树
│ │ ← 解析数据绑定,查询 WidgetRegistry
└────────┬────────┘
│
▼
┌─────────────────┐
│ Native UI │ ← 渲染为原生组件(Material/Cupertino 等)
│ (用户可见) │
└────────┬────────┘
│ 用户点击"确认预订"按钮
▼
┌─────────────────────────────────────────────────────────────────┐
│ {"userAction": { │
│ "name": "confirm_booking", │
│ "surfaceId": "booking", │
│ "context": {"date": "2025-12-20T19:00", "guests": 2} │
│ }} │
└─────────────────────────────────────────────────────────────────┘
│ 通过 A2A 消息发送回 Agent
▼
┌─────────────────┐
│ AI Agent │ ← 处理用户操作,可能更新 UI 或完成任务
└─────────────────┘
这是一个关键问题:A2UI 只负责 UI 层,真正的业务逻辑由 Agent 决定。
当用户点击按钮后,Client 会将 userAction 发送回 Agent。Agent 收到后有多种处理方式:
┌─────────────────────────────────────────────────────────────────┐
│ 用户点击"确认预订"后的处理流程 │
└─────────────────────────────────────────────────────────────────┘
userAction 到达 Agent
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 方案 A │ │ 方案 B │ │ 方案 C │
│ 模拟预订 │ │ 调用 API │ │ 委托子 Agent│
│ (Demo 场景) │ │ (MCP/HTTP) │ │ (专业预订 Agent)│
└─────────────┘ └─────────────┘ └─────────────┘
当前示例代码采用的就是这种方式——Agent 直接生成确认 UI,不调用真实预订系统:
# Agent 收到 userAction 后,LLM 根据 Prompt 生成确认界面
# 这是 Demo 演示用,没有真实预订
# Prompt 中的指令:
# "For confirming a booking: use the CONFIRMATION_EXAMPLE template"
生成的确认 UI:
{
"surfaceUpdate":{
"components":[
{
"id":"confirm-title",
"component":{
"Text":{
"text":{
"path":"title"
}
}
}
},
{
"id":"confirm-details",
"component":{
"Text":{
"text":{
"path":"bookingDetails"
}
}
}
}
]
},
"dataModelUpdate":{
"surfaceId":"confirmation",
"contents":[
{
"key":"title",
"valueString":"预订成功!"
},
{
"key":"bookingDetails",
"valueString":"川味轩 | 2 人 | 明晚 7 点"
}
]
}
}
在真实生产环境中,Agent 需要调用外部服务完成预订。这可以通过以下方式实现:
方式 1:Agent 内置 Tool(函数调用)
# 在 Agent 中定义预订工具
def book_restaurant(
restaurant_id:str, date:str, time:str, guests:int, tool_context: ToolContext
)->str:
"""调用餐厅预订 API"""
response = requests.post(
"https://api.restaurant.com/bookings",
json={
"restaurant_id": restaurant_id,
"datetime":f"{date}T{time}",
"party_size": guests
},
headers={"Authorization":f"Bearer {API_KEY}"}
)
return response.json()
# Agent 配置
agent = LlmAgent(
tools=[get_restaurants, book_restaurant],
instruction="当用户确认预订时,调用 book_restaurant 工具..."
)
方式 2:通过 MCP (Model Context Protocol) 调用
# Agent 通过 MCP 连接到餐厅预订服务
mcp_client = MCPClient("restaurant-booking-server")
# MCP 服务器暴露的工具
# - create_booking(restaurant_id, datetime, guests)
# - cancel_booking(booking_id)
# - get_availability(restaurant_id, date)
result = await mcp_client.call_tool("create_booking",{
"restaurant_id":"chuanwei-001",
"datetime":"2025-12-20T19:00:00",
"guests":2
})
在复杂的多 Agent 系统中,主 Agent 可能将预订任务委托给专业的预订 Agent:
┌─────────────────────────────────────────────────────────────────┐
│ 多 Agent 协作预订流程 │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐ A2A 协议 ┌──────────────────┐
│ 主 Agent │─────────>│ 预订专业 Agent │
│ (对话协调) │ │ (OpenTable 集成) │
└──────────────┘ └────────┬─────────┘
│
│ 调用真实 API
▼
┌──────────────────┐
│ OpenTable API │
│ 或其他预订平台 │
└──────────────────┘
# 主 Agent 通过 A2A 协议委托任务
async def delegate_booking(booking_details:dict):
a2a_client = A2AClient("https://booking-agent.example.com")
response = await a2a_client.send_message({
"message":{
"role":"user",
"parts":[{"kind":"data","data":{"task":"create_booking","details": booking_details }}]
}
})
return response
理解这一点很重要:
┌─────────────────────────────────────────────────────────────────┐
│ 职责分离 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ A2UI 协议负责: │
│ ├── UI 结构描述 (surfaceUpdate) │
│ ├── 数据绑定 (dataModelUpdate) │
│ ├── 用户交互事件传递 (userAction) │
│ └── 渲染控制 (beginRendering) │
│ │
│ A2UI 协议不负责: │
│ ├── 业务逻辑执行(预订、支付等) │
│ ├── 外部 API 调用 │
│ ├── 数据持久化 │
│ └── 身份认证 │
│ │
│ 业务逻辑由 Agent 通过以下方式实现: │
│ ├── 内置 Tools(函数调用) │
│ ├── MCP 服务器 │
│ ├── A2A 委托给专业子 Agent │
│ └── 直接 HTTP/gRPC 调用 │
│ │
└─────────────────────────────────────────────────────────────────┘
简单来说:A2UI 是 UI 层协议,业务逻辑由 Agent 自行决定如何实现。
A2UI v0.8 定义了以下标准组件:
| 类别 | 组件 | 说明 |
|---|---|---|
| 布局 | Row, Column, List | 排列子组件 |
| 展示 | Text, Image, Icon, Video, AudioPlayer, Divider | 展示内容 |
| 交互 | Button, TextField, CheckBox, DateTimeInput, Slider, MultipleChoice | 用户输入 |
| 容器 | Card, Tabs, Modal | 组织内容 |
// 使用 template 渲染动态列表
{
"surfaceUpdate":{
"components":[
{
"id":"restaurant-list",
"component":{
"List":{
"children":{
"template":{
"dataBinding":"/restaurants",
"componentId":"restaurant-card-template"
}
}
}
}
},
{
"id":"restaurant-card-template",
"component":{
"Card":{
"child":"card-content"
}
}
}
]
}
}
这是 A2UI 安全模型的核心:Agent 如何知道可以使用哪些组件?
┌─────────────────────────────────────────────────────────────────┐
│ 组件目录协商流程 │
└─────────────────────────────────────────────────────────────────┘
步骤 1: Agent 在 Agent Card 中声明支持的目录
↓
┌─────────────────────────────────────────────────────────────────┐
│ {
│ "name": "Restaurant Finder",
│ "capabilities": {
│ "extensions": [{
│ "uri": "https://a2ui.org/a2a-extension/a2ui/v0.8",
│ "params": {
│ "supportedCatalogIds": [
│ "https://github.com/google/A2UI/.../standard_catalog",
│ "https://my-company.com/custom_catalog"
│ ],
│ "acceptsInlineCatalogs": true
│ }
│ }]
│ }
│ }
└─────────────────────────────────────────────────────────────────┘
步骤 2: Client 在每条消息中声明自己支持的目录
↓
┌─────────────────────────────────────────────────────────────────┐
│ {
│ "metadata": {
│ "a2uiClientCapabilities": {
│ "supportedCatalogIds": [
│ "https://github.com/google/A2UI/.../standard_catalog"
│ ],
│ "inlineCatalogs": [
│ {
│ "catalogId": "my-app:custom-charts",
│ "components": {
│ "PieChart": { "type": "object", "properties": {...}}
│ }
│ }
│ ]
│ }
│ },
│ "message": { "prompt": { "text": "找餐厅" } }
│ }
└─────────────────────────────────────────────────────────────────┘
步骤 3: Agent 选择双方都支持的目录,在 beginRendering 中指定
↓
┌─────────────────────────────────────────────────────────────────┐
│ {
│ "beginRendering": {
│ "surfaceId": "main",
│ "catalogId": "https://github.com/google/A2UI/.../standard",
│ "root": "root-component"
│ }
│ }
└─────────────────────────────────────────────────────────────────┘
关键在于 Prompt Engineering + JSON Schema 约束:
# Agent 开发者在调用 LLM 时,将组件目录作为 Schema 约束传入
# 1. 加载客户端支持的组件目录
catalog = load_catalog("standard_catalog_definition.json")
# 2. 构建包含组件定义的 JSON Schema
resolved_schema = {
"properties":{
"surfaceUpdate":{
"properties":{
"components":{
"items":{
"properties":{
"component":{
# 这里只包含目录中定义的组件类型
"properties": catalog["components"]
}
}
}
}
}
}
}
}
# 3. 使用 Structured Output 模式调用 LLM
response = llm.generate(
prompt="生成一个餐厅预订表单",
response_schema=resolved_schema # LLM 只能输出符合 Schema 的 JSON
)
约束机制:
| 层级 | 约束方式 | 说明 |
|---|---|---|
| LLM 层 | JSON Schema / Structured Output | 现代 LLM(GPT-4、Gemini)支持强制输出符合 Schema 的 JSON |
| Agent 层 | Prompt 中包含组件目录 | 告诉 LLM 可用的组件类型和属性 |
| 协议层 | 目录协商 | Client 声明支持的目录,Agent 只能选择其中之一 |
| 渲染层 | 组件白名单 | Client 渲染器只渲染已注册的组件类型 |
// Agent 错误地发送了一个不存在的组件
{
"surfaceUpdate":{
"components":[
{
"id":"evil",
"component":{
"ScriptExecutor":{
//不在目录中
"code":"alert('hacked')"
}
}
}
]
}
}
Client 的处理方式:
ScriptExecutor,跳过该组件error 消息通知 Agent// Client 返回错误
{
"error":{
"type":"unknown_component",
"componentId":"evil",
"componentType":"ScriptExecutor",
"message":"Component type 'ScriptExecutor' is not in the supported catalog"
}
}
如果业务需要自定义组件(如图表、地图),流程如下:
┌─────────────────────────────────────────────────────────────────┐
│ 自定义组件安全流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Client 开发者实现自定义组件(本地代码,完全可控) │
│ class PieChartComponent { render(data) { ... } } │
│ │
│ 2. 在 WidgetRegistry 中注册 │
│ registry.register("PieChart", PieChartComponent) │
│ │
│ 3. 定义组件 Schema,加入自定义目录 │
│ { "PieChart": { "properties": { "data": {...} } } } │
│ │
│ 4. 在 a2uiClientCapabilities 中声明支持该目录 │
│ │
│ 5. Agent 现在可以安全地使用 PieChart 组件 │
│ │
└─────────────────────────────────────────────────────────────────┘
安全保证:自定义组件的实现代码在 Client 侧,Agent 只能传递数据参数,无法注入逻辑。
┌────────────────────────────────────────────────────────┐
│ A2UI 安全边界 │
├────────────────────────────────────────────────────────┤
│ Agent 侧(不可信) │ Client 侧(可信) │
│ ───────────────── │ ───────────────── │
│ • 生成 JSON 描述 │ • 定义组件目录 │
│ • 只能引用已知组件类型 │ • 实现组件渲染逻辑 │
│ • 无法执行任意代码 │ • 控制样式和行为 │
│ • 受 JSON Schema 约束 │ • 验证数据绑定 │
└────────────────────────────────────────────────────────┘
关键安全特性:
A2UI 通过精巧的协议设计,解决了 AI Agent 生成 UI 的核心挑战:
| 挑战 | A2UI 解决方案 |
|---|---|
| 安全性 | 声明式 JSON + 组件白名单 |
| LLM 生成难度 | 邻接表模型 + 流式传输 |
| 跨平台 | 抽象组件 + 客户端渲染 |
| 性能 | 数据/结构分离 + 增量更新 |
如果你正在构建 AI Agent 应用,A2UI 值得深入研究。它代表了 Agent UI 领域的最佳实践。
参考资料:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online