做 AI Agent 这两年,我踩过最多的坑不在模型,在 UI。
开篇:一个让我憋屈了很久的痛点
先说个真实场景。
去年我做一个 AI 助手项目,后端用 LangGraph 编排,能调用搜索、查数据库、生成报告,能力很强。兴冲冲接到前端,用户问"帮我分析下这个月的销售数据",Agent 琢磨半天,吐出一大段文字:
css
本月总销售额 1,230,500 元,环比增长 15.3%。
其中华东区占比最高,达 42%。
Top 3 产品分别是 A、B、C...
用户看完,内心毫无波澜,甚至觉得这 Agent 有点"笨"------明明是个数据看板的活,为啥非得用文字描述?
我试过让 Agent 直接吐 HTML,前端 dangerouslySetInnerHTML 渲染------结果第一天就被安全同学叫停,XSS 风险拉满。又试过自己定义一套 JSON Schema 让 Agent 填,但每接一个新场景就要改 Schema,而且这套 Schema 换个产品就用不了,Agent 每接一个项目要学一套方言。
憋屈了挺久,我才想明白一件事:后端 Agent 逻辑千千万,前端交互方案却几乎是空白。
模型能力同质化越来越严重的今天,真正能拉开产品差距的是人机交互体验 。而交互体验的核心障碍,在于 Agent 是远程运行的、跨信任边界的,并且缺乏一套让 Agent "说 UI 语言"的统一标准。
后来 Google 开源了 A2UI 协议,我研究了一圈,发现它的设计思路正好是我想要的------于是基于这个协议,我做了 BoteAI/a2ui:一个把 LLM 生成的 A2UI 协议消息,安全渲染成浏览器里可交互界面的 React SDK。
这篇文章不讲营销,就讲讲我为什么选这条技术路线、怎么设计的、以及开发时踩过的坑。
一、为什么我选了 A2UI 协议,而不是自己造?
1.1 Agent 输出 UI,难点在哪
生成式 AI 很擅长生成文本和代码,但让 Agent 向用户呈现富交互界面这件事,有三个硬骨头:
- Agent 是远程运行的 ------ 它不能直接操作用户浏览器里的 DOM
- 跨信任边界 ------ LLM 输出的内容不可全信,直接执行它的代码等于把用户安全交给运气
- 框架碎片化 ------ Web 有 React/Vue/Angular,移动端有 Flutter/SwiftUI,同一份 UI 描述要在多端复用,缺乏统一标准
我自己试过的两条路,都走不通:
| 方案 | 为什么放弃 |
|---|---|
| 让 Agent 直接吐 HTML/JS,前端渲染 | 安全噩梦,没法上生产 |
| 自己定义私有 JSON Schema | 无法跨产品复用,换一个项目要重写一套,Agent 还得专门适配 |
1.2 Google 的解法:A2UI 协议
研究了一圈,发现 Google 开源的 A2UI(Agent-to-User Interface)协议思路和我想到一块儿去了。它有一句口号,我觉得概括得很到位:
Safe like data, expressive like code. (像数据一样安全,像代码一样富有表现力)
核心思路是:A2UI 是声明式 JSON 格式,不是可执行代码。
- 客户端维护一个受信任的组件目录(Card、Button、TextField 等预批准组件)
- Agent 只能"请求"渲染这个目录里的组件,传数据进去
- Agent 永远无法执行任意代码
打个比方:Agent 像是个"点菜的人",它只能从菜单(组件目录)里点菜,不能跑进后厨自己炒。这样既保证了安全,又让 Agent 能组合出丰富多样的界面。
1.3 为什么不直接用 Google 官方渲染器,要自己再做一个?
这是我做之前反复纠结的问题。Google 官方提供了 Lit 渲染器,但我有几个实际诉求没被满足:
- 我的技术栈是 React,团队也最熟 React,Lit 的心智迁移成本不小
- 我需要开箱即用的主题系统------toB 场景下不同客户要不同皮肤,官方渲染器没有这套预设
- 我想要生产就绪的常用组件(表格、图表、KPI 卡),不用每次从零搭
- 我有微前端 / 多团队协作的诉求,需要远程加载组件 bundle 的能力
所以我做了 BoteAI/a2ui :在 A2UI 协议之上,补齐 React 渲染、主题预设、常用组件库、远程 bundle 这几层。协议层我不重复造轮子,只在渲染和工程化层做增量------这是这个项目的定位边界。
A2UI 协议目前版本是 v0.9.1(v1.0 候选版),生态已经有 Python / Kotlin / Swift / Dart 多语言 SDK,传输层兼容 A2A Protocol 和 AG-UI。我判断这个协议是有长期生命力的,值得押注。
二、项目长什么样
一句话定位:
把 LLM 生成的 A2UI 协议消息,自动渲染成可交互的 React 界面。
2.1 整体架构
数据流长这样:
java
Agent / Backend
│ A2UI messages (JSON)
▼
@boteai/a2ui-render ← 协议渲染器 (React + Lit)
│ customComponents registry
├──────────────────────────────────┐
▼ ▼
@boteai/a2ui-comp-preset @boteai/a2ui-custom-kit
(内置预设组件) (定义、注册、打包自定义组件)
│ │
└──────────────┬───────────────────┘
▼
你的业务 UI (本地 bundle 或远程 .mjs)
我把它拆成了 Monorepo,三个 npm 包各司其职:
| 包名 | 角色 |
|---|---|
@boteai/a2ui-render |
核心渲染器,接收 A2UI 消息并渲染 |
@boteai/a2ui-comp-preset |
12 个生产就绪的预设组件(表格、图表、卡片等) |
@boteai/a2ui-custom-kit |
自定义组件创作工具包(Zod API 定义、注册表合并等) |
技术栈:React + Lit + TypeScript + Zod + esbuild + Lerna,支持 Node.js 16+。
这里有个设计取舍我想说一下:底层我用了 Lit 。原因是 A2UI 官方的 Web Components 实现就是基于 Lit 的,我不想在协议底层重复造轮子,所以 a2ui-render 内部通过 LitSurfaceHost 桥接 Lit 的 Web Components,对外暴露的是 React API------这样既复用了官方组件生态,又让 React 用户无感知。
三、核心特性怎么用
这部分是重点,直接看代码。
3.1 最小渲染示例:10 行代码跑通 Agent → UI
安装:
bash
yarn add @boteai/a2ui-render
最小示例:
jsx
import { BaseRenderer } from '@boteai/a2ui-render';
export function AgentPanel({ messages }) {
return (
<BaseRenderer
messages={messages} // A2UI 协议消息数组
protocolVersion="0.9" // 协议版本
themePreset="conversation" // 主题预设
onAction={(event) => console.log(event.name, event.context)}
/>
);
}
messages 是 Agent 返回的 A2UI JSON 数组,BaseRenderer 会自动解析、渲染。用户点按钮触发的 action,会通过 onAction 回调回到你的业务层------这就完成了"Agent 渲染 UI → 用户操作 → 业务响应"的闭环。
3.2 主题系统:一个 prop 切换 5 套主题
这是我个人最喜欢的一个特性,也是我做这个项目时最花心思的地方。内置 5 套精选主题 ,通过单个 themePreset prop 即可运行时切换,无需重新构建:
| 预设名 | 风格 |
|---|---|
default |
A2UI Lit 默认 token |
conversation |
聊天友好的间距和圆角 |
cyber |
鲜艳的科技/霓虹风 |
platformInterconnect |
企业平台互联外观 |
deepBlueWisdom |
深蓝智慧仪表盘风 |
切换主题就是改一个 prop:
jsx
<BaseRenderer
messages={messages}
themePreset="cyber" // 从 conversation 换成 cyber,界面立刻变霓虹风
onAction={handleAction}
/>
为什么特意做这套主题系统?因为我做 toB 时被"不同客户要不同皮肤"折磨过------以前要改一堆样式变量甚至重新打包,现在一个 prop 搞定。主题是运行时通过 CSS 变量覆盖实现的,不碰构建。
3.3 预设组件:12 个开箱即用
@boteai/a2ui-comp-preset 里我放了 12 个生产就绪的组件,基本覆盖了常见的数据展示场景:
| 组件 | 用途 |
|---|---|
PresetTitle / PresetButton / PresetBadge |
基础元素 |
PresetSelect |
下拉选择器(基于 antd) |
PresetRow / PresetColumn |
Flex 布局容器 |
PresetMetric |
KPI 指标卡片 |
PresetDashboardCard |
仪表盘摘要卡 |
PresetDataTable |
数据表格 |
PresetBarChart / PresetPieChart |
图表(基于 recharts) |
PresetFlightCard |
航班状态卡片(领域示例) |
接入预设组件:
jsx
import { BaseRenderer } from '@boteai/a2ui-render';
import { a2uiPresetComponentRegistry } from '@boteai/a2ui-comp-preset';
<BaseRenderer
messages={messages}
protocolVersion="0.9"
customComponents={a2uiPresetComponentRegistry} // 一行接入所有预设组件
onAction={handleAction}
/>
还支持子路径导出做 tree-shaking:
@boteai/a2ui-comp-preset/registry------ 仅运行时渲染@boteai/a2ui-comp-preset/schemas------ Agent 提示、配置器、代码生成
这个细节是给打包体积敏感的场景准备的,schemas 那份能给 Agent 当提示用,让 LLM 知道有哪些组件、每个组件接受什么 props。
3.4 自定义组件:两条集成路径
当 A2UI 消息引用你自己的组件名时,用 @boteai/a2ui-custom-kit 注册。我设计了两种路径,适配不同场景:
路径一:本地注册表(组件和渲染器在同一应用)
jsx
import { defineComponentApi, defineSimpleRegistryEntry, createReactComponent } from '@boteai/a2ui-custom-kit';
import { z } from 'zod';
// 1. 用 Zod 定义组件 API(类型安全,Agent 也能据此生成)
const myCardApi = defineComponentApi({
title: z.string(),
content: z.string(),
});
// 2. 包装你的 React 组件
const MyCardElement = createReactComponent({
Component: ({ title, content }) => (
<div className="my-card">
<h3>{title}</h3>
<p>{content}</p>
</div>
),
});
// 3. 注册
const myRegistry = defineSimpleRegistryEntry({
type: 'MyCard',
api: myCardApi,
element: MyCardElement,
});
路径二:远程 ESM bundle(独立团队、CDN 分发、微前端场景)
jsx
import { loadRemoteA2UICustomRegistry, mergeRegistryEntries } from '@boteai/a2ui-custom-kit';
import { a2uiPresetComponentRegistry } from '@boteai/a2ui-comp-preset';
// 动态加载远程 .mjs 注册表
const remoteRegistry = await loadRemoteA2UICustomRegistry({
url: 'https://cdn.example.com/my-components.mjs',
});
// 合并预设 + 自定义 + 远程,一起用
<BaseRenderer
messages={messages}
customComponents={mergeRegistryEntries(
a2uiPresetComponentRegistry,
myRegistry,
remoteRegistry
)}
onAction={handleAction}
/>
远程 bundle 用 esbuild 打成 .mjs。我做这个设计是因为见过太多"一个前端项目塞五六个团队代码"的灾难------A 团队维护核心渲染,B 团队的业务组件独立打包上 CDN,互不干扰,发版也不用一起。
3.5 Playground:不搭 Agent 也能玩
这个我特意做的。项目内置了一个 Playground 应用 ,内置 30+ v0.9 演示(卡片、表单、数据视图、特殊场景),支持:
- 在 A2UI v0.8 / v0.9 间切换
- 实时主题切换并排预览
- JSON 编辑器就地编辑消息即时预览
- 自定义组件开发指南
启动方式:
bash
git clone https://github.com/BoteAI/a2ui.git
cd a2ui
yarn bs # 安装依赖
yarn build # 首次需要构建包
yarn start # 启动 Playground
浏览器访问 http://localhost:8000/#/a2ui-playgroup/v9 即可。
为什么必须做 Playground?因为我被"装半天跑不起来"劝退过太多次别人的开源项目。技术选型时决策者最怕这个,有了 Playground,5 分钟就能看到效果,不用搭完整 Agent 就能评估这东西到底好不好用。
四、实战:从 0 到 1 接入
把上面的特性串起来,完整的接入流程:
步骤 1:安装依赖
bash
yarn add @boteai/a2ui-render @boteai/a2ui-comp-preset
# 如需自定义组件
yarn add @boteai/a2ui-custom-kit
步骤 2:写一个最小可用的 Agent 面板
jsx
import { BaseRenderer, inferProtocolVersionFromMessages } from '@boteai/a2ui-render';
import { a2uiPresetComponentRegistry } from '@boteai/a2ui-comp-preset';
function AgentPanel({ messages, onUserAction }) {
// 自动检测协议版本(v0.8 / v0.9)
const version = inferProtocolVersionFromMessages(messages);
return (
<BaseRenderer
messages={messages}
protocolVersion={version}
themePreset="conversation"
customComponents={a2uiPresetComponentRegistry}
onAction={(event) => {
// event.name 是 action 名,event.context 是上下文
onUserAction(event);
}}
/>
);
}
步骤 3:模拟一个 Agent 返回的 A2UI 消息
js
const demoMessages = [
{
id: 'msg-1',
type: 'component',
component: {
type: 'PresetTitle',
props: { text: '本月销售概览' }
}
},
{
id: 'msg-2',
type: 'component',
component: {
type: 'PresetMetric',
props: { label: '总销售额', value: '¥1,230,500', trend: '+15.3%' }
}
},
{
id: 'msg-3',
type: 'component',
component: {
type: 'PresetBarChart',
props: {
data: [
{ name: '华东', value: 516810 },
{ name: '华南', value: 307625 },
{ name: '华北', value: 283015 },
{ name: '西部', value: 123050 }
]
}
}
}
];
传给 AgentPanel,浏览器里就会渲染出一个带标题、KPI 卡片和柱状图的仪表盘------而这一切,数据都是 Agent 生成的 JSON。
五、开发时踩过的坑(实战细节)
下面这几个坑,都是我开发过程中真实踩到的,提前避坑能省不少时间。
坑 1:协议版本别写错
A2UI 有 v0.8(遗留)和 v0.9(当前生产版)两个版本,消息格式不一样。我早期手动写死版本号,结果对接不同 Agent 时经常报错。后来加了 inferProtocolVersionFromMessages 自动检测,别手动写死:
js
const version = inferProtocolVersionFromMessages(messages);
坑 2:远程 ESM bundle 的 esbuild 配置
打远程 .mjs 包时,esbuild 的 target 必须设成 esnext,format 设成 esm,否则浏览器加载会报错。我在这卡过半天:
js
esbuild.build({
entryPoints: ['src/registry.ts'],
bundle: true,
format: 'esm', // 必须
target: 'esnext', // 必须
outfile: 'dist/my-components.mjs',
});
坑 3:自定义组件的 action dispatch 时机
用 createReactComponent 包装自定义组件时,处理用户交互(点击、提交)要用 dispatch,并且要在事件回调里 dispatch,别在 render 里 dispatch------我一开始在 render 里调,直接触发无限循环,组件刷到爆栈:
jsx
const MyButtonElement = createReactComponent({
Component: ({ label }, { dispatch }) => (
<button onClick={() => dispatch('submit', { formId: 'xxx' })}>
{label}
</button>
),
});
坑 4:主题切换不需要重新构建
这个不算坑,但容易误解。很多人看到 themePreset 是个 prop,会以为要配 webpack 变量。其实主题是运行时通过 CSS 变量覆盖实现的,直接改 prop 即可,不用重新构建。
六、和同类方案怎么选
横向对比一下目前 AI Agent UI 领域的几个主流方案,尽量客观:
| 维度 | BoteAI/a2ui | CopilotKit | 自己造轮子 |
|---|---|---|---|
| 协议基础 | Google A2UI 开放标准 | 自有 AG-UI 协议 | 私有 Schema |
| 安全模型 | 声明式,组件目录白名单 | 组件白名单 | 看实现 |
| 框架绑定 | React(Lit 底层) | React | 看实现 |
| 主题系统 | 5 套预设,运行时切换 | 自定义 | 自己写 |
| 预设组件 | 12 个 | 较多 | 从零 |
| 微前端支持 | 远程 ESM bundle | 弱 | 自己搞 |
| 成熟度 | 早期(beta) | 成熟(31k+ Stars) | - |
说句实话:CopilotKit 比我成熟得多,31k+ Stars,生态也全。 我的项目的差异化只有一个------基于开放标准协议。
这意味着如果你的 Agent 是按 A2UI 协议输出的,将来换前端框架(比如换成 Flutter 端),协议消息不用改,只要换渲染器。协议层和实现层解耦,这是我押注的长期价值,也是我愿意花精力做这个项目的原因。
所以选型上:诉求是"马上能用、生态最全",CopilotKit 更稳;诉求是"跟着标准走、长期可移植",A2UI 路线更值得押注。 各有适用场景,我不拉踩。
七、写在最后
回到开头的那个场景。
如果用 A2UI 协议,那个"分析销售数据"的 Agent,输出就不再是一坨文字,而是这样的结构化消息:
json
[
{ "type": "PresetTitle", "props": { "text": "本月销售概览" } },
{ "type": "PresetMetric", "props": { "label": "总销售额", "value": "¥1,230,500", "trend": "+15.3%" } },
{ "type": "PresetBarChart", "props": { "data": [...] } }
]
前端 BaseRenderer 一渲染,用户看到的就是一个带 KPI 卡片和柱状图的可视化看板,还能点柱子下钻。这才像一个 2026 年该有的 AI 助手。
AI Agent 行业已经到了分水岭:早期比谁能更快接入大模型,现在模型能力同质化,真正拉开差距的是交互体验。而交互体验的底层基建------一套让 Agent 安全地"说 UI 语言"的协议和渲染层------正在快速成型。
我做的 BoteAI/a2ui 是这个方向上的一次实践:协议层我不重复造轮子(用 Google 的 A2UI),只在 React 渲染、主题、组件库、工程化这几层做增量。项目还在 beta 阶段(v0.0.3-beta),功能在持续迭代,下一步会加轮播器、富文本、更多领域组件。
项目地址 :github.com/BoteAI/a2ui(Apache 协议) A2UI 协议 :github.com/google/A2UI
⭐ 最后,说点掏心窝的
开源一个项目,最难的不是写代码,是让它被人看见、被人用起来。
这个项目我从 6 月开始写,到现在 45 个 commit,beta 阶段,功能还在迭代。说实话,每天打开 GitHub 看到 Star 数的跳动,是我继续投入的最大动力------不是虚荣,是确认"这东西真的有人需要",那就有继续做下去的意义。
如果你读到这里,觉得这个方向有点意思,或者你的 AI Agent 项目正好缺这么一层前端方案,帮我点个 Star 吧 👉 github.com/BoteAI/a2ui
你的一个 Star,对我来说意味着:
- 更多做 AI Agent 的同学能看到这个项目
- 我有动力持续维护、加更多组件
- 也许能吸引到一起贡献的小伙伴
如果你用了之后觉得哪里不顺手,直接去 GitHub 提 Issue,不用客气,我每条都会认真回------真实场景的反馈比任何设计文档都值钱。
也欢迎来贡献组件,不管你是 React 老手还是想练手的新人,都欢迎,我会帮忙 review。
最后想问一句:你做 AI Agent 时,前端交互是怎么解决的?是让 Agent 吐 Markdown,还是自己定义 JSON Schema,还是用过类似 A2UI / AG-UI 这类协议?欢迎评论区聊聊,我想听听不同场景下的真实实践,也帮我把这个项目做得更贴合实际需求。
如果这篇文章对你有启发,掘金点个赞👍 + GitHub 点个 Star⭐,是对开源作者最实在的支持。有问题评论区或 GitHub Issue 都行,我会认真回复。