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

人工智能|大模型 —— 开发 —— opencode与agent skills的安装与使用

人工智能|大模型 —— 开发 —— opencode与agent skills的安装与使用

一、Skills下载源 常用的GitHub仓库: 1、https://github.com/anthropics/skills 2、https://github.com/nextlevelbuilder/ui-ux-pro-max-skill 3、https://github.com/hesreallyhim/awesome-claude-code 4、https://github.com/ComposioHQ/awesome-claude-skills Agent Skills市场: Agent Skills 市场 - Claude、Codex 和 ChatGPT Skills | SkillsMP Open Agent Skills Ecosystem: The Agent Skills Directory ClawHub: ClawHub 二、

By Ne0inhk
字节跳动AI IDE:Trae 完全上手指南——从零安装到熟练使用,开启AI驱动开发新范式

字节跳动AI IDE:Trae 完全上手指南——从零安装到熟练使用,开启AI驱动开发新范式

目录 * 前言:当IDE进化为智能体 * 1.初识Trae * 1.1 Trae是什么? * 1.2 Trae的核心优势 * 1.3 谁适合使用Trae? * 2.安装与初始配置 * 2.1 支持的操作系统 * 2.2 下载与安装步骤 * 2.3 验证安装成功 * 3.界面导航(五分钟熟悉布局) * 3.1 核心区域功能说明 * 3.2 常用快捷键速查 * 4.核心AI功能详解 * 4.1 Chat模式:随时提问的编程助手 * 4.2 Builder模式:自然语言生成完整项目 * 4.2.1 实战案例:做一个待办事项应用 * 4.

By Ne0inhk
AI 编程新范式:一文彻底搞懂 LLM、Agent、MCP、Skill 是怎么协作的

AI 编程新范式:一文彻底搞懂 LLM、Agent、MCP、Skill 是怎么协作的

文章目录 * 一、核心结论:AI 编程进入「分工时代」 * 二、LLM 与 Agent 🔥 * 1. LLM(大语言模型) * 2. Agent(智能体) * 3. 对比 * 4. 🧠人脑 vs AI Agent 🤖 * 5. 映射图 * 三、MCP 与 Skill 🔥 * 1. MCP:神经系统协议(神经信号标准、信号如何传递)- 协议 * 2. MCP Server:肢体/器官(真正干活的执行实体)- 服务 * 3. Skill:器官的本能动作(Server本能动作)- 内置能力

By Ne0inhk
能做影视级可商业视频的AI工具,Seedance 2.0 全球首发实测

能做影视级可商业视频的AI工具,Seedance 2.0 全球首发实测

如果你是短片导演、影视团队,或者长期做内容的自媒体,一定有同感: AI 视频不是不好,而是太“难用”。 * 想复刻一个爆款运镜,结果画面乱飞 * 想做商用级视频,角色和产品每一帧都在变 * 想快点出片,却被排队、算力、复杂参数拖住 大多数 AI 视频工具的现状是: 看 Demo 很震撼,真到实操,全靠赌。 而 Seedance2.0 给我的第一感受是—— 它不是在“秀模型能力”,而是在解决真实创作流程中的控制问题,把“做视频”这件事,拉回到像 P 图一样直觉、可控。 一、模型重磅发布:Seedance2.0 到底解决了什么? Seedance2.0 是即梦最新一代视频模型,核心定位非常明确: 影视级质量 + 商业可用 + 一站式生成。

By Ne0inhk