2026 完整指南:使用 A2UI RizzCharts 构建交互式仪表板
🎯 核心要点(TL;DR)
- RizzCharts 是一个生产就绪的示例,展示了如何使用 A2UI 和 A2A Protocol 构建交互式电商仪表板
- 学习如何创建自定义组件目录,包含 Chart 和 GoogleMap 组件,超越标准 A2UI 目录
- 理解用于渲染丰富 UI 的三消息模式(
beginRendering、surfaceUpdate、dataModelUpdate) - 了解数据绑定如何将 UI 结构与应用程序状态分离,实现响应式更新
- 与 Google 的 Agent Development Kit (ADK) 集成,构建生成原生跨平台 UI 的 AI 智能体
目录
- 什么是 A2UI RizzCharts?
- 为什么自定义组件目录很重要
- 架构深度解析
- 逐步实现
- 自定义组件:Chart 和 GoogleMap
- 数据绑定和响应式更新
- 运行 RizzCharts 示例
- 最佳实践
- 常见问题
- 总结
什么是 A2UI RizzCharts?
RizzCharts 是一个官方示例应用,展示了如何使用 A2UI(Agent to UI)协议构建AI 驱动的电商仪表板。它展示了声明式 UI 生成的强大功能,AI 智能体可以创建丰富的交互式可视化,并在各平台上原生渲染。
该示例使用:
- Google 的 Agent Development Kit (ADK) 进行智能体编排
- A2A Protocol 用于智能体到智能体和智能体到客户端的通信
- 自定义组件目录,扩展标准 A2UI 组件
- LiteLLM 用于灵活的 LLM 提供商集成(Gemini、OpenAI 等)
💡 专业提示
RizzCharts 展示了一个真实世界的模式:AI 智能体生成特定领域的可视化(图表、地图),这些可视化感觉像是应用程序的原生功能,而不是通用的聊天响应。
核心功能
| 功能 | 描述 |
|---|---|
| 销售图表 | 显示按产品类别划分的销售明细的交互式甜甜圈/饼图 |
| 地理地图 | Google Maps 集成,显示门店位置和性能异常值 |
| 实时更新 | 数据绑定组件,在数据更改时响应式更新 |
| 自定义目录 | 扩展的组件库,超越标准 A2UI 组件 |
为什么自定义组件目录很重要
标准 A2UI 目录提供了常见的 UI 元素(Text、Button、TextField、Card 等),但真实世界的应用程序通常需要特定领域的组件:
- 金融仪表板的股票行情
- 医疗保健应用的医疗图表
- 工程工具的CAD 查看器
- 基于位置服务的交互式地图
自定义目录的工作原理
流程:
- 客户端定义目录 — 列出标准和自定义组件
- 客户端注册实现 — 将组件类型映射到原生组件
- 客户端宣布支持 — 告知智能体它支持哪些目录
- 智能体选择目录 — 为 UI 界面选择适当的目录
- 智能体生成 UI — 使用目录组件创建
surfaceUpdate消息 - 客户端渲染 — 显示原生组件,无需执行任意代码
架构深度解析
项目结构
samples/agent/adk/rizzcharts/
├── __main__.py # 入口点,服务器设置
├── agent.py # 带有 LLM 指令的 RizzchartsAgent
├── agent_executor.py # 带有会话管理的 A2A 执行器
├── component_catalog_builder.py # 自定义目录加载逻辑
├── tools.py # 数据获取工具
├── rizzcharts_catalog_definition.json # 自定义组件模式
└── examples/
├── rizzcharts_catalog/ # 使用自定义 Chart/GoogleMap 的示例
│ ├── chart.json
│ └── map.json
└── standard_catalog/ # 使用标准组件的回退方案
├── chart.json
└── map.json
核心组件
1. RizzchartsAgent (agent.py)
处理用户请求并生成 A2UI 有效负载的主要智能体类:
class RizzchartsAgent(LlmAgent):
"""运行电商仪表板的智能体"""
def __init__(self, model, a2ui_enabled_provider, a2ui_schema_provider):
super().__init__(
model=model,
name="rizzcharts_agent",
description="让销售经理请求销售数据的智能体",
instruction=self.get_instructions,
tools=[
get_store_sales, # 获取区域/门店数据
get_sales_data, # 获取销售明细数据
SendA2uiToClientToolset(...) # 向客户端发送 A2UI JSON
],
planner=BuiltInPlanner(
thinking_config=types.ThinkingConfig(include_thoughts=True)
),
)
2. Agent Executor (agent_executor.py)
处理会话设置和 A2UI 扩展激活:
class RizzchartsAgentExecutor(A2aAgentExecutor):
def get_agent_card(self) -> AgentCard:
return AgentCard(
name="电商仪表板智能体",
description="使用图表和地图可视化电商数据",
capabilities=AgentCapabilities(
streaming=True,
extensions=[get_a2ui_agent_extension(
supported_catalog_ids=[STANDARD_CATALOG_ID, RIZZCHARTS_CATALOG_URI]
)],
),
skills=[
AgentSkill(id="view_sales_by_category", ...),
AgentSkill(id="view_regional_outliers", ...),
],
)
3. Component Catalog Builder (component_catalog_builder.py)
动态加载和合并组件模式:
class ComponentCatalogBuilder:
def load_a2ui_schema(self, client_ui_capabilities):
# 检查客户端支持哪个目录
if RIZZCHARTS_CATALOG_URI in supported_catalog_uris:
catalog_uri = RIZZCHARTS_CATALOG_URI # 使用自定义 Chart/GoogleMap
elif STANDARD_CATALOG_ID in supported_catalog_uris:
catalog_uri = STANDARD_CATALOG_ID # 回退到标准组件
# 将目录合并到 A2UI 模式中
a2ui_schema_json["properties"]["surfaceUpdate"]
["properties"]["components"]["items"]
["properties"]["component"]["properties"] = catalog_json
return a2ui_schema_json, catalog_uri
逐步实现
步骤 1:定义自定义组件
创建定义自定义组件的 JSON 模式。以下是 RizzCharts 目录:
{
"components": {
"$ref": "standard_catalog_definition.json#/components",
"Canvas": {
"type": "object",
"description": "在聊天旁边的有状态面板中渲染 UI",
"properties": {
"children": {
"type": "object",
"properties": {
"explicitList": {
"type": "array",
"items": { "type": "string" }
}
}
}
},
"required": ["children"]
},
"Chart": {
"type": "object",
"description": "具有分层数据的交互式图表",
"properties": {
"type": {
"type": "string",
"enum": ["doughnut", "pie"]
},
"title": {
"type": "object",
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"chartData": {
"type": "object",
"properties": {
"literalArray": { "type": "array" },
"path": { "type": "string" }
}
}
},
"required": ["type", "chartData"]
},
"GoogleMap": {
"type": "object",
"description": "具有可自定义图钉的 Google Map",
"properties": {
"center": { "type": "object" },
"zoom": { "type": "object" },
"pins": { "type": "object" }
},
"required": ["center", "zoom"]
}
}
}
步骤 2:创建数据获取工具
实现智能体用于获取数据的工具:
def get_sales_data(time_period: str = "year", **kwargs) -> dict:
"""获取按产品类别划分的销售明细"""
return {
"sales_data": [
{"label": "Apparel", "value": 41, "drillDown": [
{"label": "Tops", "value": 31},
{"label": "Bottoms", "value": 38},
{"label": "Outerwear", "value": 20},
]},
{"label": "Electronics", "value": 28, "drillDown": [...]},
{"label": "Home Goods", "value": 15},
{"label": "Health & Beauty", "value": 10},
{"label": "Other", "value": 6},
]
}
def get_store_sales(region: str = "all", **kwargs) -> dict:
"""获取带有销售表现的门店位置"""
return {
"center": {"lat": 34, "lng": -118.2437},
"zoom": 10,
"locations": [
{
"lat": 34.0195, "lng": -118.4912,
"name": "Santa Monica Branch",
"description": "高流量沿海位置",
"outlier_reason": "是的,销售额超过基线 15%",
"background": "#4285F4", # 高亮图钉
},
{"lat": 34.0488, "lng": -118.2518, "name": "Downtown Flagship"},
# ... 更多位置
],
}
步骤 3:配置智能体指令
智能体接收用于生成 A2UI 有效负载的详细指令:
def get_instructions(self, readonly_context: ReadonlyContext) -> str:
return f"""
### 系统指令
你是一位 A2UI 电商仪表板分析专家。你的主要功能是将用户请求转换为 A2UI JSON 有效负载。
**工作流程:**
1. 分析请求 - 确定意图(Chart 还是 Map)
2. 获取数据 - 使用 `get_sales_data` 或 `get_store_sales`
3. 选择模板 - 使用 CHART 或 MAP 示例作为基础
4. 构建 JSON 有效负载 - 生成唯一的 surfaceId,更新标题
5. 调用工具 - 使用 `send_a2ui_json_to_client`
**示例:**
- "显示 Q3 按类别的销售明细" → Chart
- "是否有异常门店" → Map
---BEGIN CHART EXAMPLE---
{json.dumps(chart_example)}
---END CHART EXAMPLE---
---BEGIN MAP EXAMPLE---
{json.dumps(map_example)}
---END MAP EXAMPLE---
"""
步骤 4:创建 A2UI 消息有效负载
完整的 A2UI 有效负载由三条消息组成:
Chart 示例
[
{
"beginRendering": {
"surfaceId": "sales-dashboard",
"root": "root-canvas"
}
},
{
"surfaceUpdate": {
"surfaceId": "sales-dashboard",
"components": [
{
"id": "root-canvas",
"component": {
"Canvas": {
"children": { "explicitList": ["chart-container"] }
}
}
},
{
"id": "chart-container",
"component": {
"Column": {
"children": { "explicitList": ["sales-chart"] },
"alignment": "center"
}
}
},
{
"id": "sales-chart",
"component": {
"Chart": {
"type": "doughnut",
"title": { "path": "chart.title" },
"chartData": { "path": "chart.items" }
}
}
}
]
}
},
{
"dataModelUpdate": {
"surfaceId": "sales-dashboard",
"path": "/",
"contents": [
{ "key": "chart.title", "valueString": "Sales by Category" },
{ "key": "chart.items[0].label", "valueString": "Apparel" },
{ "key": "chart.items[0].value", "valueNumber": 41 },
{ "key": "chart.items[0].drillDown[0].label", "valueString": "Tops" },
{ "key": "chart.items[0].drillDown[0].value", "valueNumber": 31 }
// ... 更多数据
]
}
}
]
Map 示例
[
{
"beginRendering": {
"surfaceId": "la-map-view",
"root": "root-canvas"
}
},
{
"surfaceUpdate": {
"surfaceId": "la-map-view",
"components": [
{
"id": "root-canvas",
"component": {
"Canvas": { "children": { "explicitList": ["map-layout-container"] } }
}
},
{
"id": "map-header",
"component": {
"Text": {
"text": { "literalString": "Points of Interest in Los Angeles" },
"usageHint": "h2"
}
}
},
{
"id": "location-map",
"component": {
"GoogleMap": {
"center": { "path": "mapConfig.center" },
"zoom": { "path": "mapConfig.zoom" },
"pins": { "path": "mapConfig.locations" }
}
}
}
]
}
},
{
"dataModelUpdate": {
"surfaceId": "la-map-view",
"path": "/",
"contents": [
{ "key": "mapConfig.center.lat", "valueNumber": 34.0522 },
{ "key": "mapConfig.center.lng", "valueNumber": -118.2437 },
{ "key": "mapConfig.zoom", "valueNumber": 11 },
{ "key": "mapConfig.locations[0].lat", "valueNumber": 34.0135 },
{ "key": "mapConfig.locations[0].name", "valueString": "Google Store Santa Monica" }
// ... 更多位置
]
}
}
]
自定义组件:Chart 和 GoogleMap
Chart 组件
Chart 组件渲染交互式甜甜圈或饼图:
| 属性 | 类型 | 描述 |
|---|---|---|
type |
"doughnut" | "pie" |
图表可视化类型 |
title |
{literalString} | {path} |
图表标题(字面量或数据绑定) |
chartData |
{literalArray} | {path} |
{label, value, drillDown?} 项的数组 |
DrillDown 支持: 每个图表项可以有嵌套的 drillDown 数据,用于分层可视化:
{
"label": "Apparel",
"value": 41,
"drillDown": [
{ "label": "Tops", "value": 31 },
{ "label": "Bottoms", "value": 38 },
{ "label": "Outerwear", "value": 20 }
]
}
GoogleMap 组件
GoogleMap 组件显示带有可自定义图钉的交互式地图:
| 属性 | 类型 | 描述 |
|---|---|---|
center |
{lat, lng} |
地图中心坐标 |
zoom |
number |
缩放级别(1-20) |
pins |
array |
图钉对象数组 |
图钉属性:
{
"lat": 34.0195,
"lng": -118.4912,
"name": "Santa Monica Branch",
"description": "高流量沿海位置",
"background": "#4285F4", // 图钉背景颜色
"borderColor": "#FFFFFF", // 图钉边框颜色
"glyphColor": "#FFFFFF" // 图钉图标颜色
}
数据绑定和响应式更新
A2UI 通过数据绑定将UI 结构与应用程序状态分离:
字面量 vs. 路径值
// 字面量(固定值)
{"text": {"literalString": "Sales Dashboard"}}
// 路径(数据绑定,响应式)
{"text": {"path": "chart.title"}}
当 chart.title 处的数据更改时,组件自动更新—无需重新生成组件。
JSON Pointer 路径
A2UI 使用 RFC 6901 JSON Pointer 语法:
| 路径 | 解析为 |
|---|---|
/user/name |
对象属性 |
/items/0 |
第一个数组元素 |
/items/0/price |
嵌套属性 |
模板中的作用域路径
在动态列表中使用模板时,路径相对于每个数组项:
{
"id": "location-name",
"component": {
"Text": {
"text": { "path": "name" } // 相对于当前项
}
}
}
对于 /mapConfig.locations/0,路径 name 解析为 /mapConfig.locations/0/name。
运行 RizzCharts 示例
先决条件
- Python 3.9+
- UV 包管理器
- LLM API 密钥(Gemini、OpenAI 等)
设置
# 导航到示例目录
cd samples/agent/adk/rizzcharts
# 创建环境文件
cp .env.example .env
# 使用你的 API 密钥编辑 .env
# 运行智能体服务器
uv run .
服务器默认在 http://localhost:10002 启动。
环境变量
| 变量 | 描述 | 默认值 |
|---|---|---|
GEMINI_API_KEY |
Google AI API 密钥 | 必需 |
GOOGLE_GENAI_USE_VERTEXAI |
使用 Vertex AI 代替 | FALSE |
LITELLM_MODEL |
LLM 模型标识符 | gemini/gemini-2.5-flash |
使用示例查询进行测试
运行后,发送如下请求:
- "显示 Q3 按产品类别的销售明细" → 生成甜甜圈图表
- "该地区是否有异常门店?" → 生成带有高亮图钉的地图
- "年同比收入趋势如何?" → 生成图表可视化
最佳实践
1. 描述性组件 ID
// ✅ 好
{"id": "sales-chart-q3-2026"}
{"id": "store-location-map"}
// ❌ 不好
{"id": "c1"}
{"id": "component"}
2. 将结构与数据分离
对动态内容使用数据绑定:
// ✅ 推荐 - 数据绑定
{"title": {"path": "chart.title"}}
// ⚠️ 谨慎使用 - 字面量值
{"title": {"literalString": "Static Title"}}
3. 生成唯一的 Surface ID
每个请求应生成唯一的 surfaceId:
surface_id = f"sales_breakdown_{time_period}_{uuid.uuid4().hex[:8]}"
4. 针对模式进行验证
始终针对 A2UI 模式验证生成的 JSON:
jsonschema.validate(instance=example_json, schema=a2ui_schema)
5. 安全考虑
⚠️ 重要安全提示
- 将所有智能体生成的内容视为不受信任的输入
- 对所有属性值实施输入清理
- 在客户端渲染器中使用内容安全策略 (CSP)
- 在渲染前严格验证数据
常见问题
Q: 标准目录和自定义目录有什么区别?
A: 标准目录包括适用于所有 A2UI 客户端的常见 UI 组件(Text、Button、Card、List 等)。自定义目录通过需要客户端实现的特定领域组件(Chart、GoogleMap、StockTicker)扩展了这一点。RizzCharts 展示了两种方法,并提供回退支持。
Q: A2UI 与发送 HTML/iframes 相比如何?
A: A2UI 是声明式数据,不是代码。客户端使用自己的原生组件渲染组件,确保:
- 无代码执行风险(安全性)
- 原生外观和感觉(用户体验)
- 一致的样式(设计)
- 跨平台支持(可移植性)
Q: 我可以将 RizzCharts 组件与其他协议一起使用吗?
A: 可以!虽然 RizzCharts 使用 A2A Protocol,但 A2UI 消息格式适用于任何传输:SSE、WebSockets、AG UI 或直接 HTTP。AP2 Protocol 也与 A2UI 集成,用于支持支付的智能体界面。
Q: 如何在客户端实现自定义组件?
A: 在你的客户端框架中注册组件实现:
- 在目录 JSON 中定义组件模式
- 实现渲染逻辑(Lit、Angular、React、Flutter)
- 向 A2UI 客户端注册目录
- 向智能体宣布支持的目录
请参阅 Lit 示例 以获取参考实现。
Q: 如果客户端不支持某个目录会怎样?
A: RizzCharts 包含回退支持。ComponentCatalogBuilder 检查客户端功能:
if RIZZCHARTS_CATALOG_URI in supported_catalog_uris:
catalog_uri = RIZZCHARTS_CATALOG_URI # 自定义组件
elif STANDARD_CATALOG_ID in supported_catalog_uris:
catalog_uri = STANDARD_CATALOG_ID # 标准组件
标准目录示例使用 List 和 Card 组件显示相同的数据,无需 Chart/GoogleMap。
总结
RizzCharts 示例展示了 A2UI 构建智能体驱动仪表板的完整功能:
- 自定义组件目录扩展了 A2UI,超越了基本 UI 元素
- 数据绑定实现响应式、高效的更新
- 模式验证确保类型安全的智能体输出
- 回退支持提供优雅降级
- 安全设计让客户端保持控制
下一步
- 探索代码:GitHub 上的 RizzCharts
- 构建你自己的目录:从自定义组件指南开始
- 学习 A2A 集成:查看 A2A Protocol 文档
- 添加支付:与 AP2 Protocol 集成以实现商务流程
最后更新:2026 年 1 月
关键词:A2UI, Agent to UI, RizzCharts, custom components, declarative UI, A2A Protocol, ecommerce dashboard, data visualization, AI agents, LLM UI generation
浙公网安备 33010602011771号