A2UI 生成式 UI 实现详解:从 Agent 到用户界面的完整数据流

A2UI(Agent-to-User Interface)是 Google 开源的生成式 UI 框架,核心理念是让 AI Agent 通过声明式 JSON 描述 UI 意图,由客户端用原生组件渲染——“安全如数据,表达如代码”。

本文基于 A2UI v0.8/v0.9/v0.10 源码,追踪从 Agent 生成到用户看到界面的完整管道。


全局数据流概览

[LLM/Agent] ──生成文本──> [Parser 解析] ──A2UI JSON──> [网络传输] ──JSON 消息──> [MessageProcessor] ──组件树──> [ComponentRegistry] ──原生组件──> [用户看到的 UI]

管道分为两个半区:

  • Agent 端(Python/Kotlin SDK):Schema 准备 → LLM 生成 → 解析 → 验证 → 传输
  • Client 端(React/Angular/Lit/Flutter):消息处理 → 组件查找 → 渲染 → 交互回传

第一阶段:Schema 准备(Agent 启动时)

核心类: A2uiSchemaManager

  • Python: agent_sdks/python/src/a2ui/schema/manager.py
  • Kotlin: agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/schema/A2uiSchemaManager.kt

Schema Manager 在 Agent 启动时加载组件目录(Catalog),并将 JSON Schema + 示例注入 LLM 的系统提示词,让 LLM “知道”可以生成哪些 UI 组件。

[A2uiCatalog] ──目录配置──> [A2uiSchemaManager] ──系统提示词──> [LLM]
                          加载 schema + 示例

Python 示例

# agent_sdks/python/src/a2ui/schema/manager.py — A2uiSchemaManager
from a2ui.basic_catalog import BasicCatalog

# 1. 加载目录(basic_catalog 包含 Card, Button, TextField 等 18 种标准组件)
catalog = A2uiCatalog(BasicCatalog())
manager = A2uiSchemaManager(catalog)

# 2. 生成系统提示词(包含 JSON Schema + 组件示例)
system_prompt = manager.generate_system_prompt()
# → 注入到 LLM 的 system instruction 中

目录(Catalog)示例

目录定义了哪些组件可用,以及每个组件的 Schema:

// specification/v0_10/catalogs/basic/catalog.json
{
  "Button": {
    "type": "object",
    "properties": {
      "label": { "type": "string" },
      "onClick": { "$ref": "#/definitions/Action" }
    }
  },
  "Card": {
    "type": "object",
    "properties": {
      "header": { "type": "string" },
      "content": { "$ref": "#/definitions/ComponentList" }
    }
  }
}

第二阶段:LLM 生成 A2UI JSON

核心类: SendA2uiToClientToolset

  • Python: agent_sdks/python/src/a2ui/adk/send_a2ui_to_client_toolset.py
  • Kotlin: agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolset.kt

LLM 根据系统提示词中的 Schema,生成符合 A2UI 协议的声明式 JSON。关键点:这是数据,不是代码。 LLM 不能执行任意逻辑,只能描述 UI 结构。

LLM 输出示例(v0.9 格式)

{
  "createSurface": {
    "surface": {
      "id": "root",
      "components": [
        {
          "id": "card1",
          "component": { "ref": "Card" },
          "properties": {
            "header": "航班状态",
            "content": [
              {
                "id": "text1",
                "component": { "ref": "Text" },
                "properties": { "text": "CA1234 准点到达" }
              },
              {
                "id": "btn1",
                "component": { "ref": "Button" },
                "properties": { "label": "查看详情" }
              }
            ]
          }
        }
      ]
    },
    "data_model": {
      "bindings": {},
      "valueDefinitions": {}
    }
  }
}

三个关键设计:

  1. 扁平组件列表 + ID 引用:组件是扁平排列的,嵌套关系通过 ID 引用表达,LLM 友好,便于增量生成
  2. component.ref:引用目录中注册的组件名,不是可执行代码
  3. data_model:数据与 UI 分离,支持双向绑定

第三阶段:解析(Parser)

核心类: parse_response / A2uiStreamParser / StreamingParser

  • Python 非流式: agent_sdks/python/src/a2ui/parser/parser.py
  • Python 流式: agent_sdks/python/src/a2ui/parser/streaming.py
  • Kotlin 流式: agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/parser/StreamingParser.kt(1114 行)

解析器从 LLM 的原始文本输出中提取 A2UI JSON 块。流式解析器还负责增量修复不完整的 JSON(LLM 流式输出经常截断)。

[LLM 原始文本] ──parse_response()──> [ResponsePart(A2UI JSON)] ──> [发送到客户端]
                  ↑ 同时支持流式:
[LLM token 流] ──StreamingParser.processChunk()──> [增量组件树更新]

Python 非流式解析

# agent_sdks/python/src/a2ui/parser/parser.py
from a2ui.parser.parser import parse_response, has_a2ui_parts

llm_output = '...LLM 生成的包含 A2UI JSON 的文本...'

# 1. 检测是否包含 A2UI 内容
if has_a2ui_parts(llm_output):
    # 2. 提取所有 A2UI JSON 块
    parts = parse_response(llm_output)
    for part in parts:
        if part.type == "a2ui":
            a2ui_json = part.content  # 提取出的纯 A2UI JSON

Kotlin 流式解析

StreamingParser 是整个 SDK 中最复杂的类(1114 行),核心方法:

// agent_sdks/kotlin/.../StreamingParser.kt
val parser = StreamingParser.create(version = A2uiVersion.V0_9)

llmTokenStream.collect { chunk ->
    parser.processChunk(chunk)

    // 内部流程:
    // 1. fixJson()             — 修复不完整的 JSON
    // 2. processComponentTopology() — 去重和拓扑排序
    // 3. updateDataModel()     — 增量更新数据绑定
    // 4. sniffPartialComponent()   — 嗅探正在生成中的组件

    // 获取当前已解析的组件树(无需等 LLM 全部生成完)
    val surface = parser.currentSurface()
    // → 已经可以发送给客户端开始渲染
}

第四阶段:验证(Validator)

核心类: A2uiValidatoragent_sdks/python/src/a2ui/schema/validator.py

在发送给客户端前,验证生成的 JSON 是否符合 Schema。

from a2ui.schema.validator import A2uiValidator

validator = A2uiValidator(catalog_schema)
issues = validator.validate(a2ui_json)
# 检查项:
# - 组件引用是否在 Catalog 中存在
# - ID 是否全局唯一
# - 嵌套是否合规(如 Card 内不能嵌套另一个 Surface)
# - 是否存在循环引用(analyze_topology)
# - 必填属性是否齐全(extract_component_required_fields)

第五阶段:网络传输

核心类: A2uiPartConverter / A2uiEventConverter

  • agent_sdks/python/src/a2ui/adk/send_a2ui_to_client_toolset.py

将解析后的 A2UI JSON 封装为协议消息,通过 A2A(Agent-to-Agent)、SSE 或 WebSocket 发送到客户端。

class SendA2uiToClientToolset:
    def send_a2ui_message(self, a2ui_json):
        # 封装为 A2A 协议消息
        message = A2uiPartConverter().convert(a2ui_json)
        # → 通过传输层发送到客户端

第六阶段:消息处理(MessageProcessor)

核心类: MessageProcessor / A2UIProvider + store

  • Angular: renderers/angular/src/v0_8/data/processor.ts
  • React: renderers/react/src/v0_8/core/A2UIProvider.tsx

客户端收到 A2UI JSON 后,MessageProcessor 负责解析消息、构建组件树、管理数据模型。

[网络 JSON] ──> MessageProcessor.processMessage() ──> [Signal/Store 中的组件树状态]
                   ↓
              解析 createSurface / updateSurface
              构建组件树(树形结构,包含嵌套引用)
              建立数据绑定(data_model → 组件属性)

Angular MessageProcessor

// renderers/angular/src/v0_8/data/processor.ts — MessageProcessor
@Injectable()
class MessageProcessor {
  // Signal 追踪版本变化以驱动 Angular 变更检测
  private surface = signal<Surface | null>(null);

  processMessage(msg: A2uiMessage) {
    if (msg.createSurface) {
      // 解析组件树,建立 ID → ComponentNode 映射
      this.surface.set(parseSurface(msg.createSurface.surface));
    }
    if (msg.updateSurface) {
      // 增量更新:只更新变化的组件
      this.applyUpdate(msg.updateSurface);
    }
  }
}

React Provider(双 Context 架构)

React 版本使用双 Context 分离稳定的 actions 和响应式的 state,避免不必要的重渲染:

// renderers/react/src/v0_8/core/A2UIProvider.tsx
function A2UIProvider({ children }) {
  const storeRef = useRef(createStore());

  return (
    <A2UIActionsContext.Provider value={storeRef.current.actions}>
      <A2UIStateContext.Provider value={storeRef.current.state}>
        {children}
      </A2UIStateContext.Provider>
    </A2UIActionsContext.Provider>
  );
}

第七阶段:组件注册与查找(ComponentRegistry)

核心类: ComponentRegistry + defaultCatalog

  • renderers/react/src/v0_8/registry/ComponentRegistry.ts
  • renderers/react/src/v0_8/registry/defaultCatalog.ts

将 A2UI JSON 中的 component.ref(如 "Card""Button")映射到框架的原生组件实现。

["Card"] ──> ComponentRegistry.lookup("Card") ──> [React Card 组件]
                  ↑
          defaultCatalog 注册了 18 种标准组件
          + 用户可注册自定义组件
// renderers/react/src/v0_8/registry/ComponentRegistry.ts
class ComponentRegistry {
  private catalog = new Map<string, ReactComponent>();

  register(name: string, component: ReactComponent) {
    this.catalog.set(name, component);
  }

  lookup(ref: string) {
    return this.catalog.get(ref); // "Card" → Card.tsx
  }
}

// renderers/react/src/v0_8/registry/defaultCatalog.ts
const defaultCatalog = new ComponentRegistry();
defaultCatalog.register("Card", Card);
defaultCatalog.register("Button", Button);
defaultCatalog.register("Text", Text);
// ... 共 18 种标准组件

18 种标准组件(specification/v0_10/catalogs/basic/):

类别组件
内容Text, Icon, Image, AudioPlayer, Video, Divider
交互Button, CheckBox, TextField, DateTimeInput, Slider, MultipleChoice
布局Card, Column, Row, List, Tabs, Modal

第八阶段:渲染(Renderer)

核心类: A2uiSurface / useA2UIComponent / ComponentBinder

  • React: renderers/react/src/v0_9/A2uiSurface.tsx, renderers/react/src/v0_8/hooks/useA2UIComponent.ts
  • Angular: renderers/angular/src/v0_9/core/component-binder.service.ts

Surface 从 root ID 开始,递归渲染整棵组件树。每个组件通过 Hook/Service 获取数据绑定和操作分发能力。

[A2uiSurface] ──root ID──> [ComponentNode("card1")] ──lookup("Card")──> [Card 组件]
                              ↓ 递归子组件
                           [ComponentNode("text1")] ──lookup("Text")──> [Text 组件]
                           [ComponentNode("btn1")]  ──lookup("Button")──> [Button 组件]

React 渲染

// renderers/react/src/v0_9/A2uiSurface.tsx
function A2uiSurface() {
  const { surface } = useA2UI();
  return <ComponentNode id="root" />;
}

function ComponentNode({ id }) {
  const { component, properties, children } = useA2UIComponent(id);
  const Impl = registry.lookup(component.ref);

  return (
    <Impl {...properties}>
      {children?.map(child => (
        <ComponentNode key={child.id} id={child.id} />
      ))}
    </Impl>
  );
}

Angular 属性绑定(Preact Signal → Angular Signal)

// renderers/angular/src/v0_9/core/component-binder.service.ts — ComponentBinder
class ComponentBinder {
  bind(componentRef, propertyDef, dataModel) {
    if (isLiteral(propertyDef)) {
      // 字面量绑定:直接赋值
      componentRef[propertyDef.name] = propertyDef.value;
    } else if (isPathBinding(propertyDef)) {
      // 路径绑定:从 data_model 读取,自动响应变化
      componentRef[propertyDef.name] = dataModel.get(propertyDef.path);
    }
  }
}

第九阶段:用户交互 → 回到 Agent

核心机制: sendAction

用户点击按钮等交互触发 action,通过回调传回 Agent,形成闭环。

// 用户点击 "查看详情" 按钮
function Button({ label, onClick }) {
  const { sendAction } = useA2UIComponent(id);

  return <button onClick={() => sendAction(onClick)}>{label}</button>;
  // sendAction → 网络传回 Agent → Agent 处理 → 生成新的 A2UI JSON → 增量更新 UI
}

完整闭环:

用户点击 → sendAction → Agent 收到 action
  → Agent 调用 LLM 生成新响应
  → LLM 输出 updateSurface(增量更新)
  → MessageProcessor.applyUpdate()
  → 只更新变化的组件(无需重建整棵树)

完整数据流总结

1. Schema 准备    A2uiSchemaManager 加载 Catalog → 生成系统提示词
                    ↓
2. LLM 生成       LLM 根据 Schema 生成声明式 A2UI JSON(纯数据,非代码)
                    ↓
3. 解析           parse_response() 或 StreamingParser.processChunk() 提取 JSON
                    ↓
4. 验证           A2uiValidator 校验组件引用、ID 唯一性、嵌套合规
                    ↓
5. 传输           SendA2uiToClientToolset 封装为 A2A 协议消息
                    ↓
6. 消息处理       MessageProcessor 解析 createSurface/updateSurface,构建组件树
                    ↓
7. 组件查找       ComponentRegistry 将 ref("Card") 映射到原生组件实现
                    ↓
8. 渲染           A2uiSurface 递归渲染组件树,ComponentBinder 绑定属性
                    ↓
9. 交互回传       用户操作 → sendAction → Agent → 新 A2UI JSON → 增量更新

核心安全设计

A2UI 的安全模型可以用一句话概括:Agent 只发送数据,客户端只渲染预注册的可信组件。

  1. 声明式 JSON,非可执行代码:Agent 无法注入脚本或执行任意逻辑
  2. Catalog 白名单:只有预先注册在 Catalog 中的组件才会被渲染,component.ref 查找失败则忽略
  3. 跨信任边界安全:Agent 可以运行在远程或不信任的环境中,客户端始终控制渲染
  4. 框架无关:同一份 A2UI JSON 可以在 React、Angular、Lit、Flutter 上渲染,Agent 不需要知道客户端用什么框架

LLM 是如何知道有哪些组件、如何使用的?

这是 A2UI 最核心的 Prompt Engineering 问题。答案是通过 三样东西 注入 LLM 的系统提示词。

注入时机

每次 LLM 请求前,SDK 自动将 Schema、示例追加到系统指令中:

# agent_sdks/python/src/a2ui/adk/send_a2ui_to_client_toolset.py:298-302
instruction = a2ui_catalog.render_as_llm_instructions()  # Schema
examples = await self._resolve_a2ui_examples(tool_context)  # 示例
llm_request.append_instructions([instruction, examples])
// agent_sdks/kotlin/.../SendA2uiToClientToolset.kt:116-120
val instruction = catalog.renderAsLlmInstructions()
val examples = a2uiExamples(toolContext)
llmRequestBuilder.appendInstructions(listOf(instruction, examples))

第一件:JSON Schema(告诉 LLM “有什么组件、属性是什么”)

render_as_llm_instructions() 拼接三层 Schema 注入系统提示词:

---BEGIN A2UI JSON SCHEMA---

### Server To Client Schema:
{"oneOf":[{"$ref":"#/$defs/CreateSurfaceMessage"},...],
 "$defs":{"CreateSurfaceMessage":{...},"UpdateComponentsMessage":{...},
          "UpdateDataModelMessage":{...},"DeleteSurfaceMessage":{...}}}

### Common Types Schema:
{"$defs":{"ComponentCommon":{...},"DynamicString":{...},"Action":{...}}}

### Catalog Schema:
{"components":{
  "Text":{"type":"object","properties":{"component":{"const":"Text"},
    "text":{"$ref":"...DynamicString"},"variant":{"enum":["h1","h2","h3","caption","body"]}},
    "required":["component","text"]},
  "Card":{"type":"object","properties":{"component":{"const":"Card"},
    "header":{...},"child":{...},"children":{...}}},
  "Button":{"type":"object","properties":{"component":{"const":"Button"},
    "label":{"$ref":"...DynamicString"},"onClick":{"$ref":"...Action"},
    "variant":{"enum":["filled","outlined","text"]}},
    "required":["component","label"]},
  ...18个组件...
}}

---END A2UI JSON SCHEMA---

三层 Schema 各自的作用:

Schema 层内容作用
Server To Client4 种消息:createSurfaceupdateComponentsupdateDataModeldeleteSurface告诉 LLM “你可以发这 4 种消息”
Common TypesComponentCommon(id、component ref)、DynamicString(字面量或数据绑定)、Action通用类型定义
Catalog Schema18 个组件的完整定义,每个组件的属性、类型、required 字段组件清单——LLM 就从这里知道有哪些组件

以 Button 为例,LLM 读到这个就知道所有信息:

"Button": {
  "type": "object",
  "allOf": [
    { "$ref": "ComponentCommon" },
    {
      "type": "object",
      "properties": {
        "component": { "const": "Button" },
        "label": { "$ref": "DynamicString", "description": "The text to display on the button." },
        "onClick": { "$ref": "Action", "description": "The action to dispatch when the button is clicked." },
        "variant": { "enum": ["filled", "outlined", "text"], "default": "filled" }
      },
      "required": ["component", "label"]
    }
  ],
  "unevaluatedProperties": false
}

LLM 能从这段 Schema 推导出:

  • 组件名叫 Buttoncomponent 字段必须是 "Button"
  • 必须提供 label(字符串或数据绑定)
  • 可选 onClick(一个 Action,用户点击时触发)
  • 可选 variant(三选一,默认 "filled"
  • 不能有其他属性(unevaluatedProperties: false

第二件:示例(告诉 LLM “怎么组合使用”)

Schema 只定义了单个组件的结构。示例教会 LLM 如何组合组件构建完整 UI。

load_examples()specification/v0_9/catalogs/basic/examples/ 加载几十个 JSON 示例文件,注入为:

---BEGIN 01_flight-status---
{
  "messages": [
    { "createSurface": { "surfaceId": "...", "catalogId": "..." } },
    { "updateComponents": {
        "components": [
          { "id": "root", "component": "Card", "child": "main-column" },
          { "id": "main-column", "component": "Column",
            "children": ["header-row", "route-row", "divider", "times-row"],
            "align": "stretch" },
          { "id": "header-row", "component": "Row",
            "children": ["header-left", "date"],
            "justify": "spaceBetween", "align": "center" },
          { "id": "flight-number", "component": "Text",
            "text": {"path": "/flightNumber"}, "variant": "h3" },
          ...更多组件
        ]
      }
    },
    { "updateDataModel": {
        "value": { "flightNumber": "OS 87", "date": "2025-12-15",
                   "origin": "Vienna", "destination": "New York" }
      }
    }
  ]
}
---END 01_flight-status---

---BEGIN 07_task-card---
...另一个示例...
---END 07_task-card---

示例传达了几个关键模式:

  1. 消息顺序:先 createSurface,再 updateComponents,最后 updateDataModel
  2. 组件拓扑child(单个子组件)和 children(多个子组件)构建嵌套
  3. 数据绑定"text": {"path": "/flightNumber"} 从 data_model 读值,而非硬编码
  4. 客户端函数"text": {"call": "formatDate", "args": {...}} 调用客户端注册的函数
  5. 组件组合:Card → Column → Row → Text,展示层级嵌套

第三件:工具定义 + 工作流规则(告诉 LLM “怎么输出”)

SDK 注册了一个 Function Calling 工具 send_a2ui_json_to_client

FunctionDeclaration(
    name="send_a2ui_json_to_client",
    description=(
        "Sends A2UI JSON to the client to render rich UI for the user. "
        "This tool can be called multiple times to render multiple UI surfaces. "
        "The A2UI JSON Schema definition is between "
        "---BEGIN A2UI JSON SCHEMA--- and ---END A2UI JSON SCHEMA--- "
        "in the system instructions."
    ),
    parameters={
        "type": "object",
        "properties": {
            "a2ui_json": {
                "type": "string",
                "description": "valid A2UI JSON Schema to send to the client."
            }
        },
        "required": ["a2ui_json"]
    }
)

加上工作流规则(constants.py 中的 DEFAULT_WORKFLOW_RULES):

The generated response MUST follow these rules:
- Each A2UI JSON block MUST be wrapped in <a2ui-json> and </a2ui-json> tags.
- The JSON part MUST validate against the provided A2UI JSON SCHEMA.
- Top-Down Component Ordering: The 'root' component MUST be the FIRST element.
  Parent components MUST appear before their child components.

LLM 看到的完整系统提示词

┌─────────────────────────────────────────────────────────┐
│ ① 角色描述(role_description)                            │
│   "你是一个航班查询助手..."                                │
│                                                         │
│ ② 工作流规则(DEFAULT_WORKFLOW_RULES)                     │
│   "JSON 必须包裹在 <a2ui-json> 标签中..."                  │
│   "root 组件必须排第一,父组件在子组件前面..."               │
│                                                         │
│ ③ UI 描述(可选,ui_description)                         │
│   "展示航班状态卡片,包含航班号、出发地、状态"               │
│                                                         │
│ ④ ---BEGIN A2UI JSON SCHEMA---                           │
│   Server To Client Schema(4种消息类型)                   │
│   Common Types Schema(通用类型)                         │
│   Catalog Schema(18个组件的完整属性定义)                 │
│   ---END A2UI JSON SCHEMA---                             │
│                                                         │
│ ⑤ 示例(flight-status, task-card, login-form, ...)      │
│   完整的 JSON 示例,展示如何组合组件                        │
│                                                         │
│ ⑥ 工具定义(send_a2ui_json_to_client)                    │
│   LLM 通过 function call 输出 JSON                       │
└─────────────────────────────────────────────────────────┘

总结:LLM 不需要”学习”UI 编程。它只需要读懂 Schema(结构约束)+ 示例(组合模式),然后输出符合约束的 JSON。 这就是为什么 A2UI 强调”LLM-friendly”——扁平的组件列表 + ID 引用的格式,比 HTML/CSS 代码更容易让 LLM 正确生成。


多语言支持

A2UI 提供了完整的跨语言 SDK:

语言路径
Agent SDKPythonagent_sdks/python/
Agent SDKKotlinagent_sdks/kotlin/
Web 渲染器Reactrenderers/react/
Web 渲染器Angularrenderers/angular/
Web 渲染器Litrenderers/lit/
移动渲染器Fluttersamples/client/flutter/
Markdown 渲染器markdown-itrenderers/markdown/
核心共享库TypeScriptrenderers/web_core/

所有 Web 渲染器共享 @a2ui/web_core 核心库,负责 A2UI JSON 解析、组件树构建和数据绑定。渲染器只需实现”将抽象组件映射到框架原生组件”这一层。