json-render 的 Jev 组合实验:用受限候选集与评估器协议驱动生成式 UI 树
【免费下载链接】json-renderThe Generative UI framework项目地址: https://gitcode.com/GitHub_Trending/js/json-render
本文是 json-render(Generative UI framework)仓库内apps/web(官网与 Playground)中Jev 组合实验(Jev composition experiment)的完整技术指南。该实验在 Playground 的/playground页面提供 "default / jev" 双模型切换,让 Jev 模型通过 Vercel AI Gateway 从组件目录中挑选原子候选、组合出一棵可交互的 UI 树,并在后续请求中以 add / replace / remove / move 的顺序编辑协议持续迭代。读完本文,你将掌握该实验的运行方式、密钥与限额配置、两阶段批处理组合流程、顺序编辑协议,以及其底层experimental_*公共 API 的调用契约与实现原理。
实验定位:一个受目录约束的 UI 组合器
apps/web/README.md中明确说明,Playground 的/playground页面内置一个 "default / jev" 切换开关:default对应常规的生成式模型(通过AI_GATEWAY_API_KEY调用),而jev是一个实验选项,鼠标悬停或聚焦其信息图标即可看到实验性状态说明。Jev 选项是@json-render/core中可复用 APIexperimental_composeSpec与experimental_createEvaluator的参考消费方(reference consumer):
- Jev 从 Playground 的组件目录(catalog)与允许的动作绑定(action bindings)中组合并编辑 UI 树;
- 服务端通过 Vercel AI Gateway 完成评估调用,Jev 专用密钥为
JEV_AI_GATEWAY_API_KEY,默认模型继续使用AI_GATEWAY_API_KEY; - 后续追问(follow-up)以所选版本作为
initialSpec,可以对树执行新增、替换、删除、移动,且早前版本保持原样不变(不可变版本历史); - 与 default 模型共用同一套提示输入、版本历史、spec/流式检查器与功能预览;
- 共享的
/api/generate端点负责流式输出 spec 补丁(spec patches)与决策元数据。
实验的完整设计文档位于 apps/web/lib/jev/README.md,其中包含运行方式、架构与限制的权威说明,是本文的骨架来源;文中所有源码引用均来自当前仓库。
运行实验:密钥、启动与试玩路径
密钥配置
在apps/web/.env.local或服务器环境中设置:
JEV_AI_GATEWAY_API_KEY=<你的 Vercel AI Gateway 密钥>要点(来自 apps/web/lib/jev/README.md):
- Playground 为 Jev 使用这个专用 Gateway 密钥;default 模型继续使用
AI_GATEWAY_API_KEY; - Jev 不会回退到 default 模型的密钥,未配置时服务端会返回 503(见下文);
- Gateway 团队必须放行
typesafe-aiprovider;无需单独的 TypeSafe API key; - 该密钥只应存在于服务端。核心包中的 experimental-evaluator.ts 对
apiKey的注释明确写着:"Server-side Vercel AI Gateway key. Never expose this in browser code.",并在空密钥时直接抛出 "A Vercel AI Gateway API key is required."。
启动开发服务器
从仓库根目录执行:
pnpm --filter web devapps/web/package.json中dev脚本为portless json-render next dev --turbopack,依赖全局工具portless(未安装时会提示先执行npm i -g portless)。使用命令打印的 portless URL 并追加/playground访问;在 HTTPS 代理开启时即为https://json-render.localhost/playground。
推荐的试玩流程
文档给出的端到端验证路径是:选择 Jev → 选择Create account settings示例 → 发送请求 → 编辑姓名、打开通知开关、点击Save changes、再点Reset。需要特别注意的语义约束:
- 动作处理器只在用户交互时运行:Jev 只负责把动作绑定写入 spec,绝不会在生成阶段执行它们;
- 表单提交仅做校验并弹出 toast 演示:它不会认证用户,也不会真的发送消息;
- 所有业务数据均为合成数据(synthetic)。
Jev 如何产出 spec:两次有限选择 + 顺序编辑协议
Jev 是"组合式"模型:它暴露 Choice、Boolean、Score 三类输出,不产出自由格式的 JSON 或散文。因此新 UI 的构建被表达为两批有限选择(batches of finite choices),完整流程共 5 步:
- 第一批(select 阶段):在一次评估中同时询问根元素(root)与各独立组件的成员资格问题。互斥的资源变体共享同一个问题(同一问题内二选一),可复用的配方(recipe)则给出有界的使用数量。候选值中包含应用自己拥有的状态/动作绑定。
- 组装并校验预览:选定内容立即组装、校验并流式输出预览。预览采用目录顺序(catalog order)与根的默认插槽(default slot)。当同一配方/资源同时被选为根时,根选择优先于推测性成员资格(对应 experimental-composition-batch.ts 中
if (first.resource && first.resource === root.resource) continue;以及可重复配方计数中减去根占用的逻辑)。 - 第二批(layout 阶段):针对实际选中集合,在第二次评估中询问最终的父级插槽(parent slots)与同级位置(sibling positions)。组合树需整体校验(包括深度与环),再流式输出。相同位置保持目录顺序。若只有一个根、或只有一个子元素且只有单个插槽,则无需第二次调用;整个流程也不需要单独的 finish 调用。
- 后续追问:以当前选中 spec 走顺序编辑协议:add(新增)、replace(替换)、remove(删除)、move/reorder(移动/重排)。替换与移动先选择目标,第二次评估再选择合法配方或目标位置。未被要求改变的元素与早前版本保持不变。
- 追踪(trace):每次评估对应一条 trace;批处理 trace 使用
select/layout作为操作名,独立决策放在answers字段中,计时与用量按每次调用计一次。provider 出错或组合布局非法时,保留最后一份有效预览并报告失败。
Batch 阶段的关键代码见 experimental-composition-batch.ts:composeBatch先并行发起select调用(root问题 + 按资源分组生成的select_N问题),立即组装出临时 spec 并yield snapshot()预热预览;随后并行发起layout调用(parent_<id>与order_<id>问题),在私有克隆上完成挂载后整体validate再发布。批处理避免了每个组件一次网络往返。作为对比,公共 API 也支持strategy: "sequential"(每次只做一个操作)以及现有的自定义评估器(custom evaluators)。
值得强调的事实(README 原话):实验中没有完整的 UI 模板,也没有生成式模型调用。示例提示按钮只填充请求文本;具体包含哪些元素、顺序、分组、采用哪些动作绑定,全部由 Jev 决定;外观与行为由 registry 负责;序列化 JSON 也不是 Jev 写的,而是代码根据选择组装而成。
平台必须提供什么:原子候选与合成内容
组件目录约束了组件名、props 与事件,但字符串与数组 props 仍是开放取值。实验用"平台自有内容 + 绑定配方"来封堵这一剩余空间,具体由 apps/web/lib/jev/grammar.ts 提供:
- 17 种组件类型:Card、Stack、Grid、Heading、Avatar、Badge、Input、Textarea、Select、Checkbox、Switch、Button、Text、Metric、BarGraph、Table、Separator;
- 表单字段、校验规则、标签、合成个人资料与商务数据,以及两个允许的目录动作(
formSubmit与setState)。个人资料候选包含头像、显示名、角色、简介、邮箱、所在地与会员徽章,全部绑定到平台提供的记录对象(platformState.profile,例如 "Maya Chen" / "Product designer"); - 若干对布局 props 与按钮文案有用的取值;请求中的带引号标题会被复制进额外的 Heading 候选中(
grammar.ts通过prompt.matchAll(/"“["”]/g)抽取引号内文本,与内置标题列表去重后生成heading_N候选)。
这些是原子元素候选(atomic element candidates),而非页面模板。宿主应用完全可以从自己的数据 schema、真实记录、本地化文案与允许的操作中构建同类候选——实验只是把这些值放在grammar.ts里,应用则向可复用的 core API 提供自己的候选。目前不支持的场景包括:同一字段在多个表单中重复出现、以及任意的全新文本/数据(例如要求模型凭空编造一段话或一条新数据)。
每个候选同时固定了一种组件配置:预置好的 revenue BarGraph 可以被选中并移动,但想换成 LineGraph 就需要另一个候选。应用既可以把 props 绑定到自己的实时状态,也可以按请求动态构造候选,数据不必硬编码。Jev 只决定树、分组与区块顺序,且只能在这些被提供的配置范围内选择。
以grammar.ts中save候选为例,可见"候选 = 固定配置 + 动作绑定"的形态:
add( "save", "Button: Save changes. Bind press to setState to update the visible saved-status text. Local demo only.", "Button", { label: "Save changes", variant: "primary", disabled: false }, "action:save", { press: { action: "setState", params: { statePath: "/status", value: "Changes saved locally." }, }, }, );候选的公共契约定义在 experimental-compose.ts 的Experimental_CompositionCandidate:id、description、element(仅允许type/props/on/visible,即"原子元素")、root(是否可作为根,默认 true)、maxUses(默认 1,可复用的布局元素可放大数量)、resource(共享同一 resource 的候选互斥)。validateCandidate会对每个候选做严格校验:组件必须存在于目录、props 必须通过对应 Zod schema、事件必须在该组件声明的事件列表内、动作必须命中目录actions或内置动作、回调(onSuccess/onError)也必须引用目录动作——这保证了"模型永远无法发明组件或动作"。
组合器公共 API 与限制:experimental_composeSpec
composeUI(apps/web/lib/jev/compose.ts)是 Playground 对公共 API 的薄封装,其调用即完整呈现了experimental_composeSpec的参数形态(核心选项见Experimental_ComposeSpecOptions):
experimental_composeSpec({ catalog: playgroundCatalog, candidates: buildCandidates(prompt), initialSpec, // 后续追问时传入所选版本 initialState: { ...platformState, ...initialSpec?.state }, elementDescriptions: /* 仅共享展示文案 */, prompt, signal, evaluate, // experimental_createEvaluator 默认实现 maxSteps: MAX_ELEMENTS, // 14 maxElements: MAX_ELEMENTS, // 14 maxDepth: 4, context: { platform: "Available: a synthetic user profile ..." }, instructions: { root, next, parent }, // 应用级构造指引 });各参数的默认值与语义(源码注释与校验逻辑):
| 参数 | 默认值 | 说明 |
|---|---|---|
strategy | "batch" | 新树默认走批处理 select/layout;"sequential"为一次一个操作 |
maxElements | 32 | 批处理创建的元素预算(含根) |
maxSteps | 32 | 评估次数预算(含顺序化的收尾决策) |
maxDepth | 8 | 根深度为 1;Playground 收紧为 4 |
initialSpec | 无 | 编辑既有树;克隆并校验后才参与评估 |
elementDescriptions | 无 | 显式共享给评估器的既有元素描述,按元素 ID 键控 |
initialState | 无 | 写入 spec、绝不发给评估器 |
context | 无 | 显式共享给评估器的应用上下文 |
instructions | 无 | root/next/parent三档应用级指引 |
三个数值参数都会先经过positiveInteger校验(必须是 ≥1 的安全整数),strategy只能是batch或sequential,候选 ID 必须匹配/^[a-zA-Z][\w-]*$/且不得为finish/unavailable。
值得展开的两个设计点:
- 表达式子集:组合器 V1 刻意不提供 repeat 作用域、计算函数或自定义指令。
checkExpressions只允许$state、$bindState、$and、$or四种以$开头的键,且$state/$bindState的值必须是状态路径字符串。因此既有的可编辑 spec 必须使用受支持的表达式子集并构成合法树。 - 隐私边界:
elementDescriptions只共享定位既有元素所需的展示文案(title/text/label/name/direction等字符串 props 与匹配候选的描述),绝不共享原始状态、字段输入值、绑定配方、动作参数或渲染器状态。
组合器的事件流是一个异步生成器:每个step事件携带一棵分离拷贝(detached clone)的 spec 快照与单步决策(choice、parent、slot、confidence、elapsedMs、inputTokens),complete事件携带最终 spec、全部步骤、总耗时、总输入 token 与stopReason(finish | limit | unavailable)。composeUI在 complete 事件上额外估算美元成本:inputTokens * 0.042 / 1e6(按每百万 token $0.042 估算),token 为 null 时成本也为 null。
限额一览
Jev 组合器把每次请求的边界写死为(README 原文):
- 每个新批处理最多14 个元素;
- 每个请求最多14 次评估调用;
- 嵌套深度最多4 层;
- 单次 provider 请求最多10 秒(
experimental_createEvaluator的timeoutMs默认值 10000); - 整体上限55 秒(response.ts 中用
AbortSignal.timeout(55000)实现,与 route 的maxDuration = 60呼应); - 所选 seed(初始 spec)最多含100 个元素(
previousSpecSchema中Object.keys(elements).length <= 100)。
达到限额、被取消或出错时,保留当前预览并标记为 partial(部分)。共享端点使用 web app 的请求频率限制器(minuteRateLimit/dailyRateLimit,见 api/generate/route.ts)。两个模型都编辑所选版本;Clear 重新开始。Stream 标签页会把构造决策与 spec 补丁并列展示。provider 调用与 spec 组装过程从不执行所选 UI 动作——动作只在真实用户交互时由运行时执行。
已知局限(诚实边界)
组合器校验树结构与候选值,但不保证 Jev 选择了正确的 UI。根选择、分组、何时停止都需要规划能力,而这是 Jev 有文档记载的弱点。置信度(confidence)会被展示,但没有质量门:多个布局选择可能都合理,且并未校准出一个通用阈值。
因此文档建议显式命名必需区块,例如:先要"顶部的订单表",再要"一行 revenue/orders/customer 指标",最后要"周营收图表"。过于简短的请求(如"带顶部表格的 dashboard")可能只选中一张表。后续追问可以移动既有表格而无需重建其数据。
推荐的追问示例:
- 用户卡片:
Design a user profile card→Remove the bio或Make the avatar smaller; - 设置页:
Remove the email notifications switch、Change the heading to "Account settings"、Move the email field above the name field。
服务端共享既有元素的展示标签与匹配候选的描述来识别编辑目标,同样不共享原始状态与已输入的字段值。编辑保留所选 spec 的状态(与 default 模型流程一致);交互式预览状态不会被保存进版本历史。
评估器传输层:Gateway v4 实验性 evaluation 协议
服务端使用 Gateway 的实验性 v4 evaluation 传输,模型标识为typesafe-ai/jev(experimental-evaluator.ts)。该实现已在@ai-sdk/gateway@4.0.85上验证通过。它用原生 fetch直连 Gateway 的 evaluation 端点,从而避免升级工作区内的 AI SDK 6 依赖、也不必绕过其最小发布年龄限制。协议可能变化;文档建议在适当时机迁移到符合资格的 AI SDK evaluation API 并用纯模型字符串(plain model string)调用。
实现要点:experimental_createEvaluator校验密钥与timeoutMs(1 ~ 2147483647 的整数)后返回一个满足Experimental_CompositionEvaluator签名的异步函数;请求体为{ state, questions },请求头携带Authorization: Bearer <apiKey>、ai-gateway-protocol-version: 0.0.1、ai-gateway-auth-method: api-key、ai-evaluation-model-specification-version: 4、ai-model-id: typesafe-ai/jev。响应经 Zod schema 严格解析:answers中每个问题必须是{ type: "choice", choice }且 choice 必须落在该问题的 criteria 键内,置信度来自providerMetadata.typesafe.confidence(0~1),usage.inputTokens为非负整数。任何越界选择都会抛错,从而保证评估器无法输出超出候选集的操作。组合器侧还有evaluateWithSignal兜底:即使自定义评估器忽略 abort 信号,也会用Promise.race强制中止。
评估器的自定义替换能力意味着应用可以不依赖 Gateway:只要实现(request: { state, questions, signal }) => Promise<{ answers, usage? }>即可接入任意后端或本地评估逻辑(测试即用 scripted 假评估器完成)。
文件地图与验证命令
实验相关文件(相对仓库根目录):
- apps/web/lib/jev/grammar.ts:Playground 自有的取值与原子候选;
- packages/core/src/experimental-compose.ts:公共、与 provider 无关的组合器;
- packages/core/src/experimental-composition-batch.ts:新树的并行成员资格与布局决策;
- packages/core/src/experimental-composition-tree.ts:内部 seed 校验与树编辑辅助(attach/detach/replaceElement/indexTree 等);
- packages/core/src/experimental-evaluator.ts:公共 Gateway 评估器适配器;
- apps/web/lib/jev/compose.ts:带 Playground 指令与成本展示的公共 API 消费方;
- apps/web/app/api/generate/route.ts:共享的频率受限端点,按所选模型分发(
model === "typesafe-ai/jev"时走createCompositionResponse,否则走 default 模型); - apps/web/lib/jev/response.ts:把组合快照适配为 Playground 的 JSONL spec 补丁与决策元数据(
__meta: "decision"/__meta: "composition"/__meta: "error"); - apps/web/components/playground.tsx:共享的模型切换、实验信息 tooltip、提示输入、版本历史、实时预览与检查器;
- apps/web/lib/jev/compose.test.ts:覆盖结构、动作边界、未知用量、取消与限额的测试。
运行验证(仓库根目录):
pnpm exec vitest run packages/core/src/experimental-compose.test.ts packages/core/src/experimental-evaluator.test.ts apps/web/lib/jev/compose.test.ts pnpm type-checkcompose.test.ts用scripted假评估器按脚本依次返回next/parent选择,既验证了批处理路径(root/select_*/order_*/parent_*问题的应答组装),也验证了顺序编辑路径(add/replace/remove/move 的决策消耗)。
对应用接入的启示
Jev 组合实验示范了一条与"自由生成 JSON spec"截然不同的路线:把生成问题转化为有限选择问题。应用若要把这类组合器接入自己的产品,核心要点可归纳为:
- 目录是硬边界:组件、props、事件、动作必须全部在 catalog 中声明,模型无法越界;
- 候选是内容载体:真实数据、本地化文案与允许操作都应预构造成原子候选(可绑定 live state,也可按请求动态生成),模型不发明文本与数据;
- 两阶段批处理省往返:select 并行决定成员资格,layout 并行决定挂载点与顺序,均按一次调用计费;
- 编辑协议是顺序的:add 之外,replace/move 采用"先选目标、再选方案"的两段式决策,remove 直接删除子树,未触达元素保持原样;
- 安全与隐私默认成立:provider 调用与组装过程不执行任何 UI 动作;状态不进评估器;共享给模型的只有描述性文案;
experimental_API 可在任何版本中变化:文档明确要求接入方锁定精确版本(pin exact versions)。
对希望从源码继续深入研究的读者,建议从 packages/core/src/experimental-compose.ts 的experimental_composeSpec入口读起,依次对照 experimental-composition-batch.ts 与 experimental-composition-tree.ts,最后用 apps/web/lib/jev/compose.test.ts 中的脚本化评估器反推每一步的状态机行为。
【免费下载链接】json-renderThe Generative UI framework项目地址: https://gitcode.com/GitHub_Trending/js/json-render
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考