本文对比 A2UI 与传统 Agent UI 方案,从架构、安全性、开发效率和 Token 消耗等维度进行深度分析。

一、传统 Agent UI 方案的困境

在 A2UI 出现之前,AI Agent 与用户交互主要有以下几种方式:

方案 1:纯文本对话

用户: 帮我预订明晚7点的餐厅,2人
Agent: 好的,请问您想预订哪家餐厅?
用户: 川味轩
Agent: 请确认:川味轩,明晚7点,2人,对吗?
用户: 对
Agent: 预订成功!

问题

  • 交互轮次多,用户体验差
  • 无法展示复杂信息(图片、表格、表单)
  • 每轮对话都消耗 Token

方案 2:LLM 直接生成 HTML/React

// Agent 返回的代码
const BookingForm = () => {
  const [date, setDate] = useState('2025-12-20');
  return (
    <div>
      <h1>预订餐厅</h1>
      <input type="date" value={date} onChange={e => setDate(e.target.value)} />
      <button onClick={() => submitBooking(date)}>确认</button>
    </div>
  );
};

问题

  • 严重安全风险:执行 LLM 生成的代码可能包含恶意逻辑
  • 框架绑定:生成的 React 代码无法在 Flutter/Angular 中使用
  • Token 消耗高:完整代码比声明式描述长得多

方案 3:iframe 嵌入远程 UI

<iframe src="https://agent-server.com/booking-ui?session=xxx"></iframe>

问题

  • 视觉风格不统一
  • 安全隔离复杂
  • 性能开销大
  • 无法与宿主应用深度集成

二、A2UI 方案概述

A2UI 采用声明式 JSON + 客户端渲染的模式:

// Agent 发送声明式描述
{"surfaceUpdate": {"components": [
  {"id": "title", "component": {"Text": {"text": {"literalString": "预订餐厅"}}}},
  {"id": "date", "component": {"DateTimeInput": {"value": {"path": "/booking/date"}}}}
]}}
客户端使用自己的组件库渲染 → 原生 UI

三、全方位对比

3.1 架构对比

维度 纯文本 生成代码 iframe A2UI
交互丰富度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
安全性 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
跨平台 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
原生体验 N/A ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
实现复杂度 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐

3.2 安全性对比

┌─────────────────────────────────────────────────────────────────┐
│                        安全风险矩阵                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  高风险 ┃  ████████████████████  生成代码(XSS/代码注入)        │
│        ┃  ████████████          iframe(点击劫持/CSP绕过)       │
│        ┃                                                        │
│  低风险 ┃  ██                    A2UI(声明式数据)              │
│        ┃  █                     纯文本(无UI风险)               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

A2UI 安全机制

  1. Agent 只能引用客户端预定义的组件类型
  2. 不执行任何 Agent 生成的代码
  3. 数据绑定路径在客户端验证
  4. 组件行为完全由客户端控制

3.3 开发效率对比

场景 纯文本 生成代码 A2UI
Agent 开发 简单 复杂(需精确 prompt) 中等
Client 开发 复杂(沙箱/安全) 一次性(渲染器)
调试难度 中等
迭代速度

四、Token 消耗深度对比

这是很多开发者关心的核心问题。我们以一个餐厅预订表单为例进行量化分析。

4.1 场景定义

需要生成的 UI 包含:

  • 标题文本
  • 日期选择器
  • 时间选择器
  • 人数输入框
  • 餐厅选择下拉框(5个选项)
  • 确认按钮

4.2 各方案输出对比

方案 A:纯文本多轮对话
轮次1 - Agent: "请选择日期(格式:YYYY-MM-DD)"
轮次2 - 用户: "2025-12-20"
轮次3 - Agent: "请选择时间(格式:HH:MM)"
轮次4 - 用户: "19:00"
轮次5 - Agent: "请输入用餐人数"
轮次6 - 用户: "2"
轮次7 - Agent: "请选择餐厅:1.川味轩 2.粤香楼 3.江南春 4.北京烤鸭 5.日料亭"
轮次8 - 用户: "1"
轮次9 - Agent: "确认预订:川味轩,2025-12-20 19:00,2人?(是/否)"
轮次10 - 用户: "是"
轮次11 - Agent: "预订成功!"

Token 统计(估算):

  • 每轮 Agent 响应:~50 tokens
  • 每轮需要完整上下文:累积增长
  • 总计:约 800-1200 tokens(含上下文)
方案 B:生成 React 代码
import React, { useState } from 'react';
import { DatePicker, TimePicker, Select, InputNumber, Button, Card, Typography } from 'antd';

const BookingForm = () => {
  const [formData, setFormData] = useState({
    date: null,
    time: null,
    guests: 2,
    restaurant: null
  });

  const restaurants = [
    { value: 'chuanwei', label: '川味轩' },
    { value: 'yuexiang', label: '粤香楼' },
    { value: 'jiangnan', label: '江南春' },
    { value: 'beijing', label: '北京烤鸭' },
    { value: 'riliaotin', label: '日料亭' }
  ];

  const handleSubmit = async () => {
    const response = await fetch('/api/booking', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData)
    });
    if (response.ok) {
      alert('预订成功!');
    }
  };

  return (
    <Card title="预订餐厅" style={{ maxWidth: 400, margin: '0 auto' }}>
      <Typography.Title level={4}>预订餐厅</Typography.Title>
      <div style={{ marginBottom: 16 }}>
        <DatePicker 
          placeholder="选择日期"
          onChange={(date) => setFormData({...formData, date})}
          style={{ width: '100%' }}
        />
      </div>
      <div style={{ marginBottom: 16 }}>
        <TimePicker 
          placeholder="选择时间"
          format="HH:mm"
          onChange={(time) => setFormData({...formData, time})}
          style={{ width: '100%' }}
        />
      </div>
      <div style={{ marginBottom: 16 }}>
        <InputNumber
          min={1}
          max={20}
          value={formData.guests}
          onChange={(guests) => setFormData({...formData, guests})}
          addonBefore="人数"
          style={{ width: '100%' }}
        />
      </div>
      <div style={{ marginBottom: 16 }}>
        <Select
          placeholder="选择餐厅"
          options={restaurants}
          onChange={(restaurant) => setFormData({...formData, restaurant})}
          style={{ width: '100%' }}
        />
      </div>
      <Button type="primary" onClick={handleSubmit} block>
        确认预订
      </Button>
    </Card>
  );
};

export default BookingForm;

Token 统计

  • 代码长度:约 1800 字符
  • Token 数:约 450-550 tokens(单次)
  • 如需修改重新生成:每次都是完整代码
方案 C:A2UI 声明式 JSON
{"surfaceUpdate":{"surfaceId":"booking","components":[
{"id":"root","component":{"Column":{"children":{"explicitList":["title","date-row","time-row","guests-row","restaurant-row","submit-btn"]}}}},
{"id":"title","component":{"Text":{"text":{"literalString":"预订餐厅"},"usageHint":"h1"}}},
{"id":"date-row","component":{"DateTimeInput":{"value":{"path":"/booking/date"},"enableDate":true}}},
{"id":"time-row","component":{"DateTimeInput":{"value":{"path":"/booking/time"},"enableTime":true}}},
{"id":"guests-row","component":{"Slider":{"value":{"path":"/booking/guests"},"minValue":1,"maxValue":20}}},
{"id":"restaurant-row","component":{"MultipleChoice":{"selections":{"path":"/booking/restaurant"},"options":[{"label":{"literalString":"川味轩"},"value":"chuanwei"},{"label":{"literalString":"粤香楼"},"value":"yuexiang"},{"label":{"literalString":"江南春"},"value":"jiangnan"},{"label":{"literalString":"北京烤鸭"},"value":"beijing"},{"label":{"literalString":"日料亭"},"value":"riliaotin"}],"maxAllowedSelections":1}}},
{"id":"submit-btn","component":{"Button":{"child":"submit-text","action":{"name":"confirm_booking","context":[{"key":"booking","value":{"path":"/booking"}}]}}}},
{"id":"submit-text","component":{"Text":{"text":{"literalString":"确认预订"}}}}
]}}
{"dataModelUpdate":{"surfaceId":"booking","contents":[{"key":"booking","valueMap":[{"key":"guests","valueInt":2}]}]}}
{"beginRendering":{"surfaceId":"booking","root":"root"}}

Token 统计

  • JSON 长度:约 1400 字符
  • Token 数:约 280-350 tokens(单次)
  • 增量更新只需发送变更部分

4.3 Token 消耗汇总(仅输出部分)

方案 首次生成 修改人数为4人 添加备注字段 总计(完整流程)
纯文本对话 800-1200 +200 +200 ~1400-1600
生成代码 450-550 450-550(重新生成) 500-600 ~1400-1700
A2UI 280-350 ~50(增量) ~80(增量) ~410-480

4.4 重要补充:组件目录的 Token 开销

上面的对比只计算了 LLM 输出的 Token。但 A2UI 有一个隐藏成本:组件目录 Schema 需要作为 Prompt 的一部分发送给 LLM

让我们看看实际的开销:

┌─────────────────────────────────────────────────────────────────┐
│                 A2UI 组件目录 Token 开销                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  标准组件目录 Schema (standard_catalog_definition.json)          │
│  ├── 文件大小: ~22,600 字符                                      │
│  └── Token 数: ~5,000-6,000 tokens                              │
│                                                                 │
│  完整协议 Schema (server_to_client_with_standard_catalog.json)   │
│  ├── 文件大小: ~37,700 字符                                      │
│  └── Token 数: ~8,000-10,000 tokens                             │
│                                                                 │
│  UI 示例模板 (few-shot examples)                                 │
│  └── Token 数: ~2,000-4,000 tokens(视示例数量)                  │
│                                                                 │
│  总计 Prompt 开销: ~10,000-20,000 tokens(每次请求)              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

这意味着什么?

场景 纯文本 生成代码 A2UI
Prompt 开销 ~100 tokens ~500 tokens ~10,000-20,000 tokens
输出开销 ~1,500 tokens ~1,550 tokens ~450 tokens
单次总计 ~1,600 tokens ~2,050 tokens ~10,450-20,450 tokens

4.5 A2UI 的真实 Token 经济学

看起来 A2UI 反而更费 Token?不完全是。需要考虑以下因素:

因素 1:Prompt Caching(提示缓存)

现代 LLM API(如 Anthropic Claude、OpenAI GPT-4)支持 Prompt Caching

┌─────────────────────────────────────────────────────────────────┐
│                    Prompt Caching 机制                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  首次请求:                                                       │
│  [System Prompt + Schema] + [用户消息] → 全价计费                 │
│       ↓ 缓存                                                     │
│                                                                 │
│  后续请求(同一会话或相同前缀):                                   │
│  [缓存命中] + [用户消息] → Schema 部分 90% 折扣                   │
│                                                                 │
│  实际开销:                                                       │
│  - 首次: ~15,000 tokens (全价)                                   │
│  - 后续: ~1,500 tokens (缓存) + ~450 tokens (输出)               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
因素 2:会话内增量更新

A2UI 的核心优势在多轮交互中体现。但需要澄清一点:增量更新发生在客户端渲染层,而非 LLM 生成层

┌─────────────────────────────────────────────────────────────────┐
│                 A2UI 增量更新机制详解                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  客户端维护的状态:                                               │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Surface Map                                            │   │
│  │  ├── surfaceId: "booking"                               │   │
│  │  │   ├── components: Map<id, ComponentDefinition>       │   │
│  │  │   ├── dataModel: Map<path, value>                    │   │
│  │  │   ├── rootComponentId: "root"                        │   │
│  │  │   └── componentTree: (渲染时构建)                     │   │
│  │  └── surfaceId: "confirmation"                          │   │
│  │       └── ...                                           │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  增量更新流程:                                                   │
│                                                                 │
│  1. 收到 surfaceUpdate → 合并到 components Map(按 ID 覆盖)     │
│  2. 收到 dataModelUpdate → 合并到 dataModel Map(按 path 覆盖)  │
│  3. 触发 rebuildComponentTree() → 重新构建渲染树                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

关键代码逻辑(来自 A2uiMessageProcessor):

// 处理组件更新 - 按 ID 合并
private handleSurfaceUpdate(message, surfaceId) {
  const surface = this.getOrCreateSurface(surfaceId);
  for (const component of message.components) {
    // 关键:按 ID 覆盖,不是替换整个 Map
    surface.components.set(component.id, component);
  }
  this.rebuildComponentTree(surface);
}

// 处理数据更新 - 按 path 合并
private handleDataModelUpdate(message, surfaceId) {
  const surface = this.getOrCreateSurface(surfaceId);
  const path = message.path ?? "/";
  // 关键:只更新指定 path,不影响其他数据
  this.setDataByPath(surface.dataModel, path, message.contents);
  this.rebuildComponentTree(surface);
}

这意味着什么?

场景:用户修改预订人数从 2 改为 4

传统方案(LLM 重新生成完整 UI):
  LLM 输出: 完整的表单代码 ~500 tokens

A2UI 方案:
  方式 A - LLM 只生成数据更新:
    {"dataModelUpdate": {"path": "/booking/guests", "contents": [{"key": ".", "valueInt": 4}]}}
    LLM 输出: ~50 tokens 

  方式 B - 客户端直接更新(无需 LLM):
    用户在 UI 上修改 → 客户端直接调用 setData()
    LLM 输出: 0 tokens 

重要澄清

  • 增量更新的 Token 节省取决于 Agent 的实现方式
  • 如果 Agent 每次都让 LLM 重新生成完整 UI,则无法享受增量更新的好处
  • 最佳实践:Agent 应该设计 Prompt 让 LLM 只输出变更部分,或者利用客户端的本地状态管理
因素 3:Structured Output 模式

使用 Gemini/GPT-4 的 Structured Output 模式时,Schema 可以通过 API 参数传递,而非放在 Prompt 中:

# Gemini 示例
response = model.generate_content(
    "生成餐厅预订表单",
    generation_config={
        "response_mime_type": "application/json",
        "response_schema": a2ui_schema  # Schema 通过参数传递,不占用 Prompt Token
    }
)

4.6 Token 对比结论

┌─────────────────────────────────────────────────────────────────┐
│                    Token 消耗真实对比                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  场景 1: 单次简单交互                                            │
│  ├── 纯文本: ⭐⭐⭐⭐⭐ (最省)                                     │
│  ├── 生成代码: ⭐⭐⭐⭐                                           │
│  └── A2UI: ⭐⭐ (Schema 开销大)                                  │
│                                                                 │
│  场景 2: 多轮复杂交互(5+ 轮修改)                                │
│  ├── 纯文本: ⭐⭐ (累积上下文)                                    │
│  ├── 生成代码: ⭐⭐ (每次重新生成)                                │
│  └── A2UI: ⭐⭐⭐⭐ (增量更新 + 缓存)                              │
│                                                                 │
│  场景 3: 高频用户(启用 Prompt Caching)                          │
│  ├── 纯文本: ⭐⭐⭐                                               │
│  ├── 生成代码: ⭐⭐⭐                                             │
│  └── A2UI: ⭐⭐⭐⭐⭐ (Schema 缓存 + 增量更新)                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

结论

  • A2UI 的 Token 优势不在单次请求,而在多轮交互启用缓存的场景
  • 如果只是简单的一次性 UI 生成,纯文本或生成代码可能更经济
  • 对于复杂的、需要多次修改的 UI 场景,A2UI 的增量更新机制能显著节省 Token
  • 生产环境建议启用 Prompt Caching 以最大化 A2UI 的成本优势

五、实际应用场景对比

场景 1:动态表单生成

需求 传统方案 A2UI
根据用户类型显示不同字段 重新生成整个表单代码 更新 surfaceUpdate 中的组件列表
表单验证失败高亮 需要生成验证逻辑代码 更新 dataModelUpdate 中的错误状态
多语言支持 每种语言生成一套代码 只更新 literalString

场景 2:实时数据展示

// A2UI:只更新数据,UI 结构不变
{"dataModelUpdate": {
  "surfaceId": "dashboard",
  "path": "/metrics",
  "contents": [
    {"key": "cpu", "valueNumber": 78.5},
    {"key": "memory", "valueNumber": 62.3}
  ]
}}

传统方案需要重新生成整个仪表盘代码,A2UI 只需 ~50 tokens。

场景 3:多 Agent 协作

┌─────────────────────────────────────────────────────────────────┐
│  主 Agent                                                       │
│    ↓ 委托任务                                                   │
│  餐厅推荐 Agent → 返回 A2UI (surfaceId: "recommendations")      │
│  预订 Agent     → 返回 A2UI (surfaceId: "booking-form")         │
│  支付 Agent     → 返回 A2UI (surfaceId: "payment")              │
│                                                                 │
│  客户端统一渲染所有 Surface,风格一致                            │
└─────────────────────────────────────────────────────────────────┘

传统方案中,每个 Agent 生成的代码风格可能不一致,需要额外适配。

六、迁移建议

从纯文本迁移到 A2UI

  1. 识别高频交互场景:表单填写、列表选择、确认操作
  2. 定义组件目录:根据业务需求选择标准组件或自定义组件
  3. 改造 Agent Prompt:让 LLM 输出 A2UI JSON 而非文本
  4. 集成渲染器:选择 Lit/Angular/Flutter 渲染器

从生成代码迁移到 A2UI

  1. 抽象 UI 模式:将常用 UI 模式映射到 A2UI 组件
  2. 移除代码执行:用 A2UI 渲染器替代 eval/动态组件
  3. 建立组件白名单:确保安全性
  4. 渐进式迁移:先迁移简单场景,逐步扩展

七、总结

A2UI 的核心优势

  1. 安全:声明式数据,无代码执行风险
  2. 高效:Token 消耗降低 70%
  3. 灵活:一次定义,多端渲染
  4. 可维护:增量更新,结构清晰

如果你正在构建 AI Agent 应用,强烈建议评估 A2UI 方案。它不仅能提升用户体验,还能显著降低运营成本。


参考资料

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐