在 CopilotKit 中构建 A2A + A2UI 餐厅预订 Agent:基于 Google ADK 的服务端实现与安全实践
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
本指南基于 CopilotKit 仓库中的 A2A + A2UI 集成示例,深入讲解如何用 Google Agent Development Kit(ADK)配合 A2A(Agent-to-Agent)协议,将一个"餐厅搜索与桌位预订"Agent 以 A2A Server 的形式托管,并通过 A2UI 消息让前端动态渲染富交互界面。读完本文,你将掌握该示例的完整运行方法、源码级实现原理(提示词构建、JSON Schema 校验、UI 事件回传、扩展协商)以及在生产环境中必须遵循的不可信 Agent 安全准则。
示例概览:一个用 A2UI 驱动界面的 A2A 服务端 Agent
该示例位于 examples/integrations/a2a-a2ui,其中agent/子目录是一个独立的 Python 服务端 Agent 程序,它的定位是:
- 使用 Google ADK 构建 LLM Agent(
LlmAgent),负责理解用户意图、调用工具、生成响应; - 通过 A2A 协议将该 Agent 暴露为标准的 A2A Server,供任何符合 A2A 规范的客户端发现与调用;
- 借助 A2UI(Agent to UI)扩展,在响应中携带声明式的 UI JSON(如组件树、数据模型更新),让前端把"列表卡片、预订表单、确认卡片"等界面直接渲染出来,而不是让 Agent 输出纯文本。
从整体架构看,这是一个典型的三段式链路:CopilotKit 前端(Next.js)→A2A 协议请求→ADK Agent + A2UI 扩展响应。前端与 Agent 之间通过 A2A 的 AgentCard、Message/Part 等标准概念通信,而 UI 内容则通过 A2UI 的DataPart(MIME 类型application/json+a2ui)随消息返回。
环境准备
原文档列出的前置条件如下,本节结合仓库的依赖声明做进一步核对:
- Python 3.9 或更高版本。需要注意的是,当前仓库中该 Agent 的 pyproject.toml 声明的
requires-python = ">=3.13",实际运行请以仓库为准(若使用仓库锁定的环境,建议直接采用 Python 3.13+)。 - **UV。
- 可访问的 LLM 与 API Key。默认通过环境变量
GEMINI_API_KEY提供(详见下文"启动参数与配置")。
核心依赖(来自 pyproject.toml)包括:
| 依赖包 | 版本要求 | 作用 |
|---|---|---|
a2a-sdk | >=0.3.0 | A2A 协议的服务端 SDK,提供 A2AStarletteApplication、AgentCard 等 |
google-adk | >=1.8.0 | Google Agent Development Kit,构建 LLM Agent 与 Runner |
google-genai | >=1.27.0 | Gemini 模型调用客户端 |
litellm | - | 统一模型网关,用于LiteLlm模型封装 |
a2ui | workspace 内版本 | A2UI 扩展辅助库(create_a2ui_part等) |
jsonschema | >=4.0.0 | 对 LLM 输出的 A2UI JSON 做 Schema 校验 |
click | >=8.1.8 | 命令行参数解析(host/port) |
python-dotenv | >=1.1.0 | 加载.env环境文件 |
运行示例:三步启动 A2A Server
原文档给出了三步运行方法,这里补充仓库中的实际路径对应关系:
步骤 1:进入示例目录
原文档中的a2a_samples/a2ui_restaurant_finder对应本仓库中的 agent 目录:
cd examples/integrations/a2a-a2ui/agent步骤 2:创建环境文件写入 API Key
echo "GEMINI_API_KEY=your_api_key_here" > .env启动代码在 agent/main.py 开头调用load_dotenv()加载该文件。启动前会做一次校验(MissingAPIKeyError):当GOOGLE_GENAI_USE_VERTEXAI未设置为TRUE时,GEMINI_API_KEY必须存在,否则进程会打印错误并以退出码 1 结束。
步骤 3:运行 Agent Server
uv run .uv run .会依据main.py 的入口启动 uvicorn,默认绑定localhost:10002。启动完成后,该 A2A Server 会:
- 通过
AgentCard广播自身能力(name="Restaurant Agent"、version="1.0.0"、支持 streaming、声明 A2UI 扩展); - 注册一个名为
find_restaurants的AgentSkill(AgentSkill定义在 agent/main.py),描述"根据菜系、位置帮助查找餐厅",并带有示例查询; - 挂载
InMemoryTaskStore管理任务状态,DefaultRequestHandler处理 A2A 请求; - 通过 CORS 中间件允许
http://localhost:5173(前端开发服务器)跨域访问; - 将
agent/images/目录挂载为/static静态资源,供 UI 中的餐厅图片引用(如http://localhost:10002/static/shrimpchowmein.jpeg)。
如果不想手敲命令,仓库还提供了封装脚本 run-agent.sh(以及 Windows 的run-agent.bat),它们内部就是切换到 agent 目录后执行uv run .。
源码拆解:Agent 是如何工作的
1. 核心 Agent 与 Runner(agent.py)
agent/agent.py 定义了RestaurantAgent类,核心要点如下:
- 双模式设计:构造函数接收
use_ui参数。为True时使用"指令 + UI 提示词"构建 Agent(能生成 A2UI JSON);为False时仅用纯文本提示词。这是为了让不支持 A2UI 扩展的客户端也能获得文本回复。 - 模型配置:通过
LiteLlm(model=LITELLM_MODEL)封装模型,默认值为gemini/gemini-2.5-flash,可用环境变量LITELLM_MODEL覆盖。 - 工具注册:将 tools.py 中的
get_restaurants函数直接挂载为tools=[get_restaurants]。 - 运行时:使用
Runner组合InMemoryArtifactService、InMemorySessionService、InMemoryMemoryService等内存态服务,stream()方法按 session_id 复用会话,并把base_url写入会话状态供工具读取。
系统指令(AGENT_INSTRUCTION)明确约束了 LLM 的执行逻辑:
- 查找餐厅:必须先调用
get_restaurants工具,从用户查询中提取 cuisine、location 和数量count(例如"top 5 chinese places"中的 5),拿到数据后严格按照prompt_builder.py中合适的 UI 示例生成最终 A2UI JSON; - 预订桌位:当收到
USER_WANTS_TO_BOOK...形式的查询时,使用预订表单模板生成 UI,并把查询中的详情填入dataModelUpdate.contents; - 确认预订:当收到
User submitted a booking...形式的查询时,使用确认模板生成 UI 并填入最终预订信息。
2. 提示词与 A2UI Schema(prompt_builder.py)
agent/prompt_builder.py 是整个示例的"UI 心脏",包含三块内容:
A2UI JSON Schema(A2UI_SCHEMA):完整的 JSON Schema 定义了 A2UI 消息必须且只能包含以下四种 action 之一:
| Action | 作用 | 必填字段 |
|---|---|---|
beginRendering | 通知客户端开始渲染一个 surface(根组件 + 样式) | root、surfaceId;styles可选(含font与primaryColor,后者要求#RRGGBB十六进制格式) |
surfaceUpdate | 用一组新组件更新 surface | surfaceId、components(数组,每项含id与component,component有且仅有一个组件类型键) |
dataModelUpdate | 更新 surface 的数据模型(path省略或为/时整体替换) | surfaceId、contents(每项含key与唯一的类型化value*字段) |
deleteSurface | 删除指定的 surface | surfaceId |
Schema 中还完整定义了受支持的组件类型:Text、Image、Icon、Video、AudioPlayer、Row、Column、List、Card、Tabs、Divider、Modal、Button、CheckBox、TextField、DateTimeInput、MultipleChoice、Slider。其中布局类组件(Row/Column/List)通过children.explicitList或children.template(componentId+dataBinding动态生成子项)组织组件树;文本、图片、图标等属性值既可传literalString字面量,也可传path引用数据模型中的值(例如/items/0/name),这是 A2UI 数据与视图解耦的关键设计。
UI 模板示例(RESTAURANT_UI_EXAMPLES):提供四种可直接复用的 A2UI 消息序列:
SINGLE_COLUMN_LIST_EXAMPLE:单列列表模板(餐厅数量 ≤ 5 时使用),用List+template数据绑定渲染卡片,每张卡片含图片、名称、评分、详情、链接与"Book Now"按钮;TWO_COLUMN_LIST_EXAMPLE:双列卡片模板(餐厅数量 > 5 时使用),用Row并排两张卡片;BOOKING_FORM_EXAMPLE:预订表单模板,包含人数输入(TextField)、日期时间(DateTimeInput)、饮食要求(TextField)与"Submit Reservation"按钮;CONFIRMATION_EXAMPLE:预订确认卡片模板,展示餐厅图片、预订详情、饮食要求与"期待您的光临"文案。
模板选择规则被写进提示词:≤5 家用单列、>5 家用双列、USER_WANTS_TO_BOOK用预订表单、User submitted a booking用确认卡片。注意模板是.format(base_url=base_url)字符串模板,base_url在运行时由 get_ui_prompt() 注入,用于把示例中的占位符替换为真实服务地址。
提示词组装(get_ui_prompt/get_text_prompt):get_ui_prompt把输出规则、UI 模板规则、格式化后的示例与完整 A2UI Schema 拼成一个大的系统提示词,并要求 LLM 的最终输出必须用---a2ui_JSON---分隔符拆成两部分:前半部分为对话文本,后半部分为符合 Schema 的 A2UI JSON 数组(get_text_prompt则是纯文本模式的对应版本)。这也解释了为何整个流程对输出格式如此依赖——JSON 是被解析、校验、再分片发送的。
3. UI 校验与重试机制(agent.py stream)
为了确保 LLM 产出的 UI JSON 可被前端安全解析,agent.py 的stream()方法实现了完整的校验-重试闭环:
- 将单条消息 Schema 包装为
{"type": "array", "items": single_message_schema}(因为提示词要求返回消息列表); - 若开启 UI 模式且 Schema 加载失败,直接返回错误消息;
- 每次尝试时先检查响应中是否包含
---a2ui_JSON---分隔符,然后剥离可能的 ```json 代码块围栏; - 依次执行
json.loads解析和jsonschema.validate校验,捕获ValueError、JSONDecodeError、ValidationError; - 校验失败则最多重试一次(
max_retries = 1,总计 2 次尝试),重试提示词会带上失败原因并强调"必须生成严格符合 Schema 的合法响应"; - 若全部重试耗尽,向客户端返回文本形式的兜底错误信息。
这一机制是生产级生成式 UI 的关键实践:任何由 LLM 生成的声明式 UI 都必须经过 Schema 校验再交给前端渲染,否则格式错误的 JSON 可能破坏前端解析。
4. 执行器与 UI 事件回传(agent_executor.py)
agent/agent_executor.py 中的RestaurantAgentExecutor继承 A2A SDK 的AgentExecutor,承担"请求分发"职责:
- 按扩展协商选 Agent:通过
try_activate_a2ui_extension(context)检查客户端请求中是否声明了 A2UI 扩展 URI,是则使用ui_agent,否则使用text_agent; - 解析 UI 事件:遍历消息中的
DataPart,识别userAction载荷,将按钮点击转换成内部查询。例如:book_restaurant→USER_WANTS_TO_BOOK: {restaurantName}, Address: {address}, ImageURL: {imageUrl}submit_booking→User submitted a booking for {restaurantName} for {partySize} people at {reservationTime} with dietary requirements: {dietary}...
- 组装最终 Part:把 Agent 的最终输出按
---a2ui_JSON---拆分,文本部分包装为TextPart,JSON 列表中的每条 A2UI 消息通过create_a2ui_part()包装成带application/json+a2uiMIME 元数据的DataPart后发送; - 任务状态机:预订提交(
submit_booking)完成时置为completed,其余场景置为input_required,等待前端继续交互。
5. 工具与数据(tools.py / restaurant_data.json)
agent/tools.py 定义了唯一的工具函数get_restaurants(cuisine, location, tool_context, count=5):
- 当查询涉及 "new york" 或 "ny" 时,读取同目录的 restaurant_data.json(66 行数据,包含纽约多家餐厅的 name、detail、imageUrl、rating、infoLink、address);
- 会从
tool_context.state读取base_url,把数据中的http://localhost:10002替换为会话实际地址,确保前端能正确加载/static图片; - 按
count切片返回指定数量的餐厅,最终以 JSON 字符串形式交回 LLM。
6. A2UI 扩展定义(a2ui_extension.py)
a2ui_extension/src/a2ui/a2ui_extension.py 是 A2UI 与 A2A 的桥接层:
- 定义了扩展 URI
https://a2ui.org/a2a-extension/a2ui/v0.8与 MIME 类型application/json+a2ui; create_a2ui_part(a2ui_data):把 A2UI 消息包装成带 MIME 元数据的DataPart;get_a2ui_agent_extension():生成声明在 AgentCardcapabilities.extensions中的扩展描述(可选acceptsInlineCustomCatalog参数);try_activate_a2ui_extension(context):客户端请求 A2UI 扩展时将其标记为已激活,并返回True,供执行器决策。
安全警示:把外部 Agent 视为不可信实体
这是原文档中特别强调、必须完整保留的核心实践指导。示例代码仅用于演示 A2A 协议的机制,在构建生产应用时,必须把任何不受你直接控制的 Agent 视为潜在的不可信实体:
- 所有来自外部 Agent 的数据都应视为不可信输入,包括但不限于其 AgentCard、messages、artifacts 与 task statuses;
- 警惕提示注入(Prompt Injection):恶意 Agent 可以在 AgentCard 的字段(如
description、name、skills.description)中携带精心构造的数据。如果未经净化就把这些数据拼进发给 LLM 的提示词,就可能让你的应用暴露在提示注入攻击之下; - 必须做输入校验与净化:在使用这些数据前必须进行验证和清洗,否则会引入安全漏洞;
- 开发者责任:需要自行落实适当的安全措施,例如输入校验、凭证的安全处理,以保护系统和用户。
在本文的示例中,"Schema 校验"本身就是一种输入净化手段——它保证了进入前端的 A2UI 数据结构严格符合契约,但这并不替代对 AgentCard、技能描述等文本字段的信任边界设计。
常见问题与排查
结合 examples/integrations/a2a-a2ui/README.md 的说明,运行中常见问题如下:
- 提示 "I'm having trouble connecting to my tools":请确认 A2A Agent 正常运行在端口 10002、
GEMINI_API_KEY已正确设置、UI 与 Agent 两个服务都已启动; - Python 依赖导入报错:进入 agent 目录执行
uv sync同步依赖,再uv run .启动; - 无需自建前端时:也可以只跑 Agent,通过 A2A 协议客户端(或其他支持 A2UI 的前端)直接访问
http://localhost:10002的 AgentCard 与任务接口。
小结
这个示例完整展示了现代 Agent 前端化的一条可落地路径:用 ADK 构建工具型 Agent,用 A2A 标准化服务发现与消息交换,用 A2UI 让 Agent 直接"画"出可交互界面。无论是 ≤5 单列、>5 双列的列表规则,还是---a2ui_JSON---分隔符 + JSON Schema 校验 + 重试的容错管线,抑或book_restaurant/submit_booking的 UI 事件回传闭环,都可以作为你自建"生成式 UI Agent"的直接参考。最后请牢记:演示代码可以优雅,生产系统必须对 Agent 的每一项输入保持警惕。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考