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 领域的最佳实践。


参考资料

相关推荐
AngelPP3 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年3 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼3 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS3 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区4 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈5 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang5 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx
shengjk16 小时前
NanoClaw 深度剖析:一个"AI 原生"架构的个人助手是如何运转的?
人工智能
西门老铁8 小时前
🦞OpenClaw 让 MacMini 脱销了,而我拿出了6年陈的安卓机
人工智能