跳转到内容

生成式 UI:Agent 流式渲染交互式组件

生成式 UI:Agent 流式渲染交互式组件

学习目标

理解 CopilotKit 的 Open Generative UI 中间件实现:

  • 中间件如何拦截和解析 Agent 的 SSE 流
  • clarinet SAX 解析器如何增量解析流式 JSON
  • Activity 事件的发射与消费
  • 沙盒 UI 的安全隔离

前置知识

本章涉及生成式 UI 模式和流式传输的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 CopilotKit 的 Open Generative UI 实现。

项目实践

中间件架构

OpenGenerativeUIMiddleware 是 AG-UI 协议的中间件,拦截 Agent 的 SSE 事件流:

工具定义

Agent 使用名为 generateSandboxedUi 的工具生成 UI:

工具参数结构:
{
html: "<div>...</div>", // HTML 内容
jsFunctions: "function foo() {...}", // JavaScript 函数
jsExpressions: ["expr1", "expr2"], // JS 表达式数组
css: ".my-class { ... }", // CSS 样式
initialHeight: 300, // 初始高度
placeholderMessages: ["加载中..."] // 占位消息
}

ArgsParser:流式 JSON 解析

ArgsParser 类是核心解析器,使用 clarinet(SAX 式 JSON 解析器)增量解析工具参数:

状态跟踪(源码验证):

  • currentKey:当前正在解析的 JSON key
  • depth:嵌套深度(跟踪对象开闭)
  • currentArrayKey:当前是否在数组中
  • snapshotEmitted:是否已发射完整快照
  • streamingHtmlKey:是否正在流式解析 html 字段
  • htmlEmittedLength:已发射的 html 长度

解析事件序列(源码验证):

SSE Chunk: "html": "<div class=
onkey("html") → streamingHtmlKey = true
onvalue("<div class=") → params.html += chunk
→ 发射 ActivityDeltaEvent
SSE Chunk: ">Hello</div>", "initialHeight": 300
onvalue("Hello</div>") → params.html += chunk
→ 发射 ActivityDeltaEvent(更新后的 html)
onkey("initialHeight") → streamingHtmlKey = false
onvalue(300) → params.initialHeight = 300
对象结束: }
→ 发射 ActivitySnapshotEvent(完整参数快照)
→ snapshotEmitted = true

Activity 事件

两种活动事件类型:

ActivityDeltaEvent(增量)

{
type: "ACTIVITY_DELTA",
activityType: "open-generative-ui",
toolCallId: "tool_abc",
delta: { html: "<div>新解析的片段</div>" }
}

ActivitySnapshotEvent(完整快照)

{
type: "ACTIVITY_SNAPSHOT",
activityType: "open-generative-ui",
toolCallId: "tool_abc",
snapshot: {
html: "<div>完整 HTML</div>",
jsFunctions: "...",
initialHeight: 300
}
}

中间件启用

在 CopilotRuntime 中启用 Open Generative UI:

const runtime = new CopilotSseRuntime({
agents: { default: agent },
openGenerativeUI: true // 或配置对象:{ agents: ["my-agent"] }
});

作用域配置

  • openGenerativeUI: true → 对所有 Agent 启用
  • openGenerativeUI: { agents: ["agent-a"] } → 仅对指定 Agent 启用

沙盒渲染

生成的 UI 在沙盒中渲染,防止恶意代码执行。沙盒的关键安全考虑:

  1. HTML 和 JS 在隔离环境中执行
  2. 限制对父页面 DOM 的访问
  3. 控制网络请求权限

问题与规避

问题规避策略
流式 JSON 中的字符串截断(如在 \n 中间断开)SAX 解析器正确处理不完整的字符串;下一个 chunk 到达后继续解析
数组参数的增量解析不完整htmlArrayEmitted 标记数组是否完整;只有数组结束时才发射快照
并发 tool call 的解析交错每个 toolCallId 独立的 ArgsParser 实例
XSS 风险(Agent 生成的恶意 JS)沙盒隔离(iframe sandbox);限制 allow-scripts 后的权限

设计取舍

流式 JSON 解析 vs 等待完整 JSON

流式解析(clarinet SAX)

  • 优势:UI 逐步呈现,用户感知延迟更低
  • 代价:解析逻辑复杂,需要处理不完整 JSON 的各种边界情况

等待完整 JSON(JSON.parse)

  • 优势:实现简单
  • 代价:用户需等待 Agent 完成整个工具调用

CopilotKit 选择流式解析,因为生成式 UI 的核心价值就是”渐进式展示”。

ActivityDelta vs ActivitySnapshot

Delta(增量)

  • 优势:每次只传输变化的部分,数据量小
  • 代价:前端需要合并增量更新

Snapshot(全量)

  • 优势:前端无需维护状态合并逻辑
  • 代价:每次传输完整参数

CopilotKit 同时提供两种事件:Delta 用于渐进式更新,Snapshot 用于最终一致性确认。

参考来源

  • packages/runtime/src/v2/runtime/open-generative-ui-middleware.ts
  • packages/runtime/src/v2/runtime/runner/in-memory.ts(中间件应用)
  • clarinet SAX JSON 解析器