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

引言:Agent 时代的 UI 困境

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

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

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

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


一、A2UI 全景架构图

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

scss 复制代码
┌──────────────────────────────────────────────────────────────┐
│                        用户 (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 五个关键词

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

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

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

bash 复制代码
传统嵌套树(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 路径连接:

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

三、消息生命周期图解

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

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

四、实战示例:由浅入深

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

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

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

json 复制代码
// 消息 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。

渲染效果示意:

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

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

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

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

json 复制代码
// 消息 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" 时,名字自动变化,不需要重新发送组件定义。这就是结构与状态分离带来的高效更新。

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

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

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

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

json 复制代码
// 消息 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 指向数据模型,客户端在点击时解析出当前值。

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

json 复制代码
{
  "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 处理代码类似:

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 返回一组搜索结果时,不需要为每条结果分别定义组件------用一个模板 + 数据数组即可自动渲染:

json 复制代码
// 组件定义:一个模板驱动的列表
{
  "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,即 "韩朝"。

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

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

🔴 高级:多 Agent 编排(Orchestrator)

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

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

scss 复制代码
                           ┌───────────────────┐
              用户问题       │   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 的数据泄露。

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

🔴 高级:自定义组件 Catalog

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

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

json 复制代码
{
  "$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 就可以这样使用自定义组件:

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

整个协商流程如下:

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

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

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

css 复制代码
       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 方案的核心优势:

javascript 复制代码
┌────────────────────────────────────────────────────────────┐
│                      安全防御层级                            │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  第 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 的数据模型后再转发                        │
│                                                            │
└────────────────────────────────────────────────────────────┘

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

css 复制代码
                 A2UI              MCP Apps           AG UI
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
本质          UI 描述格式        预构建 HTML          传输协议
                                (iframe)
                                
渲染方式
相关推荐
米小虾2 小时前
hackerbot-claw 攻击事件深度解析:AI Agent 时代的安全警钟
github·ai编程
进击的野人3 小时前
Prompt工程入门指南:写给AI学习新手的提示词秘籍
人工智能·aigc·ai编程
甲维斯3 小时前
用完火山,腾讯,阿里的编程模型,我失眠了!
ai编程
树獭叔叔3 小时前
别再盲目堆残差了!Moonshot AI 的 AttnRes 如何让 LLM 训练提速 25%?
后端·aigc·openai
码路飞3 小时前
GPT-5.4 mini 和 nano 昨天刚发,我连夜测了一下,说说真实感受
gpt·openai·api
KevinZhang135794 小时前
第 8 节:集成 CubeJS 数据模型
ai编程·vibecoding
晨欣4 小时前
如何根据 config.json 核对 MoE 模型的激活参数:以 gpt-oss-120b 为例(GPT-5.4-high 生成)
gpt·大模型·json·openai
一块小方糖4 小时前
OA工时填报Skill,打通gitlab与禅道,实现每日工时自动提交
ai编程
QX_hao4 小时前
Codex_AGENTS_设置教学文档
ai编程