用 ADK + A2A 构建 A2UI 多智能体编排器:酒店前台 Orchestrator 示例全解析
用 ADK A2A 构建 A2UI 多智能体编排器酒店前台 Orchestrator 示例全解析【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui本指南围绕当前仓库中 samples/community/agent/adk/orchestrator 示例展开讲解如何基于 Google Agent Development KitADK与 A2A 协议构建一个集成了 A2UI 扩展的多智能体编排器Orchestrator Agent它负责把用户的每一次请求路由到不同的专家子代理并把各子代理生成的 A2UI 界面surface回传给客户端。读完本文你将掌握 A2UI 在 A2A 多智能体架构中的启用方式、基于transfer_to_agent的动态路由机制、surfaceId 归属管理A2uiSubagentMap、数据模型隔离以及完整的本地运行与测试流程。示例要解决的问题在真实的 Agent 应用中单一 Agent 往往难以覆盖所有领域能力。更常见的形态是一个编排器Orchestrator负责理解用户意图把请求分发给若干专家子代理expert subagent再由子代理返回结果。本示例用酒店场景把这一模式具象化Front Desk前台处理入住/退房Check me in pleaseHousekeeping客房清洁处理房间打扫、补给My room needs cleaningMaintenance维修处理设备故障The AC is brokenRoom Service客房送餐处理点餐配送I want to order a burger to my room。每个子代理不仅返回文本还会通过 A2UI 扩展返回结构化 UI 表单如入住表单、清洁服务表单最终由支持 A2UI 的渲染器呈现给用户。这正是 README.md 所描述的架构ADK 负责 Agent 编排逻辑A2A 负责 Agent 间通信A2UI 扩展负责 UI 定义的下发。架构拆解ADK、A2A 与 A2UI 如何协同A2UI 扩展的启用方式A2UI 是建立在 A2A 协议之上的扩展能力需要在请求中携带扩展头extension header。示例 README.md 明确指出编排器需要通过在请求中加入X-A2A-Extensionshttps://a2ui.org/a2a-extension/a2ui/v0.8头来启用 A2UI 扩展。在实现上这一逻辑位于 orchestrator_agent_executor.py 中的A2UIMetadataInterceptor客户端调用拦截器当会话状态中存在活跃的 UI 版本ACTIVE_UI_VERSION_STATE_KEY时它通过get_a2ui_extension_uri计算对应的扩展 URI并写入http_kwargs[headers]if context and context.state and context.state.get(ACTIVE_UI_VERSION_STATE_KEY): # Add A2UI extension header a2ui_extension_uri get_a2ui_extension_uri( context.state.get(ACTIVE_UI_VERSION_STATE_KEY) ) http_kwargs[headers] {HTTP_EXTENSION_HEADER: a2ui_extension_uri}该拦截器还负责把 A2UI 客户端能力支持哪些 catalog、是否接受内联 catalog 等写入外发消息的 metadata供子代理协商 UI 协议细节。动态路由inference transfer_to_agentREADME 强调的核心路由机制是编排器对每一次请求都做一次推理inference call决定把请求路由给哪个子代理然后通过 ADK 的transfer_to_agent把原始消息转交给子代理。这一过程在后续调用中持续生效——包括客户端回传的 A2UIuserAction用户在界面上点击按钮触发的动作。路由决策由编排器 LLM 的系统提示词驱动。在 _build_agent 中可以看到编排器LlmAgent的指令instruction( You are an orchestrator agent. Your sole responsibility is to analyze the incoming user request, determine the users intent, and route the task to exactly one of your expert subagents ),同时它通过BuiltInPlanner(thinking_config...include_thoughtsTrue...)让模型先思考再决策。子代理的接入RemoteA2aAgent子代理以独立的 A2A 服务形式运行各自监听独立端口编排器通过RemoteA2aAgent与之通信。RemoteA2aAgent的作用是把 ADK 事件翻译成 A2A 消息发送给子代理的 A2A 服务器从RemoteA2aAgent发出的 HTTP 请求同样携带X-A2A-Extensions头以启用 A2UI 扩展。在_build_agent中编排器启动时会逐个拉取子代理的AgentCard通过A2ACardResolver.get_agent_card()做四件事收集子代理声明的 A2UI 扩展信息AGENT_EXTENSION_SUPPORTED_CATALOG_IDS_KEY、AGENT_EXTENSION_ACCEPTS_INLINE_CATALOGS_KEY并去重合并汇总所有子代理的 skills一并声明到编排器自己的 AgentCard 上将子代理的名字清洗为合法标识符非字母数字替换为下划线、避免数字开头用于 ADK 内部引用把子代理的id、name、description、skills序列化为 JSON 作为RemoteA2aAgent的description——这段描述会被追加到编排器系统指令中让 LLM 知道有哪些子代理可用。对应代码片段orchestrator_agent_executor.pyremote_a2a_agent RemoteA2aAgent( clean_name, subagent_card, descriptiondescription, # This will be appended to system instructions a2a_client_factoryA2AClientFactoryWithA2UIMetadata(...), ) subagents.append(remote_a2a_agent)其中A2AClientFactoryWithA2UIMetadata在标准A2AClientFactory基础上为每个远程客户端追加A2UIMetadataInterceptor保证所有对子代理的 A2A 调用都带上 A2UI 扩展头与客户端能力信息。关键基础设施A2uiSubagentMap多子代理场景有一个核心难题A2UI 的surfaceId是全局唯一的客户端回传事件时如何知道该路由给哪个子代理答案在 a2ui_subagent_map.py 的A2uiSubagentMap工具类中。该模块提供三个核心能力服务端事件记录归属子代理每次下发新的 A2UI 消息如beginRendering创建 surface时通过update_from_server_event(a2a_part, author, session_service, session)把surfaceId - 子代理的映射写入 ADK 会话State客户端事件查询归属收到客户端 A2UI 事件userAction、校验错误等时通过get_subagent_name_for_client_event(a2a_part, state)反查出目标子代理数据模型剥离向某个子代理转发a2uiClientDataModelmetadata 前通过strip_unowned_surfaces_from_data_model(agent_name, data_model, state)剔除属于其他子代理的 surface防止数据泄漏。在A2UIMetadataInterceptor中编排器对每个外发消息都会执行数据模型剥离strip_unowned_surfaces_from_data_model确保子代理只能看到自己创建的 surface 数据。面向未来的优化before_model_callback 短路路由README 指出当前版本每次请求都让编排器 LLM 做一次推理来决定路由目标未来版本可以用before_model_callback在调用 LLM 之前程序化地把userAction直接路由到创建该 surface 的子代理从而跳过编排器 LLM降低延迟与成本。这一优化方向在代码中已有雏形——OrchestratorAgentExecutor.programmatically_route_client_event_to_subagentorchestrator_agent_executor.py作为before_model_callback挂载到编排器上当请求的最后一段内容能转换为 A2A part且A2uiSubagentMap.get_subagent_name_for_client_event能查出目标子代理时它直接返回一个transfer_to_agent的 function call 响应不再走 LLMreturn LlmResponse( contentgenai_types.Content( parts[ genai_types.Part( function_callgenai_types.FunctionCall( nametransfer_to_agent, args{agent_name: target_agent}, ) ) ] ) )测试 test_orchestrator_agent_executor.py 中的test_programmatically_route_client_event_to_subagent与test_programmatically_route_client_error_to_subagent分别验证了「子代理创建 surface → 用户触发 action → 路由回子代理」与「客户端上报校验错误 → 路由回子代理」两条路径。编排器服务端生命周期OrchestratorAgentExecutor继承自 ADK 的A2aAgentExecutor通过覆写钩子把 A2UI 能力接入 A2A 服务端会话准备激活扩展并写入状态在_prepare_session中try_activate_a2ui_extension(context, self._agent_card)根据请求头判断是否激活 A2UI 扩展成功后把活跃 UI 版本与客户端能力写入会话状态ACTIVE_UI_VERSION_STATE_KEY/CLIENT_CAPABILITIES_STATE_KEY。后续所有对子代理的外发调用A2UIMetadataInterceptor正是读取这两个状态值来决定扩展头与 metadata 内容。事件后处理记录归属与冲突处理after_event_save_surface_id_to_subagent_name作为ExecuteInterceptor的after_event钩子在每次事件发生后执行根据事件作者event.author找到对应的子代理把其 AgentCard 写入 A2A 事件 metadataa2a_subagent对消息中的每个 part 调用A2uiSubagentMap.update_from_server_event记录/更新 surface 归属处理 surfaceId 冲突若某子代理试图创建已被占用的surfaceId会抛出SurfaceIdAlreadyExistsError编排器构造一个SURFACE_ID_ALREADY_EXISTS错误消息版本、错误码、冲突的 surfaceId、提示信息齐全通过后台任务回传给该子代理让它可以重新生成界面并丢弃冲突的 part。测试test_surface_id_collision验证了冲突 part 被丢弃、子代理收到run_async错误调用处理删除收到deleteSurface时调用A2uiSubagentMap.remove_subagent清理映射见测试test_delete_surface_removes_from_map。子代理的实现方式以 subagent_front_desk.py 为例子代理是独立的 ADKLlmAgent A2A 服务器用DirectJsonFormat(versionVERSION_0_9, catalogs[BasicCatalog...])配置 A2UI 推理格式与 basic catalog并用A2uiPartConverter把 ADK 事件中的 A2UI 内容转换为 A2A part在系统提示词中直接嵌入a2ui-json.../a2ui-json模板先createSurface声明surfaceId与catalogId再updateComponents描述组件树Column布局、TextField输入框、ChoicePicker选项、Button按钮及event动作在 AgentCard 的capabilities.extensions中通过get_a2ui_agent_extension(VERSION_0_8, False, [])与get_a2ui_agent_extension(VERSION_0_9, False, [])声明支持 0.8/0.9 两个版本的 A2UI 扩展通过AgentSkill声明自己的技能如checkin、checkout并给出示例触发语供编排器 LLM 做意图识别。其余三个子代理housekeeping、maintenance、room_service结构一致区别仅在于职责指令、表单结构与端口。前台表单展示了完整的 A2UI v0.9 组件定义是可复制参考的 UI 模板{ version: v0.9, createSurface: { surfaceId: front_desk, catalogId: basic } } { version: v0.9, updateComponents: { surfaceId: front_desk, components: [ {id: root, component: Column, children: [guest_name, action, submit_btn]}, {id: guest_name, component: TextField, label: Guest Name}, {id: action, component: ChoicePicker, label: Action, options: [{label: Check In, value: check_in}, {label: Check Out, value: check_out}], value: []}, {id: submit_btn, component: Button, child: submit_btn_txt, action: {event: {name: front_desk_action}}}, {id: submit_btn_txt, component: Text, text: Submit} ] } }运行示例前置条件按 README.md 的要求需要准备Python 3.9 或更高版本项目的 pyproject.toml 声明requires-python 3.10实际依赖还包括a2a-sdk[http-server]0.3.0、google-adk2.3.0、google-genai1.27.0、a2ui-agent-sdk0.2.4等UV 包管理器示例通过uv sync/uv run管理环境a2ui-agent-sdk以 workspace 方式引用可访问的 LLM 与 API Key。配置 API Keycp .env.example .env # Edit .env with your actual API key (do not commit .env).env.example 定义了必需变量GEMINI_API_KEY若使用 Vertex AI可设置GOOGLE_GENAI_USE_VERTEXAITRUE来跳过 Gemini API Key 校验对应main.py 中的启动检查。模型默认值为gemini/gemini-3.8-flash可通过环境变量LITELLM_MODEL覆盖。一键启动chmod x ./run_demo.sh ./run_demo.shrun_demo.sh 会先执行uv sync然后并行启动五个服务并用不同颜色前缀区分各进程日志服务端口前缀颜色Front Desk前台10011蓝色Housekeeping客房清洁10012品红Maintenance维修10013黄色Room Service客房送餐10014绿色Orchestrator编排器10002红色加粗编排器进程会在子代理启动 2 秒后通过--subagent_urls参数可多次指定把四个子代理的地址传给__main__.py。脚本通过trap kill $(jobs -p)保证退出时清理全部子进程。也可以手动单独启动uv run --no-sync . --port10002 --subagent_urlshttp://localhost:10011 ...或用uv run --no-sync subagent_front_desk.py --port10011启动单个子代理。验证路由效果启动后向编排器发送以下指令即可观察路由与 A2UI 表单下发Check me in please→ 路由到 front desk返回入住/退房表单My room needs cleaning→ 路由到 housekeeping返回清洁服务表单The AC is broken→ 路由到 maintenanceI want to order a burger to my room→ 路由到 room service。每个子代理的表单都通过createSurface声明了全局唯一的surfaceId如front_desk、housekeeping渲染器据此渲染界面用户在界面上触发的事件userAction会回到编排器再按 surfaceId 归属路由到对应子代理。安全注意事项重要README.md 用专门章节强调示例代码仅用于演示 A2UI 与 A2A 协议机制生产环境必须把任何不受你直接控制的 Agent 视为潜在不可信实体。具体包括外部 Agent 返回的所有数据——AgentCard、消息、artifacts、任务状态——都应视为不可信输入。恶意 Agent 可能在name、skills.description等字段中注入精心构造的数据若未清洗就拼进 LLM 提示词可能引发prompt injection攻击收到的任何 UI 定义或数据流同样不可信恶意 Agent 可能伪造界面欺骗用户phishing、通过属性值注入恶意脚本XSS、或生成过度复杂的布局拖垮客户端DoS若应用支持可选嵌入内容如 iframe / web view还需防范恶意外部站点开发者有责任实施充分的防护输入清洗、Content Security PolicyCSP、对可选嵌入内容的严格隔离、安全的凭据管理否则可能引入严重漏洞。值得一提的是示例代码本身已体现了部分防线意识A2UIMetadataInterceptor在向子代理转发数据模型前会调用strip_unowned_surfaces_from_data_model剔除不属于该子代理的 surface避免跨代理数据泄漏after_event_save_surface_id_to_subagent_name则保证 surfaceId 全局唯一、防止子代理之间相互覆盖界面。小结与延伸阅读本示例给出了「ADK 编排 A2A 通信 A2UI 界面」三件套的完整落地路径编排器通过推理动态路由、通过RemoteA2aAgent接入异构子代理、通过A2uiSubagentMap维护 surface 归属、通过拦截器统一注入 A2UI 扩展头与数据模型隔离。对希望在自己项目里复用的读者建议按需阅读编排器核心实现orchestrator_agent_executor.py子代理参考实现subagent_front_desk.py 等四个文件surface 归属映射工具a2ui_subagent_map.py路由、冲突与数据隔离测试test_orchestrator_agent_executor.pyA2UI v0.9 协议与 Basic Catalog 规范v0.9-a2ui.md、v0.9.1-a2ui.md。在此基础上你可以把「before_model_callback 短路路由」从当前雏形升级为正式能力让userAction不再经过编排器 LLM从而获得更低延迟与更省 token 的多智能体 UI 交互链路。【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考