创建日期:2026-10-20 | 最近更新:2026-10-23 内容基准:A2UI v0.9.1(0.9 协议族的当前生产版本,规范文件标注 Created 2025-11-20、Last Updated 2025-12-03),对应仓库 a2ui-project/a2ui 的 commit
ba4c47b(2026-10-19)。本文与后续几篇只讨论协议本身,不涉及任何渲染器的实现。
A2UI 入门:让 Agent「说 UI」,而不是「写代码」
一句话:A2UI 是一套让 Agent 用 JSON 描述界面的声明式协议。Agent 不发送 HTML/JavaScript,也不直接操作你的组件,而是发送「这里放一个 Column、里面有一个 Text 和一个 Button」这样的数据;客户端用自己的原生组件库把它渲染出来。
官方对它的定位只有六个字:
Agent-generated UIs are safe like data, but expressive like code.
—— A2UI README
这句话解释了 A2UI 的全部取舍:像数据一样安全(客户端只认自己目录里预置的组件,不执行 Agent 发来的代码),像代码一样有表达力(不只是纯文本,而是可交互的表单、卡片、列表)。
1. 它要解决什么问题
纯文本 Agent 收集信息的体验很差:
用户:帮我订明晚 7 点、两个人的位子
Agent:哪一天?
用户:明晚
Agent:几点?
用户:7 点
...
更好的做法是让 Agent 直接给出一张表单:日期选择器、人数输入框、提交按钮。用户填完点一下,Agent 收到结构化结果。
问题在于:远端 Agent 不能直接操作你的 UI。它可能跑在另一台服务器、另一家公司,跨着信任边界。传统方案是让 Agent 生成 HTML 塞进 iframe,但这带来三个麻烦:
- 重,而且视觉上和应用割裂;
- 安全边界复杂(iframe、CSP、脚本执行);
- 无法继承宿主应用的样式、可访问性和性能特性。
A2UI 的答案是:只传 UI 的「意图」,不传 UI 的「实现」。Agent 说「我要一个 Card,里面是 Column,第一个孩子是 Text」,客户端决定这个 Card 长什么样、用什么框架渲染。
官方 Gallery 里的渲染结果长这样(仓库路径 docs/public/assets/a2ui_gallery_examples.png):

2. 四个角色,三条边界
| 角色 | 负责什么 | 不负责什么 |
|---|---|---|
| Agent | 理解用户意图,生成 A2UI JSON | 不渲染、不执行客户端代码 |
| Renderer(客户端) | 解析消息、维护数据模型、映射到原生组件 | 不信任 Agent 发来的任意组件 |
| Catalog | 规定「允许使用哪些组件、函数、主题」 | 不是运行时下载的远程资源 |
| Transport | 按顺序搬运 JSON 消息 | 不解释消息语义 |
三条关键边界:
- 组件白名单:Agent 只能使用 Catalog 里声明过的组件。Catalog 是客户端预先编译/部署时就知道的,不是运行时去 URL 下载的。
catalogId只是一个稳定标识符,长得像 URI,但不需要可访问。 - 无代码执行:Agent 不能注入 JavaScript。需要「做一件事」时,它只能引用 Catalog 里注册过的函数(例如
openUrl、required),或者发一个action事件回服务端。 - 结构与状态分离:组件树描述「长什么样」,数据模型描述「显示什么」。改数据不需要重发组件树。
3. 四个核心名词
- Surface:一块可独立管理的 UI 区域。一个 surface 有自己的
surfaceId、catalogId、组件树和数据模型。Surface 不能嵌套。 - Component:UI 元素,例如
Text、Button、Column。每个组件有唯一id,用 ID 引用父子关系。 - Data Model:每个 surface 一份的 JSON 状态树。组件通过 JSON Pointer 绑定到它。
- Catalog:组件、函数、主题的集合。它决定了 Agent 的「词汇表」。
再加一个词:Message。Agent 与 Renderer 之间传的就是一条条 JSON 消息。
4. 端到端数据流
先把整条链路画出来。下图按 v0.9 术语,把「Agent 生成 → Transport 搬运 → Renderer 渲染 → 用户回传」串起来:
官方仓库里的旧图(docs/public/assets/end-to-end-data-flow.png)和旧文档用的是 v0.8 命名(surfaceUpdate、dataModelUpdate、beginRendering、userAction)。v0.9 改了消息名和组件结构,但数据流的形状没变。读旧资料时按这张表对照:
| v0.8 名字 | v0.9 / v0.9.1 对应物 | 变化 |
|---|---|---|
surfaceUpdate | updateComponents | 组件从「类型包裹对象」改成扁平对象 |
dataModelUpdate | updateDataModel | 从 contents 邻接表改成 JSON Pointer + 普通 JSON 值 |
beginRendering | createSurface | 创建 surface 与「开始渲染」合并;root 改为约定 id: "root" |
userAction | action | 顶层键名变化,字段基本一致 |
| WidgetRegistry | Catalog | 概念不变,v0.9 用 JSON Schema 描述 |
| Client Buffering | 组件表 + 数据模型 | v0.9 仍然先缓冲,直到 root 出现 |
同一件事按时序展开:
图里还有两个容易被忽略的点,v0.9 依然成立:
- 客户端先缓冲:组件和数据可以一条条到达,客户端先把它们存进表里;
root出现之前,其他更新「没有可见效果」。 - 动态更新走原通道:用户操作触发 Agent 后,Agent 继续在原来的流上发
updateComponents/updateDataModel,而不是换一条连接。
5. 0.9 的全部消息:四条下行,两条上行
A2UI v0.9 的协议面非常小。服务端到客户端只有四种消息:
| 消息 | 作用 | 关键字段 |
|---|---|---|
createSurface | 创建一块 surface,锁定 catalog | surfaceId、catalogId、theme?、sendDataModel? |
updateComponents | 新增或更新组件(扁平列表) | surfaceId、components[] |
updateDataModel | 新增、更新或删除数据 | surfaceId、path?、value? |
deleteSurface | 删除整块 surface | surfaceId |
客户端到服务端只有两种消息:
| 消息 | 作用 | 关键字段 |
|---|---|---|
action | 用户触发了某个组件的动作 | name、surfaceId、sourceComponentId、timestamp、context |
error | 客户端报告校验或运行错误 | code、surfaceId、path?、message |
每条消息都带 version 字段("v0.9" 或 "v0.9.1"),并且顶层恰好包含一个消息键。这是 JSON Schema 用 oneOf 强制的,不是约定俗成。
6. 为什么 0.9 要「prompt-first」
这是理解 0.9 的关键。官方规范原话:
Version 0.9 introduced A2UI's prompt-first protocol family. While v0.8 was optimized for LLMs that support structured output, v0.9 is designed to be embedded directly within a model's prompt. The LLM is then asked to produce JSON that matches the provided examples and schema descriptions.
——
specification/v0_9_1/docs/a2ui_protocol.md
翻成我的理解(以下是我的分析,不是官方原文):
- v0.8 假设你用的是「结构化输出 / JSON mode」,字段类型由解码器约束,所以协议可以写得很紧凑、很程序化。
- v0.9 假设你直接把 Catalog 的 JSON Schema 和几个示例塞进提示词,让模型「照着写」。既然模型看到的是自然语言 + 示例,协议就应当更可读:字符串直接写字符串,组件属性平铺,别搞
valueString/valueMap这种类型包裹。
代价是校验责任转移到生成侧:模型不再被解码器硬约束,可能写出 Tabs 却用了 tabItems、checks 少了 condition。所以 0.9 明确要求一个 prompt → generate → validate 的循环:生成、校验、把错误喂回去让它改。
7. 版本地图:0.8、0.9、0.9.1、1.0
截至本文写作时,仓库给出的状态是:
| 版本 | 状态 | 要点 |
|---|---|---|
| v0.8 | Legacy | surfaceUpdate / dataModelUpdate / beginRendering / userAction;组件用类型包裹 |
| v0.9 | Previous Stable | prompt-first;四条下行消息;扁平组件;JSON Pointer 数据绑定 |
| v0.9.1 | Current Production | 0.9 的补丁版,只改了两处(见下) |
| v1.0 | Release Candidate | 新增 actionResponse 等,尚不是生产基线 |
v0.9 → v0.9.1 只有两处变化(逐字来自 specification/v0_9_1/docs/evolution_guide.md):
- MIME 类型统一为
application/a2ui+json,替换旧的application/json+a2ui; surfaceId不再要求「渲染器生命周期内全局唯一」,只要求「当前活跃 surface 中唯一」;对已存在的 surface 重复createSurface仍然是错误。
两者完全兼容:schema 的 version 枚举同时接受 "v0.9" 和 "v0.9.1",所以老 payload 不需要改。本系列统一写 v0.9.1,遇到只属于 0.9 家族的细节会单独标注。
8. 这个系列怎么读
| 篇 | 主题 | 你会得到 |
|---|---|---|
| 0 | 入门(本文) | 全局图景、消息清单、版本关系 |
| 1 | 消息与生命周期 | 四条下行消息逐字段、顺序约束、完整 JSONL 流、传输绑定 |
| 2 | 组件与目录 | 扁平邻接表、ChildList、18 个基础组件、Catalog 协商 |
| 3 | 数据绑定 | JSON Pointer、绝对/相对作用域、双向绑定、sendDataModel |
| 4 | 动作与函数 | event / functionCall、checks、14 个基础函数、校验闭环 |
本系列刻意不写:Lit / Angular / React / Flutter 渲染器怎么实现,组件怎么画,样式怎么映射。这些属于 Renderer SDK 的职责;协议只规定「消息长什么样、语义是什么」。
上手阶段最常问的「客户端和 Agent 要不要自己实现」等问题,单独整理在 A2UI 初期问题。
9. 事实核对说明
本系列的所有协议结论都来自仓库内的一手文件,而不是搜索摘要。抓取方式:2026-10-20 用 git clone --depth 1 拉取 main(commit ba4c47b),然后直接读文件;文中的完整消息示例,都用 JSON Schema Draft 2020-12 对 server_to_client.json / client_to_server.json 实际跑过校验,含占位符或标为「错误写法」的片段除外。
| 材料 | 仓库路径(commit-pinned 链接) |
|---|---|
| 规范正文 | specification/v0_9_1/docs/a2ui_protocol.md |
| 下行消息 schema | json/server_to_client.json |
| 上行消息 schema | json/client_to_server.json |
| 通用类型 | json/common_types.json |
| 基础目录 | catalogs/basic/catalog.json |
| 测试用例 | test/cases/(这些才是「能过 schema」的样例) |
| 概念文档 | docs/public/concepts/ |
| 消息参考 | docs/public/reference/messages.md |
文中的 Gallery 示例图复制自仓库 docs/public/assets/,仅做分辨率压缩,未改动内容;A2UI 项目采用 Apache 2.0 许可证。
一个重要提醒:官方文档页和规范正文并不是永远同步的。仓库里至少有三处「文档示例 / 描述」落后于 schema 的情况(组件属性命名、checks 结构、扩展规范里的组件样例)。本系列以 schema + 测试用例 为准,遇到冲突会在对应篇章里明确指出。这也是 0.9「prompt-first」带来的现实:给模型看的文档,必须自己先校对一遍。
下一篇从最小闭环开始:四条消息各自管什么、为什么顺序不能乱、一条完整 JSONL 流到底长什么样。