Affordable and efficient Sora video watermark removal. Sign up now and get 1 free credits!
A2A Protocol

2026 完整指南:使用 A2UI RizzCharts 构建交互式仪表板

MILO
Share
The Complete 2026 Guide: Building Interactive Dashboards with A2UI RizzCharts

2026 完整指南:使用 A2UI RizzCharts 构建交互式仪表板

🎯 核心要点(TL;DR)

  • RizzCharts 是一个生产就绪的示例,展示了如何使用 A2UIA2A Protocol 构建交互式电商仪表板
  • 学习如何创建自定义组件目录,包含 Chart 和 GoogleMap 组件,超越标准 A2UI 目录
  • 理解用于渲染丰富 UI 的三消息模式beginRenderingsurfaceUpdatedataModelUpdate
  • 了解数据绑定如何将 UI 结构与应用程序状态分离,实现响应式更新
  • 与 Google 的 Agent Development Kit (ADK) 集成,构建生成原生跨平台 UI 的 AI 智能体

目录

  1. 什么是 A2UI RizzCharts?
  2. 为什么自定义组件目录很重要
  3. 架构深度解析
  4. 逐步实现
  5. 自定义组件:Chart 和 GoogleMap
  6. 数据绑定和响应式更新
  7. 运行 RizzCharts 示例
  8. 最佳实践
  9. 常见问题
  10. 总结

什么是 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 查看器
  • 基于位置服务的交互式地图

自定义目录的工作原理

graph TD
    A[客户端定义目录] --> B[客户端注册组件]
    B --> C[客户端宣布支持]
    C --> D[智能体选择目录]
    D --> E[智能体生成 UI]
    E --> F[客户端渲染原生组件]

流程:

  1. 客户端定义目录 — 列出标准和自定义组件
  2. 客户端注册实现 — 将组件类型映射到原生组件
  3. 客户端宣布支持 — 告知智能体它支持哪些目录
  4. 智能体选择目录 — 为 UI 界面选择适当的目录
  5. 智能体生成 UI — 使用目录组件创建 surfaceUpdate 消息
  6. 客户端渲染 — 显示原生组件,无需执行任意代码

架构深度解析

项目结构

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: 在你的客户端框架中注册组件实现:

  1. 在目录 JSON 中定义组件模式
  2. 实现渲染逻辑(Lit、Angular、React、Flutter)
  3. 向 A2UI 客户端注册目录
  4. 向智能体宣布支持的目录

请参阅 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 构建智能体驱动仪表板的完整功能:

  1. 自定义组件目录扩展了 A2UI,超越了基本 UI 元素
  2. 数据绑定实现响应式、高效的更新
  3. 模式验证确保类型安全的智能体输出
  4. 回退支持提供优雅降级
  5. 安全设计让客户端保持控制

下一步


最后更新:2026 年 1 月

关键词:A2UI, Agent to UI, RizzCharts, custom components, declarative UI, A2A Protocol, ecommerce dashboard, data visualization, AI agents, LLM UI generation

Related Articles

Explore more content related to this topic