生成式 UI 实战:用 JSON Schema + React 动态渲染 AI 界面

《AI 前端实战》第 5/8 篇

上篇:Tool Calling 前端接入

下篇预告:Agent 前端状态机

前面四篇解决了:流式文本、工程化失败路径、工具调用。

下一步更激进一点:

让模型不只返回文字,还返回「可渲染的界面描述」。

这就是生成式 UI(Generative UI)。2026 年主流做法不是让模型直接吐 HTML/JS,而是:

JSON Schema 约束 → 白名单组件映射 → React 安全渲染。

你将学到

  • 为什么用 JSON,而不是 HTML
  • 一套最小 Schema 设计
  • React Catalog(组件白名单)怎么写
  • 流式增量渲染与校验
  • 安全边界:防 XSS、防非法组件

一、为什么不是「模型直接写 React/HTML」

直接生成代码看起来爽,但生产上很难:

  1. XSS / 注入风险极高
  2. 样式与设计系统失控
  3. 无法稳定测试与权限控制
  4. 流式过程中半截代码不可执行

JSON 描述的好处:

  • 可 Schema 校验
  • 可映射到已审核组件
  • 可版本化、可缓存、可 A/B
  • 更适合流式「边到边渲染」

二、最小协议:UI Document

ts 复制代码
type UINode =
  | { type: "Card"; title: string; children: UINode[] }
  | { type: "Text"; text: string }
  | { type: "Button"; label: string; action: string; payload?: Record<string, unknown> }
  | { type: "Table"; columns: string[]; rows: string[][] }
  | { type: "Form"; fields: { name: string; label: string; kind: "text" | "number" }[]; submitAction: string };

对应 JSON Schema 思路:type 必须是枚举;未知 type 直接拒绝。

三、React Catalog:只渲染你允许的组件

tsx 复制代码
const catalog = {
  Card: ({ title, children }: any) => (
    <section className="rounded-xl border p-4">
      <h3 className="font-semibold mb-2">{title}</h3>
      {children}
    </section>
  ),
  Text: ({ text }: any) => <p>{text}</p>,
  Button: ({ label, action, payload, onAction }: any) => (
    <button className="border rounded px-3 py-1" onClick={() => onAction?.(action, payload)}>
      {label}
    </button>
  ),
  Table: ({ columns, rows }: any) => (
    <table>
      <thead>
        <tr>{columns.map((c: string) => <th key={c}>{c}</th>)}</tr>
      </thead>
      <tbody>
        {rows.map((r: string[], i: number) => (
          <tr key={i}>{r.map((cell, j) => <td key={j}>{cell}</td>)}</tr>
        ))}
      </tbody>
    </table>
  ),
} as const;

function renderNode(node: any, onAction?: Function): React.ReactNode {
  const Comp = catalog[node.type as keyof typeof catalog];
  if (!Comp) return <div className="text-red-500">不支持的组件:{String(node.type)}</div>;
  const children = (node.children || []).map((c: any, i: number) => (
    <div key={i}>{renderNode(c, onAction)}</div>
  ));
  return <Comp {...node} onAction={onAction}>{children}</Comp>;
}

原则:

  • 没有 Catalog 的 type = 不渲染
  • 组件 props 再做一次运行时校验(zod 等)
  • action 只允许白名单事件名

四、和 Tool Calling 怎么配合

常见两种模式:

模式 A:模型直接输出 UI JSON

适合:表单、对比表、下一步按钮。

模式 B:工具返回数据,前端/模型再生成 UI

适合:查库结果 → Table;需要强权限的操作按钮。

推荐生产路径:

text 复制代码
Tool 拿可信数据 → 模型生成 UI JSON(仅展示与次要交互) → 高风险动作仍走确认

五、流式生成式 UI:边到边渲染

模型可能一段段输出 JSON。前端策略:

  1. 缓冲文本,尝试 JSON.parse
  2. 解析失败则等待更多 token
  3. 解析成功先 ajv/zod 校验
  4. 校验通过再进入 Catalog 渲染
  5. 校验失败显示「界面协议错误」,回退纯文本

进阶:用 JSON Lines / 分片协议,避免等完整大 JSON。

六、安全清单(必须做)

  1. 禁止原始 HTML/JS 节点 (没有 DangerousHTML
  2. 禁止任意 URL 跳转(链接域名白名单)
  3. Button action 白名单 (不能让模型触发 deleteAll
  4. 文本做转义 (React 默认文本安全,仍避免 dangerouslySetInnerHTML
  5. 大小限制(节点数、深度、字符串长度)
  6. 权限二次校验(服务端)

生成式 UI 的攻击面,比普通 Chat 更大:把它当「远程组件协议」来防。

七、一个业务例子:退款助手

用户问:「我订单 123 能退吗?」

模型(在工具结果基础上)输出:

json 复制代码
{
  "type": "Card",
  "title": "退款评估",
  "children": [
    { "type": "Text", "text": "订单 123 仍在可退期内,预计 1-3 个工作日到账。" },
    {
      "type": "Button",
      "label": "申请退款",
      "action": "create_refund",
      "payload": { "orderId": "123" }
    }
  ]
}

前端:

  • 渲染 Card/Text/Button
  • 点击后走 create_refund 确认框(第 4 篇权限模型)
  • 真正退款 API 在服务端鉴权执行

八、什么时候不要上生成式 UI

  • 交互极简单,纯 Markdown 足够
  • 设计系统不稳定,组件语义不清
  • 团队还没有 Schema 校验与白名单机制

生成式 UI 是加速定制界面,不是让模型取代设计系统。

小结

生成式 UI 的可靠公式:

  1. Schema 约束输出
  2. Catalog 白名单渲染
  3. 流式校验再上屏
  4. 高风险动作仍人确认

这样,AI 才能「画界面」,而你的产品仍可控。


下篇预告 :《AI 前端实战》第 6/8 篇

Agent 前端状态机:多工具并发、Abort 与错误隔离。

系列导航:

1 · 2 · 3 · 4

相关推荐
其美杰布-富贵-李1 小时前
04 watch 与 Vue 响应式数据流
前端·javascript·vue.js
kyriewen4 小时前
我排查了一个React内存泄漏——罪魁祸首是这3个被忽略的清理函数
前端·javascript·面试
IT_陈寒4 小时前
我又被JavaScript的隐式类型转换坑了
前端·人工智能·后端
Fluxproxy4 小时前
AI数据集采集、批量模型推理报错频发?AI项目网络稳定性优化实战
python·ai·ai编程
其美杰布-富贵-李4 小时前
03 ref、reactive 与 computed 响应式数据
前端·javascript·vue.js
OpenTiny社区4 小时前
GenUI SDK v1.3.0 发布|多框架兼容,一键换物料,渲染器 & 演练场全面增强!
前端·ai编程
用户938515635074 小时前
useRef + Web Worker 实战:React 如何优雅地拥抱多线程
前端·javascript·react.js
他们叫我秃子5 小时前
前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript
前端·后端·go