我怎么交付生成式 UI(而不让 LLM 写 React)

Zoominfo 工程师 Dinesh Keerthipati 讲他怎么交付生成式 UI:不让 LLM 直接写 React,而是让它从已有组件里挑选与组合,把校验、权限与无障碍留在应用侧。

中文
复制
插画:模型从一叠预置组件里挑选并拼装界面,而不是从零写代码

第一次听说生成式 UI 时,我的理解是:你给 LLM 一个提示,它就能即时生成 React 组件。这让我对生成式 UI 产生了兴趣,因为它提供了一条途径,让我们不再向每个用户展示同样的 UI——哪怕那套 UI 未必最适合他们想做的事。有了生成式 UI,同一份数据可以根据用户的意图以不同方式呈现。一个用户可能只需要一段简单的文字回答,而另一个用户可能用图表、表格或交互界面来理解同一份数据会更好。但我越是琢磨让 LLM 直接即时生成 React 组件这件事,看到的问题就越多。测试变得更难,因为我们不能指望模型每次都生成相同的结构或格式。此外还有安全、无障碍、主题、可靠性,以及与应用程序设计系统一致性方面的顾虑。即便生成的组件在技术上能跑通,它的行为方式也可能不符合产品的预期。实现生成式 UI 的一种务实做法,是让模型使用应用程序中已有的可信组件,而不是直接生成任意的 React 组件。

让模型组合 UI,而不是实现 UI

设想一个应用程序已经有这样的组件:

Card
BarChart
LineChart
Table
Tabs
Button
Alert
Stack

与其让 LLM 编写 JSX,模型可以生成一份结构化描述,说明它想展示什么。例如:

{
 "type": "bar-chart",
 "props": {
 "title": "Spending by Category",
 "data": [
 { "category": "Dining", "amount": 620 },
 { "category": "Travel", "amount": 480 },
 { "category": "Groceries", "amount": 410 }
 ]
 }
}

应用程序随后可以把这份响应映射到一个可信的 React 组件。

const componentRegistry = {
 "bar-chart": BarChart,
 table: Table,
 card: Card
};

一个简单的渲染器可以长这样:

function GenerativeUI({ element }: { element: UIElement }) {
 const Component = componentRegistry[element.type];

 if (!Component) {
 return ;
 }

 return ;
}

模型决定什么样的 UI 有用,组件怎么实现仍由应用说了算。在我看来,这条边界安全得多。模型可以决定界面需要什么,但能用哪些积木块,仍然由应用控制。

用现成组件,不等于 UI 是死的

这种做法有一个顾虑:听起来限制太死。如果模型只能用现成组件,那还算是在生成 UI 吗?我认为算。模型不必从预置屏幕里挑一个,它可以把现成组件组合成新的结构。比如:

Card
 ├── Spending summary
 ├── Bar chart
 └── View transactions action

或者:

Stack
 ├── SummaryCard
 ├── CategoryBreakdown
 └── TransactionTable

布局本身依然可以是动态的。模型可以判断这个请求该配图表、那个请求该配表格,也可以为更复杂的回答组合多个组件。在我看来,它不该做的是凭空造一套新的图表实现,或者生成任意 JavaScript 直接在应用里跑。这样既给了模型灵活性,又没有交出 UI 系统的控制权。

不是每个回答都该变成图表

生成式 UI 不该意味着把每个 AI 回答都变成可视化界面。假设用户问:

我这个月最大的一笔交易是什么?

如果答案就是:

你最大的一笔交易是 $1,240,在 Example Airlines。

那纯文本大概就够了。加个图表只会增加复杂度,价值却有限。但如果用户问的是:

我这个月哪几类花得最多?

那可视化对比就更有用。柱状图可能比一段文字更快把答案说清楚。所以目标不该是:

AI response
 ↓
Generate visual UI

而应该更接近:

Understand user intent
 ↓
Choose useful representation
 ↓
Text / Table / Chart / Form / Composite UI

最好的 UI 取决于用户想弄明白什么、想做什么。

先澄清,再生成

看这个问题:

我这个月钱花在哪儿最多?

这个问题有歧义。用户指的是:

  • 按类别?
  • 按商家?
  • 还是单笔最大的那笔交易?

模型可以自行假设,然后生成一张漂亮的图表。但如果假设错了,界面看起来有用,回答的却是错误的问题。我觉得更好的做法是先追问更多上下文。例如:

Do you mean:

• Spending by category
• Spending by merchant
• Your largest individual transaction

用户澄清之后,系统就有足够的信息去选择更合适的呈现方式。在我看来,这是 Generative UI 很重要的一部分。它不应该取代对话,而应该借助对话做出更好的 UI 决策。

模型该如何选择呈现方式?

我也不认为 LLM 在这里应该拥有无限的自由。生产应用可以定义规则和允许的模式。例如:

Single fact
 ↓
Text

Exact values across multiple items
 ↓
Table

Category comparison
 ↓
Bar chart

Trend over time
 ↓
Line chart

模型可以结合这些规则和用户意图,选择最有用的呈现方式。例如:

"Compare my spending categories"
 ↓
Bar chart

"Give me the exact amount for each category"
 ↓
Table

"Show the biggest categories and explain the change"
 ↓
Summary + Chart + Explanation

这样既给了模型一定的自由,又能把输出限制在可预期的应用边界之内。

不要暴露整个设计系统

真实应用可能有几百个组件。我不认为模型需要访问全部组件。我会只暴露一组经过筛选、适合 AI 生成体验的组件。例如:

Card
Table
BarChart
LineChart
Tabs
Alert
Button
Form
Stack

金融应用还可以暴露领域特定的组件:

TransactionTable
SpendingSummary
CategoryBreakdown
AccountCard

膳食规划应用可以暴露:

MealPlan
RecipeCard
ShoppingList
NutritionSummary

更小的组件目录让系统更容易推理和验证,也降低了模型选中那些本不打算用于生成体验的组件的概率。模型应该拿到足够多的构建块来做出有用的界面,但不必拿到整个内部设计系统。

模型输出应当作不可信输入处理

即使模型只产出结构化的 UI 描述,我也不会直接渲染。输出仍然需要校验。例如:

const result = uiSchema.safeParse(modelOutput);

if (!result.success) {
 return fallbackResponse;
}

如果模型请求了不支持的组件,应用应当安全失败。

const Component = componentRegistry[element.type];

if (!Component) {
 return ;
}

兜底方案可以是纯文本、错误提示,或者用受支持的组件再试一次。关键在于,模型输出应当被当作数据,而不是可信的应用代码。这也让应用继续充当安全边界。模型可以建议 UI 或操作,但正常的应用权限和校验依然要生效。

并非每次用户操作都需要再次调用模型

假设生成的 UI 长这样:

Spending by Category

Dining $620
Travel $480
Groceries $410

[View Dining Transactions]
[Explain Dining Increase]

这两个操作在界面上看起来差不多,但处理方式未必应该一样。如果用户点击的是:查看餐饮交易,应用已经知道该做什么。它可以筛选当前数据,或者发一个普通的 API 请求。

function viewDiningTransactions() {
 filterTransactions({
 category: "Dining"
 });
}

没有任何理由把这件事再交给 LLM。但如果用户点击的是:解释餐饮支出增长,这就需要解读了。这才是重新调用模型的合理理由。

function handleAction(action: Action) {
 if (action.type === "view-transactions") {
 filterTransactions(action.category);
 return;
 }

 if (action.type === "explain-spending") {
 sendToAgent(action);
 }
}

由此得到一条简单的规则:

需要智能的时候才用 LLM,而不是因为 UI 是 AI 生成的就要用。

确定性操作就该保持确定性。这样应用更容易测试,也避免在正常的 UI 交互中掺入不必要的模型调用。

我会从这套架构开始

把这些放在一起,我会从这样一套架构开始:

User
 ↓
LLM / Agent
 ↓
Understand intent
 ↓
Structured UI Spec
 ↓
Schema validation
 ↓
Component Registry
 ↓
Trusted React UI

模型可能返回类似这样的东西:

{
 "elements": [
 {
 "type": "text",
 "content": "Dining was your highest spending category."
 },
 {
 "type": "bar-chart",
 "props": {
 "title": "Spending by Category",
 "data": [
 { "category": "Dining", "amount": 620 },
 { "category": "Travel", "amount": 480 },
 { "category": "Groceries", "amount": 410 }
 ]
 }
 }
 ]
}

应用校验这个结构,只渲染受支持的组件。在我看来,这仍然算 Generative UI。模型决定接下来该出现什么样的体验,但它没有绕开前端架构。

难的部分不是渲染 React

真正用 React 渲染,大概是最简单的环节之一。把:

{
 "type": "bar-chart"
}

映射成:

没什么难度。更难的问题都出在边界上。模型请求了一个不存在的组件怎么办?props 不符合 schema 怎么办?用户没有权限执行某个操作怎么办?选中的可视化有效但会误导人怎么办?怎么测试不同的生成组合?怎么保证无障碍?组件不断演进时,UI schema 怎么管理版本?怎么决定哪些组件应该开放给模型?这些才是我觉得 Generative UI 有意思的地方。它不只是让 LLM 生成 React,而是概率模型与确定性前端系统之间的一种全新互动。

我的思考最终落到了哪里

我最初对 Generative UI 的理解很简单:

给 LLM 一个 prompt,让它即时生成 React 组件。

让界面动态适应用户这个想法我依然喜欢,但我不认为无限制地生成组件是合适的生产边界。我更愿意让模型决定用户需要什么样的体验,而由应用来掌控实际的组件、校验、权限、无障碍和运行。模型不是在取代前端,它是在帮忙判断用户接下来需要什么样的前端体验。在我看来,这比单纯让 LLM 写 React 有意思得多。

来源: HackerNoon← 返回首页