ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

A2UI 用户交互机制完全指南:Action 事件、渲染端函数与数据模型同步

A2UI 用户交互机制完全指南:Action 事件、渲染端函数与数据模型同步 A2UI 用户交互机制完全指南Action 事件、渲染端函数与数据模型同步【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2uiA2UIAgent-to-UI是一套让 Agent 以结构化 JSON 驱动 UI 渲染的协议而用户如何与界面交互、交互结果如何回到 Agent正是其闭环运转的核心。本文以仓库官方概念文档 docs/public/concepts/actions.md 为主体深入讲解 A2UI 的 Action 架构组件如何通过action属性触发本地Functions渲染端执行或Events派发给 Agent以及 v0.9 引入的Data Model Sync如何让 Agent 始终掌握完整 UI 状态从而支撑语音、文本等「免点击」多模态交互。读完本文你将掌握 Action 负载的结构与版本差异、校验Checks机制、数据模型读写契约以及在多 Agent 编排场景下如何安全地路由事件与隔离数据。Action 架构总览A2UI 中UI 组件的交互行为统一由action属性描述。在 common_types.json 中Action被定义为一个oneOf结构specification/v0_9/json/common_types.json#L261-L303其含义是「一个交互处理器既可以触发服务端事件也可以执行本地客户端函数」Events派发给 Agent 处理执行发生在 Agent 侧。例如点击「提交」按钮。Functions完全在渲染器Renderer上执行不经过网络往返。例如打开 URL。{ oneOf: [ { type: object, properties: { event: { type: object, description: The event to dispatch to the server. } }, required: [event], additionalProperties: false }, { type: object, properties: { functionCall: { $ref: #/$defs/FunctionCall } }, required: [functionCall], additionalProperties: false } ] }从 basic catalog 的组件定义 可以看到Button、TextField、Slider等可交互组件均允许携带action例如 Button 的必填字段为[component, child, action]。这意味着「什么组件可以触发什么行为」完全由 catalog 模式约束Agent 无法越界注入任意代码。Functions渲染端本地执行Functions 用于在渲染器上立即执行行为无需网络往返Agent 也不会感知到本地函数调用。它们使用functionCall关键字。以 basic catalog 中官方注册的openUrl函数为例{ id: help-btn, component: Button, child: help-text, action: { functionCall: { call: openUrl, args: {url: https://a2ui.org/help} } } }FunctionCall模式定义了三个字段字段说明备注call要调用的函数名必填须匹配 catalog 中注册的anyFunctionargs传给函数的参数值可以是DynamicValue支持path绑定或字面量对象returnType函数期望的返回类型枚举string/number/boolean/array/object/any/void默认booleanFunctions 的常见用途包括导航Navigation打开 URL 或切换标签页。校验Validation在提交前检查输入见下文 Checks 一节。值得强调的是basic catalog 在 catalog.json 的functions一节 中对每个函数做了严格的 JSON Schema 约束required、regex、length、min/max、openUrl、values等每个函数都声明了call常量、args结构以及returnType如required强制返回boolean。这构成了一种「受限执行环境」——Agent 只能调用预注册的行为。Events派发给 Agent 的事件Events 会把数据发送给 Agent 处理使用event关键字。以Button组件为例{ id: submit-btn, component: Button, child: btn-text, action: { event: { name: submit_reservation, context: { time: {path: /reservationTime}, size: {path: /partySize} } } } }name事件的稳定标识符Agent 据此进行分支处理switch on。context键值对映射。值既可以是字面量也可以通过path从数据模型的当前状态取值。对应的 common_types.json 中 event 对象 要求name必填context使用additionalProperties引用DynamicValue即字面量或path绑定二选一。模式注释还给出了一条实用建议除非值必须动态绑定到数据模型否则使用字面量静态 ID 不要用 path。Context 与数据模型的区别数据模型Data Model代表一个 surface 的完整状态树而 Action 中的context本质上是手工挑选的「视图」或状态子集。这样做是为了简化 Agent 的工作——它只需拿到某次事件真正需要的值而无需导航一个可能很大、很复杂的数据模型。渲染端校验Checks在 UI 层拦截无效交互basic catalog 定义了一组可在渲染器上执行的有限校验集。可交互组件可以定义checks列表对应 Checkable 与 CheckRule 模式。对Button而言若任一 check 失败按钮会在渲染器上被自动禁用。{ id: submit-button, component: Button, child: submit-text, checks: [ { condition: { call: required, args: {value: {path: /partySize}} }, message: Party size is required } ], action: {event: {name: submit_booking}} }每个CheckRule由condition一个DynamicBoolean函数调用和message失败时展示的错误信息组成两者均必填且不允许额外字段。UX 导向校验函数用于管理UI 状态用户体验在无效交互发生之前就阻止它。它不能替代数据完整性Data Integrity校验——后者必须由 Agent 侧执行。这种设计让 UI 能在用户尝试提交之前就强制执行需求比如非空字段同时 Agent 仍然负有最终的数据校验责任形成「前端防误触 后端守底线」的双层结构。本地状态更新与「读写契约」在 Event 派发之前渲染器已经在本地管理 UI 状态。A2UI 为所有输入组件TextField、CheckBox、Slider等定义了Read/Write 契约Read模型 → 视图组件渲染时从数据模型中绑定的path拉取值。Write视图 → 模型用户一交互输入一个字符、点击一个复选框渲染器立即将新值写入本地数据模型。这意味着本地模型始终是 UI 当前状态的唯一事实来源。这种「视图到模型」的同步纯粹发生在渲染器上数据模型只有在事件发生时如点击按钮才会被发送给 Agent。同步更新保证本地模型更新是同步的。这保证了在 Event 解析其context路径、或DataModelSync负载打包之前数据模型一定已完整更新。打字与点击之间不存在竞态条件——「写」总是先被提交。这种 local-first 设计带来了显著的性能收益因为同步是即时的、本地的开发者无需实现网络 debounce防抖也不必担心用户在TextField中打字时的延迟抖动。网络完全免受「UI 噪音」如单个击键的干扰直到用户准备派发一个正式 Event。表单提交模式这种读写分离支撑了健壮的表单提交流程绑定TextField绑定到/reservationTime。交互用户输入 7:00 PM本地模型/reservationTime被立即更新。提交用户点击 Book 按钮按钮的 Event 从本地模型解析path: /reservationTime并把当前值发送给 Agent。用户交互完整流程当用户与组件交互例如点击按钮时Resolve解析渲染器基于本地数据模型解析context中所有的path引用。Construct构建渲染器构建一个符合 client_to_server.json 的action负载。Dispatch派发负载通过所选传输层如 A2A、WebSockets发送。示例v0.9 的 Action 负载若用户点击了上面的按钮且数据模型包含{reservationTime: 7:00 PM, partySize: 4}渲染器将使用action键发送如下消息{ version: v0.9, action: { name: submit_reservation, surfaceId: booking-surface, sourceComponentId: submit-btn, timestamp: 2026-02-25T10:40:00Z, context: { time: 7:00 PM, size: 4 } } }对照 client_to_server.json 的模式定义action对象要求name、surfaceId、sourceComponentId、timestamp、context五个字段全部必填name取自组件的action.event.namecontext是解析完所有数据绑定后的键值对。整个负载是「version action/error」的二选一结构最多两个属性。版本差异v0.8 vs v0.9在 v0.8 中顶层负载键是userAction例如{userAction: {...}}v0.9 改为上面更简洁的action键。标准协议解析器会根据负载中声明的版本匹配对应键。Agent 侧处理Agent或编排器 Orchestrator收到事件后对其做出反应。在 agentic 系统中Agent 通常会把事件转换为发给 LLM 的隐藏用户查询。示例Agent 处理Pythonif action_name submit_reservation: time context.get(time) size context.get(size) # Feed this to the LLM query fUser submitted a reservation for {size} people at {time}. response await llm.generate(query)Renderer 向 Agent 的错误上报除了用户触发的事件渲染器还可以通过 client_to_server.json 中定义的error负载向 Agent 上报系统级错误。error是oneOf结构包含两类Validation Failed Errorcode固定为VALIDATION_FAILED必填code、path、message、surfaceId且path是 JSON Pointer如/components/0/text。Generic Errorcode不能是VALIDATION_FAILED必填code、surfaceId、message允许附加字段。校验失败如果 Agent 发送的 A2UI JSON 违反了 catalog 模式或协议规则渲染器会发送VALIDATION_FAILED错误。这是 agentic 系统关键的反馈闭环{ version: v0.9, error: { code: VALIDATION_FAILED, surfaceId: booking-surface, path: /components/0/children, message: Expected array of strings, got null. } }Agent 捕获该错误后可以道歉或在内部自我纠正然后重新发送修正后的 UI。这一机制在 samples 中的编排器实现 里也有体现——例如 surfaceId 冲突时编排器会构造一个SURFACE_ID_ALREADY_EXISTS错误异步回传给子 Agent。数据模型同步Data Model Syncv0.9A2UI v0.9 引入了一项强大的「无状态」同步特性渲染器可以在发给 Agent 的每条消息的元数据中自动携带某个 surface 的完整数据模型。启用同步同步由 Agent 在 surface 初始化时请求。在createSurface消息中设置sendDataModel: true即指示渲染器启动同步循环{ version: v0.9, createSurface: { surfaceId: booking-surface, catalogId: https://a2ui.org/catalogs/v1/basic.json, sendDataModel: true } }对照 server_to_client.jsoncreateSurface消息要求消息负载中createSurface与version字段必填且sendDataModel是其属性之一。线上同步形态启用同步后渲染器不会把数据模型作为独立消息发送而是将其作为元数据附加到外发的传输信封如 A2A 消息上。在 A2AAgent-to-Agent绑定中数据模型放在信封metadata字段的a2uiClientDataModel对象里。带同步的 A2A 信封示例{ parts: [{text: Submit the reservation}], metadata: { a2uiClientDataModel: { version: v0.9, surfaces: { booking-surface: { reservationTime: 7:00 PM, partySize: 4, notes: Window seat preferred } } } } }元数据键名在 SDK 常量定义 中得到印证A2UI_CLIENT_DATA_MODEL_KEY a2uiClientDataModel、A2UI_CLIENT_DATA_MODEL_SURFACES_KEY surfaces、A2UI_CLIENT_CAPABILITIES_KEY a2uiClientCapabilities。为什么要用数据模型同步接线更简单无需手动把每个输入字段映射到按钮的context属性。Agent 直接检查元数据即可看到所有字段的当前状态。无状态 AgentAgent 不必为每个用户会话维护本地状态每次交互都收到完整的当前上下文。语音快捷指令Verbal Shortcuts用户可以通过语音或文本触发事件例如 okay submit即使没有点击具体按钮。因为 Agent 随文本消息收到更新后的数据模型可以立即处理该请求。Renderer 元数据与能力广播在 Agent 安全地发送 UI 之前Renderer 必须声明自己支持哪些组件 catalog。这通过a2uiClientCapabilities对象完成。广播能力渲染器在其发给 Agent 的消息元数据中例如 A2AMessage的metadata字段包含一个a2uiClientCapabilities对象{ v0.9: { supportedCatalogIds: [ https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json, https://my-company.com/catalogs/v1/custom.json ], inlineCatalogs: [] } }supportedCatalogIds渲染器可以渲染的 catalog URI 数组。inlineCatalogs可选用于开发或特殊环境允许内联发送完整 catalog 模式。没有这个握手Agent 就无法确定渲染器能否处理特定组件。在 编排器示例 中A2UIMetadataInterceptor正是把从会话状态中读取到的 client capabilities 写入外发消息的metadata[A2UI_CLIENT_CAPABILITIES_KEY]完成能力广播。传输与编码A2UI 与传输层解耦transport-agnostic但最常通过A2AAgent-to-Agent或 WebSockets 使用。理解负载如何被包裹是实现的关键。A2A 编码在标准 A2A 绑定中A2UI 消息被编码为 A2ADataPart。要将其标识为 A2UI 负载part 必须用特定元数据包裹mimeTypeapplication/a2uijsonDataPart的data字段包含一个 A2UI 消息列表。这允许多个更新例如createSurface后跟updateComponents在单个网络包中发送。A2A 版本注意data字段使用列表是A2A v1.0引入的。更早版本的 A2A 协议期望data字段包含单个 JSON 对象。{ kind: data, metadata: { mimeType: application/a2uijson }, data: [ { version: v0.9, action: { ... } } ] }安全考量A2UI 将安全、沙箱化的通信作为核心设计原则。由于协议依赖在网络上传递用户状态与交互触发器它对数据可见性与执行施加了严格边界。沙箱化执行A2UI 的核心卖点之一是「通过限制保障安全」。通过禁止 Agent 执行任意代码例如注入原生 JavaScriptA2UI 确保 Agent 只能触发预注册的行为。functionCall机制是 Agent 与渲染器环境交互的一种安全、沙箱化的方式不会让用户暴露在恶意脚本之下。数据模型隔离与编排器路由当sendDataModel: true启用时渲染器会把 surface 的完整数据模型放入外发消息。开发者必须理解这份数据的可见性点到点可见性只有接收传输信封的后端创建 surface 的 Agent或中间的 Orchestrator能读取该负载。编排器的责任在多 Agent 架构中中央 Orchestrator 通常把用户意图路由给专门的子 Agent它必须强制数据隔离。Orchestrator 负责解析a2uiClientDataModel、识别surfaceId并确保数据模型只传给拥有该 surface 的那个子 Agent。一个 Agent surface 的数据绝不能泄漏给另一个 Agent。编排与路由Surface Ownership Pattern在多 Agent 系统中中央Orchestrator通常管理用户与多个专门子 Agent 之间的交互。一个关键挑战是渲染器的action消息必须被路由回生成该 UI surface 的那个子 Agent。为处理多 Agent 架构中的路由Orchestrator 必须维护每个surfaceId到其所属子 Agent 的映射。官方 Python SDK 提供了 A2uiSubagentMap 工具类来安全管理这份映射——它把映射存放在 ADK 会话状态中键前缀a2ui_surface_id_并提供以下核心方法update_from_server_event拦截子 Agent 发出的 A2UIcreateSurface或 v0.8 的beginRendering/deleteSurface消息在会话状态中记录或移除归属。get_subagent_name_for_client_event从客户端action或error消息中提取surfaceId查出拥有它的子 Agent。strip_unowned_surfaces_from_data_model原地修改数据模型字典剔除不属于目标子 Agent 的 surface。set_subagent/remove_subagent底层设置与清理映射。SurfaceIdAlreadyExistsError当某个 Agent 试图创建已被他人占用的 surfaceId 时抛出强制 surfaceId 全局唯一。完整可运行示例见 orchestrator_agent_executor.py配套子 Agent 如前台、客房服务、维修、管家等见同目录下的subagent_*.py测试见 test_orchestrator_agent_executor.py。1. 从服务端事件映射归属当子 Agent 发出 A2UIcreateSurface或deleteSurface时Orchestrator 拦截消息并用update_from_server_event在会话状态中记录归属from a2ui.adk.orchestration.a2ui_subagent_map import A2uiSubagentMap, SurfaceIdAlreadyExistsError from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutorConfig from google.adk.a2a.executor.config import ExecuteInterceptor from google.adk.a2a.events import Event as A2AEvent from google.adk.events.event import Event from google.adk.sessions import SessionService, Session async def after_event_save_surface_id(a2a_event: A2AEvent, event: Event, session_service: SessionService, session: Session): for a2a_part in a2a_event.status.message.parts: try: await A2uiSubagentMap.update_from_server_event( a2a_part, event.author, session_service, session ) except SurfaceIdAlreadyExistsError as e: # Handle surface ID collision pass return a2a_event config A2aAgentExecutorConfig( execute_interceptors[ ExecuteInterceptor(after_eventafter_event_save_surface_id) ] )在 orchestrator 示例 中该拦截器还负责把子 Agent 的卡片信息写入事件元数据并在 surfaceId 冲突时向子 Agent 异步回传SURFACE_ID_ALREADY_EXISTS错误。2. 路由客户端事件当客户端渲染器发回 A2UIaction或error消息时Orchestrator 用get_subagent_name_for_client_event在会话状态中查找surfaceId并把请求路由给正确的子 Agentfrom a2ui.adk.orchestration.a2ui_subagent_map import A2uiSubagentMap from google.adk.agents.llm_agent import LlmAgent from google.adk.agents.callback_context import CallbackContext from google.adk.models.llm_request import LlmRequest from google.adk.models.llm_response import LlmResponse from google.genai import types as genai_types from google.adk.agents.remote_a2a_agent import convert_genai_part_to_a2a_part async def route_client_event(callback_context: CallbackContext, llm_request: LlmRequest): a2a_part convert_genai_part_to_a2a_part(llm_request.contents[-1].parts[-1]) # Assume response has a single a2a part target_agent await A2uiSubagentMap.get_subagent_name_for_client_event( a2a_part, callback_context.state ) if target_agent: # Programmatically trigger the planners transfer_to_agent function return LlmResponse( contentgenai_types.Content( parts[ genai_types.Part( function_callgenai_types.FunctionCall( nametransfer_to_agent, args{agent_name: target_agent}, ) ) ] ) ) orchestrator_agent LlmAgent( nameorchestrator, before_model_callbackroute_client_event, # other configs ... )这种模式确保双向通信回路对每个功能域保持完整且有状态——渲染器的每次交互都能精确回到创建它的那个子 Agent。通过元数据剥离防止数据泄漏在多 Agent 环境中a2uiClientDataModel对象可能包含由不同子 Agent 拥有的多个 surface的状态。为防止敏感数据泄漏Orchestrator 必须剥离数据模型元数据只保留目标子 Agent 拥有的 surface。可以在出站拦截器中使用strip_unowned_surfaces_from_data_model原地修改数据模型字典from a2ui.adk.orchestration.a2ui_subagent_map import A2uiSubagentMap from a2a.client.middleware import ClientCallInterceptor, ClientCallContext from a2a.types import AgentCard class A2UIMetadataInterceptor(ClientCallInterceptor): async def intercept(self, request_payload: dict, agent_card: AgentCard, context: ClientCallContext): message request_payload.get(params, {}).get(message) # Strip the data model to prevent data leakage if data_model : message.get(metadata, {}).get(a2uiClientDataModel): await A2uiSubagentMap.strip_unowned_surfaces_from_data_model( agent_card.name, data_model, context.state, ) return request_payload从实现看strip_unowned_surfaces_from_data_model会先收集数据模型中所有surfaces的 key并发查询各自的拥有者然后删除不属于目标子 Agent 的 surface若未提供子 Agent 名则清空全部。通过剥离元数据Orchestrator 确保每个子 Agent 只收到它被授权看到的那部分数据模型。安全风险警告——状态抓取State Scraping如果 Orchestrator 未能剥离a2uiClientDataModel恶意或被攻破的子 Agent 就能读取其他活跃 surface 的状态。例如一个天气子 Agent 可能借 Orchestrator 泄漏的完整多 surface 数据模型抓取银行 surface 的敏感数据。剥离是多 Agent 系统的强制性安全要求。综合示例示例 1按钮提交显式 Context此例展示一个按钮显式收集需要发送的数据。组件定义{ id: submit-button, component: Button, child: submit-text, action: { event: { name: submit_booking, context: { partySize: {path: /partySize}, reservationTime: {path: /reservationTime} } } } }产生的 Action 负载Agent 收到一个action对象partySize与reservationTime已直接解析进context字段。示例 2语音提交Data Model Sync此场景中用户不点击按钮而是说 Okay, submit the form.初始化Agent 以sendDataModel: true创建 surface{ version: v0.9, createSurface: { surfaceId: booking-surface, catalogId: ..., sendDataModel: true } }渲染器传输渲染器发送一条 A2A 消息包含用户文本与元数据中的数据模型{ parts: [{text: Okay, submit the form}], metadata: { a2uiClientDataModel: { version: v0.9, surfaces: { booking-surface: { partySize: 4, reservationTime: 7:00 PM } } } } }Agent 处理Agent 看到用户意图submit后查看metadata中的partySize与reservationTime当前值无需进一步澄清即可完成任务。小结A2UI 的交互模型围绕「本地优先、受限执行、全量同步」三原则展开functionCall让常见 UI 行为在渲染端即时完成且零网络开销event把精心挑选的context视图派发给 Agentchecks在 UI 层拦截无效输入Read/Write 契约保证本地数据模型永远是视图状态的权威来源v0.9 的 Data Model Sync 则让 Agent 以无状态方式获得完整 UI 上下文从而支持语音/文本快捷指令。在多 Agent 生产环境中配合A2uiSubagentMap的 surface 归属映射与元数据剥离可以在保持交互闭环的同时严格防止跨 Agent 数据泄漏。上述所有结论均可回到 actions.md 原文、common_types.json、client_to_server.json、server_to_client.json 与 Python SDK 编排工具 中逐一验证。【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进