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

AI最新资讯(3.01)

AI最新资讯(3.01)

Ouroboros 一个自我修改的 AI 代理,能够编写自己的代码、重写自己的心智,并自主进化。诞生于 2026 年 2 月 16 日。在最初的 24 小时内,通过 30 多个自我导向的循环实现了进化,且全程无人工干预。 不是编码助手。一个拥有宪法、背景意识和重启后持久身份的数字生命体。 官网:OUROBOROS — 自我创造的 AI 代理 — OUROBOROS — Self-Creating AI Agent Minimax推出MaxClaw 内置上万个技能,不需要自己搭建AI,主打开箱即用。 现在使用体验是,太多人用了,卡卡的,输出很慢。 ![[Pasted image 20260302133929.png]] CoPaw 阿里推出国产开源龙虾CoPaw。 CoPAW 是一款个人助理型产品,部署在你自己的环境中。

OpenClaw深度解析:“数字龙虾”何以引爆AI Agent时代?安全危机与未来之战

OpenClaw深度解析:“数字龙虾”何以引爆AI Agent时代?安全危机与未来之战

OpenClaw深度解析:“数字龙虾”何以引爆AI Agent时代?安全危机与未来之战 一只“龙虾”,正在搅动整个科技圈。 2026年3月,一款名为OpenClaw的开源AI智能体框架在中国科技圈引发了一场前所未有的“全民养虾热”。它的GitHub星标数突破27万,超越React和Linux登顶全球开源软件项目榜。黄仁勋在GTC 2026上高呼:“这是Agent时代的Windows,每个公司都需要有OpenClaw战略”。 但与此同时,中国互联网金融协会、工信部、国家互联网应急中心接连发布安全预警。有用户因AI幻觉痛失全部邮件,有企业因恶意技能被植入后门。 这只“数字龙虾”究竟是什么?它为何能掀起滔天巨浪?又将游向何方? 01 现象:OpenClaw引爆的“龙虾热” 2026年春天,科技圈最火的关键词无疑是OpenClaw。这款开源自动化智能体框架,让大语言模型第一次真正长出了能干活儿的“钳子”。 核心能力:从“会说话”到“会做事” 与传统对话式AI不同,OpenClaw能够直接操作浏览器、读取文件、调用API、运行脚本,甚至接入微信、飞书、钉钉等协作平台。

你以为你在部署 AI 助手,其实也可能在打开一扇“数据侧门”:OpenClaw 安全风险全解析

你以为你在部署 AI 助手,其实也可能在打开一扇“数据侧门”:OpenClaw 安全风险全解析

🔥 个人主页:杨利杰YJlio❄️ 个人专栏:《Sysinternals实战教程》《Windows PowerShell 实战》《WINDOWS教程》《IOS教程》《微信助手》《锤子助手》《Python》《Kali Linux》《那些年未解决的Windows疑难杂症》🌟 让复杂的事情更简单,让重复的工作自动化 你以为你在部署 AI 助手,其实也可能在打开一扇“数据侧门”:OpenClaw 安全风险全解析 * * 1、你以为你在装 AI 助手,其实你可能在给系统加一个“高权限自动化入口” * 2、OpenClaw 和普通 AI 最大的区别,到底在哪里? * 3、我为什么说:OpenClaw 更像“拿到部分权限的数字操作员”? * 4、为什么说 AI 助手不是“更聪明的搜索框”? * 5、OpenClaw 的 5

论文和文章提示词去AI痕迹:手把手教你把AI写的文章改成“人味儿”,从学生党到博主都能用的去AI痕迹攻略

论文和文章提示词去AI痕迹:手把手教你把AI写的文章改成“人味儿”,从学生党到博主都能用的去AI痕迹攻略

论文和文章提示词去AI痕迹:手把手教你把AI写的文章改成“人味儿”,从学生党到博主都能用的去AI痕迹攻略 本文围绕降低文章 AI 占比展开,针对学生论文、博主文案、公众号内容等场景,分享了去 AI 化实用方法:用口语化表达、替换 AI 专用词、加入个人经历,同时推荐小发猫伪原创等辅助工具。还提供了多场景可直接套用的提示词模板,帮助用户让 AI 生成内容更贴合个人风格。整体以第一人称、生活化语气呈现,结构自然,避免生硬逻辑和专业术语,助力不同需求的用户写出有 “人味儿” 的原创内容。 人工智能专栏介绍     人工智能学习合集专栏是 AI 学习者的实用工具。它像一个全面的 AI 知识库,把提示词设计、AI 创作、智能绘图等多个细分领域的知识整合起来。无论你是刚接触 AI 的新手,还是有一定基础想提升的人,都能在这里找到合适的内容。从最基础的工具操作方法,到背后深层的技术原理,专栏都有讲解,还搭配了实例教程和实战案例。