A2UI 用户交互机制完全指南:Action 事件、渲染端函数与数据模型同步
【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui
A2UI(Agent-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 中注册的anyFunction |
args | 传给函数的参数 | 值可以是DynamicValue(支持path绑定)或字面量对象 |
returnType | 函数期望的返回类型 | 枚举string/number/boolean/array/object/any/void,默认boolean |
Functions 的常见用途包括:
- 导航(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.name,context是解析完所有数据绑定后的键值对。整个负载是「version + action/error」的二选一结构,最多两个属性。
版本差异(v0.8 vs v0.9):在 v0.8 中,顶层负载键是
userAction(例如{"userAction": {...}});v0.9 改为上面更简洁的action键。标准协议解析器会根据负载中声明的版本匹配对应键。
Agent 侧处理
Agent(或编排器 Orchestrator)收到事件后对其做出反应。在 agentic 系统中,Agent 通常会把事件转换为发给 LLM 的隐藏用户查询。
示例:Agent 处理(Python)
if action_name == "submit_reservation": time = context.get("time") size = context.get("size") # Feed this to the LLM query = f"User 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 Error:
code固定为VALIDATION_FAILED,必填code、path、message、surfaceId,且path是 JSON Pointer(如/components/0/text)。 - Generic Error:
code不能是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 Sync,v0.9)
A2UI 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.json,createSurface消息要求消息负载中createSurface与version字段必填,且sendDataModel是其属性之一。
线上同步形态
启用同步后,渲染器不会把数据模型作为独立消息发送,而是将其作为元数据附加到外发的传输信封(如 A2A 消息)上。在 A2A(Agent-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 直接检查元数据即可看到所有字段的当前状态。 - 无状态 Agent:Agent 不必为每个用户会话维护本地状态,每次交互都收到完整的当前上下文。
- 语音快捷指令(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),但最常通过A2A(Agent-to-Agent)或 WebSockets 使用。理解负载如何被包裹是实现的关键。
A2A 编码
在标准 A2A 绑定中,A2UI 消息被编码为 A2ADataPart。要将其标识为 A2UI 负载,part 必须用特定元数据包裹:
- mimeType:
application/a2ui+json
DataPart的data字段包含一个 A2UI 消息列表。这允许多个更新(例如createSurface后跟updateComponents)在单个网络包中发送。
A2A 版本注意:
data字段使用列表是A2A v1.0引入的。更早版本的 A2A 协议期望data字段包含单个 JSON 对象。
{ "kind": "data", "metadata": { "mimeType": "application/a2ui+json" }, "data": [ { "version": "v0.9", "action": { ... } } ] }安全考量
A2UI 将安全、沙箱化的通信作为核心设计原则。由于协议依赖在网络上传递用户状态与交互触发器,它对数据可见性与执行施加了严格边界。
沙箱化执行
A2UI 的核心卖点之一是「通过限制保障安全」。通过禁止 Agent 执行任意代码(例如注入原生 JavaScript),A2UI 确保 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_event=after_event_save_surface_id) ] )在 orchestrator 示例 中,该拦截器还负责把子 Agent 的卡片信息写入事件元数据,并在 surfaceId 冲突时向子 Agent 异步回传SURFACE_ID_ALREADY_EXISTS错误。
2. 路由客户端事件
当客户端渲染器发回 A2UIaction或error消息时,Orchestrator 用get_subagent_name_for_client_event在会话状态中查找surfaceId,并把请求路由给正确的子 Agent:
from 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 planner's transfer_to_agent function return LlmResponse( content=genai_types.Content( parts=[ genai_types.Part( function_call=genai_types.FunctionCall( name="transfer_to_agent", args={"agent_name": target_agent}, ) ) ] ) ) orchestrator_agent = LlmAgent( name="orchestrator", before_model_callback=route_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视图派发给 Agent;checks在 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),仅供参考