A2UI 和 AG-UI 到底有什么区别?用支付宝阿宝讲清楚 | Adrian Zephyr Notes返回首页A2UI和AG-UI到底有什么区别?用支付宝阿宝讲清楚
从支付宝阿宝一次同时处理蚂蚁森林与眼科医院查询的多任务对话出发,教程式拆解 AG-UI 的事件流、messageId、多工具调用,以及 A2UI 如何用声明式 JSON 渲染业务卡片、列表和动作。
发表于 2026年7月19日更新于 2026年7月21日4,010 字14 分钟 文章以支付宝阿宝同时处理蚂蚁森林和眼科医院查询的多任务对话为例,拆解 AG-UI 与 A2UI 的职责分工:前者用事件流把 Agent 的运行过程(run、消息、工具调用、状态)送到前端,后者用声明式 JSON 描述需要渲染的卡片、列表和动作。还介绍了多任务回复在顺序、局部失败、操作确认和重连幂等方面的工程要点,并给出落地检查清单。文章以支付宝阿宝同时处理蚂蚁森林和眼科医院查询的多任务对话为例,拆解 AG-UI 与 A2UI 的职责分工:前者用事件流把 Agent 的运行过程(run、消息、工具调用、状态)送到前端,后者用声明式 JSON 描述需要渲染的卡片、列表和动作。还介绍了多任务回复在顺序、局部失败、操作确认和重连幂等方面的工程要点,并给出落地检查清单。文章以支付宝阿宝同时处理蚂蚁森林和眼科医院查询的多任务对话为例,拆解 AG-UI 与 A2UI 的职责分工:前者用事件流把 Agent 的运行过程(run、消息、工具调用、状态)送到前端,后者用声明式 JSON 描述需要渲染的卡片、列表和动作。还介绍了多任务回复在顺序、局部失败、操作确认和重连幂等方面的工程要点,并给出落地检查清单。
版权声明
本文采用 CC BY-NC-SA 4.0 协议,转载请注明作者与链接。
原创notice.configstatus: empty $load site-config
ok公告配置暂时为空。
notice config01 / 01
A2UI 和 AG-UI 到底有什么区别?用支付宝阿宝讲清楚#

用户没有点开两个业务入口,也没有分别发两次消息,只是在一个气泡里交代了两件事:
帮我看看今天的蚂蚁森林怎么样,再找找今天可以预约的眼科医院。
阿宝的回答也没有变成一篇长文。它先解释蚂蚁森林今天的情况,紧接着给出一个可以点击的“蚂蚁森林”入口;后面再继续处理眼科医院的查询,展示医院或挂号相关卡片,最后补充提醒。
也就是说,一条用户输入,最后变成了:
- 多个可独立处理的任务
- 多条 assistant 消息
- 文本解释和业务卡片混排
- 可点击、可跳转、可继续操作的界面
这正好适合用来讲清楚 A2UI 和 AG-UI。
先给结论:
AG-UI 负责把 Agent 的运行过程送到前端,A2UI 负责描述其中需要显示的界面。
再说得工程化一点:
- AG-UI 是 Agent 和用户界面之间的事件协议。它回答的是“Agent 正在发生什么”:运行开始、消息流式输出、工具调用、状态更新、运行结束。
- A2UI 是 Agent 生成界面的声明式协议。它回答的是“这里应该显示什么 UI”:卡片、列表、表单、按钮、数据绑定和可触发的动作。
- AG-UI 像轨道,A2UI 像货物。 轨道负责把过程稳定送达,货物描述某一段结果应该长成什么界面。
从截图本身无法判断支付宝内部是否真的使用了这两个开源协议。本文不是在反推支付宝架构,而是借阿宝的产品效果,拆解如果我们要做出类似体验,AG-UI 和 A2UI 各自会承担什么职责。
先把阿宝这轮对话拆开#
用户看见的是一次聊天,后端看见的不是一句话,而是一组任务。
const userMessage =
"帮我看看今天的蚂蚁森林怎么样,再找找今天可以预约的眼科医院。";
const plan = [
{
id: "ant-forest",
title: "查询蚂蚁森林今日状态",
tool: "getAntForestStatus",
args: {
date: "2026-07-20",
},
},
{
id: "eye-hospital",
title: "查询今日可预约眼科医院",
tool: "searchHospitalAppointments",
args: {
city: "杭州",
district: "余杭区",
department: "眼科",
date: "2026-07-20",
},
},
];
模型在这里更像一个规划者。它理解用户说了几件事,每件事应该交给哪个工具,并补齐工具需要的参数。
但真正的业务结果不能靠模型编。今天有多少蚂蚁森林能量、哪些医院有号源、能不能预约,都必须由业务系统返回。
const tools = {
getAntForestStatus: antForestService.getDailyStatus,
searchHospitalAppointments:
hospitalService.searchAppointments,
};
const results = await Promise.allSettled(
plan.map(async (task) => {
const runTool = tools[task.tool as keyof typeof tools];
return {
taskId: task.id,
data: await runTool(task.args as never),
};
}),
);
这里用 Promise.allSettled 有一个现实好处:医院查询失败时,蚂蚁森林结果仍然可以先展示;蚂蚁森林服务慢一点,也不影响医院结果最终补上。
到这一步,Agent 后端已经拿到了业务结果。接下来才是本文的重点:
这些结果怎么变成截图里的“多条气泡 + 可点击业务卡片”?
AG-UI 全称是 Agent-User Interaction Protocol。官方介绍里,它是一个开放、轻量、基于事件的协议,用来标准化 AI Agent 和用户侧应用的连接方式。
它不负责定义“挂号卡片长什么样”,也不关心按钮圆角是 8px 还是 16px。它关心的是前端如何实时理解 Agent 的运行过程。
在一个典型的 Agent 应用里,前端不能只等一个 HTTP response。因为 Agent 可能会:
- 一边思考一边流式输出
- 调用多个工具
- 更新共享状态
- 等待用户确认
- 中途失败或重试
- 在同一轮运行里生成多条消息
- 生命周期事件:
RUN_STARTED、RUN_FINISHED、RUN_ERROR
- 文本消息事件:
TEXT_MESSAGE_START、TEXT_MESSAGE_CONTENT、TEXT_MESSAGE_END
- 工具调用事件:
TOOL_CALL_START、TOOL_CALL_ARGS、TOOL_CALL_END
- 状态事件:
STATE_SNAPSHOT、STATE_DELTA、MESSAGES_SNAPSHOT
- 扩展事件:
RAW、CUSTOM
把阿宝这轮对话写成 AG-UI 风格的事件流,大概是这样:
emit({
type: "RUN_STARTED",
runId: "run-42",
threadId: "thread-8",
});
emit({
type: "TOOL_CALL_START",
toolCallId: "tool-forest",
toolCallName: "getAntForestStatus",
});
emit({
type: "TOOL_CALL_END",
toolCallId: "tool-forest",
});
emit({
type: "TEXT_MESSAGE_START",
messageId: "msg-forest",
role: "assistant",
});
emit({
type: "TEXT_MESSAGE_CONTENT",
messageId: "msg-forest",
delta:
"蚂蚁森林今天的能量需要你自己进页面收取,系统不会自动到账。今天周一,可以关注步行、公交地铁等绿色出行产生的能量,一般下午 3 点后成熟。",
});
emit({
type: "TEXT_MESSAGE_END",
messageId: "msg-forest",
});
emit({
type: "CUSTOM",
name: "a2ui.surface",
value: antForestSurface,
});
emit({
type: "TEXT_MESSAGE_START",
messageId: "msg-hospital",
role: "assistant",
});
emit({
type: "TEXT_MESSAGE_CONTENT",
messageId: "msg-hospital",
delta: "我帮你查到这些今天可关注的眼科预约入口。",
});
emit({
type: "TEXT_MESSAGE_END",
messageId: "msg-hospital",
});
emit({
type: "CUSTOM",
name: "a2ui.surface",
value: hospitalSurface,
});
emit({
type: "RUN_FINISHED",
runId: "run-42",
threadId: "thread-8",
});
注意这里最关键的地方:同一次 run 里出现了多个 messageId。
msg-forest -> 蚂蚁森林说明气泡
msg-hospital -> 医院查询说明气泡
这解释了一个很常见的误会:多条回复不等于调用了多次模型,也不等于前端把一段长文硬切成几段。
同一个 messageId 收到多次 TEXT_MESSAGE_CONTENT
= 一个气泡里的文字持续增长
结束旧 messageId,再创建新 messageId
= 对话里出现新的气泡
所以截图中“阿宝连续说了几段话”,可以理解成后端在同一次运行中创建了多个消息对象。前端只要按 messageId 聚合内容,按事件顺序追加消息,就能自然渲染成多个气泡。
AG-UI 把“事情发生了什么”送到了前端,但截图里还有另一个关键部分:业务卡片。
- 左侧有业务图标
- 中间有标题“蚂蚁森林”
- 右侧有进入箭头
- 点击后进入支付宝内部页面
医院预约结果也不是一句“你可以去某某医院”,而是更适合操作的卡片或列表。
A2UI 可以理解为 Agent-to-User Interface。官方介绍中,它是面向 agent-driven interfaces 的声明式 UI 协议。Agent 不发送一段任意 HTML 或 JavaScript,而是发送一份结构化 JSON,告诉客户端“我希望这里显示一个什么界面”。
一个蚂蚁森林入口可以被描述成这样。下面是教学用伪代码,不是逐字段复刻官方 schema:
const antForestSurface = {
kind: "a2ui.surface",
id: "surface-ant-forest",
attachToMessageId: "msg-forest",
components: [
{
id: "forest-entry",
type: "ActionCard",
props: {
icon: "ant-forest",
title: "蚂蚁森林",
description: "进入页面查看并收取今日能量",
trailing: "chevron",
},
actions: {
onPress: {
type: "openDeepLink",
payload: {
url: "alipays://platformapi/startapp?appId=antForest",
},
},
},
},
],
};
const hospitalSurface = {
kind: "a2ui.surface",
id: "surface-eye-hospital",
attachToMessageId: "msg-hospital",
components: [
{
id: "appointment-list",
type: "AppointmentList",
props: {
title: "今日可关注的眼科预约入口",
items: [
{
id: "hospital-1",
name: "余杭区某眼科医院",
department: "眼科",
availability: "今日可预约",
distance: "3.2km",
},
{
id: "hospital-2",
name: "某综合医院眼科门诊",
department: "眼科",
availability: "部分时段可预约",
distance: "5.8km",
},
],
},
actions: {
onSelect: {
type: "openAppointmentDetail",
payloadPath: "$.selectedItem.id",
},
},
},
],
};
前端拿到这份 JSON 后,不会把它当成代码执行。正确的做法是映射到自己预先注册好的组件。
const componentCatalog = {
ActionCard,
AppointmentList,
Notice,
ConfirmPanel,
};
function renderA2UIComponent(node: A2UINode) {
const Component = componentCatalog[node.type];
if (!Component) {
return <UnsupportedComponent type={node.type} />;
}
return (
<Component
key={node.id}
{...sanitizeProps(node.props)}
onAction={(action) => handleA2UIAction(node, action)}
/>
);
}
第一,Agent 只能请求渲染“客户端已经认识的组件”。它不能随手发一段脚本,要求 App 执行。
第二,样式和安全边界仍然掌握在客户端手里。支付宝里的卡片应该长得像支付宝,企业微信里的卡片应该长得像企业微信,桌面端工具里的卡片应该长得像桌面工具。A2UI 描述意图,客户端负责落地。
用户输入
|
v
Agent 规划任务
|
+--> 查询蚂蚁森林工具
|
+--> 查询眼科医院工具
|
v
消息编排器
|
+--> AG-UI 发送文字消息事件
|
+--> AG-UI 发送工具状态事件
|
+--> AG-UI 通过 CUSTOM 或状态事件携带 A2UI payload
|
v
客户端
|
+--> 按 messageId 渲染多个聊天气泡
|
+--> 按 A2UI 描述渲染业务卡片
- AG-UI 负责“这轮对话跑到哪一步了”
- A2UI 负责“这一步的结果应该显示成什么界面”
- 前端负责“用可信组件把它渲染出来”
- 业务系统负责“数据是否真实、操作是否有权限”
如果只有 AG-UI,没有 A2UI,前端仍然可以展示流式文字、工具状态和运行进度,但业务结果可能只能退化成文本或固定组件。
如果只有 A2UI,没有 AG-UI,Agent 也可以给出一份 UI JSON,但前端很难知道这份 UI 是哪个 run 产生的、应该挂在哪条消息下、工具调用是否还在进行、失败时怎么恢复。
两者放在一起,才能做出“像聊天,又不像普通聊天”的产品体验。
async function runAbaoLikeAgent(input: string, emit: Emit) {
const runId = crypto.randomUUID();
const threadId = "current-thread";
emit({ type: "RUN_STARTED", runId, threadId });
const plan = await planner.plan(input);
const [forestResult, hospitalResult] =
await Promise.allSettled([
runToolWithEvents("getAntForestStatus", emit),
runToolWithEvents("searchHospitalAppointments", emit),
]);
if (forestResult.status === "fulfilled") {
const messageId = "msg-forest";
emitTextMessage(emit, {
messageId,
text: summarizeForest(forestResult.value),
});
emit({
type: "CUSTOM",
name: "a2ui.surface",
value: buildForestEntry({
messageId,
data: forestResult.value,
}),
});
}
if (hospitalResult.status === "fulfilled") {
const messageId = "msg-hospital";
emitTextMessage(emit, {
messageId,
text: summarizeHospitals(hospitalResult.value),
});
emit({
type: "CUSTOM",
name: "a2ui.surface",
value: buildHospitalList({
messageId,
data: hospitalResult.value,
}),
});
}
emit({ type: "RUN_FINISHED", runId, threadId });
}
emitTextMessage 可以封装消息事件:
function emitTextMessage(
emit: Emit,
input: {
messageId: string;
text: string;
},
) {
emit({
type: "TEXT_MESSAGE_START",
messageId: input.messageId,
role: "assistant",
});
for (const delta of splitForStreaming(input.text)) {
emit({
type: "TEXT_MESSAGE_CONTENT",
messageId: input.messageId,
delta,
});
}
emit({
type: "TEXT_MESSAGE_END",
messageId: input.messageId,
});
}
function onAgentEvent(event: AgentEvent) {
switch (event.type) {
case "TEXT_MESSAGE_START":
messages.add({
id: event.messageId,
role: event.role,
content: "",
});
break;
case "TEXT_MESSAGE_CONTENT":
messages.append(event.messageId, event.delta);
break;
case "TEXT_MESSAGE_END":
messages.markComplete(event.messageId);
break;
case "CUSTOM":
if (event.name === "a2ui.surface") {
surfaces.add(event.value);
}
break;
}
}
function ChatMessage({ message }: { message: Message }) {
const attachedSurfaces = surfaces.findByMessageId(message.id);
return (
<Bubble role={message.role}>
<Markdown>{message.content}</Markdown>
{attachedSurfaces.map((surface) => (
<A2UIRenderer key={surface.id} surface={surface} />
))}
</Bubble>
);
}
这样就能解释截图里“先有一段文字,再跟一个卡片”的结构。
如果 Agent 直接返回 HTML、CSS、JavaScript,短期看起来很灵活,长期会遇到很多问题:
- 安全风险高:模型生成的脚本不能随便执行
- 样式不一致:外部生成的 UI 很难自然融入宿主 App
- 跨端困难:Web HTML 到了 iOS、Android、桌面端就不好复用
- 权限不清楚:按钮到底能做什么,很难被统一审计
- 可维护性差:业务组件升级后,生成出来的 HTML 不会自动跟着升级
A2UI 的思路是:Agent 发送“界面意图”,客户端用自己的组件系统渲染。
不要这样:
Agent -> "<button onclick='book()'>预约</button>"
更合理:
Agent -> { type: "AppointmentList", props, actions }
Client -> 渲染本地 AppointmentList 组件
这就像后端 API 不直接返回一整页前端代码,而是返回结构化数据。不同的是,A2UI 返回的不只是业务数据,还包含“这些数据适合怎样被组织成界面”的信息。
在产品体验上,它告诉用户:这段答案不是凭空编的,背后有若干资料或工具结果。
- RAG 检索到的知识片段
- 工具调用返回的业务结果
- 可以跳转的来源页面或业务入口
AG-UI 可以用事件告诉前端“资料检索开始、资料检索结束、有哪些引用”;A2UI 可以把这些引用显示成卡片、来源列表或可展开的依据面板。
const citationSurface = {
kind: "a2ui.surface",
id: "surface-citations",
attachToMessageId: "msg-forest",
components: [
{
id: "citations",
type: "CitationList",
props: {
title: "参考 3 篇资料",
items: [
{ title: "蚂蚁森林今日能量说明", source: "ant-forest" },
{ title: "绿色出行能量规则", source: "transport" },
{ title: "能量成熟时间说明", source: "help-center" },
],
},
},
],
};
所以,“参考资料”不是普通装饰,它是可信度、可追溯性和可操作界面的结合点。
教程到这里,概念已经讲完了。真正上线时,还要处理一些更细的工程问题。
两个工具并行执行时,谁先返回不一定。医院查询可能比蚂蚁森林更快,也可能反过来。
如果你希望前端始终按“蚂蚁森林 -> 医院”展示,就要在事件里带上排序信息:
emit({
type: "CUSTOM",
name: "a2ui.surface",
value: {
...surface,
runId,
taskId: "ant-forest",
order: 10,
},
});
前端按 runId + order 组织渲染,而不是按网络到达时间盲目插入。
蚂蚁森林成功 -> 展示能量说明和入口
医院查询超时 -> 展示“暂时查不到号源,可以稍后再试”
整轮 run 仍然可以正常结束
这类局部失败最好也变成结构化事件,而不是只在控制台打一行错误。
展示“蚂蚁森林入口”和“真的替用户收能量”不是一回事。
展示“医院预约卡片”和“真的帮用户提交挂号”也不是一回事。
当动作涉及付款、挂号、账号资产、隐私数据或不可逆提交时,客户端要重新确认,并让业务系统做权限校验。Agent 生成的 action 参数只能作为“建议动作”,不能直接等同于用户授权。
async function handleA2UIAction(action: A2UIAction) {
if (action.type === "openDeepLink") {
return openAllowedDeepLink(action.payload.url);
}
if (action.type === "submitAppointment") {
const confirmed = await showConfirmDialog({
title: "确认预约?",
description: "提交后可能占用号源,请确认医院、科室和时间。",
});
if (!confirmed) return;
return appointmentService.submit(action.payload);
}
}
移动端网络断开后重连,前端可能会重新收到部分事件。服务端也可能因为重试重复发送同一个工具结果。
type AgentEventEnvelope = {
eventId: string;
runId: string;
sequence: number;
type: string;
payload: unknown;
};
前端按 eventId 去重,按 sequence 排序,按 runId 归档。业务动作也要有幂等键,避免用户或网络重复触发。
| 对比项 | AG-UI | A2UI |
|---|
| 解决的问题 | Agent 如何与前端持续交互 | Agent 希望前端显示什么界面 |
| 核心单位 | 事件 | 声明式 UI 描述 |
| 典型内容 | run、message、tool call、state、custom event | card、list、form、button、layout、action |
| 是否负责传输过程 | 是 | 否 |
| 是否负责 UI 结构 | 不直接负责 | 是 |
| 是否能表达多条聊天气泡 | 可以,通过多个 messageId | 不负责消息生命周期 |
| 是否能表达业务卡片 | 可以承载相关事件或 payload | 负责描述卡片结构和动作 |
| 前端职责 | 消费事件并维护会话状态 | 用可信组件渲染 UI JSON |
| 安全重点 | 事件顺序、状态一致性、重连恢复 | 组件白名单、动作校验、禁止任意代码执行 |
- 你只想让 Agent 像聊天一样实时输出、显示工具状态、维护会话:优先看 AG-UI。
- 你想让 Agent 生成表单、卡片、列表、审批面板、业务入口:需要 A2UI 这类声明式 UI 协议。
- 你想做阿宝截图里的完整体验:两者都需要,或者至少需要一套等价的“事件流 + UI 描述”分层。
如果你要在自己的产品里做类似阿宝的体验,可以按这个顺序设计。
- 查询账户状态
- 查询订单进度
- 查询附近可预约服务
- 生成推荐列表
- 填写或补全表单
- 发起需要确认的业务动作
每个任务都要能映射到明确工具,而不是只靠模型自由发挥。
type AppointmentSearchResult = {
queryId: string;
hospitals: Array<{
id: string;
name: string;
department: string;
availableDate: string;
slots: Array<{
id: string;
timeRange: string;
doctorTitle?: string;
}>;
}>;
};
明确什么情况下创建新气泡,什么情况下继续写同一个气泡。
同一个任务的长文本解释 -> 同一个 messageId 流式追加
不同任务的结果 -> 不同 messageId
同一任务下的业务 UI -> attach 到对应 messageId
全局提醒 -> 单独 messageId 或系统提示
const allowedComponents = [
"ActionCard",
"AppointmentList",
"ProductComparison",
"OrderTimeline",
"ConfirmPanel",
"CitationList",
];
每个组件要有 schema、可用动作、权限边界和 fallback。
低风险:打开详情页、展开更多、复制文本
中风险:带参数跳转、填充表单、筛选结果
高风险:付款、挂号、提交订单、修改账户资料
低风险动作可以直接执行。中高风险动作必须二次确认,且最终由业务系统校验。
Agent 应用不像普通接口那样“一次请求一次响应”。你需要考虑:
- 事件重复到达怎么办
- 用户刷新页面怎么恢复
- 某个工具失败怎么局部展示
- 多端同时打开同一会话怎么同步
- 已经展示的 A2UI 卡片是否还能更新
这些问题通常由 AG-UI 的 run、message、state、event ID,以及你自己的业务状态表一起解决。
AG-UI 的核心是事件通信。它可以携带自定义事件,也可以携带某些 UI payload,但“怎么画界面”不是它的主要职责。
A2UI 让 Agent 能描述界面,但客户端仍然要有组件库、渲染器、权限控制、样式规范和动作处理。它减少的是“每种 Agent 结果都手写一套固定页面”的成本,不是让前端消失。
一次模型规划、多次工具调用、一次结果整理,也可以产生多条消息。气泡数量由消息编排决定,不由模型调用次数决定。
误区四:Agent 生成按钮,就可以直接执行按钮动作#
按钮只是一个操作入口。真正的业务动作必须经过客户端和业务服务校验。尤其是挂号、支付、资产、隐私相关操作,不能把模型输出当成授权。
用户的一句话先被拆成“蚂蚁森林”和“眼科医院”两个任务。Agent 调用对应业务工具拿到结果。消息编排层把不同任务组织成多条 assistant 消息。AG-UI 一类事件流负责把 run、message、tool、state 这些过程传给前端。遇到“蚂蚁森林入口”“医院预约列表”这类非纯文本结果时,再用 A2UI 一类声明式 UI 描述告诉客户端要渲染什么卡片、列表和动作。
这就是截图里“像聊天,但又能直接操作业务”的原因。
AG-UI 让 Agent 把过程一件件说清楚,A2UI 让其中一些结果不止停留在文字里。