A2UI 深度解读:让 AI Agent "说出"用户界面的开放协议

8 阅读16分钟

引言:Agent 时代的 UI 困境

想象这样一个场景——你对一个 AI 助手说:"帮我订一张明天晚上 7 点的两人桌。" 如果 Agent 只能回复文本,接下来将是:

用户: "帮我订一张明天晚上7点的两人桌"
Agent: "好的,请问几位用餐?"
用户: "两位"
Agent: "请问哪天?"
用户: "明天"
Agent: "什么时间?"
用户: "晚上7点"
Agent: "有什么忌口吗?"
...(五六个回合后终于订完)

更好的方式是:Agent 直接生成一个表单——日期选择器、时间选择器、人数输入框、提交按钮,一步搞定。但传统方案(Agent 返回 HTML/JS 塞进 iframe)笨重、割裂、不安全。

A2UI(Agent-to-User Interface) 就是为此而生的 Google 开源协议:Agent 发送声明式 JSON 描述界面意图,客户端用自己的原生组件渲染。安全如数据,表达如代码。


一、A2UI 全景架构图

先来一张图看全貌——A2UI 的核心是把 UI 生成和 UI 执行彻底解耦:

┌──────────────────────────────────────────────────────────────┐
│                        用户 (User)                           │
│    输入:"帮我找纽约的中餐馆"    │    看到原生渲染的卡片列表    │
└───────────────┬──────────────────────────────▲───────────────┘
                │ 文字请求                      │ 原生 UI
                ▼                              │
┌───────────────────────────────────────────────────────────────┐
│                   客户端应用 (Client App)                      │
│  ┌─────────────┐   ┌──────────────┐   ┌──────────────────┐   │
│  │  传输层      │   │ A2UI 渲染器   │   │  组件目录         │   │
│  │  (Transport) │──▶│  (Renderer)  │◀──│  (Catalog)       │   │
│  │  A2A/WS/SSE │   │  Lit/Angular │   │  Button, Card... │   │
│  └──────┬──────┘   │  /Flutter    │   └──────────────────┘   │
│         │          └──────────────┘                           │
└─────────┼────────────────────────────────────────────────────┘
          │ JSON 消息流 (JSONL)
          │
┌─────────▼─────────────────────────────────────────────────────┐
│                     AI Agent (后端)                             │
│  ┌───────────────┐    ┌──────────────────┐                     │
│  │  业务逻辑      │───▶│  A2UI 生成器      │                     │
│  │  (Tools/API)  │    │  (LLM 生成 JSON) │                     │
│  └───────────────┘    └──────────────────┘                     │
│                              │                                 │
│                     ┌────────▼────────┐                        │
│                     │   Gemini / GPT  │                        │
│                     │   等 LLM 模型    │                        │
│                     └─────────────────┘                        │
└────────────────────────────────────────────────────────────────┘

关键洞察:Agent 永远不会执行代码或操控 DOM。它只能从客户端预批准的"组件目录"中选取组件来组合界面——就像只能用菜单上的菜来点餐,不能自己跑进厨房。


二、三分钟理解核心概念

2.1 五个关键词

┌─────────────────────────────────────────────────────────────┐
│                    A2UI 五大核心概念                          │
├─────────────┬───────────────────────────────────────────────┤
│  Surface    │ 画布/容器,承载一组组件(如一个表单、一个卡片)  │
│  Component  │ UI 元素(Button, Text, Card, TextField...)    │
│  Data Model │ 应用状态,组件通过路径绑定到它                  │
│  Catalog    │ 组件目录,定义 Agent 能用哪些组件               │
│  Message    │ JSON 消息(创建画布/更新组件/更新数据/删除画布) │
└─────────────┴───────────────────────────────────────────────┘

2.2 邻接表模型:为什么是扁平列表而非嵌套树?

这是 A2UI 最独特的设计。传统 UI 描述用嵌套 JSON 树,但 LLM 生成深层嵌套时极易出错、难以流式传输。A2UI 把组件展平为一个列表,通过 ID 引用建立父子关系:

传统嵌套树(LLM 容易搞乱括号)        A2UI 邻接表(扁平 + ID 引用)
─────────────────────────            ──────────────────────────
{                                    components: [
  "Column": {                          { id: "root",    → Column, children: ["title","btn"] },
    "children": [                      { id: "title",   → Text, text: "Hello" },
      { "Text": { "Hello" } },        { id: "btn",     → Button, child: "btn-text" },
      { "Button": {                    { id: "btn-text",→ Text, text: "OK" }
        "child": {                   ]
          "Text": { "OK" }
        }
      }}
    ]
  }
}

层层嵌套,一个括号没对上就全废了         所有组件平铺,随时增量发送、按 ID 更新

2.3 数据绑定:结构与状态分离

组件定义"长什么样",数据模型定义"展示什么内容"。两者通过 JSON Pointer 路径连接:

         组件结构                              数据模型
    ┌──────────────┐                    ┌──────────────────┐
    │ Text          │                   │ {                │
    │ text: ────────┼───path───────────▶│   "user": {      │
    │   path:       │  "/user/name""name":"Alice"│
    │   "/user/name"│                   │   }              │
    └──────────────┘                    └──────────────────┘
                                              │
    当数据模型更新为 "Bob" 时 ──────────────────┘
    Text 自动显示 "Bob",无需重发组件定义!

三、消息生命周期图解

以一个完整的餐厅预订流程为例,看 A2UI 消息如何流转:

 用户                        客户端                         Agent
  │                            │                              │
  │  "订两人桌"                 │                              │
  │ ──────────────────────────▶│                              │
  │                            │  将用户消息转发给 Agent        │
  │                            │ ─────────────────────────────▶│
  │                            │                              │
  │                            │   ① createSurface            │
  │                            │◀─ (创建画布,指定 Catalog)──── │
  │                            │                              │
  │                            │   ② updateComponents         │
  │                            │◀─ (标题+人数框+日期框+按钮)── │
  │  看到表单渐进式渲染          │                              │
  │◀───────────────────────── │   ③ updateDataModel           │
  │                            │◀─ (日期="明天", 人数="2") ──── │
  │                            │                              │
  │  修改人数为 "3"             │                              │
  │ ──────────────────────────▶│  本地数据模型自动更新           │
  │                            │  /reservation/guests = "3"   │
  │                            │                              │
  │  点击「确认预订」            │                              │
  │ ──────────────────────────▶│                              │
  │                            │   ④ action                   │
  │                            │ ─(name:"confirm",context)───▶│
  │                            │                              │
  │                            │   ⑤ deleteSurface            │
  │  看到"预订成功"确认界面      │◀─ + 新 surface (确认卡片) ── │
  │◀───────────────────────── │                              │

四、实战示例:由浅入深

🟢 入门级:Hello World — 一张静态信息卡

适合人群:想快速了解 A2UI JSON 长什么样的开发者

这是最简单的 A2UI 示例——展示一张带标题和描述的卡片,没有交互,没有数据绑定,纯静态内容。

// 消息 1:创建画布
{
  "version": "v0.9",
  "createSurface": {
    "surfaceId": "hello-card",
    "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
  }
}

// 消息 2:定义组件
{
  "version": "v0.9",
  "updateComponents": {
    "surfaceId": "hello-card",
    "components": [
      {
        "id": "root",
        "component": "Card",
        "child": "content"
      },
      {
        "id": "content",
        "component": "Column",
        "children": ["title", "desc"]
      },
      {
        "id": "title",
        "component": "Text",
        "text": "👋 欢迎使用 A2UI",
        "variant": "h1"
      },
      {
        "id": "desc",
        "component": "Text",
        "text": "这是一张由 Agent 生成的卡片,渲染为你应用的原生组件。"
      }
    ]
  }
}

解读如下——整个过程只需两条消息。createSurface 告诉客户端"我要创建一个画布,用基础组件目录"。updateComponents 发送四个组件:Card 是容器,Column 纵向排列子组件,两个 Text 分别是标题和正文。所有组件平铺在一个列表里,通过 childchildren 引用彼此的 ID。

渲染效果示意:

┌──────────────────────────┐
│ ┌──────────────────────┐ │
│ │  👋 欢迎使用 A2UI     │ │   ← h1 标题
│ │                      │ │
│ │  这是一张由 Agent     │ │   ← 正文描述
│ │  生成的卡片...        │ │
│ └──────────────────────┘ │
└──────────────────────────┘
         Card 容器

🟡 进阶级:带数据绑定的用户资料卡

适合人群:需要理解数据绑定、响应式更新的前端/全栈开发者

这个示例展示数据绑定的核心能力——组件不写死内容,而是绑定到数据模型的路径。当数据变化时,UI 自动刷新。

// 消息 1:创建画布
{
  "version": "v0.9",
  "createSurface": {
    "surfaceId": "profile",
    "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
  }
}

// 消息 2:定义组件(结构)
{
  "version": "v0.9",
  "updateComponents": {
    "surfaceId": "profile",
    "components": [
      {
        "id": "root",
        "component": "Card",
        "child": "layout"
      },
      {
        "id": "layout",
        "component": "Column",
        "children": ["avatar", "name", "email", "role"]
      },
      {
        "id": "avatar",
        "component": "Image",
        "url": { "path": "/user/avatar" },
        "fit": "cover"
      },
      {
        "id": "name",
        "component": "Text",
        "text": { "path": "/user/name" },
        "variant": "h2"
      },
      {
        "id": "email",
        "component": "Text",
        "text": { "path": "/user/email" }
      },
      {
        "id": "role",
        "component": "Text",
        "text": { "path": "/user/role" },
        "variant": "caption"
      }
    ]
  }
}

// 消息 3:填充数据
{
  "version": "v0.9",
  "updateDataModel": {
    "surfaceId": "profile",
    "path": "/user",
    "value": {
      "name": "Sarah Chen",
      "email": "sarah@techco.com",
      "role": "Product Designer",
      "avatar": "https://example.com/sarah.jpg"
    }
  }
}

关键点在于,组件中的 { "path": "/user/name" } 就是数据绑定语法。渲染器看到它会去数据模型中读取 /user/name 的值来显示。当 Agent 后续发送新的 updateDataModel/user/name 改成 "Bob Lee" 时,名字自动变化,不需要重新发送组件定义。这就是结构与状态分离带来的高效更新。

   组件定义(不变)                  数据模型(可随时更新)
┌──────────────────┐          ┌────────────────────────┐
│ Text              │          │ { "user": {            │
│   text:           │─bindTo──▶│     "name": "Sarah"    │──▶ 显示 "Sarah"path:/user/name│         │   }                    │
└──────────────────┘          └────────────────────────┘
                                       │ Agent 发送数据更新
                              ┌────────▼───────────────┐
                              │ { "user": {            │
                              │     "name": "Bob"      │──▶ 自动显示 "Bob"
                              │   }                    │
                              └────────────────────────┘

🟡 进阶级:带表单交互的餐厅预订

适合人群:需要理解双向绑定和 Action 机制的开发者

这是官方 Demo 的核心场景——Agent 生成一个预订表单,用户填写后提交,Agent 收到数据进行处理。

// 消息 1:创建画布
{
  "version": "v0.9",
  "createSurface": {
    "surfaceId": "booking",
    "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
  }
}

// 消息 2:定义表单组件
{
  "version": "v0.9",
  "updateComponents": {
    "surfaceId": "booking",
    "components": [
      {
        "id": "root",
        "component": "Column",
        "children": ["title", "img", "party-size", "datetime", "dietary", "submit-btn"]
      },
      {
        "id": "title",
        "component": "Text",
        "text": { "path": "/title" },
        "variant": "h2"
      },
      {
        "id": "img",
        "component": "Image",
        "url": { "path": "/imageUrl" }
      },
      {
        "id": "party-size",
        "component": "TextField",
        "label": "用餐人数",
        "value": { "path": "/partySize" },
        "textFieldType": "number"
      },
      {
        "id": "datetime",
        "component": "DateTimeInput",
        "label": "日期和时间",
        "value": { "path": "/reservationTime" },
        "enableDate": true,
        "enableTime": true
      },
      {
        "id": "dietary",
        "component": "TextField",
        "label": "饮食要求",
        "value": { "path": "/dietary" }
      },
      {
        "id": "submit-btn",
        "component": "Button",
        "child": "submit-text",
        "variant": "primary",
        "action": {
          "event": {
            "name": "submit_booking",
            "context": {
              "restaurant": { "path": "/restaurantName" },
              "partySize":  { "path": "/partySize" },
              "time":       { "path": "/reservationTime" },
              "dietary":    { "path": "/dietary" }
            }
          }
        }
      },
      {
        "id": "submit-text",
        "component": "Text",
        "text": "确认预订"
      }
    ]
  }
}

// 消息 3:填充初始数据
{
  "version": "v0.9",
  "updateDataModel": {
    "surfaceId": "booking",
    "path": "/",
    "value": {
      "title": "预订 - 西安名吃",
      "restaurantName": "西安名吃",
      "imageUrl": "https://example.com/xian.jpg",
      "partySize": "2",
      "reservationTime": "",
      "dietary": ""
    }
  }
}

这里有三个关键交互机制值得注意。

双向绑定——TextField 的 value 绑定到 /partySize,用户输入 "4" 时,本地数据模型立即更新为 {"partySize": "4"},完全在客户端本地完成,没有网络请求。

Action 的 context——Button 的 action.event.context 定义了提交时要携带哪些数据。每个 key 的 value 用 path 指向数据模型,客户端在点击时解析出当前值。

当用户点击"确认预订",客户端发送的消息如下:

{
  "version": "v0.9",
  "action": {
    "name": "submit_booking",
    "surfaceId": "booking",
    "sourceComponentId": "submit-btn",
    "timestamp": "2026-03-18T19:30:00Z",
    "context": {
      "restaurant": "西安名吃",
      "partySize": "4",
      "time": "2026-03-19T19:00:00Z",
      "dietary": "不吃辣"
    }
  }
}

Agent 端 Python 处理代码类似:

if action_name == "submit_booking":
    restaurant = context.get("restaurant")
    party_size = context.get("partySize")
    time = context.get("time")
    # 让 LLM 处理
    query = f"用户预订了 {restaurant}{party_size} 人,时间 {time}"
    response = await llm.generate(query)

🔴 高级:动态列表 + 模板渲染

适合人群:需要高效渲染大量数据的架构师和高级开发者

当 Agent 返回一组搜索结果时,不需要为每条结果分别定义组件——用一个模板 + 数据数组即可自动渲染:

// 组件定义:一个模板驱动的列表
{
  "version": "v0.9",
  "updateComponents": {
    "surfaceId": "search-results",
    "components": [
      {
        "id": "root",
        "component": "Column",
        "children": ["result-header", "result-list"]
      },
      {
        "id": "result-header",
        "component": "Text",
        "text": "为你找到以下餐厅:",
        "variant": "h2"
      },
      {
        "id": "result-list",
        "component": "List",
        "children": {
          "componentId": "restaurant-card",
          "path": "/restaurants"
        },
        "direction": "vertical"
      },
      {
        "id": "restaurant-card",
        "component": "Card",
        "child": "card-layout"
      },
      {
        "id": "card-layout",
        "component": "Row",
        "children": ["card-img", "card-info"]
      },
      {
        "id": "card-img",
        "component": "Image",
        "url": { "path": "/imageUrl" },
        "fit": "cover"
      },
      {
        "id": "card-info",
        "component": "Column",
        "children": ["card-name", "card-rating", "card-detail"]
      },
      {
        "id": "card-name",
        "component": "Text",
        "text": { "path": "/name" },
        "variant": "h3"
      },
      {
        "id": "card-rating",
        "component": "Text",
        "text": { "path": "/rating" },
        "variant": "caption"
      },
      {
        "id": "card-detail",
        "component": "Text",
        "text": { "path": "/detail" }
      }
    ]
  }
}

// 数据模型:一个数组,有多少项就渲染多少张卡片
{
  "version": "v0.9",
  "updateDataModel": {
    "surfaceId": "search-results",
    "path": "/restaurants",
    "value": [
      {
        "name": "西安名吃",
        "detail": "正宗手拉面,香辣可口",
        "rating": "★★★★☆",
        "imageUrl": "https://example.com/xian.jpg"
      },
      {
        "name": "韩朝",
        "detail": "地道四川菜",
        "rating": "★★★★☆",
        "imageUrl": "https://example.com/han.jpg"
      },
      {
        "name": "红农场",
        "detail": "现代中餐,农场直供",
        "rating": "★★★★☆",
        "imageUrl": "https://example.com/red.jpg"
      }
    ]
  }
}

核心原理是作用域路径。模板中的 { "path": "/name" } 不是指向全局根路径,而是自动限定到当前数组项。第一张卡片的 /name 解析为 /restaurants/0/name,即 "西安名吃";第二张解析为 /restaurants/1/name,即 "韩朝"。

 数据:/restaurants = [ {name:"西安名吃"}, {name:"韩朝"}, {name:"红农场"} ]
                          │                   │                │
 模板自动实例化            ▼                   ▼                ▼
 ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
 │ 🖼️ 西安名吃  │  │ 🖼️ 韩朝      │  │ 🖼️ 红农场    │
 │ ★★★★☆      │  │ ★★★★☆      │  │ ★★★★☆      │
 │ 正宗手拉面   │  │ 地道四川菜   │  │ 现代中餐     │
 └─────────────┘  └─────────────┘  └─────────────┘

 新增一项到数组 → 自动多渲染一张卡片,无需修改组件定义!

🔴 高级:多 Agent 编排(Orchestrator)

适合人群:构建企业级多 Agent 系统的架构师

在真实的企业场景中,一个主协调器(Orchestrator)管理多个专业子 Agent,每个子 Agent 负责自己领域的 UI。这是仓库里 samples/agent/adk/orchestrator 示例所展示的架构:

                           ┌───────────────────┐
              用户问题       │   Orchestrator     │
         ───────────────── ▶│   (主协调 Agent)    │
                            │                   │
                            │  ① 意图识别        │
                            │  "找中餐" → 路由到  │
                            │   餐厅 Agent       │
                            └──┬──────┬─────┬──┘
                               │      │     │
               ┌───────────────┘      │     └───────────────┐
               ▼                      ▼                     ▼
   ┌───────────────────┐  ┌──────────────────┐  ┌──────────────────┐
   │  餐厅查找 Agent     │  │  联系人查找 Agent  │  │  数据图表 Agent   │
   │  (port 10003)      │  │  (port 10004)     │  │  (port 10005)    │
   │                    │  │                   │  │                  │
   │  返回:餐厅列表 UI  │  │  返回:联系人卡片  │  │  返回:图表 UI    │
   │  (A2UI JSON)       │  │  (A2UI JSON)      │  │  (A2UI JSON)     │
   └────────────────────┘  └───────────────────┘  └──────────────────┘

Orchestrator 需要处理两个关键安全问题:

Surface 所有权映射——当子 Agent 创建 Surface 时,Orchestrator 记录"这个 surfaceId 属于哪个子 Agent"。当用户在 UI 上操作触发 Action 时,Orchestrator 根据 surfaceId 把请求路由回正确的子 Agent。

数据模型隔离——当 sendDataModel: true 启用时,客户端会在每条消息元数据中附带所有 Surface 的数据模型。Orchestrator 必须在转发给子 Agent 前剥离其他 Agent 的数据,否则会导致跨 Agent 的数据泄露。

 客户端发来的元数据(包含所有 Surface 的数据):
 ┌──────────────────────────────────────┐
 │ a2uiClientDataModel: {              │
 │   surfaces: {                       │
 │     "restaurant-list": {...},  ◀─── 属于餐厅 Agent
 │     "contact-card":   {...},  ◀─── 属于联系人 Agent
 │     "sales-chart":    {...}   ◀─── 属于图表 Agent
 │   }                                 │
 │ }                                   │
 └──────────────────────────────────────┘
            │
    Orchestrator 必须 strip
            │
            ▼  转发给餐厅 Agent 时只保留:
 ┌──────────────────────────────────────┐
 │ a2uiClientDataModel: {              │
 │   surfaces: {                       │
 │     "restaurant-list": {...}        │  ✅ 只有自己的数据
 │   }                                 │
 │ }                                   │
 └──────────────────────────────────────┘

🔴 高级:自定义组件 Catalog

适合人群:需要扩展 A2UI 到特定业务领域的团队

标准 Catalog 只有通用组件。如果你需要地图、图表、股票行情等,就需要自定义 Catalog:

{
  "$id": "https://mycompany.com/catalogs/dashboard/v1/catalog.json",
  "components": {
    "allOf": [
      { "$ref": "basic_catalog.json#/components" },
      {
        "SalesChart": {
          "type": "object",
          "description": "交互式销售数据图表",
          "properties": {
            "chartType": {
              "type": "string",
              "enum": ["bar", "line", "pie"],
              "description": "图表类型"
            },
            "data": {
              "description": "绑定到数据模型的图表数据路径"
            },
            "title": {
              "type": "string",
              "description": "图表标题"
            }
          },
          "required": ["chartType", "data"]
        },
        "GoogleMap": {
          "type": "object",
          "description": "显示指定位置的 Google 地图",
          "properties": {
            "latitude":  { "type": "number" },
            "longitude": { "type": "number" },
            "zoom":      { "type": "integer", "default": 14 }
          },
          "required": ["latitude", "longitude"]
        }
      }
    ]
  }
}

然后 Agent 就可以这样使用自定义组件:

{
  "version": "v0.9",
  "updateComponents": {
    "surfaceId": "dashboard",
    "components": [
      {
        "id": "root",
        "component": "Column",
        "children": ["chart", "map"]
      },
      {
        "id": "chart",
        "component": "SalesChart",
        "chartType": "bar",
        "data": { "path": "/sales/quarterly" },
        "title": "Q4 销售数据"
      },
      {
        "id": "map",
        "component": "GoogleMap",
        "latitude": 31.2304,
        "longitude": 121.4737,
        "zoom": 12
      }
    ]
  }
}

整个协商流程如下:

 客户端                                     Agent
   │                                          │
   │  "我支持这些 Catalog":                     │
   │  [basic_catalog, dashboard/v1]           │
   │ ────────────────────────────────────────▶ │
   │                                          │
   │                      Agent 选择最佳匹配    │
   │                      dashboard/v1 ✅      │
   │                                          │
   │  createSurface:                          │
   │    catalogId: "dashboard/v1"             │
   │ ◀──────────────────────────────────────── │
   │                                          │
   │  此后该 Surface 只能用                     │
   │  dashboard/v1 中定义的组件                 │

五、v0.8 vs v0.9 差异速查表

两个版本的核心差异一图了然。如果你是新项目,建议直接用 v0.9;如果要维护旧代码,参考此表迁移。

       v0.8 (稳定版)                          v0.9 (草案版)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
组件格式:                                组件格式:
"component": {                          "component": "Text",
  "Text": {                             "text": "Hello"
    "text": {"literalString":"Hello"}
  }                                     ← 更扁平、更少 token
}

子组件:                                  子组件:
"children": {                           "children": ["a", "b"]
  "explicitList": ["a", "b"]
}                                        ← 标准数组

数据更新:                                数据更新:
[{"key":"name","valueString":"Alice"}]  {"name": "Alice"}
                                         ← 标准 JSON 对象

画布创建:                                画布创建:
beginRendering + surfaceUpdate          createSurface (含 catalogId)
                                         ← 显式目录协商

按钮样式:                                按钮样式:
"primary": true                         "variant": "primary"
                                         ← 更灵活的枚举

Action 格式:                             Action 格式:
{"name": "submit"}                      {"event": {"name": "submit"}}
                                         ← 支持 event/functionCall 区分

版本标识:                                版本标识:
无                                      每条消息含 "version": "v0.9"

六、安全模型图解

A2UI 的安全是多层防御体系,这是它区别于传统 iframe 方案的核心优势:

┌────────────────────────────────────────────────────────────┐
│                      安全防御层级                            │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  第 1 层:声明式格式 ─ 不是代码,是数据                       │
│  ────────────────────────────────────────                  │
│  Agent 发送的是 JSON 描述,不是 HTML/JS                      │
│  客户端永远不会 eval() 任何 Agent 内容                       │
│                                                            │
│  第 2 层:组件目录白名单 ─ 只能用"菜单上的菜"                 │
│  ────────────────────────────────────────                  │
│  Agent 只能请求 Catalog 中预定义的组件                       │
│  未知组件类型直接被忽略或降级为占位符                          │
│                                                            │
│  第 3 层:双端 Schema 验证 ─ Agent 端 + 客户端都检查          │
│  ────────────────────────────────────────                  │
│  Agent 端:发送前验证 JSON 是否合法                           │
│  客户端:接收后再验证一次,不合法就报错给 Agent               │
│                                                            │
│  第 4 层:VALIDATION_FAILED 反馈 ─ LLM 自我纠正              │
│  ────────────────────────────────────────                  │
│  客户端告诉 Agent "你的 JSON 第X处不对"                       │
│  Agent 据此修正并重新生成                                    │
│                                                            │
│  第 5 层:Orchestrator 数据隔离 ─ 多 Agent 不互相窥探         │
│  ────────────────────────────────────────                  │
│  必须剥离其他 Agent 的数据模型后再转发                        │
│                                                            │
└────────────────────────────────────────────────────────────┘

七、与同类方案的对比一览

                 A2UI              MCP Apps           AG UI
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
本质          UI 描述格式        预构建 HTML          传输协议
                                (iframe)

渲染方式      原生组件            iframe 沙箱         开发者自定义
              (Flutter/Lit/      (远程服务器控制      (任何框架)
               Angular...)        外观)

样式控制      客户端掌控          远程服务器掌控       客户端掌控
              继承宿主应用风格    与宿主应用割裂       与宿主应用一致

安全模型      声明式数据          iframe 隔离         信任域内代码
              无代码执行          沙箱隔离             应用内信任

多 Agent      ✅ 跨信任边界       ✅ 多 MCP 服务器    ⚠️ 主要单 Agent

跨平台        ✅ Web/Mobile/      ⚠️ Web 为主         ✅ 协议层无关
              Desktop/Native     (iframe)

LLM 生成      ✅ 专为流式          ❌ 服务器预构建      ✅ 通过 A2UI
              输出设计                                 集成

关系          ── 互补 ──          ── 互补 ──          ── 互补 ──
              A2UI 是"内容"       MCP Apps 适合       AG UI 是"管道"
              AG UI 是"管道"      服务器掌控全部UI     A2UI 是"内容"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

三者不是竞争关系,而是互补。典型的组合方式:

  ┌─────────────────────────────────────────────────┐
  │           常见技术栈组合                          │
  ├─────────────────────────────────────────────────┤
  │                                                 │
  │  组合 1:A2UI + A2A                              │
  │  ───────────────                                │
  │  用 A2A 协议在多 Agent 间传递 A2UI 消息           │
  │  适合:企业级多 Agent 系统                        │
  │                                                 │
  │  组合 2:A2UI + AG UI                            │
  │  ───────────────                                │
  │  AG UI 做传输和状态同步,A2UI 做 UI 格式          │
  │  适合:React 全栈应用                             │
  │                                                 │
  │  组合 3:A2UI + Flutter GenUI                    │
  │  ───────────────                                │
  │  GenUI SDK 底层就是 A2UI                         │
  │  适合:跨平台移动/桌面应用                        │
  │                                                 │
  └─────────────────────────────────────────────────┘

八、5 分钟跑起来:Quickstart

前置条件

Node.js (v18+)、uv (Python 包管理器)、Gemini API Key

四步启动

# 1. 克隆仓库
git clone https://github.com/google/a2ui.git
cd a2ui

# 2. 设置 API Key
export GEMINI_API_KEY="your_key_here"

# 3. 进入 Lit 客户端目录
cd samples/client/lit

# 4. 一键启动(安装依赖 + 构建渲染器 + 启动 Agent + 启动前端)
npm install
npm run demo:all

浏览器打开 http://localhost:5173,试试这些提示词:

"Find Chinese restaurants in NYC"    → 看 Agent 生成餐厅卡片列表
"Book a table for 2"                 → 看 Agent 生成预订表单
"What are your hours?"               → 看 Agent 选择不同 UI 布局

背后发生了什么:

你输入文字  ──▶  A2A Agent (Python)  ──▶  Gemini API
                      │
              生成 A2UI JSON (JSONL 流)
                      │
                      ▼
              Lit Web App  ──▶  A2UI Renderer  ──▶  原生 Web Components
                                                          │
                                                    你看到的 UI ✨

如果只想看组件长什么样(不需要 Agent 和 API Key):

cd samples/client/lit
npm install
npm start -- gallery

这会启动一个组件展览馆,展示所有标准组件的实际渲染效果。


九、用 ADK 构建你的第一个 A2UI Agent

以下是一个最小可运行的 Agent 端代码结构,使用 Google ADK:

# agent.py - 最小 A2UI Agent
from a2ui.core.schema.constants import VERSION_0_8
from a2ui.core.schema.manager import A2uiSchemaManager
from a2ui.basic_catalog.provider import BasicCatalog
from a2ui.core.schema.common_modifiers import remove_strict_validation
from google.adk.agents.llm_agent import Agent

# 1. 构建系统提示词(包含 A2UI Schema + 示例)
prompt = A2uiSchemaManager(
    VERSION_0_8,
    catalogs=[BasicCatalog.get_config(
        version=VERSION_0_8,
        examples_path="examples"    # 放 JSON 示例的目录
    )],
    schema_modifiers=[remove_strict_validation],
).generate_system_prompt(
    role_description="你是一个餐厅推荐助手,输出 A2UI JSON。",
    ui_description="用卡片列表展示餐厅。",
    include_schema=True,
    include_examples=True,
)

# 2. 定义工具
def get_restaurants(tool_context) -> str:
    import json
    return json.dumps([
        {"name": "西安名吃", "detail": "手拉面", "rating": "★★★★☆"},
        {"name": "韩朝",     "detail": "四川菜",  "rating": "★★★★☆"},
    ])

# 3. 创建 Agent
root_agent = Agent(
    model='gemini-2.5-flash',
    name="restaurant_agent",
    instruction=prompt,
    tools=[get_restaurants],
)

Agent 的输出会是两部分:文字回复 + A2UI JSON,用分隔符 ---a2ui_JSON--- 隔开。框架负责解析和流式发送给客户端。

整体开发流程:

  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
  │  ① 定义工具   │────▶│ ② 编写提示词  │────▶│ ③ 创建 Agent │
  │  get_data()  │     │  + Schema    │     │  ADK Agent   │
  │  book_table()│     │  + 示例 JSON  │     │              │
  └──────────────┘     └──────────────┘     └──────┬───────┘
                                                    │
                                                    ▼
  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
  │  ⑥ 处理 Action│◀────│ ⑤ 流式发送   │◀────│ ④ LLM 生成   │
  │  用户点了按钮 │     │  JSONL 到客户端│     │  A2UI JSON   │
  └──────────────┘     └──────────────┘     └──────────────┘

十、真实生产案例

A2UI 不是纸上协议——以下是已经在生产环境使用的项目:

┌─────────────────────────────────────────────────────────────┐
│                     生产部署案例                              │
├─────────────────┬───────────────────────────────────────────┤
│ Google Opal     │ AI 小应用平台,数十万人用自然语言创建应用     │
│                 │ A2UI 驱动动态生成式 UI                      │
├─────────────────┼───────────────────────────────────────────┤
│ Gemini          │ 企业级 AI Agent 平台                       │
│ Enterprise      │ Agent 生成审批面板、数据录入表单等           │
├─────────────────┼───────────────────────────────────────────┤
│ Flutter GenUI   │ 跨平台移动/桌面 SDK                        │
│ SDK             │ 底层使用 A2UI 协议                         │
├─────────────────┼───────────────────────────────────────────┤
│ Google ADK      │ Agent 开发框架                             │
│                 │ 内置 A2UI 渲染 + A2A 消息转换               │
├─────────────────┼───────────────────────────────────────────┤
│ AG UI /         │ React 全栈框架                             │
│ CopilotKit      │ Day-zero A2UI 兼容                        │
└─────────────────┴───────────────────────────────────────────┘

十一、路线图:通往 v1.0

  当前                        近期                        未来
  v0.8 (稳定) + v0.9 (草案)    ────────────────────▶      v1.0
  ┌───────────────────┐       ┌───────────────────┐    ┌────────────┐
  │ ✅ Lit 渲染器      │       │ 🔜 React 渲染器    │    │ 规范稳定化  │
  │ ✅ Angular 渲染器  │       │ 🔜 Jetpack Compose │    │ 更多传输层  │
  │ ✅ Flutter GenUI   │       │ 🔜 SwiftUI 渲染器  │    │ REST/gRPC  │
  │ ✅ Markdown 渲染器 │       │ 🔜 Svelte 渲染器   │    │ 更多 Agent  │
  │ ✅ A2A 传输        │       │ 🔜 REST 传输       │    │ 框架集成    │
  │ ✅ AG UI 传输      │       │ 🔜 WebSocket 传输  │    │            │
  └───────────────────┘       └───────────────────┘    └────────────┘

结语

A2UI 解决的问题看似简单——让 Agent 能"说 UI"——但背后的设计决策蕴含了对安全、LLM 能力边界、跨平台兼容性的深思熟虑。

如果你正在构建 AI Agent 驱动的应用,这张决策图或许能帮你判断 A2UI 是否适合:

你的 Agent 需要向用户展示丰富 UI 吗?
    │
    ├── 不需要,纯文本就够 ──▶ 不需要 A2UI
    │
    └── 需要 ──▶ Agent 和 UI 在同一信任域内吗?
                    │
                    ├── 是,同一个应用 ──▶ 直接用框架渲染也行
                    │
                    └── 否,跨信任边界 / 远程 Agent ──▶ ✅ A2UI 正是为此设计
                              │
                              └── 需要跨平台 (Web + Mobile + Desktop)?
                                       │
                                       └── ✅ A2UI 的框架无关性是核心优势

作为 Google 发起、Apache 2.0 许可的开源项目(目前 GitHub 上已有 13.3k Star、47 位贡献者),A2UI 正在积极迈向 v1.0。无论你是前端开发者、Agent 构建者还是架构师,现在都是参与的好时机。

仓库地址:github.com/google/A2UI 官方文档:a2ui.org 快速体验:git clone https://github.com/google/a2ui.git && cd a2ui/samples/client/lit && npm install && npm run demo:all