📌 项目地址:google/A2UI | ⭐ 15,043 颗星 | 🔧 TypeScript | 📜 Apache-2.0
问题:远程Agent能生成文本,但不能安全生成UI
LLM输出文本和代码已经常见。但当一个远程Agent(比如运行在云端的对话系统)想给用户展示一张表单、一个确认对话框或一组卡片时,事情就变复杂了。直接让Agent生成HTML/JS,等于把前端安全大门敞开——XSS、恶意脚本、外部资源加载防不胜防。只返回文本,交互又退化到命令行。
Google开源的A2UI(Agent-to-User Interface)给出了一个不同于“过滤脚本”的思路:Agent输出声明式JSON,客户端用自己信任的原生组件渲染。项目目前15k star,处于v0.9 Public Preview阶段。
核心设计:Agent说“我要什么”,客户端决定“怎么画”
README里有一句话:“safe like data, but expressive like code”。A2UI的JSON只描述UI的意图,不包含任何可执行逻辑。
一个典型的A2UI JSON片段长这样(来自官方spec):
{
"components": [
{
"id": "card1",
"type": "Card",
"properties": {
"title": "航班确认",
"description": "北京 → 上海,2025-03-15"
}
},
{
"id": "btn1",
"type": "Button",
"properties": {
"label": "取消",
"onTap": "action: cancel_booking"
}
}
]
}
注意onTap的值是一个字符串,不是回调函数。客户端会把它映射到自己平台的事件处理系统(比如Flutter里的ElevatedButton的onPressed),但Agent无法注入任意代码。JSON里也没有<script>、onerror、href=javascript:这类危险入口。
安全机制:白名单组件目录,从数据结构封死执行
A2UI不依赖黑名单过滤(比如用DOMPurify清洗HTML),因为黑名单总有遗漏。它的做法在数据结构层面:客户端维护一个组件注册表,开发者只把信任的组件(Card、Button、TextField等)注册进去。Agent能选择的type只能来自这个白名单。
意味着什么?Agent返回一个恶意字段,比如"onerror": "alert(1)",客户端解析时只读取自己定义的properties,不认这个字段,自然不执行。这个安全模型比清洗HTML更可靠,因为客户端根本不会解释任何代码。
增量更新:扁平列表+ID引用,LLM生成更顺畅
README专门写了“LLM-friendly and incrementally updatable”。传统UI描述常用嵌套结构(比如XML或JSX),LLM生成时容易丢失括号平衡、产生无效语法。A2UI采用扁平列表,每个组件有唯一ID,父子关系通过"children"字段引用ID(而不是嵌套对象)。
这让LLM逐条输出组件变得容易。更实用的场景是增量更新:用户看完初始界面说“把按钮文字改成取消”,Agent只需要回复:
{
"updates": [
{
"id": "btn1",
"properties": { "label": "取消" }
}
]
}
客户端只更新那一个组件,不重新渲染整个UI。用户不必等待所有组件生成完毕就能看到第一屏内容,交互更连贯。
跨平台:一份JSON,多个原生渲染器
“Framework-agnostic and portable”是另一个设计原则。JSON只描述UI结构和数据模型,不绑定任何前端框架。项目官方提供了三个参考实现:Flutter、Angular、Lit。同一个TextField类型,在Flutter客户端被渲染为TextFormField,在Lit客户端被渲染为<sl-input>。社区可以基于规范写React Native、SwiftUI、Qt等平台渲染器。
README还提到“Smart Wrapper”模式:开发者可以把团队已有的复杂组件(比如交易确认弹窗)注册为一个A2UI类型。Agent在JSON中引用这个类型,但实际渲染代码完全由客户端掌控。安全边界不打破,同时能复用现有业务组件。
当前状态:v0.9,适合原型和探索,生产环境需谨慎
A2UI在README中明确标注“Early stage public preview”。规范虽然能用,但还在迭代。几个尚未定案的地方:动态图表、复杂动画的支持方式;跨版本兼容策略。Google开放项目的主要意图是收集反馈、吸引社区贡献各平台渲染器。
如果你打算在生产项目中使用,需要评估迭代期间的迁移成本。目前最合适的场景是:与LLM交互的原型、聊天机器人里的动态UI、以及需要远程Agent生成界面的内部工具。接入步骤很清晰:客户端实现一个组件注册表,把平台原生控件包装成A2UI类型;解析Agent返回的JSON,根据type字段查找对应渲染器,填充属性后挂载到视图树。
还有一些问题我也没完全搞清楚:比如表单提交后的数据回传规范、列表分页的增量加载如何处理。这些可能需要读spec目录下的规范草案,或者跑一下官方的Flutter demo亲自试试。
和其他常见做法的对比
- 直接生成HTML/JS:安全风险无法根治,而且依赖浏览器运行时,Flutter或SwiftUI里用不了。
- LLM调用预定义API(如Slack Block Kit、Discord回调):受限于特定平台,Agent需要学习每个平台的SDK细节。
- A2UI:Agent只需学一种JSON格式,客户端各自实现渲染。安全性来自数据结构本身,不是依赖事后过滤。
如果你想在自己项目里试试,可以从官方Flutter或Lit Demo入手,读一下spec目录下的规范定义。先接入一个简单的Card组件,感受一下“声明式JSON→原生控件”的链路。之后逐步加入按钮交互、表单提交、列表滚动等复杂场景。