A2UI (Agent to UI) 是一个基于 JSON 的流式 UI 协议,旨在让 AI Agent 能够动态生成、控制并响应用户界面。从技术本质上看,它将 UI 视为纯粹的数据 payload,由前端运行时(Runtime)负责解析并映射为原生组件。
后端一直在生成固定格式的 json ,复杂的是前端runtime ,第一轮对话后 LLM call生成 JSON => UI ,UI 点击交互后又是新的 LLM call 之后生成的新 JSON => UI。
初始交互流程与消息结构
在 A2UI 系统中,所有交互都始于标准的 JSON-RPC 消息。当用户在聊天框输入内容时,前端会封装一个 message/send 请求:
json
{
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"message": {
"messageId": "28f374eb-5682-454a-9366-2350761362e4",
"role": "user",
"parts": [
{
"kind": "text",
"text": "查找纽约评分最高的5家中餐厅"
}
]
}
},
"id": 1
}
后端 Agent 接收到此请求后,会调用大语言模型(LLM)。此时 LLM 的任务不仅是提供文本回复,还需要生成符合 A2UI 规范的 UI 描述 JSON。一个典型的 LLM 返回结构包含以下三个核心 DataPart:
json
[
{
"beginRendering": {
"surfaceId": "restaurant-surface",
"root": "main-container"
}
},
{
"surfaceUpdate": {
"surfaceId": "restaurant-surface",
"components": [
{
"id": "main-container",
"component": { "Column": { "children": { "explicitList": ["header-text", "result-list"] } } }
},
{
"id": "header-text",
"component": { "Text": { "text": { "path": "title" }, "usageHint": "h1" } }
},
{
"id": "result-list",
"component": {
"List": {
"children": {
"template": { "componentId": "item-card", "dataBinding": "/restaurants" }
}
}
}
},
{
"id": "item-card",
"component": { "Card": { "child": "item-btn" } }
},
{
"id": "item-btn",
"component": {
"Button": {
"child": "btn-label",
"action": {
"name": "view_detail",
"context": [{ "key": "id", "value": { "path": "id" } }]
}
}
}
},
{
"id": "btn-label",
"component": { "Text": { "text": { "path": "name" } } }
}
]
}
},
{
"dataModelUpdate": {
"surfaceId": "restaurant-surface",
"path": "/",
"contents": [
{ "key": "title", "valueString": "推荐餐厅清单" },
{
"key": "restaurants",
"valueMap": [
{
"key": "r1",
"valueMap": [
{ "key": "id", "valueString": "rest_001" },
{ "key": "name", "valueString": "西安名吃" }
]
}
]
}
]
}
}
]
结论: A2UI 的核心逻辑是"UI 即数据"。Agent 通过 surfaceUpdate 定义组件的邻接表结构,通过 dataModelUpdate 注入业务数据。前端运行时负责维护这个状态并递归渲染。
用户交互触发的二次 LLM Call
当用户点击 UI 上的按钮(例如上述生成的"西安名吃"按钮)时,前端并不会执行本地的业务跳转,而是产生一个 userAction 回传给后端:
json
{
"userAction": {
"name": "view_detail",
"surfaceId": "restaurant-surface",
"sourceComponentId": "item-btn",
"timestamp": "2026-01-14T01:00:00Z",
"context": {
"id": "rest_001"
}
}
}
后端 Agent 接收到此 Action 后,将其作为上下文再次请求 LLM,LLM 随后生成一段新的 UI JSON(例如该餐厅的详细预订表单)。
交互后 LLM 返回:动态生成的详情界面
后端 Agent 接收到 userAction 后,将其作为新的 Context 输入给 LLM。LLM 理解用户想要查看 rest_001 的详情,于是生成第二轮 UI JSON。这段 JSON 会覆盖或更新当前的渲染表面(Surface):
json
[
{
"beginRendering": {
"surfaceId": "restaurant-surface",
"root": "detail-container"
}
},
{
"surfaceUpdate": {
"surfaceId": "restaurant-surface",
"components": [
{
"id": "detail-container",
"component": { "Column": { "children": { "explicitList": ["detail-header", "booking-form"] } } }
},
{
"id": "detail-header",
"component": { "Text": { "text": { "path": "restaurant_name" }, "usageHint": "h2" } }
},
{
"id": "booking-form",
"component": {
"Card": {
"child": "form-fields"
}
}
},
{
"id": "form-fields",
"component": {
"Column": {
"children": { "explicitList": ["phone-input", "date-picker", "confirm-btn"] }
}
}
},
{
"id": "phone-input",
"component": {
"TextField": {
"label": { "literalString": "预订电话" },
"text": { "path": "/form/phone" }
}
}
},
{
"id": "date-picker",
"component": {
"DateTimeInput": {
"label": { "literalString": "预订日期" },
"value": { "path": "/form/date" },
"enableDate": true
}
}
},
{
"id": "confirm-btn",
"component": {
"Button": {
"child": "confirm-text",
"primary": true,
"action": {
"name": "submit_booking",
"context": [
{ "key": "restaurantId", "value": { "literalString": "rest_001" } },
{ "key": "phone", "value": { "path": "/form/phone" } }
]
}
}
}
},
{
"id": "confirm-text",
"component": { "Text": { "text": { "literalString": "确认预订" } } }
}
]
}
},
{
"dataModelUpdate": {
"surfaceId": "restaurant-surface",
"path": "/",
"contents": [
{ "key": "restaurant_name", "valueString": "西安名吃 - 第九大道店" },
{ "key": "form", "valueMap": [] }
]
}
}
]
关键点: - 上下文继承:LLM 记住了上一步选择的餐厅。
- 动态组件切换 :原本是列表页,现在变成了带有
TextField和DateTimeInput的预订表单。 - 数据回传绑定 :
confirm-btn按钮通过path绑定,会将用户在phone-input中输入的内容作为 Context 发回给后端。
标准组件库与渲染协议
A2UI 提供了一套标准组件库,目前包含 18 种原子组件,分为展示、布局、交互、表单四大类。
展示类组件 (Text, Image, Icon, Video, Audio)
这些组件负责内容呈现。以 Image 为例,其属性支持响应式填充:
json
{
"id": "poster",
"component": {
"Image": {
"url": { "literalString": "https://cdn.example.com/img.png" },
"usageHint": "mediumFeature",
"fit": "cover"
}
}
}
布局类组件 (Row, Column, List, Card, Tabs, Divider, Modal)
布局组件通过 children 属性构建组件树。特别需要关注的是 List 组件的模板机制:
JSON
css
{
"List": {
"direction": "vertical",
"children": {
"template": {
"componentId": "reusable-card",
"dataBinding": "/dataSourceArray"
}
}
}
}
运行时会遍历 dataBinding 指向的数据数组,为每个元素实例化一个 componentId 指定的模板组件。
交互与表单类组件 (Button, TextField, CheckBox, MultipleChoice, Slider, DateTimeInput)
TextField 等组件支持数据双向绑定。当用户输入时,运行时会自动更新内部 DataModel:
json
{
"TextField": {
"label": { "literalString": "联系电话" },
"text": { "path": "/form/phone" },
"textFieldType": "number"
}
}
数据绑定系统 (DynamicValue)
A2UI 弃用了硬编码字符串,所有属性均使用 DynamicValue 结构,支持 literal(字面量)和 path(路径绑定)两种模式:
| 类型 | 数据模型示例 | JSON 绑定配置 | 渲染结果 |
|---|---|---|---|
| DynamicString | {"name": "Alice"} |
{"path": "name"} |
"Alice" |
| DynamicNumber | {"price": 99} |
{"path": "price"} |
99 |
| DynamicBoolean | {"valid": true} |
{"path": "valid"} |
true |
数据路径支持绝对路径(以 / 开头)和相对路径(在模板循环中使用),这使得 UI 结构可以保持高度复用,仅通过切换数据源即可改变展示内容。
定制性边界
要理解 A2UI,必须明确 LLM 能做什么,以及它被禁止做什么。这类似于现代 Web 开发中的"设计系统"与"业务逻辑"的分离。
LLM 拥有完全控制权的部分(What & Where)
LLM 决定界面的业务逻辑和结构,这部分是完全动态的:
- 组件树拓扑:决定是先显示文字还是先显示图片,决定使用 Row 还是 Column 进行包装。
- 组件类型 :根据上下文选择最合适的组件(如:发现需要用户输入时,动态生成
TextField)。 - 数据映射:定义 UI 组件读取数据模型的哪个分支。
- 布局权重 :通过
weight属性(flex-grow 的 JSON 映射)决定组件占据的空间比例。 - 交互逻辑:定义按钮点击后发送给后端的 Action 名称和参数名。
前端 Runtime 强制控制的部分(How it looks)
为了防止 LLM 生成风格诡异、不可用或不安全的界面,样式权限被锁死在前端:
- 视觉样式 (CSS) :LLM 无法指定具体的十六进制颜色(如 #FF0011)或像素值(如 14px)。所有的颜色、圆角、内边距、字体族都由前端的主题文件(Theme)预先定义。
- 交互细节:按钮点击时的缩放动画、列表滚动的物理惯性、输入框聚焦时的边框高亮。
- 响应式规则:在窄屏手机和宽屏显示器上如何折行,这由前端的 CSS Media Queries 固定处理。
- 底层实现 :
Button在 HTML 中是用<button>还是<div>模拟,LLM 并不感知。
| 维度 | LLM 控制 (JSON 描述) | 前端控制 (Hardcoded/Theme) |
|---|---|---|
| 内容 | ✅ 文字内容、图片 URL、选项列表 | ❌ 字体族、行高、字符间距 |
| 结构 | ✅ 组件层级、排列顺序 | ❌ 组件内部 DOM 嵌套 |
| 样式 | ⚠️ 语义标记 (usageHint="h1") | ✅ 具体的 px、hex 颜色、阴影值 |
| 布局 | ✅ 水平/垂直、flex 比例 | ❌ 具体的 gap 间距、响应式断点 |
| 交互 | ✅ 点击触发什么 Action | ✅ 点击时的视觉反馈、动画过渡 |
技术架构总结
A2UI 渲染器本质上是一个状态机。其核心逻辑如下:
ts
// 渲染器核心伪代码逻辑
class A2uiRenderer {
private surfaceState: Map<string, Surface>;
processMessages(payload: JSON) {
if (payload.surfaceUpdate) {
// 1. 更新扁平化的组件注册表
this.updateComponents(payload.surfaceUpdate);
}
if (payload.dataModelUpdate) {
// 2. 更新响应式数据源
this.updateDataModel(payload.dataModelUpdate);
}
// 3. 触发 UI 递归重绘
this.requestRender();
}
renderNode(nodeId: string) {
const config = this.getComponentConfig(nodeId);
const componentClass = registry.get(config.type);
// 自动解析数据绑定
const props = this.resolveProps(config.props, this.dataModel);
return new componentClass(props, config.children);
}
}
总结:作为"协议"而非"框架"的 A2UI
从工程角度看,A2UI 类似于一种"强约束的低代码协议"。它的判断逻辑如下:
- 它是声明式的:后端不发送 JavaScript 代码,只发送 UI 的状态快照。
- 它是闭环的:前端的每一个有效操作(Action)都会导致后端生成新的 JSON 响应,从而驱动界面进入下一个状态。
- 它是安全的 :由于 CSS 和底层 DOM 结构的解释权在前端,恶意 LLM 无法通过注入
style或script标签来进行 XSS 攻击或破坏 UI 规范。
这种设计的核心价值在于:它让 AI Agent 拥有了"长出手脚"的能力,能够直接通过界面引导用户完成复杂的业务流程,而不仅仅是作为一个聊天窗口存在。