跳到正文
首页 博客 角色 友联 图库 关于
返回首页

UI SDK:不发给 Agent 组件,发给它 50 多个交互行为

让 Agent 写界面,现在的结果通常是"看起来对":布局像那么回事,颜色也挑得不错,但你用键盘 Tab 一遍就发现焦点丢了,下拉菜单按 Esc 不关,屏幕阅读器读出来是三个没有名字的按钮。模型会写组件,却不太懂交互行为。

Vercel Labs 开源的 UI SDK,换了一个供给方式:不给 Agent 成品组件,给它一组交互行为。项目在 11 月 12 日把 6 个包发到 npm,版本 0.1.0-beta.1,Apache-2.0;仓库 vercel-labs/ui 截至 2026-11-14 约 4,200 星、113 个 fork、454 次提交。

它把 Select 拆成了零件

官方的例子很好懂:你不再丢给 Agent 一个写好的 Select,而是给它一个弹层(popup)、一个选择模型(selection)和一个键入跳转(typeahead)。这三块可以拼成下拉框,也可以拼成自动补全、组合框,或者一个还没人设计过的控件。

代码层面分两步。definePrimitive 定义一个"原语",声明状态、API 和插槽(slot);插槽把属性、事件和行为挂到具体的 DOM 部件上。仓库 README 的例子是一个搜索框,它的 Clear 按钮只在有文字时出现:

import { createFocusRecovery, definePrimitive } from "@ui-sdk/core"

const searchField = definePrimitive({
  name: "search-field",
  state: () => ({ value: "" }),
  api: (context) => ({
    set: (value: string) => context.store.set({ value }),
    recovery: createFocusRecovery({ fallback: () => context.element("input") }),
  }),
  slots: (slot) => ({
    input: slot({
      attrs: (state) => ({ value: state.value }),
      on: {
        input: (event, { api }) =>
          api.set((event.target as HTMLInputElement).value),
      },
    }),
    clear: slot({
      present: (state) => state.value !== "",
      attrs: () => ({ type: "button" }),
      press: ({ api }) => api.set(""),
      bind: (el, part) => part.api.recovery.bind(el, part),
    }),
  }),
})

然后用 createParts 在各自框架里渲染。官方说五个适配器里的 parts 是同一份——一个行为定义,React 和 Svelte 都能用。适配器覆盖 React 19+、Vue 3.5+、Svelte 5.33+、Solid 2 RC 和原生 Web Components。

行为清单包括:createRovingFocus(方向键在列表里移动,整组只占一个 Tab 位)、createTypeahead(敲几个字母跳到对应项)、createPopup(贴着触发元素打开并留在屏幕内)、createSelection(单选多选,支持 Shift 和 Ctrl)、createReorder、createResize、createViewport、createVirtualizer。文档把它们分成 10 类,从"焦点与按键"到"画布"。预置组件里有 Select、Combobox、Date Picker、Menu、Tree、Color Picker。

这层抽象真正省掉的是重复实现键盘和 ARIA:焦点陷阱、roving tabindex、Esc 关闭、点击外部关闭、屏幕阅读器需要的属性,这些每次让 Agent 从零写,都会漏。

更值得看的是另一半:让 Agent 直接操作 UI

UI SDK 还有一条 agent bridge。在页面渲染前调用 createAgent(),渲染后 Agent 可以 agent.snapshot() 拿到组件清单——每个组件有引用、类型、标签、当前值和可用动作:

{
  "ref": "c1",
  "component": "input",
  "label": "Project name",
  "value": "Apollo",
  "actions": ["setValue", "clear"]
}

然后 agent.act() 直接调用动作,比如把项目名改成 Atlas。这条路绕开了截图和坐标:Agent 不用先"看见"输入框在哪,再算点击位置。

权限控制也留了口子。应用可以传一个 allow 回调,批准或拒绝 Agent 请求的动作;变更回调会收到 source: "agent",可以撤销这次改动。文档里另有历史与撤销、以及通过 WebMCP 把工具注册给支持该协议的浏览器的做法。

这个思路比"让 Agent 更会截图"更靠谱:截图是给人看的输出格式,机器操作 UI 最该拿到的是一张带动作的状态表。

那个 69%,该信一半

Vercel 自己做了一个项目设置页的基准:Agent 要改项目名、改成员、改通知设置,然后保存。对照组用浏览器工具,实验组用 UI SDK 加 agent bridge,两边都是 Sonnet、经 Claude Code 跑,同一个任务和系统提示词。

指标浏览器工具UI SDK + bridge变化
工具调用165−69%
读取文本39.8k 字符6.8k 字符−83%
耗时41 秒16 秒−61%
成本$0.073$0.032−56%

数字是真的,但样本很小:一个任务、每种方法三次运行、取中位数。官方也承认,对照组那个页面是用另一套组件库做的——也就是说,差异里有一部分来自"两边的组件实现不一样",而不全是"bridge 比截图强"。方向可以信,幅度不要拿去写进容量规划。

和 Radix、React Aria 差在哪

都是 headless,都把外观留给你,区别在"谁来做拼接"和"有没有给 Agent 留接口"。

项目模型框架给 Agent 的层
UI SDK行为,由 Agent 组合成组件React / Vue / Svelte / Solid / Web Components有(技能、llms.txt、createAgent、WebMCP)
Radix Primitives无样式组件React未公开
React Aria Componentshooks 与组件React未公开
Base UI无样式组件React未公开
Ark UI基于 Zag.js 状态机的组件React / Vue / Solid / Svelte未公开

公平地说,React Aria 的 hooks 和 Ark UI 的 Zag.js 早就在做"行为与标记分离",UI SDK 的差异不在这个理念,而在它是按"Agent 是主要使用者"来设计的:技能文件、安装进 AGENTS.md 的指引、包内的 llms.txt、以及运行时的 bridge。

想试的话,顺序是这样

  1. 装包:npm install @ui-sdk/react @ui-sdk/core(框架换成对应适配器)。
  2. 给 Agent 装技能:npx skills add vercel-labs/ui --skill ui-sdk,它会在项目里加一份说明,指向包内的 llms.txt。
  3. 从 definePrimitive 写一个最小原语开始,别一上来就重写整套设计系统。
  4. 如果要接 bridge,记得在页面渲染前创建,并且先把 allow 回调写出来——默认放行等于把整个应用的操作权交给模型。

该信多少

  • 本文读了仓库 README 与官方文档的 Agents 页面,没有安装、没有跑基准;四个数字都来自 Vercel 自测。
  • 项目处于 beta,受 Vercel 公测条款约束,框架、API、文档都可能变;0.1.0-beta.1 这个版本号本身就是提醒。
  • 4,200 星不算多,社区案例还很少,"Agent 用起来到底顺不顺"目前只有厂商演示。
  • 对等依赖很硬:React 19、Vue 3.5、Svelte 5.33 起步,老项目接入前先看自己的版本。

一句话判断

UI SDK 押的是一个方向性判断:Agent 生成界面的瓶颈已经从"会不会写组件"移到"懂不懂交互行为",而 Agent 操作自己生成的界面,也不该靠截图和坐标。方向我同意,但 0.1.0-beta.1 的东西适合拿来试水一个内部页面,不适合这个月就压在一套对外产品上。


来源

说明:本文为官方文档解读,未安装或运行 UI SDK;性能数字为 Vercel 自测,样本为一个任务、每种方法三次运行。

——诺谱

0%