跳转到内容

生成式 UI(Generative UI)模式

生成式 UI(Generative UI)模式

概述

生成式 UI 是一种让 AI Agent 在对话中动态生成并渲染交互式 UI 组件的模式。与传统的”纯文本回复”不同,Agent 可以生成按钮、表单、图表、卡片等真正的交互式组件,用户可以直接在聊天界面中操作。

核心挑战:Agent 的输出是流式的(SSE 逐块传输),而 JSON 参数通常是完整的结构。如何在流式传输过程中渐进式地解析和渲染 UI?

架构概览

流式 JSON 解析

SAX 式事件驱动解析

生成式 UI 中间件的核心是一个 SAX 式 JSON 解析器(如 clarinet),而非 JSON.parse。SAX 解析器的特点是:

  • 事件驱动:每当解析到一个 key、value、对象或数组边界时,发射对应事件
  • 增量处理:不需要等待完整 JSON,边接收边解析
  • 状态跟踪:维护当前解析深度、当前 key、是否在数组中等状态
解析流程(伪代码):
SSE Chunk → "html": "<div"
onkey("html") → currentKey = "html"
onvalue("<div") → params.html = "<div"
发射 ActivityDeltaEvent → UI 增量渲染

解析状态管理

解析器需要跟踪的关键状态:

状态说明
currentKey当前正在解析的 JSON key
depth嵌套深度(对象层级)
currentArrayKey当前是否在数组中,数组的 key 是什么
snapshotEmitted是否已发射完整快照(防止重复发射)

数组参数的增量解析

数组元素的解析比标量更复杂:

流式输入:
"jsExpressions": ["alert('hello')", "console.log('world')"]
解析事件序列:
1. onkey("jsExpressions") → 标记数组开始
2. onvalue("alert('hello')") → 第一个元素完成
→ 发射 ActivityDeltaEvent(包含已解析的数组)
3. onvalue("console.log('world')") → 第二个元素完成
→ 发射 ActivityDeltaEvent(更新后的完整数组)
4. 对象结束 → 发射 ActivitySnapshotEvent

活动事件协议

生成式 UI 中间件通常定义两种活动事件:

ActivityDeltaEvent(增量事件)

每当解析器完成一个参数的解析时发射:

{
type: "ACTIVITY_DELTA",
activityType: "generative-ui",
toolCallId: "tool_abc123",
delta: { html: "<div>...</div>" } // 本次新增的参数
}

ActivitySnapshotEvent(快照事件)

所有参数解析完成后发射完整状态:

{
type: "ACTIVITY_SNAPSHOT",
activityType: "generative-ui",
toolCallId: "tool_abc123",
snapshot: {
html: "<div>...</div>",
jsFunctions: "function handleClick() {...}",
initialHeight: 300
}
}

沙盒渲染

生成的 UI 组件需要在沙盒中执行,原因:

  1. 安全性:Agent 生成的 HTML/JS 可能包含恶意代码
  2. 隔离性:防止生成的 CSS 影响主页面样式
  3. 资源限制:限制沙盒内的网络请求、DOM 访问等

沙盒实现方案

方案隔离强度性能复杂度
<iframe srcdoc>中等
<iframe sandbox>
Web Worker + OffscreenCanvas
Shadow DOM低(仅 CSS 隔离)最好

设计权衡

流式解析 vs 完整解析

流式解析优势:用户能看到 UI 逐步构建,感知延迟更低。 流式解析代价:解析逻辑复杂,需要处理不完整的 JSON 片段边界、字符串转义、并发 tool call 的隔离。

完整解析优势:实现简单,只需等待完整 JSON 后 JSON.parse完整解析代价:用户需要等待 Agent 完成整个 tool call 才能看到 UI。

沙盒隔离 vs 直接渲染

沙盒优势:安全,防止恶意代码。 沙盒代价:iframe 通信需要通过 postMessage,增加复杂度;某些 API(如 localStorage)在沙盒中不可用。

直接渲染优势:简单,组件可以直接访问父页面 API。 直接渲染代价:安全隐患,Agent 生成的代码可能执行任意操作。

潜在陷阱

  1. SSE 缓冲:CDN 或反向代理可能缓冲 SSE 流,导致”批量”到达而非逐块到达
  2. 不完整 JSON 缓存:流式解析中,最后一个 chunk 可能截断在字符串中间,需要正确的缓存策略
  3. 并发 tool call:多个 tool call 的流式输出可能交错,每个 tool call 需要独立的解析器实例
  4. XSS 风险:即使使用 iframe sandbox,仍需注意 allow-scripts 开启后的潜在风险

参考来源