核心内容摘要
XBKK.CC性巴克:不止于触动,更在于唤醒
本文深入解析 Google 开源的 A2UI 协议探讨其核心架构、数据流设计以及为何它是 LLM 生成 UI 的最佳实践。
A2UI 是什么A2UI (Agent-to-User Interface)是 Google 于 2025 年开源的声明式 UI 协议。
它解决了一个核心问题如何让 AI Agent 安全地跨信任边界发送富交互 UI传统的 Agent 交互往往是纯文本对话效率低下。
而直接让 LLM 生成 HTML/JS 代码又存在严重的安全风险。
A2UI 提供了一个中间方案Agent 发送声明式 JSON 描述 UI 意图客户端使用自己的原生组件渲染。
安全性像数据一样安全 表达力像代码一样丰富
核心设计理念
1 三层解耦架构A2UI 的核心哲学是将三个关键元素解耦┌─────────────────────────────────────────────────────────┐ │ A2UI 三层架构 │ ├─────────────────────────────────────────────────────────┤ │
组件树 (Structure) - Agent 提供的抽象 UI 结构 │ │
数据模型 (State) - 动态填充 UI 的应用状态 │ │
组件目录 (Catalog) - 客户端定义的可信组件映射 │ └─────────────────────────────────────────────────────────┘这种设计带来的好处安全性Agent 只能使用客户端预定义的组件无法注入恶意代码灵活性同一份 UI 描述可在不同框架Angular/Flutter/React上渲染高效性数据变更无需重发整个 UI 结构
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 种服务端到客户端的消息类型
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 区域支持多个独立 Surfacecomponents扁平组件列表通过 ID 引用建立父子关系组件属性支持字面值或数据绑定
2 dataModelUpdate - 填充数据{dataModelUpdate:{surfaceId:booking-form,path:/reservation,contents:[{key:date,valueString:
T19:00:00Z},{key:guests,valueInt:2},{key:restaurant,valueMap:[{key:name,valueString:川味轩},{key:rating,valueNumber:
8}]}]}}设计亮点数据与 UI 结构分离修改数据无需重发组件定义支持嵌套数据结构valueMap类型安全valueString/valueInt/valueBoolean/valueNumber
3 beginRendering - 触发渲染{beginRendering:{surfaceId:booking-form,root:title,catalogId:https://github.com/google/A2UI/.../standard_catalog_definition.json}}为什么需要这个消息防止闪烁客户端缓冲组件等待明确信号再渲染指定根组件从哪个组件开始构建树指定组件目录告诉客户端使用哪套组件定义
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:
T19:00, guests: 2} │ │ }} │ └─────────────────────────────────────────────────────────────────┘ │ 通过 A2A 消息发送回 Agent ▼ ┌─────────────────┐ │ AI Agent │ ← 处理用户操作可能更新 UI 或完成任务 └─────────────────┘
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 需要调用外部服务完成预订。
这可以通过以下方式实现方式 1Agent 内置 Tool函数调用# 在 Agent 中定义预订工具defbook_restaurant(restaurant_id:str,date:str,time:str,guests:int,tool_context:ToolContext)-str:调用餐厅预订 APIresponserequests.post(https://api.restaurant.com/bookings,json{restaurant_id:restaurant_id,datetime:f{date}T{time},party_size:guests},headers{Authorization:fBearer{API_KEY}})returnresponse.json()# Agent 配置agentLlmAgent(tools[get_restaurants,book_restaurant],# 添加预订工具instruction当用户确认预订时调用 book_restaurant 工具...)方式 2通过 MCP (Model Context Protocol) 调用# Agent 通过 MCP 连接到餐厅预订服务mcp_clientMCPClient(restaurant-booking-server)# MCP 服务器暴露的工具# - create_booking(restaurant_id, datetime, guests)# - cancel_booking(booking_id)# - get_availability(restaurant_id, date)resultawaitmcp_client.call_tool(create_booking,{restaurant_id:chuanwei-001,datetime:
T19: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_clientA2AClient(https://booking-agent.example.com)responseawaita2a_client.send_message({message:{role:user,parts:[{kind:data,data:{task:create_booking,details:booking_details}}]}})returnresponse
2 A2UI 的职责边界理解这一点很重要┌─────────────────────────────────────────────────────────────────┐ │ 职责分离 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ A2UI 协议负责 │ │ ├── UI 结构描述 (surfaceUpdate) │ │ ├── 数据绑定 (dataModelUpdate) │ │ ├── 用户交互事件传递 (userAction) │ │ └── 渲染控制 (beginRendering) │ │ │ │ A2UI 协议不负责 │ │ ├── 业务逻辑执行预订、支付等 │ │ ├── 外部 API 调用 │ │ ├── 数据持久化 │ │ └── 身份认证 │ │ │ │ 业务逻辑由 Agent 通过以下方式实现 │ │ ├── 内置 Tools函数调用 │ │ ├── MCP 服务器 │ │ ├── A2A 委托给专业子 Agent │ │ └── 直接 HTTP/gRPC 调用 │ │ │ └─────────────────────────────────────────────────────────────────┘简单来说A2UI 是 UI 层协议业务逻辑由 Agent 自行决定如何实现。
标准组件目录A2UI v
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 如何知道可以使用哪些组件
1 协商流程┌─────────────────────────────────────────────────────────────────┐ │ 组件目录协商流程 │ └─────────────────────────────────────────────────────────────────┘ 步骤 1: Agent 在 Agent Card 中声明支持的目录 ↓ ┌─────────────────────────────────────────────────────────────────┐ │ { │ │ name: Restaurant Finder, │ │ capabilities: { │ │ extensions: [{ │ │ uri: https://a2ui.org/a2a-extension/a2ui/v
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 │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────┘
2 LLM 如何被约束只使用已知组件关键在于Prompt Engineering JSON Schema 约束# Agent 开发者在调用 LLM 时将组件目录作为 Schema 约束传入#
加载客户端支持的组件目录catalogload_catalog(standard_catalog_definition.json)#
构建包含组件定义的 JSON Schemaresolved_schema{properties:{surfaceUpdate:{properties:{components:{items:{properties:{component:{# 这里只包含目录中定义的组件类型properties:catalog[components]}}}}}}}}#
使用 Structured Output 模式调用 LLMresponsellm.generate(prompt生成一个餐厅预订表单,response_schemaresolved_schema# LLM 只能输出符合 Schema 的 JSON)约束机制层级约束方式说明LLM 层JSON Schema / Structured Output现代 LLMGPT-
Gemini支持强制输出符合 Schema 的 JSONAgent 层Prompt 中包含组件目录告诉 LLM 可用的组件类型和属性协议层目录协商Client 声明支持的目录Agent 只能选择其中之一渲染层组件白名单Client 渲染器只渲染已注册的组件类型
3 如果 Agent 发送了未知组件会怎样// Agent 错误地发送了一个不存在的组件{surfaceUpdate:{components:[{id:evil,component:{ScriptExecutor:{//不在目录中code:alert(hacked)}}}]}}Client 的处理方式忽略未知组件渲染器在 WidgetRegistry 中找不到ScriptExecutor跳过该组件显示占位符渲染一个错误提示组件发送错误消息通过error消息通知 Agent// Client 返回错误{error:{type:unknown_component,componentId:evil,componentType:ScriptExecutor,message:Component type ScriptExecutor is not in the supported catalog}}
4 自定义组件的安全扩展如果业务需要自定义组件如图表、地图流程如下┌─────────────────────────────────────────────────────────────────┐ │ 自定义组件安全流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │
Client 开发者实现自定义组件本地代码完全可控 │ │ class PieChartComponent { render(data) { ... } } │ │ │ │
在 WidgetRegistry 中注册 │ │ registry.register(PieChart, PieChartComponent) │ │ │ │
定义组件 Schema加入自定义目录 │ │ { PieChart: { properties: { data: {...} } } } │ │ │ │
在 a2uiClientCapabilities 中声明支持该目录 │ │ │ │
Agent 现在可以安全地使用 PieChart 组件 │ │ │ └─────────────────────────────────────────────────────────────────┘安全保证自定义组件的实现代码在 Client 侧Agent 只能传递数据参数无法注入逻辑。
安全模型
总结┌────────────────────────────────────────────────────────┐ │ A2UI 安全边界 │ ├────────────────────────────────────────────────────────┤ │ Agent 侧不可信 │ Client 侧可信 │ │ ───────────────── │ ───────────────── │ │ • 生成 JSON 描述 │ • 定义组件目录 │ │ • 只能引用已知组件类型 │ • 实现组件渲染逻辑 │ │ • 无法执行任意代码 │ • 控制样式和行为 │ │ • 受 JSON Schema 约束 │ • 验证数据绑定 │ └────────────────────────────────────────────────────────┘关键安全特性声明式数据Agent 发送的是数据不是代码组件白名单只能使用客户端预定义的组件目录协商双向声明取交集Schema 约束LLM 输出受 JSON Schema 强制约束无 eval/innerHTML客户端渲染器不执行任意字符串数据绑定验证路径解析在客户端控制
八、
总结A2UI 通过精巧的协议设计解决了 AI Agent 生成 UI 的核心挑战挑战A2UI 解决方案安全性声明式 JSON 组件白名单LLM 生成难度邻接表模型 流式传输跨平台抽象组件 客户端渲染性能数据/结构分离 增量更新如果你正在构建 AI Agent 应用A2UI 值得深入研究。
它代表了 Agent UI 领域的最佳实践。
参考资料A2UI GitHub 仓库A2UI 官方文档A2UI v