news 2026/9/14 13:07:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
A2UI 用户交互机制完全指南:Action 事件、渲染端函数与数据模型同步

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),其含义是「一个交互处理器,既可以触发服务端事件,也可以执行本地客户端函数」:

  1. Events:派发给 Agent 处理,执行发生在 Agent 侧。例如点击「提交」按钮。
  2. 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 的组件定义 可以看到,ButtonTextFieldSlider等可交互组件均允许携带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 约束(requiredregexlengthmin/maxopenUrlvalues等),每个函数都声明了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"}} }

每个CheckRulecondition(一个DynamicBoolean函数调用)和message(失败时展示的错误信息)组成,两者均必填且不允许额外字段。

  • UX 导向:校验函数用于管理UI 状态(用户体验),在无效交互发生之前就阻止它。它不能替代数据完整性(Data Integrity)校验——后者必须由 Agent 侧执行。

这种设计让 UI 能在用户尝试提交之前就强制执行需求(比如非空字段),同时 Agent 仍然负有最终的数据校验责任,形成「前端防误触 + 后端守底线」的双层结构。

本地状态更新与「读写契约」

在 Event 派发之前,渲染器已经在本地管理 UI 状态。A2UI 为所有输入组件(TextFieldCheckBoxSlider等)定义了Read/Write 契约

  1. Read(模型 → 视图):组件渲染时,从数据模型中绑定的path拉取值。
  2. Write(视图 → 模型):用户一交互(输入一个字符、点击一个复选框),渲染器立即将新值写入本地数据模型。

这意味着本地模型始终是 UI 当前状态的唯一事实来源。这种「视图到模型」的同步纯粹发生在渲染器上;数据模型只有在事件发生时(如点击按钮)才会被发送给 Agent。

同步更新保证:本地模型更新是同步的。这保证了在 Event 解析其context路径、或DataModelSync负载打包之前,数据模型一定已完整更新。打字与点击之间不存在竞态条件——「写」总是先被提交。

这种 local-first 设计带来了显著的性能收益:因为同步是即时的、本地的,开发者无需实现网络 debounce(防抖),也不必担心用户在TextField中打字时的延迟抖动。网络完全免受「UI 噪音」(如单个击键)的干扰,直到用户准备派发一个正式 Event。

表单提交模式

这种读写分离支撑了健壮的表单提交流程:

  • 绑定TextField绑定到/reservationTime
  • 交互:用户输入 "7:00 PM",本地模型/reservationTime被立即更新。
  • 提交:用户点击 "Book" 按钮,按钮的 Event 从本地模型解析path: "/reservationTime",并把当前值发送给 Agent。

用户交互完整流程

当用户与组件交互(例如点击按钮)时:

  1. Resolve(解析):渲染器基于本地数据模型解析context中所有的path引用。
  2. Construct(构建):渲染器构建一个符合 client_to_server.json 的action负载。
  3. 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对象要求namesurfaceIdsourceComponentIdtimestampcontext五个字段全部必填: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 处理(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 上报系统级错误。erroroneOf结构,包含两类:

  • Validation Failed Errorcode固定为VALIDATION_FAILED,必填codepathmessagesurfaceId,且path是 JSON Pointer(如/components/0/text)。
  • Generic Errorcode不能是VALIDATION_FAILED,必填codesurfaceIdmessage,允许附加字段。

校验失败

如果 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消息要求消息负载中createSurfaceversion字段必填,且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 的消息元数据中(例如 A2AMessagemetadata字段)包含一个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 必须用特定元数据包裹:

  • mimeTypeapplication/a2ui+json

DataPartdata字段包含一个 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:从客户端actionerror消息中提取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 发出 A2UIcreateSurfacedeleteSurface时,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. 路由客户端事件

当客户端渲染器发回 A2UIactionerror消息时,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对象,partySizereservationTime已直接解析进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中的partySizereservationTime当前值,无需进一步澄清即可完成任务。

小结

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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 13:01:05

阿基米德优化算法在路径规划中的应用与原理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 12:57:47

外观模式:简化复杂系统的设计艺术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 12:52:56

MuJoCo MJX Shadow Hand 模型详解:E3M5 灵巧手如何适配 GPU 批量仿真

MuJoCo MJX Shadow Hand 模型详解:E3M5 灵巧手如何适配 GPU 批量仿真 【免费下载链接】mujoco Multi-Joint dynamics with Contact. A general purpose physics simulator. 项目地址: https://gitcode.com/GitHub_Trending/mu/mujoco 本文围绕 MuJoCo 仓库中…

作者头像 李华