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": {}
}
}
}
三个关键设计:
- 扁平组件列表 + ID 引用:组件是扁平排列的,嵌套关系通过 ID 引用表达,LLM 友好,便于增量生成
component.ref:引用目录中注册的组件名,不是可执行代码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)
核心类: A2uiValidator — agent_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.tsrenderers/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 只发送数据,客户端只渲染预注册的可信组件。
- 声明式 JSON,非可执行代码:Agent 无法注入脚本或执行任意逻辑
- Catalog 白名单:只有预先注册在 Catalog 中的组件才会被渲染,
component.ref查找失败则忽略 - 跨信任边界安全:Agent 可以运行在远程或不信任的环境中,客户端始终控制渲染
- 框架无关:同一份 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 Client | 4 种消息:createSurface、updateComponents、updateDataModel、deleteSurface | 告诉 LLM “你可以发这 4 种消息” |
| Common Types | ComponentCommon(id、component ref)、DynamicString(字面量或数据绑定)、Action | 通用类型定义 |
| Catalog Schema | 18 个组件的完整定义,每个组件的属性、类型、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 推导出:
- 组件名叫
Button,component字段必须是"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---
示例传达了几个关键模式:
- 消息顺序:先
createSurface,再updateComponents,最后updateDataModel - 组件拓扑:
child(单个子组件)和children(多个子组件)构建嵌套 - 数据绑定:
"text": {"path": "/flightNumber"}从 data_model 读值,而非硬编码 - 客户端函数:
"text": {"call": "formatDate", "args": {...}}调用客户端注册的函数 - 组件组合: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 SDK | Python | agent_sdks/python/ |
| Agent SDK | Kotlin | agent_sdks/kotlin/ |
| Web 渲染器 | React | renderers/react/ |
| Web 渲染器 | Angular | renderers/angular/ |
| Web 渲染器 | Lit | renderers/lit/ |
| 移动渲染器 | Flutter | samples/client/flutter/ |
| Markdown 渲染器 | markdown-it | renderers/markdown/ |
| 核心共享库 | TypeScript | renderers/web_core/ |
所有 Web 渲染器共享 @a2ui/web_core 核心库,负责 A2UI JSON 解析、组件树构建和数据绑定。渲染器只需实现”将抽象组件映射到框架原生组件”这一层。