A2UI 技术原理深度解析:AI Agent 如何安全生成富交互 UI

本文深入解析 Google 开源的 A2UI 协议,探讨其核心架构、数据流设计以及为何它是 LLM 生成 UI 的最佳实践。

一、A2UI 是什么?

A2UI (Agent-to-User Interface) 是 Google 于 2025 年开源的声明式 UI 协议。它解决了一个核心问题:

如何让 AI Agent 安全地跨信任边界发送富交互 UI?

传统的 Agent 交互往往是纯文本对话,效率低下。而直接让 LLM 生成 HTML/JS 代码又存在严重的安全风险。A2UI 提供了一个中间方案:Agent 发送声明式 JSON 描述 UI 意图,客户端使用自己的原生组件渲染

复制代码
安全性:像数据一样安全
表达力:像代码一样丰富

二、核心设计理念

2.1 三层解耦架构

A2UI 的核心哲学是将三个关键元素解耦:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    A2UI 三层架构                         │
├─────────────────────────────────────────────────────────┤
│  1. 组件树 (Structure)    - Agent 提供的抽象 UI 结构      │
│  2. 数据模型 (State)      - 动态填充 UI 的应用状态        │
│  3. 组件目录 (Catalog)    - 客户端定义的可信组件映射      │
└─────────────────────────────────────────────────────────┘

这种设计带来的好处:

  • 安全性:Agent 只能使用客户端预定义的组件,无法注入恶意代码
  • 灵活性:同一份 UI 描述可在不同框架(Angular/Flutter/React)上渲染
  • 高效性:数据变更无需重发整个 UI 结构

2.2 邻接表模型 vs 嵌套树

这是 A2UI 最精妙的设计之一。传统 UI 描述使用嵌套 JSON 树:

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 采用扁平的邻接表

json 复制代码
//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 种服务端到客户端的消息类型:

3.1 surfaceUpdate - 定义 UI 结构

json 复制代码
{
  "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 区域,支持多个独立 Surface
  • components:扁平组件列表,通过 ID 引用建立父子关系
  • 组件属性支持字面值数据绑定

3.2 dataModelUpdate - 填充数据

json 复制代码
{
  "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}
      ]}
    ]
  }
}

设计亮点

  • 数据与 UI 结构分离,修改数据无需重发组件定义
  • 支持嵌套数据结构(valueMap)
  • 类型安全(valueString/valueInt/valueBoolean/valueNumber)

3.3 beginRendering - 触发渲染

json 复制代码
{
  "beginRendering": {
    "surfaceId": "booking-form",
    "root": "title",
    "catalogId": "https://github.com/google/A2UI/.../standard_catalog_definition.json"
  }
}

为什么需要这个消息?

  • 防止"闪烁":客户端缓冲组件,等待明确信号再渲染
  • 指定根组件:从哪个组件开始构建树
  • 指定组件目录:告诉客户端使用哪套组件定义

3.4 deleteSurface - 清理 UI

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 或完成任务
└─────────────────┘

4.1 用户点击"确认预订"后发生了什么?

这是一个关键问题:A2UI 只负责 UI 层,真正的业务逻辑由 Agent 决定

当用户点击按钮后,Client 会将 userAction 发送回 Agent。Agent 收到后有多种处理方式:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│              用户点击"确认预订"后的处理流程                        │
└─────────────────────────────────────────────────────────────────┘

                    userAction 到达 Agent
                           │
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
    │  方案 A     │ │  方案 B     │ │  方案 C     │
    │  模拟预订   │ │  调用 API   │ │  委托子Agent│
    └─────────────┘ └─────────────┘ └─────────────┘
           │               │               │
           ▼               ▼               ▼
    直接返回确认UI   调用餐厅真实API   通过 A2A 委托
    (Demo 场景)     (MCP/HTTP)      专业预订 Agent
方案 A:模拟预订(Demo 场景)

当前示例代码采用的就是这种方式------Agent 直接生成确认 UI,不调用真实预订系统:

python 复制代码
# Agent 收到 userAction 后,LLM 根据 Prompt 生成确认界面
# 这是 Demo 演示用,没有真实预订

# Prompt 中的指令:
# "For confirming a booking: use the CONFIRMATION_EXAMPLE template"

生成的确认 UI:

json 复制代码
{
  "surfaceUpdate": {
    "surfaceId": "confirmation",
    "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点"}
    ]
  }
}
方案 B:调用真实 API(生产场景)

在真实生产环境中,Agent 需要调用外部服务完成预订。这可以通过以下方式实现:

方式 1:Agent 内置 Tool(函数调用)

python 复制代码
# 在 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) 调用

python 复制代码
# 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
    }
)
方案 C:委托专业子 Agent(多 Agent 协作)

在复杂的多 Agent 系统中,主 Agent 可能将预订任务委托给专业的预订 Agent:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    多 Agent 协作预订流程                          │
└─────────────────────────────────────────────────────────────────┘

┌──────────────┐      A2A 协议      ┌──────────────────┐
│  主 Agent    │ ──────────────────>│  预订专业 Agent   │
│  (对话协调)   │                    │  (OpenTable集成)  │
└──────────────┘                    └────────┬─────────┘
                                             │
                                             │ 调用真实 API
                                             ▼
                                    ┌──────────────────┐
                                    │  OpenTable API   │
                                    │  或其他预订平台   │
                                    └──────────────────┘
python 复制代码
# 主 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

4.2 A2UI 的职责边界

理解这一点很重要:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      职责分离                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   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 组织内容

组件示例:动态列表

json 复制代码
// 使用 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 如何知道可以使用哪些组件?

6.1 协商流程

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    组件目录协商流程                               │
└─────────────────────────────────────────────────────────────────┘

步骤 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"                                    │
│   }                                                             │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘

6.2 LLM 如何被约束只使用已知组件?

关键在于 Prompt Engineering + JSON Schema 约束

python 复制代码
# 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 渲染器只渲染已注册的组件类型

6.3 如果 Agent 发送了未知组件会怎样?

json 复制代码
// Agent 错误地发送了一个不存在的组件
{
  "surfaceUpdate": {
    "components": [
      {
        "id": "evil",
        "component": {
          "ScriptExecutor": {  //不在目录中
            "code": "alert('hacked')"
          }
        }
      }
    ]
  }
}

Client 的处理方式

  1. 忽略未知组件 :渲染器在 WidgetRegistry 中找不到 ScriptExecutor,跳过该组件
  2. 显示占位符:渲染一个错误提示组件
  3. 发送错误消息 :通过 error 消息通知 Agent
json 复制代码
// Client 返回错误
{
  "error": {
    "type": "unknown_component",
    "componentId": "evil",
    "componentType": "ScriptExecutor",
    "message": "Component type 'ScriptExecutor' is not in the supported catalog"
  }
}

6.4 自定义组件的安全扩展

如果业务需要自定义组件(如图表、地图),流程如下:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    自定义组件安全流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  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 约束      │  • 验证数据绑定           │
└────────────────────────────────────────────────────────┘

关键安全特性

  1. 声明式数据:Agent 发送的是数据,不是代码
  2. 组件白名单:只能使用客户端预定义的组件
  3. 目录协商:双向声明,取交集
  4. Schema 约束:LLM 输出受 JSON Schema 强制约束
  5. 无 eval/innerHTML:客户端渲染器不执行任意字符串
  6. 数据绑定验证:路径解析在客户端控制

八、总结

A2UI 通过精巧的协议设计,解决了 AI Agent 生成 UI 的核心挑战:

挑战 A2UI 解决方案
安全性 声明式 JSON + 组件白名单
LLM 生成难度 邻接表模型 + 流式传输
跨平台 抽象组件 + 客户端渲染
性能 数据/结构分离 + 增量更新

如果你正在构建 AI Agent 应用,A2UI 值得深入研究。它代表了 Agent UI 领域的最佳实践。


参考资料

相关推荐
kicikng2 小时前
智能体来了(西南总部)完整拆解:AI Agent 指挥官 + AI调度官架构图
大数据·人工智能·多智能体系统·ai agent指挥官·ai调度官
夜斗小神社2 小时前
【黑马RAG与Agent智能体项目】(二)提示词工程
人工智能
C++ 老炮儿的技术栈2 小时前
不调用C++/C的字符串库函数,编写函数strcmp
c语言·开发语言·c++·人工智能·windows·git·visual studio
码农三叔2 小时前
(6-1)手部、足部与末端执行器设计:仿生手设计
人工智能·架构·机器人·人形机器人
liliangcsdn2 小时前
RL中GAE的计算过程详解
大数据·人工智能·算法
yhyvc2 小时前
人形具身机器人国产/进口快速选型优先级清单
人工智能·机器人
wangmengxxw2 小时前
SpringAI-mysql
java·数据库·人工智能·mysql·springai
考證寶題庫網2 小时前
Designing and Implementing a Microsoft Azure AI Solution 微軟Azure AI-102 認證全攻略
人工智能·microsoft·azure
逄逄不是胖胖2 小时前
《动手学深度学习》-52文本预处理实现
人工智能·pytorch·python·深度学习