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 树:

//传统嵌套结构 - 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 采用扁平的邻接表

//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 结构

{"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 - 填充数据

{"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 - 触发渲染

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

为什么需要这个消息?

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

3.4 deleteSurface - 清理 UI

{"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,不调用真实预订系统:

# Agent 收到 userAction 后,LLM 根据 Prompt 生成确认界面# 这是 Demo 演示用,没有真实预订# Prompt 中的指令:# "For confirming a booking: use the CONFIRMATION_EXAMPLE template"

生成的确认 UI:

{"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(函数调用)

# 在 Agent 中定义预订工具defbook_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) 调用

# 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 │ │ 或其他预订平台 │ └──────────────────┘ 
# 主 Agent 通过 A2A 协议委托任务asyncdefdelegate_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组织内容

组件示例:动态列表

// 使用 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 约束

# 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 发送了未知组件会怎样?

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

Client 的处理方式

  1. 忽略未知组件:渲染器在 WidgetRegistry 中找不到 ScriptExecutor,跳过该组件
  2. 显示占位符:渲染一个错误提示组件
  3. 发送错误消息:通过 error 消息通知 Agent
// 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 领域的最佳实践。


参考资料

Read more

Kubernetes与边缘AI最佳实践

Kubernetes与边缘AI最佳实践 1. 边缘AI核心概念 1.1 什么是边缘AI 边缘AI是指在边缘设备上运行AI模型,而不是在云端数据中心。边缘AI可以减少延迟、节省带宽、保护隐私,并在网络连接不稳定时保持服务可用性。 1.2 边缘AI的优势 * 低延迟:数据不需要传输到云端,响应时间更短 * 带宽节省:减少数据传输,降低网络成本 * 隐私保护:敏感数据在本地处理,不离开设备 * 离线运行:在网络连接中断时仍能正常工作 * 分布式计算:充分利用边缘设备的计算资源 2. 边缘Kubernetes集群搭建 2.1 边缘节点配置 边缘节点要求 * 硬件:至少2GB RAM,2核CPU,10GB存储空间 * 网络:稳定的网络连接 * 操作系统:支持Docker的Linux发行版 安装Docker和kubeadm # 安装Docker apt-get update apt-get install -y

从0到1:AI Coding新手入门全攻略

从0到1:AI Coding新手入门全攻略

目录 一、AI Coding 是什么 二、为什么要学习 AI Coding (一)提升效率 (二)降低门槛 (三)紧跟技术趋势 三、准备工作 (一)选择合适的 AI Coding 工具 (二)安装与配置 (三)基础知识储备 四、学习过程 (一)基础语法学习 (二)项目实践 (三)解决常见问题 五、高级技巧与优化 (一)提示词优化 (二)与其他工具协作 (三)持续学习与提升 六、总结与展望 一、AI Coding 是什么 AI Coding,

Vercel Labs Skills:AI 编程安装「技能Skills」的工具

Vercel Labs Skills:AI 编程安装「技能Skills」的工具

🛠️ Vercel Labs Skills:AI 编程安装「技能Skills」的工具 本文介绍 vercel-labs/skills —— 一个通过 npx skills 为多种 AI 编程代理(如 Cursor、Codex、Claude Code、OpenCode 等)统一安装、管理「技能」的 CLI 工具,并配有快速开始与图文示例。 📑 目录 * 💡 Skills 是什么? * ⚡ 快速开始 * 📦 安装技能 * 🖼️ 安装过程说明 * ⌨️ 其他常用命令 * 🔍 以 find-skills 为例:技能的工作流程与原理 * 🤖 支持的代理 💡 Skills 是什么? Skills 是 Vercel Labs 开源的「开放代理技能生态」