生成式 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 组件需要在沙盒中执行,原因:
- 安全性:Agent 生成的 HTML/JS 可能包含恶意代码
- 隔离性:防止生成的 CSS 影响主页面样式
- 资源限制:限制沙盒内的网络请求、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 生成的代码可能执行任意操作。
潜在陷阱
- SSE 缓冲:CDN 或反向代理可能缓冲 SSE 流,导致”批量”到达而非逐块到达
- 不完整 JSON 缓存:流式解析中,最后一个 chunk 可能截断在字符串中间,需要正确的缓存策略
- 并发 tool call:多个 tool call 的流式输出可能交错,每个 tool call 需要独立的解析器实例
- XSS 风险:即使使用 iframe sandbox,仍需注意
allow-scripts开启后的潜在风险