从 Markdown 到 Generative UI:AI 如何从"生成答案"进化到"生成界面"?
一、一个正在发生的变化:AI 不再只生成内容,也开始生成界面
在传统的 AI 聊天应用中,用户输入问题,大模型返回一段文本,前端负责把这段文本渲染出来。
例如:
帮我分析一下家庭月度预算。
模型可能会返回:
- 房租:3000 元
- 饮食:2000 元
- 交通:500 元
- 娱乐:800 元
- 月收入:10000 元
- 月结余:3700 元
前端使用 Markdown 渲染器,就能把这些内容展示得比较清晰。如果再增加 Markdown 表格、代码块、图表代码块等自定义渲染能力,用户甚至可以看到图表和一些可交互的组件。
但如果用户接着说:
把饮食预算调到 2500 元,把娱乐预算调到 500 元,看看月结余变化。再帮我找出哪些项目可以继续优化。
这时,传统聊天界面的局限就开始显现了。
用户需要反复输入修改要求,模型需要重新理解上下文、计算数据,再生成一段新的回答。即使前端嵌入了滑块和输入框,也可能需要重新请求模型才能更新相关内容。
能不能让 AI 直接生成一个预算面板?用户修改数字,结余、图表和建议立即同步更新;需要分析时,再由 AI 介入。
这就是 Generative UI 值得关注的原因。
它代表了一种不同的交互思路:AI 不仅可以生成用户要阅读的内容,还可以生成用户要操作的界面。
不过,要理解这项技术,我们需要先区分几个容易混淆的概念:Markdown 富内容、传统的动态组件渲染、Generative UI,以及更强调任务决策和持续协作的 Intelligent UI。
二、Markdown 富内容:给聊天消息增加展示能力
对于大多数使用 React 构建的 AI 应用,最常见的方案是使用 react-markdown。
整个流程很直接:
- 用户向模型发送问题。
- 模型返回 Markdown 文本。
- 前端接收模型的流式输出。
- Markdown 解析器把文本转换为对应的 React 元素。
- 浏览器把结果展示在聊天消息中。
例如,模型返回:
ts
## 月度预算
| 项目 | 金额 |
|---|---:|
| 房租 | 3000 |
| 饮食 | 2000 |
| 交通 | 500 |
```chart
{
"type": "pie",
"data": [
{"name": "房租", "value": 3000},
{"name": "饮食", "value": 2000},
{"name": "交通", "value": 500}
]
}
这里的 chart 代码块可以被前端拦截,再交给自己注册的图表组件处理。
示意代码如下:
tsx
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
function MarkdownMessage({ content }) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children }) {
const language =
/language-(\w+)/.exec(className || "")?.[1];
if (language === "chart") {
return (
<ChartRenderer
source={String(children)}
/>
);
}
return <code className={className}>{children}</code>;
},
}}
>
{content}
</ReactMarkdown>
);
}
这是一种简单而有效的扩展方式。通过自定义渲染器,我们可以将 Markdown 中的特定标记转换为图表、卡片、图片、表格甚至交互式表单。
2.1 这种方案的本质是什么?
本质上,模型输出的是一种内容描述,前端按照事先约定的规则解释这些内容。
例如:
##表示标题;|表示表格;language-chart表示图表数据;language-form表示表单配置;- 某种约定的标记表示按钮或卡片。
前端需要提前知道每种标记代表什么,以及如何渲染。
模型负责生成内容,前端负责决定如何呈现。
如果模型输出一个 chart 代码块,前端就渲染图表;如果没有输出,前端就不渲染。
这种方式完全可以做出非常复杂的应用。问题不在于它能不能实现交互,而在于随着交互逻辑越来越复杂,Markdown 是否还是合适的主要协议。
2.2 Markdown 富内容的边界在哪里?
假设预算面板需要以下功能:
- 一个收入输入框;
- 五个支出滑块;
- 一个实时更新的结余数字;
- 一张支出占比图;
- 一张历史预算趋势图;
- 一个优化建议面板;
- 一个"生成优化方案"按钮。
如果全部通过 Markdown 和自定义代码块来描述,就必须为每种组件约定格式,还需要处理多个组件之间的状态共享。
例如,用户调整饮食预算后,支出占比图需要更新,月结余需要更新,预算建议也可能需要更新。
如果每次修改都重新请求模型,并重新生成整个 Markdown 消息,就会出现几个问题:
- 用户每次调整都产生一次模型请求,增加延迟和成本。
- 前端需要处理新旧消息之间的状态同步。
- 表单填写过程可能因为重新渲染而丢失。
- 图表、输入框和文字说明之间的关联逐渐变得复杂。
- 一旦引入多栏布局、复杂表单或跨组件联动,Markdown 就开始承担本来应该由应用状态和组件系统处理的工作。
但要注意:这些问题并不是 Markdown 天生无法解决的。开发者可以通过 React 状态管理、独立的组件实例、稳定的组件 ID 和局部更新来解决。
真正的问题是,如果我们已经需要一套独立的组件描述协议、状态管理机制和事件系统,那么继续把所有东西塞进 Markdown 代码块,就不一定是最合适的设计了。
三、什么是 Generative UI?
Generative UI,通常翻译为生成式 UI,指的是让 AI 系统能够根据任务动态提出或组合界面,而不是只能返回固定格式的文本内容。
传统 UI 的开发方式是:
开发者设计页面,编写组件,定义布局和交互,然后把用户数据填充到界面中。
Generative UI 则可以让模型根据用户需求,选择一组已经注册的组件,组织成新的界面结构,再交给前端渲染。
例如,用户说:
帮我做一个家庭预算计算器,可以调整收入和各项开支,并实时查看结余。
模型可以提出一个界面描述:
css
{
"type": "Column",
"children": [
{
"type": "Heading",
"props": {
"text": "家庭月度预算"
}
},
{
"type": "NumberInput",
"props": {
"label": "月收入",
"value": 10000
}
},
{
"type": "Slider",
"props": {
"label": "饮食预算",
"min": 0,
"max": 5000,
"value": 2000
}
},
{
"type": "Metric",
"props": {
"label": "月结余",
"value": 3700
}
}
]
}
这份 JSON 不是 HTML,也不是 React 源码,而是一份声明式的界面描述。
它表达的是:
- 需要什么组件;
- 组件之间是什么层级关系;
- 每个组件有哪些属性;
- 初始数据是什么;
- 在允许的情况下,组件应该绑定什么操作。
前端收到这份描述后,按照自己的组件注册表,将它转换成真正的 React 组件。
这就是 Generative UI 最核心的工程思路。
3.1 为什么使用 JSON,而不是让模型直接生成 JSX?
假设让模型直接生成下面的代码:
ini
<div className="budget">
<input type="range" />
<button onClick={() => fetch("/api/pay")}>
生成方案
</button>
</div>
这看起来很灵活,但会带来严重的工程问题。
第一,模型生成的代码可能不完整,甚至无法编译。
第二,任意 JSX 或 JavaScript 可能包含危险的执行逻辑。
第三,模型可以引用应用中不存在的组件或函数。
第四,前端很难对任意生成代码的权限、网络访问和副作用进行统一管理。
因此,更可控的做法是让模型输出声明式数据,由应用决定哪些组件能够被创建,以及这些组件能够执行什么操作。
这类似于数据库查询和 SQL 执行之间的关系:描述请求和执行请求是两个不同的阶段。前者可以被校验,后者必须受到运行环境的约束。
当然,JSON 本身并不自动保证安全。即使模型只输出 JSON,恶意或错误的属性、危险 URL、越权操作等问题仍然需要在前端和后端进行校验。
3.2 Generative UI 的关键:组件注册表
前端可以维护一个组件注册表:
ini
const componentRegistry = {
Column,
Row,
Card,
Heading,
Text,
NumberInput,
Slider,
Button,
Chart,
Table,
Metric,
};
然后编写一个递归渲染器:
javascript
function renderNode(node, context) {
const Component = componentRegistry[node.type];
if (!Component) {
return null;
}
const children = node.children?.map((child) =>
renderNode(child, context)
);
return (
<Component
{...validateProps(node.type, node.props)}
context={context}
>
{children}
</Component>
);
}
这里的 validateProps 代表需要由开发者实现的属性校验逻辑,并不是一个自动存在的 API。
实际工程还需要处理:
- 输入数据的结构验证;
- 组件属性的类型和范围校验;
- 事件绑定;
- 错误降级;
- 流式输出中的不完整数据;
- 组件实例的稳定标识;
- 状态和生命周期管理。
整个过程可以概括为:
模型生成 UI 描述 → 校验描述 → 查找组件 → 创建组件树 → 浏览器渲染。
模型决定的是受约束的组合方式,而不是获得执行任意前端代码的权限。
相关实现可以参考 assistant-ui 的 Generative UI 文档,其中介绍了通过 present 工具输出 JSON 组件树,并由组件库进行解析和渲染。
四、Generative UI 到底怎样渲染?是不是页面里面又嵌套了一个页面?
这是一个非常重要的问题。
答案是:通常不是。
如果前端使用 React 实现,生成式 UI 可以直接作为现有 React 应用中的一个子树。
例如:
javascript
function AssistantMessage({ message }) {
return (
<article className="assistant-message">
<MarkdownRenderer content={message.text} />
{message.uiSpec && (
<GenerativeUIRenderer
spec={message.uiSpec}
/>
)}
</article>
);
}
GenerativeUIRenderer 接收 UI 描述,生成普通的 React 元素。
假设模型生成了:
json
{
"type": "Card",
"children": [
{
"type": "Heading",
"props": {
"text": "预算分析"
}
},
{
"type": "Chart",
"props": {
"chartType": "pie",
"data": [
{"name": "房租", "value": 3000},
{"name": "饮食", "value": 2000}
]
}
}
]
}
前端会把它解析为类似这样的 React 结构:
ini
<Card>
<Heading text="预算分析" />
<Chart
chartType="pie"
data={data}
/>
</Card>
这段 JSX 仅用于说明最终组件树的形态,并不是要求模型返回或执行 JSX。
最终页面中的 DOM 仍然由 React 正常管理。样式、事件处理、响应式布局、组件状态和无障碍属性,都可以使用现有的前端工程体系实现。
4.1 三种常见的展示方式
方式一:嵌入聊天消息
markdown
聊天页面
└── AI 消息
├── 文本说明
└── 预算计算器
├── 收入输入框
├── 支出滑块
└── 结余图表
适合短小、临时、与当前问题紧密相关的工具。
方式二:在独立工作区展示
markdown
应用页面
├── 对话区
│ └── AI 的解释和操作建议
└── 工作区
└── 预算计算器
├── 参数面板
├── 图表区
└── 优化建议
适合复杂的任务、较长的交互过程和需要持续保留状态的工作流。
方式三:嵌入独立应用
通过 iframe 等机制嵌入单独部署的应用,或者通过受控的远程 UI 协议展示来自其他服务的界面。
这种方式更适合已有独立 Web 应用、第三方工具或需要隔离执行环境的场景,但需要额外考虑跨域通信、权限控制和生命周期管理。
因此,Generative UI 不要求采用某一种特定的页面结构。它关注的是界面能否被动态描述和组合,以及运行时如何安全地将其呈现出来。
五、从 Generative UI 到 Intelligent UI:区别究竟在哪里?
如果 Generative UI 解决的是"如何生成和渲染界面",那么更完整的 Intelligent UI 体验还要回答另外两个问题:
- 什么情况下应该生成界面?
- 生成的界面如何与任务状态、用户操作和 Agent 协作?
这两个问题会把前端渲染问题扩展成一个完整的应用架构问题。
5.1 模型决定什么时候需要 UI
并不是所有问题都适合生成界面。
用户问:
JavaScript 中的 Promise 是什么?
直接回答通常更好。
用户问:
帮我对比三种缓存策略,调整缓存命中率和请求量,比较不同场景下的性能。
这时,交互式表格、图表或模拟器可能更合适。
因此,一个更完整的系统可以让模型在文本回答、结构化数据、图表、表单和交互式工具之间选择。
这种选择可以通过多种方式实现:
- 在模型提示中说明可用的呈现方式;
- 将
present_ui注册成模型可调用的工具; - 让 Agent 根据任务类型选择预定义的 UI 模板;
- 由后端工作流决定何时向前端发送 UI 描述;
- 使用混合策略,允许模型提出方案,但由应用的规则决定是否接受。
注意,真正的决策权不一定完全属于模型。对于高风险操作或固定业务流程,系统可能要求使用确定性的规则,而不是让模型自由选择。
5.2 UI 不再只是回答,而是任务的操作界面
以预算工具为例。
用户调整饮食预算后,系统可以分为两种更新路径。
第一种是纯前端更新:
markdown
用户拖动滑块
↓
React 状态更新
↓
重新计算预算
↓
图表和结余立即更新
第二种是需要 AI 参与的更新:
markdown
用户点击"分析这次调整"
↓
前端发送预算状态和用户意图
↓
Agent 分析预算变化
↓
必要时调用业务工具
↓
返回建议或新的 UI 描述
↓
前端更新对应区域
这两种路径可以同时存在。
一个重要的设计原则是:不需要模型参与的操作,不要强制每次都调用模型。
调整滑块、修改输入框和重新计算加减法,都可以由前端即时完成。
而当用户要求解释变化、寻找节省空间、生成方案或者查询外部数据时,再调用模型和业务工具。
这样才能兼顾交互速度、成本和智能程度。
5.3 组件之间共享状态,才是真正的工程难点之一
生成一个包含三个组件的界面,并不等于实现了完整的交互体验。
假设预算面板中包含:
- 收入输入框;
- 支出滑块;
- 结余数字;
- 支出占比图;
- AI 优化建议。
如果这些组件各自维护独立状态,就很容易出现数据不一致。
例如,滑块已经从 2000 元调整到 2500 元,但图表仍然显示旧数据。
解决方式通常不是让模型重新生成整个页面,而是由应用维护一份统一的任务状态。
ini
type BudgetState = {
income: number;
rent: number;
food: number;
transport: number;
entertainment: number;
};
function calculateBalance(state: BudgetState) {
return (
state.income -
state.rent -
state.food -
state.transport -
state.entertainment
);
}
多个组件订阅同一份状态:
BudgetState
├── IncomeInput
├── FoodSlider
├── BalanceMetric
├── ExpenseChart
└── BudgetSummary
当 food 发生变化时,依赖它的组件可以自动更新。
在 React 中,可以用 useState、useReducer、Context 或其他状态管理工具实现。
如果 UI 描述需要跨消息、跨页面甚至跨会话保留,还需要将状态持久化,并设计状态恢复和版本兼容机制。
要注意,组件树和业务状态是两个不同的东西。模型可以重新生成组件树,但这不意味着用户已经输入的数据就应该被覆盖。
六、真正的交互闭环:UI 事件怎样回到 Agent?
这是从"能展示组件"走向"能够完成任务"的关键。
在传统页面中,按钮点击通常会触发前端回调:
xml
<Button onClick={handleClick}>
提交
</Button>
而在 Generative UI 中,按钮可能是模型动态组合出来的。
但动态生成并不意味着事件处理逻辑也要由模型随意生成。
更安全的方式是让模型声明一个已注册的动作。
例如:
css
{
"type": "Button",
"props": {
"label": "分析预算"
},
"action": {
"type": "analyze_budget"
}
}
前端维护动作注册表:
csharp
const actionRegistry = {
analyze_budget: async (context) => {
return sendAgentEvent({
event: "analyze_budget",
payload: context.budget,
});
},
};
当用户点击按钮时:
- 前端找到对应的动作处理器;
- 收集经过校验的业务状态;
- 将事件和必要的数据发送给 Agent;
- Agent 根据事件决定是否继续推理、调用工具或更新界面;
- 前端接收新结果,并更新相应区域。
完整流程如下:
markdown
模型生成 UI 描述
↓
前端验证并渲染组件树
↓
用户操作组件
↓
前端产生结构化事件
↓
Agent 接收事件和任务状态
↓
模型推理 / 业务工具执行
↓
返回数据、动作结果或新 UI 描述
↓
前端局部更新或替换组件树
这是一种双向交互架构。
不过,UI 事件不一定每次都要回到大模型。纯前端的即时交互可以在本地完成;需要业务操作时可以调用后端;只有需要智能决策时才需要进入 Agent。
这也解释了为什么一个成熟的生成式 UI 系统通常需要事件协议、动作注册表、任务状态管理和工具执行机制,而不只是一个 JSON 渲染器。
七、流式渲染:为什么不能等模型全部生成完再显示?
传统聊天应用通常已经支持 token 流式输出。
例如:
正在生成文字......
正在生成表格......
正在生成图表数据......
生成完成
但如果 UI 是通过一份结构化描述生成的,前端还需要解决一个问题:结构尚未完整时,能不能开始渲染?
假设模型正在输出:
json
{
"type": "Card",
"children": [
{
"type": "Heading",
"props": {
"text": "预算分析"
}
},
{
"type": "Chart",
"props": {
"data": [
这时 JSON 还不完整,直接执行 JSON.parse 就会失败。
因此,工程实现通常有几种选择。
方案一:完整解析后渲染。
等待模型输出完整结构,再验证并创建组件树。
优点是简单可靠;缺点是大型界面需要等待更久。
方案二:流式结构化解析。
采用支持增量解析的协议或解析器,当某个节点已经完整时,就先渲染该节点。
例如:
markdown
收到 Card 节点
↓
先展示卡片容器
收到 Heading 节点
↓
展示标题
收到 Chart 节点
↓
展示图表占位符
收到完整数据
↓
渲染图表
方案三:分阶段生成界面。
模型先返回一个初始界面,再逐步补充数据、组件和动作。
例如先生成一个预算面板骨架,再填入计算结果和建议。
这种方法可以避免让前端直接解析任意不完整的 JSON 字符串。
实际工程中,还要处理节点 ID、重复事件、更新顺序、组件卸载、网络中断和不完整数据等问题。
因此,流式 UI 并不是简单地把文本 token 直接塞进 JSON.parse,而是需要一套明确的增量更新协议和运行时机制。
八、安全问题:为什么组件白名单仍然不够?
很多文章会把 Generative UI 的安全优势总结为一句话:
使用 JSON 和组件白名单,就不会有 XSS 风险。
这种说法并不准确。
组件白名单可以限制模型创建哪些类型的组件,但不能自动保证组件属性、业务操作和数据内容都是安全的。
例如:
css
{
"type": "Button",
"props": {
"label": "删除全部订单"
},
"action": {
"type": "delete_all_orders"
}
}
即使 Button 是合法组件,delete_all_orders 也不应该因为它出现在模型输出中,就自动获得执行权限。
一个完整的安全设计至少应该包括以下几个方面。
8.1 组件白名单
模型只能选择开发者注册的组件。
css
const registry = {
Card,
Text,
Table,
Chart,
Input,
Button,
};
未知组件必须拒绝渲染或安全降级。
8.2 属性验证
对模型输出进行结构和类型校验。
例如:
- 滑块的
min不能大于max; - 图表数据必须符合规定的结构;
- 文本长度需要受到限制;
- URL 必须经过协议和域名校验;
- 组件嵌套层级和总数量需要设置上限。
8.3 动作白名单
模型只能引用已注册的动作类型。
前端负责分发事件,后端负责重新验证权限、业务参数和用户身份。
8.4 高风险操作需要授权
删除数据、付款、提交订单等操作,不能只凭模型生成了一个按钮就直接执行。
应当结合用户确认、服务端权限校验、幂等控制以及必要的撤销机制。
8.5 避免任意代码执行
不要对模型返回的字符串使用 eval、new Function 或其他方式执行任意 JavaScript。
如果需要富文本或 HTML,必须单独考虑可信内容和消毒策略。
总之,声明式 UI 的安全优势在于缩小了可执行能力的范围,而不是让安全问题自动消失。
九、工程上如何把 ReactMarkdown 方案升级成 Generative UI?
对于已经有 ReactMarkdown、自定义代码块和流式消息处理的项目,不需要推倒重来。
更合理的方案是逐步引入结构化 UI。
第一步:保留 Markdown,增加独立的 UI 消息类型
不要急着把所有内容改成 JSON。
可以让一条 AI 消息同时包含文本和 UI:
css
type AssistantPart =
| {
type: "text";
content: string;
}
| {
type: "ui";
spec: UISpec;
};
例如:
go
const message = [
{
type: "text",
content: "下面是你的预算计算器,可以直接调整支出。",
},
{
type: "ui",
spec: budgetUISpec,
},
];
这样,普通问答仍然使用 Markdown;需要交互界面时,再使用结构化 UI。
这种设计也比在 Markdown 文本中混入大量自定义标记更容易维护。
第二步:定义一份有限的 UI 描述协议
例如:
ini
type UISpec = {
id: string;
type: string;
props?: Record<string, unknown>;
children?: UISpec[];
};
实际项目应进一步使用 Zod、JSON Schema 或其他运行时校验工具,定义每一种组件允许的属性。
不要让 Record<string, unknown> 成为最终的安全边界。
第三步:实现组件注册表和递归渲染器
将 UI 描述转换为 React 元素。
渲染器需要负责:
- 查找组件;
- 校验属性;
- 创建子节点;
- 处理稳定 ID;
- 分发动作;
- 处理错误和未知节点。
组件样式仍然由前端开发者控制。
第四步:增加状态管理
不要把用户每一次输入都转成新的模型请求。
例如预算计算器可以维护统一的 BudgetState,所有表单、图表和指标都依赖同一份状态。
对于纯计算逻辑,直接在前端执行。
第五步:增加事件协议
为需要进入 Agent 的交互定义统一事件格式:
ini
type UIEvent = {
event: string;
taskId: string;
componentId: string;
payload: unknown;
};
例如:
json
{
"event": "analyze_budget",
"taskId": "budget-task-01",
"componentId": "budget-panel",
"payload": {
"income": 10000,
"rent": 3000,
"food": 2500,
"transport": 500,
"entertainment": 500
}
}
这里的 ID 只是示例,真实项目需要正确生成和管理任务标识。
事件传到后端后,应当由 Agent 或业务服务进行校验和处理,再返回结果。
第六步:支持局部更新和流式更新
模型返回新的 UI 描述时,不一定要重新创建整个界面。
可以根据稳定的节点 ID 更新特定节点,也可以将模型输出设计成增量操作,例如新增节点、修改属性和替换数据。
但增量更新协议必须定义清楚,不能简单地对两个 JSON 对象做浅层合并,就假设状态和生命周期一定正确。
第七步:让模型决定何时使用 UI
最后再将 UI 生成能力接入模型的决策过程。
可以提供一个 present_ui 工具,或者让 Agent 输出经过约束的 UI 数据。
模型可以根据任务选择:
- 直接回答;
- 返回表格;
- 创建图表;
- 生成交互式表单;
- 创建一个完整的任务面板。
需要强调的是,这里的"自主决定"是在应用所提供的组件、权限和策略范围内做选择,并不是允许模型无限制地创建和执行任何界面代码。
十、一个完整的例子:家庭预算 Intelligent UI
下面把前面的概念串起来,看看实际应用应该如何工作。
用户输入:
帮我规划家庭月度预算,能够调整每项开支,查看结余,并在需要时给出优化建议。
10.1 意图识别
Agent 判断这是一个适合交互式 UI 的任务,因为用户需要反复调整参数,并观察多个指标的变化。
系统选择预算面板模板或提出一份新的 UI 描述。
10.2 创建初始界面
前端渲染:
css
家庭月度预算
────────────────────────
月收入 [ 10000 ]
房租 [ 3000 ]
饮食 [ 2000 ]
交通 [ 500 ]
娱乐 [ 800 ]
月支出 6300
月结余 3700
[支出占比图表]
[分析预算] [生成优化方案]
这个界面是 React 组件树,不是一张截图,也不必是嵌入的独立网页。
10.3 用户调整开支
用户将饮食预算从 2000 元调整到 2500 元。
前端更新状态:
ini
const nextBudget = {
...budget,
food: 2500,
};
假设收入为 10000 元,其他支出不变,则月支出从 6300 元增加到 6800 元,月结余从 3700 元减少到 3200 元。
这一步由前端计算即可,不需要请求大模型。
图表、结余指标和表单会根据统一状态同步更新。
10.4 用户请求 AI 分析
用户点击"分析预算"。
前端发送当前预算状态和事件,Agent 可以进一步:
- 验证数据;
- 计算各类支出占比;
- 查询用户授权的数据源(如果需要);
- 生成节省建议;
- 返回文字解释,或者更新建议面板。
例如,Agent 返回:
饮食预算增加了 500 元,月结余相应减少 500 元。如果希望保持 3500 元的月结余,可以考虑将娱乐预算调整到 200 元,或者在其他支出中寻找节省空间。
这只是一个示例建议,不代表实际用户一定应该采用该方案。
10.5 用户进一步调整方案
用户说:
帮我比较两种方案,一种保持饮食预算,另一种减少娱乐和交通开支。
Agent 可以生成一个对比面板,显示两种方案的支出结构、月结余和各项差异。
前端更新对比区域,保留用户原先填写的预算数据。
此时,界面不只是 AI 回答的附属内容,而是用户与 AI 共同操作和修改的任务载体。
这才是从静态内容展示到交互式 AI 应用的完整演进过程。
十一、Generative UI 与 Intelligent UI 的关系
为了避免术语混乱,可以用下面这张表概括。
| 维度 | Markdown 富内容 | Generative UI | Intelligent UI 体验 |
|---|---|---|---|
| 输出形式 | 文本和约定标记 | 结构化界面描述或工具驱动的 UI | 根据任务选择合适的表达方式 |
| 界面组合 | 以预定义渲染规则为主 | 模型可以组合已注册组件 | 组合能力与任务决策相结合 |
| 交互能力 | 可以实现 | 可以实现 | 强调与任务流程的持续协作 |
| 状态管理 | 由应用实现 | 由应用实现 | 通常需要跨组件、跨步骤协调 |
| Agent 闭环 | 可有可无 | 可有可无,也可以接入 | 通常是核心体验之一 |
| 流式更新 | 文本 token 流式输出较常见 | 可以支持结构化增量渲染 | 可以根据任务状态持续更新 |
| 安全机制 | Markdown 解析和组件规则 | 组件白名单、结构校验、动作权限 | 还需要任务权限、工具授权和状态控制 |
这张表表达的是常见设计倾向,不是绝对边界。
Generative UI 并不必然支持状态共享、流式更新或 Agent 闭环;这些能力需要额外的协议和运行时实现。Intelligent UI 也不必然使用 Generative UI,它同样可以通过预定义界面、确定性业务逻辑和传统工具调用来实现。
十二、为什么值得做这项技术?
从工程角度来看,Generative UI 和 Intelligent UI 的价值并不在于"让 AI 能生成更多组件",而在于改善用户完成任务的方式。
12.1 降低交互成本
传统对话要求用户通过语言不断描述修改。
交互式界面则允许用户直接调整数字、筛选条件、时间范围和选项。
对于需要反复试验的任务,直接操作往往比反复描述更高效。
12.2 将自然语言和精确操作结合起来
自然语言擅长表达目标,例如:
帮我降低支出,但尽量不要影响生活质量。
界面擅长表达精确参数,例如:
将饮食预算设为 2500 元,娱乐预算设为 500 元。
两者结合,可以同时利用 AI 的理解能力和传统 UI 的确定性。
12.3 减少为每一种需求单独开发页面的成本
传统应用往往需要为每个业务场景提前设计完整页面。
生成式 UI 可以让模型在一个受控的组件库中组合界面,从而覆盖更多长尾任务。
但它不能消除前端开发成本。组件、设计系统、权限、业务接口、状态管理和错误处理仍然需要开发者建设。
12.4 让 AI 的结果更容易被检查和修改
一段文字建议不一定容易直接执行。
结构化表格、参数面板和对比图则可以让用户直接检查数据、修改输入和观察结果。
不过,界面可视化并不等于结果正确。重要的业务计算和数据校验仍应由可信的程序逻辑完成。
12.5 为 Agent 提供更自然的操作入口
Agent 不再只需要等待用户在聊天框中输入下一句话。
用户可以通过界面发起操作、确认参数、选择候选方案和授权业务动作。
这让 Agent 更容易融入真实的工作流。
十三、工程实践中的几个误区
最后,有几个值得特别强调的结论。
误区一:Generative UI 就是让模型输出 JSON。
不是。JSON 只是常见的序列化形式。真正重要的是组件描述协议、验证规则、渲染器和运行时。
误区二:Generative UI 一定比 Markdown 更高级。
不一定。简单问答、文档说明和代码解释通常使用 Markdown 更合适。只有当任务确实需要动态组合界面时,结构化 UI 才更有优势。
误区三:Intelligent UI 必须每次操作都请求模型。
恰恰相反。能够把本地交互、业务计算和智能推理分开,才是良好的架构设计。
误区四:组件白名单就能解决所有安全问题。
组件白名单只是第一层防护。属性校验、动作权限、后端授权、数据校验和高风险操作确认仍然必不可少。
误区五:生成式 UI 就是把另一个网页嵌套进当前页面。
不一定。最常见的方式是在现有应用中动态创建组件树。iframe 只是多种展示与隔离方式中的一种。
误区六:Intelligent UI 是 Generative UI 的另一个名字。
两者有关联,但不是严格的同义词。Generative UI 更容易用来描述动态界面的生成和渲染机制;Intelligent UI 更适合描述 AI 根据任务选择和调整交互形式的产品体验。涉及具体厂商时,还需要以其公开定义和技术文档为准。
十四、总结:从聊天消息走向任务界面
回到最初的问题:如果已经有 ReactMarkdown 和自定义代码块渲染,还有必要做 Generative UI 吗?
答案取决于产品需要。
如果你的应用主要是问答、解释、代码生成和内容总结,Markdown 富内容完全可以满足需求。
如果你希望模型动态组合表格、表单、图表和布局,可以引入声明式 UI 协议与组件注册表。
如果你希望用户通过这些界面持续调整参数、触发业务工具、查看执行结果,并让 Agent 在整个任务过程中保持上下文,就需要进一步设计状态管理、事件协议、工具调用和增量更新机制。
这不是一次简单的渲染器升级,而是从内容展示架构向任务交互架构的演进。
Markdown 解决的是如何呈现内容,Generative UI 解决的是如何动态组合界面,而 Intelligent UI 所强调的,是如何根据任务选择合适的界面,并让界面成为 AI 与用户共同完成任务的入口。
对于技术团队来说,最务实的路线不是抛弃 Markdown、追求完全动态的页面,而是采用混合架构:文本交给 Markdown,动态界面交给结构化组件协议,确定性计算交给前端或业务服务,复杂决策交给 Agent,再通过受控的事件通道将它们连接起来。
当这些部分协同工作时,AI 应用才有机会从一个能够生成答案的聊天框,演进为一个能够帮助用户完成工作的交互式应用。