AI → JSON → UI

背景

过去两年,AI 生成 UI 的实践基本集中在两种路径上。第一种是直接让模型生成 JSX、HTML 或 CSS。这条路线的优势在于自由度极高,模型几乎不受约束,看起来"什么都能写"。但在真实工程环境中,这种方式几乎不可控:输出结构不稳定,无法保证组件边界,难以做权限与审计控制,生成的代码经常无法编译或违背工程约定,更重要的是,它与实际业务中的组件体系和设计系统严重脱节。

另一条路线是低代码或 schema 驱动 UI,例如基于 JSON Schema 或表单 schema 的方案。这类方案在工程上是可控的,结构稳定、可校验、可复用,但它们本质上是为"人编写配置"设计的,而不是为"模型生成结构"设计的。schema 表达能力有限,扩展成本高,并且与自然语言之间的映射并不自然,Prompt 往往需要大量人工约束。

Vercel 刚刚开源了 json-render,json-render 的出现,本质上是对这两条路线的重新切分与组合。它并没有试图让 AI 写前端代码,也没有把 AI 限制在传统低代码 schema 中,而是引入了一个中间层:JSON UI AST。AI 只能生成这种 AST,而 AST 的能力边界完全由开发者定义。渲染、状态、行为解释全部留在业务侧完成。开发者因此可以安全地让用户通过自然语言生成仪表盘、小部件或数据视图,而不需要把执行权交给模型。

整体架构:json-render 是一个 DSL 解释系统

从架构视角看,json-render 并不是一个 UI 框架,而是一个 DSL 执行系统。系统由三层构成:最底层是 Catalog,用来声明"系统允许 AI 使用哪些 UI 能力";中间层是 JSON UI Tree,这是 AI 的唯一输出形式;最上层是 Renderer,由业务侧实现,用于解释 JSON 并渲染真实 UI。

它们之间的关系可以用下面这张结构图来理解:

复制代码
┌────────────┐
│   Prompt   │
└─────┬──────┘
      │
      ▼
┌──────────────────┐
│  LLM / AI Model  │
└─────┬────────────┘
      │  JSON UI AST(受 Catalog 严格约束)
      ▼
┌──────────────────┐
│     Catalog      │  ← 能力白名单 / Schema / Grammar
└─────┬────────────┘
      │ 校验 + 解析
      ▼
┌──────────────────┐
│    Renderer      │  ← React / Vue / Native
└─────┬────────────┘
      │
      ▼
┌──────────────────┐
│   Real UI View   │
└──────────────────┘

在这个模型中,AI 只参与"结构生成",不参与"执行"。这也是 json-render 在工程上成立的根本原因。

从 Catalog 到 UI

1. Catalog:系统的能力边界定义

下面这段代码是整个系统中最重要的入口,它定义了 AI 能使用的全部 UI 能力。

ts 复制代码
import { createCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = createCatalog({
  components: {
    Card: {
      props: z.object({
        title: z.string()
      }),
      hasChildren: true
    },
    Metric: {
      props: z.object({
        label: z.string(),
        valuePath: z.string(),
        format: z.enum(['number', 'currency', 'percent']).optional()
      })
    }
  },
  actions: {
    refresh: {
      params: z.object({})
    }
  }
})

这里没有任何 UI 代码,只有能力声明。props 使用 Zod 定义,这意味着它不仅是类型提示,还包含运行时校验规则。如果你对 Zod 没有了解,可以看看这篇博文,Zod:TypeScript 类型守卫与数据验证。action 并不是函数实现,而是一个"意图声明",它只描述"可以发生什么",不描述"怎么发生"。

Catalog 在系统中的地位,相当于一门语言的语法定义文件。AI 后续生成的所有 JSON,本质上都必须符合这套 grammar。

2. AI 输出的 JSON UI AST

当用户输入类似"生成一个收入仪表盘"的提示时,模型生成的结果不是 JSX,而是下面这样的 JSON:

json 复制代码
{
  "type": "Card",
  "props": { "title": "Revenue Overview" },
  "children": [
    {
      "type": "Metric",
      "props": {
        "label": "Total Revenue",
        "valuePath": "/metrics/revenue",
        "format": "currency"
      }
    }
  ]
}

这个 JSON 有几个非常关键的特征。它不包含任何函数、不包含条件表达式、不包含样式或状态逻辑。它只是结构化地描述"使用哪个组件,用什么参数,组件之间如何嵌套"。所有能力完全来源于 Catalog,因此这个 JSON 是可校验、可存储、可 diff、可审计、可回放的。

3. Renderer:JSON 的解释执行

在 React 侧,Renderer 扮演的是解释器的角色。

tsx 复制代码
import { Renderer } from '@json-render/react'
import { catalog } from './catalog'

function App() {
  return (
    <Renderer
      catalog={catalog}
      components={{
        Card: ({ title, children }) => (
          <div className="card">
            <h2>{title}</h2>
            {children}
          </div>
        ),
        Metric: ({ label, value }) => (
          <div>
            {label}: {value}
          </div>
        )
      }}
      data={{
        metrics: { revenue: 120000 }
      }}
    />
  )
}

Renderer 并不关心 UI 长什么样,它只做三件事:根据 type 找到对应组件定义,根据 Catalog 校验 props 和 children,根据 valuePath 等规则完成数据注入。

为什么 json-render 是"可控的"

下面的借助 AI 能力分析基于 vercel-labs/json-render 主仓库。如果你对此不感兴趣,跳过这部分内容。

1. createCatalog:能力被冻结的起点

文件路径位于 packages/core/src/create-catalog.ts。这个函数的核心作用不是"注册组件",而是"冻结能力边界"。

简化后的核心逻辑可以理解为:

ts 复制代码
export function createCatalog(definition) {
  return {
    components: definition.components,
    actions: definition.actions,
    validateNode(node) {
      // 校验 type 是否存在
      // 校验 props 是否符合 Zod schema
      // 校验 children 是否被允许
    }
  }
}

每一行代码都在服务一个目标:让 Catalog 成为一个不可突破的白名单。Renderer 和 AI 都无法绕过它。这也是为什么 json-render 把 Catalog 放在 core 包中,而不是 React 包中。

2. Schema 校验:AI 输出必须"先编译再执行"

在 JSON Tree 进入 Renderer 之前,系统会逐节点校验。type 是否在 Catalog 中声明,props 是否通过 Zod 校验,children 是否符合 hasChildren 约束,action 是否存在于白名单。这一过程本质上就是一次 AST 校验。

这意味着 AI 的输出不是"运行时报错",而是"不通过即拒绝执行"。在 AI UI 系统中,这是一个极其关键但经常被忽视的工程点。

3. Renderer:真正的解释器模型

React Renderer 的内部逻辑并不是简单的 switch-case,而是一个递归解释过程。它根据节点的 type 查 Catalog,构造 props,解析 valuePath 注入数据,绑定 action handler,然后递归渲染 children。

从架构角度看,它更接近一个 JSON AST Interpreter,而不是模板引擎。这也是 json-render 可以跨 React、Vue、Native 复用核心思想的原因。

4. valuePath:刻意避免 AI 参与状态逻辑

valuePath 使用字符串路径描述数据依赖,例如:

json 复制代码
"valuePath": "/metrics/revenue"

这样设计的直接结果是,AI 不需要理解状态结构,也不需要写任何状态逻辑。Renderer 统一负责解析路径、读取数据、触发更新。这在架构上刻意切断了"AI 直接操作状态"的可能性。

下面是仅包含新增内容的补充章节,重点放在可落到源码层面的机制,避免概念化描述。示例代码与解释均基于 vercel-labs/json-render 当前仓库结构与实现思路。

Prompt 与 Catalog 的自动对齐

Prompt 与 Catalog 的自动对齐:不是"调 Prompt",而是"导出 Grammar"。json-render 中,Prompt 与 Catalog 的对齐并不是通过人肉 Prompt Engineering 完成的,而是通过从 Catalog 派生一份机器可理解的能力描述,并将其注入到模型上下文中。这一点在 packages/core 中的设计非常关键。

在 core 层,Catalog 本身并不是一个简单的对象,它包含了完整的组件定义、props schema 以及 action 描述。这些信息会被转换为一种"描述性结构",用于告诉模型当前系统支持的 UI grammar。

类似这样的逻辑:

ts 复制代码
export function catalogToPrompt(catalog) {
  return `
You can generate a JSON UI tree.
Available components:
${Object.entries(catalog.components).map(([name, def]) => `
- ${name}
  props: ${describeSchema(def.props)}
  hasChildren: ${def.hasChildren}
`).join('\n')}

Available actions:
${Object.keys(catalog.actions).join(', ')}

Rules:
- Output must be valid JSON
- Only use listed components
- Follow prop schemas strictly
`
}

这里的关键点不在于字符串本身,而在于信息来源完全来自 Catalog。换句话说,Catalog 是 single source of truth,Prompt 只是它的一种序列化视图。当开发者新增或修改组件定义时,Prompt 中允许模型使用的能力会自动发生变化,不存在"代码和 Prompt 不一致"的问题。这也是 json-render 能够避免大量"Prompt 腐化"的根本原因。

从模型视角看,它面对的不是一段模糊的自然语言说明,而是一套接近 BNF 的 UI grammar 描述。模型生成 JSON UI Tree 的过程,本质上类似于在给定语法约束下生成 AST。这也是为什么 json-render 要使用 Zod 而不是仅靠 TypeScript 类型。Zod schema 可以被同时用于运行时校验和 Prompt 语义描述,形成闭环。

Streaming UI 的实现细节

流式构建 AST,而不是流式拼字符串。json-render 的 Streaming UI 能力,核心并不在"模型支持流式输出",而在于 UI 的中间表示是可增量合并的 JSON AST。这一点在 React 包中的实现非常清晰。

packages/react 中,可以看到类似 useUIStream 的 hook,其核心职责是:

维护一棵当前 UI Tree,并在模型流式输出时不断向这棵树中合并新节点。

简化后的内部结构大致如下:

ts 复制代码
// packages/react/src/use-ui-stream.ts(概念结构)
export function useUIStream() {
  const [tree, setTree] = useState<UITree | null>(null)

  function onChunk(chunk: string) {
    const partialNode = parseChunkToNode(chunk)
    if (!partialNode) return

    setTree(prevTree => {
      return mergeTree(prevTree, partialNode)
    })
  }

  return { tree, onChunk }
}

这里有两个非常关键但容易被忽略的点。

parseChunkToNode 并不是简单的 JSON.parse。模型在 streaming 模式下输出的通常是不完整 JSON,因此 json-render 采用的是逐段解析、延迟成型的策略。只有当一个节点在结构上是完整且通过 Catalog 校验时,才会被提升为"可合并节点"。mergeTree 是一个纯函数。它不依赖外部状态,只根据已有 UI Tree 和新节点生成下一棵 Tree。这使得每一次更新都是确定性的,也天然适合 React 的状态模型。

在 Renderer 层,这棵 Tree 会被直接用于递归渲染:

tsx 复制代码
function RenderNode({ node }) {
  const Component = components[node.type]

  const resolvedProps = resolveProps(node.props)
  const children = node.children?.map(child =>
    <RenderNode key={child.id} node={child} />
  )

  return <Component {...resolvedProps}>{children}</Component>
}

由于 Tree 始终是"已校验的合法结构",Renderer 不需要关心节点是否完整,只需要关心"当前有哪些节点已经存在"。这也是 Streaming UI 能在生成未完成时就安全渲染的根本原因。

Streaming 与 Catalog 校验如何协同工作

Streaming UI 并不是绕过校验机制的捷径,恰恰相反,它依赖校验机制才能成立。在实际流程中,每一个候选节点在被合并进 UI Tree 之前,都会经过 Catalog 的校验逻辑:

ts 复制代码
// packages/core/src/validate-node.ts(概念结构)
export function validateNode(node, catalog) {
  const def = catalog.components[node.type]
  if (!def) throw new Error('Unknown component')

  def.props.parse(node.props)

  if (node.children && !def.hasChildren) {
    throw new Error('Children not allowed')
  }
}

Streaming 模式下,这个校验发生得更频繁,但粒度更小。系统宁可"暂时不渲染",也不会把一个非法节点交给 Renderer。这保证了 UI 在任何时刻都是一个合法子集,而不是半成品垃圾状态。

Prompt 与 Catalog 的自动对齐,确保模型"不会幻想不存在的能力";Streaming UI 的 AST 级增量构建,确保 UI"可以在不完整时仍然正确运行"。两者结合,使 json-render 的执行模型更接近编译器与解释器,而不是模板生成器。从工程视角看,这意味着一个重要转变:**UI 生成不再是一次性结果,而是一个可观察、可中断、可回滚的过程。**这也是 json-render 能够真正进入生产系统,而不仅停留在 Demo 层面的根本原因。

json-render 真正解决了什么

json-render 本身并不是一种全新的技术范式。**"用受限结构描述 UI,再由运行时解释执行"这一思想,在前端工程中早已反复出现过。**早期的 JSON Schema Form、react-jsonschema-form、Formily、本质上都是用结构化数据描述界面,再由渲染器生成真实 UI。低代码平台、搭建系统、配置化后台,几乎全部建立在同一逻辑之上。即便在 AI 出现之前,这种模式也已经非常成熟:工程师通过 schema 描述组件、属性和布局,运行时负责校验与渲染,业务侧只操作结构而不直接触碰代码。json-render 并没有发明这种模式,它继承的正是这一整条技术脉络。

json-render 的不同之处在于,它首次把"模型生成"作为一等公民纳入设计前提。传统 schema UI 假设配置由人编写,因此更强调完整性、可读性和编辑体验;而 json-render 假设结构由模型生成,因此更强调语法边界清晰、失败可恢复、部分结果可执行,以及与 Prompt 的自动对齐能力。从这个角度看,json-render 更像是"为 AI 重新设计的一代 schema UI 执行模型"。它真正解决的问题并不是"怎么用 JSON 渲染 UI",而是当结构来源变成不可靠的模型时,工程边界应该在哪里。它给出的答案非常明确:AI 只负责生成结构化意图,工程师负责能力定义、执行与渲染,JSON 作为唯一中介和约束层。这使得 AI UI 不再是一次性 Demo,而是可以进入生产系统的工程能力。在当前阶段,这是少数真正站在工程立场思考 AI UI 的方案之一。

参考资料

相关推荐
魏杨杨5 小时前
一个程序员眼中的 AI 核心概念,讲透 LLM 、Agent 、MCP 、Skill 、RAG...
ai·.net·agent·claude code
RyFit6 小时前
SpringAI 常见问题及解决方案大全
java·ai
元拓数智6 小时前
智能分析落地卡壳?先补好「数据关系+语义治理」这层技术基建
大数据·分布式·ai·spark·数据关系·语义治理
企学宝6 小时前
企学宝5月专题课程丨《OpenClaw AI 智能体实战营:从零基础部署到全场景自动化落地》
人工智能·ai·企业培训
涵涵(互关)7 小时前
Naive-ui树型选择器只显示根节点
前端·ui·vue
测试员周周10 小时前
【Appium 系列】第13节-混合测试执行器 — API + UI 的协同执行
开发语言·人工智能·python·功能测试·ui·appium·pytest
莽夫搞战术10 小时前
【Google Stitch】AI原生画布重新定义设计,让想法变成可交互界面
前端·人工智能·ui
malog_10 小时前
大语言模型后训练全解析
人工智能·深度学习·机器学习·ai·语言模型
低代码行业资讯11 小时前
五大实锤证据:AI不会终结低代码,只会倒逼技术进化
低代码·ai
神秘的土鸡11 小时前
Agent 落地:贴合健身真实场景的 AI 人物跟练方案
ai·语言模型·agent