导读
本文围绕一个核心问题展开:如何让AI助手从"输出文字"进化到"生成界面"?我们基于Google A2UI协议,自研了Vue渲染器和 Agent 完整工具链,形成了一套完整的生成式UI体系。文章将详细阐述Runtime Schema装配、双重校验机制、SSE双通道输出、Wrapper组件扩展等关键设计,为构建标准化、可复用的AI交互界面提供参考。
引言
在 B 站商业广告业务中,我们开发了统一的 AI 助手框架,创建不同业务的知识库,以 markdown 渲染的形式输出内容,实现了不同业务中 AI 助手的快速应用。
但是,随着时间的推移和 AI 在业务中的快速应用,当大模型的能力从"聊天"走向"办事"时,如何让 AI 稳定、可靠地生成真正的"可交互界面",提高信息传达和交互的效率,成为越来越多公司和业务关注的方向,从而带来了大模型生成 UI 的发展趋势。
关于这一点,我们内部的 AI 助手框架已经支持在 Markdown 中通过自定义语法嵌入组件,例如 ::: ProductCard ::: ,具有直观、轻量的特点,能在对话流中快速引入可交互元素。但在多业务、复杂任务的规模化落地中,它的局限性逐渐显现,例如前端、后端、Agent 耦合严重,需要 Agent 严格配合前端的特定格式来填充内容。这种方案的本质是 Agent 按前端预定义的模板填充数据,而非让 Agent 自主描述 UI 结构,不符合生成式 UI 的核心思想。
如何快速和我们的 AI 助手框架结合,形成完整的生成式 UI 体系?
在探索这个问题的过程中,我们发现这不是一个单纯的前端渲染问题,而是一个涉及大模型生成、协议标准、后端校验、前端渲染、多业务治理的系统工程。基于此,本文将完整拆解我们的实践路径。
一、B站商业广告的 AI 助手框架
在展开生成式UI的方案之前,有必要先回顾一下我们的起点。
1.1 统一 AI 助手框架
在B站商业广告业务中,我们面对的是多个差异化的业务场景:
- 广告投放助手:帮助广告主理解投放数据、优化投放策略
- 产品帮助助手:介绍广告产品功能、引导操作流程
- 数据分析助手:生成广告数据报表、提供洞察分析
为了快速响应各业务场景的AI需求,我们构建了统一的 AI 助手框架:
-
统一前端框架:标准化的对话接口、视觉样式,业务方只需实现对应的接口,并按需实现自定义功能
-
Markdown 渲染:前端统一支持Markdown格式输出,快速实现内容呈现
1.2 从"可读"到"可交互"的需求演进
随着业务深入,纯文本/Markdown的局限性开始显现:
场景一:广告投放诊断
- 用户问:"帮我看看最近三天某计划的投放效果?"
- AI返回一段文字描述:"曝光量xx,点击率xx%,消耗xx元,建议优化素材..."
- 用户需要:一个包含关键指标卡片、趋势图表
场景二:意图识别
- 用户问:"我要下单,目标是xxx..."
- AI返回:"您提供的缺少xxx参数...请您补充参数重新告知我下单。"
- 用户需要:一个结构化表单,可以直接在对话中修改并重新提交
这些场景的共同需求是:从"告诉用户怎么做"升级为"让用户在对话中直接完成操作",这就引出了生成式UI的核心命题。
我们固然可以使用模板填充的方式:
css
::: ProductCard {"name":"手机","id":1}
:::
这种方案可以快速实现大部分简单场景------前端预定义好各类卡片组件,Agent按约定格式输出,前端匹配渲染。
它的优势很明显:
- 实现简单,短期内快速上线
- 卡片样式可控,符合设计规范
- 适合场景固定、UI形态稳定的业务
但随着场景复杂化,其局限性也逐渐暴露:

本质问题:模板填充方案让Agent扮演的是"填表员"角色,而非"设计师"角色,Agent无法根据上下文动态决定UI形态。
二、生成式 UI 的思路
2.1 为什么选择 A2UI
在协议选型上,我们调研了多种方案,最终选择 Google 的 A2UI 协议,原因如下:
-
标准化:大厂提出,业界规范,有社区支持,避免自研协议的技术债务
-
声明式:JSON Schema 描述UI,天然适合大模型生成
-
可增量:支持 dataModelUpdate / surfaceUpdate,实现细粒度更新
-
框架无关:同一份 UI 描述可在 Vue / React / 小程序等多端渲染
-
安全可控:组件基于组件库白名单,Agent 自由选择,客户端负责渲染
2.2 整体架构

三、后端 + Agent 设计
3.1 请求入参设置
每个业务请求需要携带能力边界声明:

3.2 Runtime Schema 装配
根据业务标识和白名单,动态装配该业务可用的组件和动作集合。
装配流程举例:

这样设计的价值在于,一是能力隔离,不同业务在统一协议下安全、独立地演进;二是动态扩展,新增业务只需要补充组件包,不改核心逻辑;三是输出可控,白名单机制限制了模型输出的边界。
3.3 双重校验机制
尽管我们通过能力隔离,已经生成了安全的 Prompt,但是由于模型输出可能不稳定,因此针对大模型产出的 A2UI JSON,必须经过严格校验才可放行。我们设计了双层校验机制:
第一层:结构校验
- 消息类型是否合法,符合 A2UI 协议结构(beginRendering / surfaceUpdate / dataModelUpdate)
- 组件类型是否在白名单
- 组件属性是否完整
- 动作名称是否在白名单
第二层:过渡校验
Surface 是 A2UI 中一个独立的 UI 实例载体,每个 Surface 有唯一的 surfaceId,代表一个独立的界面区域,具备从"未创建"→"已创建"→"可交互"的状态转换。我们需要确保这个过程合法有序,避免出现更新不存在的界面、重复初始化等异常。为此,我们维护了每个 Surface 的状态机:

校验规则:
-
不能更新未创建的 Surface
-
对于同一个 Surface,不能重复产出 beginRendering
-
更新时 SurfaceId 必须匹配当前会话
3.4 SSE 双通道输出
传统方案中,前端需要从文本流中正则匹配和解析JSON,链路脆弱。我们将文本流和结构化数据分离,两条通道独立传输,互不影响。若 a2ui_message 通道出现数据丢失,前端可依据 message_stream 中的文本内容降级展示,或通过 finish 事件中的完整消息进行重试补发,确保 UI 渲染的可靠性。

四、 前端通用渲染器 SDK
4.1 整体结构
我们基于 Google 官方开源的 A2UI 协议、React 渲染器等,自研实现了 Vue 渲染器,以 npm 包的形式交付,业务方安装即可使用:

4.2 消息处理器
我们实现了消息处理器,为同一个 Surface 的每条 A2UI JSON 消息生成唯一签名。已处理过的消息会被识别并跳过,不再重复执行,保证逻辑的幂等性。另外,由于网络传输延迟、历史会话回放等场景,消息可能出现乱序或倒序到达,我们也设计了相应的处理机制。极端情况下,有极小概率会出现个别消息丢失,但仅会导致 Surface 的 UI 状态短暂不一致(比如,没有回显已经填写过的字段),不会引发严重渲染问题。
消息处理器链路如下:

4.3 DataModel 设计
统一状态存储,支持 path 级别读写和数据绑定:
json
DataModel结构示例:
{
"/form/name": "张三",
"/form/email": "zhang@example.com",
"/cart/items": [{"id": 1, "name": "商品A"}],
"/ui/loading": false
}
组件通过path引用数据,任一数据变化,所有绑定组件自动更新。
4.4 Wrapper 组件体系
业务方新增自定义组件时,只需遵循统一规范,无需理解渲染器内部逻辑。
接入流程:
- 编写业务组件(普通Vue组件)
- 使用 SDK 提供的 Wrapper 组合函数包装
- 注册到组件映射表(组件类型名 → 业务组件)
- Agent 即可使用
Wrapper提供的能力:
-
自动解析 node 中的属性
-
自动绑定 DataModel 数据(通过 path 引用)
-
自动构建动作回调函数
-
统一处理 loading、error 等通用状态
4.5 动作闭环
组件提交的 action,分为前端处理和后端处理,业务方可自定义 action 处理方法,SDK 提供的默认有以下2类:

前端处理的事件比较简单。对于后端处理的事件,会经历以下流程:
- 用户交互触发动作
- 动作冒泡到业务层
- 业务层通过HTTP接口回传后端
- 后端返回dataModelUpdate或surfaceUpdate
- 前端执行增量刷新(原位更新)
这样,我们便实现了从 Agent 到前端 SDK 的、可复用的完整链路。
五、与现有框架的集成
我们并非将现有业务中已经实现的 AI 助手推倒重来,而是进行了扩展。Agent 返回的 finish 事件中,包含了 SDK 提供的标准组件 A2UIMessage.vue,业务方可直接将其作为模板组件使用:
css
::: a2ui-message {"message": [...]}
:::
业务方的接入成本也非常小,只需要三步:
- 前端实现业务组件,并接入 SDK
- 前端注册组件包
- 业务方后端或者业务方 Agent 调用接口,生成 A2UI 消息
至此,我们完整阐述说明了 A2UI 实践的完整框架和流程。可以看到,它具备以下价值:
对业务方:
- 无需关心UI实现,聚焦业务逻辑
- 新场景接入周期从天级缩短到小时级
对 Agent:
- 从"记忆数据格式"转向"理解UI描述语言"
- 具备真正的"界面生成"能力
对整体架构:
-
前后端解耦,协议标准化
-
多业务可复用同一套基础设施
六、DEMO 示例
我们已经实现了完整的可复用工具链,以下是一些 demo,展示了从用户输入 Prompt 到产出组件的过程。实际使用中,由业务方的后端或者 Agent 调用 A2UI 的 Agent 服务获取真实的 A2UI JSON,再交给业务方的前端渲染。
注意:以下为简化示例,实际 JSON 结构以 A2UI 协议为准。
6.1 图表示例
- 用户在对话框中输入:"我想查看最近12个月某5个车企(化名)的销量变化趋势图"
- Agent 生成 A2UI JSON:
json
{
"sessionId": "session-xxx",
"messages": [
{ "createSurface": { "surfaceId": "surface-xxx" } },
{
"updateComponents": {
"surfaceId": "surface-xxx",
"components": [
{ "id": "root", "component": "Card", "child": "chart-container" },
{
"id": "sales-chart",
"component": "Charts",
"type": "line",
"title": "近12个月车企销量趋势",
"options": {
"xAxis": { "data": ["1月", "2月", ...] },
"series": [
{ "name": "车企A", "data": [12500, 13200, ...] },
{ "name": "车企B", "data": [10200, 10800, ...] },
...
]
}
}
]
}
}
]
}
- 渲染结果:

6.2 表单示例
- 用户在对话框中输入:"生成一个用户报名信息的填写表单"
- Agent 生成 A2UI JSON:
json
{
"sessionId": "session-xxx",
"messages": [
{ "createSurface": { "surfaceId": "surface-xxx", "root": "root" } },
{ "updateDataModel": { "surfaceId": "surface-xxx", "value": { "userName": "", "userEmail": "", "userPhone": "" } } },
{
"updateComponents": {
"surfaceId": "surface-xxx",
"components": [
{ "id": "root", "component": "Card", "child": "form-column" },
{ "id": "form-column", "component": "Column", "children": ["title-text", "name-field", "email-field", "phone-field", "submit-btn"] },
{ "id": "title-text", "component": "Text", "text": "用户报名信息填写" },
{ "id": "name-field", "component": "TextField", "label": "姓名", "placeholder": "请输入姓名" },
{ "id": "email-field", "component": "TextField", "label": "邮箱", "placeholder": "请输入邮箱" },
{ "id": "phone-field", "component": "TextField", "label": "手机号", "placeholder": "请输入手机号" },
{ "id": "submit-btn", "component": "Button", "text": "提交报名", "action": { "name": "marketing.submitLead" } }
]
}
}
]
}
- 渲染结果:

- 用户填写并点击提交后,前端将表单数据回传至后端:
rust
// 发送消息示例:后端读取注释中的数据作为结果
用户提交了数据<!-- {"action":"marketing.submitLead","data":{"userName":"小明","userEmail":"aaa@bbb.com","userPhone":"12345612345"}} -->
- 后端处理完成后,再次调用 A2UI 服务,返回更新界面的指令:
json
{
"sessionId": "session-xxx",
"messages": [
{ "updateDataModel": { "surfaceId": "surface-xxx", "value": {} } },
{
"updateComponents": {
"surfaceId": "surface-xxx",
"components": [
{ "id": "success-icon", "component": "Text", "text": "✓" },
{ "id": "title-text", "component": "Text", "text": "报名成功" },
{ "id": "message-text", "component": "Text", "text": "感谢您的报名,我们会尽快与您联系。" },
{ "id": "form-column", "component": "Column", "children": ["success-icon", "title-text", "message-text"] }
]
}
}
]
}
- 前端接收消息,并处理 UI 更新:

七、总结
目前,我们的 A2UI 方案已在部分业务中接入,正在快速迭代中。由于 A2UI 协议仍处于早期阶段,大模型能力也在持续演进,尚无法支持全部复杂组件的自主生成。因此,我们目前采用 A2UI 实现通用交互、业务自行实现复杂组件的混合模式。
但可以预见,随着协议和大模型能力的成熟,生成式 UI 必将释放更强大的能力和更丰富的应用场景。接下来,我们将继续探索 A2UI 在移动端、小程序等架构上的应用,期待在不远的未来做出更优质的成果。
-End-
作者丨Nie