跳到主要内容

创建日期: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):

A2UI 渲染出的卡片示例(仓库原图)

2. 四个角色,三条边界​

角色负责什么不负责什么
Agent理解用户意图,生成 A2UI JSON不渲染、不执行客户端代码
Renderer(客户端)解析消息、维护数据模型、映射到原生组件不信任 Agent 发来的任意组件
Catalog规定「允许使用哪些组件、函数、主题」不是运行时下载的远程资源
Transport按顺序搬运 JSON 消息不解释消息语义

三条关键边界:

  1. 组件白名单:Agent 只能使用 Catalog 里声明过的组件。Catalog 是客户端预先编译/部署时就知道的,不是运行时去 URL 下载的。catalogId 只是一个稳定标识符,长得像 URI,但不需要可访问。
  2. 无代码执行:Agent 不能注入 JavaScript。需要「做一件事」时,它只能引用 Catalog 里注册过的函数(例如 openUrl、required),或者发一个 action 事件回服务端。
  3. 结构与状态分离:组件树描述「长什么样」,数据模型描述「显示什么」。改数据不需要重发组件树。

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 对应物变化
surfaceUpdateupdateComponents组件从「类型包裹对象」改成扁平对象
dataModelUpdateupdateDataModel从 contents 邻接表改成 JSON Pointer + 普通 JSON 值
beginRenderingcreateSurface创建 surface 与「开始渲染」合并;root 改为约定 id: "root"
userActionaction顶层键名变化,字段基本一致
WidgetRegistryCatalog概念不变,v0.9 用 JSON Schema 描述
Client Buffering组件表 + 数据模型v0.9 仍然先缓冲,直到 root 出现

同一件事按时序展开:

图里还有两个容易被忽略的点,v0.9 依然成立:

  • 客户端先缓冲:组件和数据可以一条条到达,客户端先把它们存进表里;root 出现之前,其他更新「没有可见效果」。
  • 动态更新走原通道:用户操作触发 Agent 后,Agent 继续在原来的流上发 updateComponents / updateDataModel,而不是换一条连接。

5. 0.9 的全部消息:四条下行,两条上行​

A2UI v0.9 的协议面非常小。服务端到客户端只有四种消息:

消息作用关键字段
createSurface创建一块 surface,锁定 catalogsurfaceId、catalogId、theme?、sendDataModel?
updateComponents新增或更新组件(扁平列表)surfaceId、components[]
updateDataModel新增、更新或删除数据surfaceId、path?、value?
deleteSurface删除整块 surfacesurfaceId

客户端到服务端只有两种消息:

消息作用关键字段
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.8LegacysurfaceUpdate / dataModelUpdate / beginRendering / userAction;组件用类型包裹
v0.9Previous Stableprompt-first;四条下行消息;扁平组件;JSON Pointer 数据绑定
v0.9.1Current Production0.9 的补丁版,只改了两处(见下)
v1.0Release Candidate新增 actionResponse 等,尚不是生产基线

v0.9 → v0.9.1 只有两处变化(逐字来自 specification/v0_9_1/docs/evolution_guide.md):

  1. MIME 类型统一为 application/a2ui+json,替换旧的 application/json+a2ui;
  2. 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
下行消息 schemajson/server_to_client.json
上行消息 schemajson/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 流到底长什么样。