
生成式 UI 的约束系统设计基于 Schema 约束模型输出与界面一致性保障一、生成式 UI 的一致性问题生成式 UI 的核心流程是让大语言模型根据自然语言描述生成界面组件树。模型的生成结果具有内在随机性同一个 prompt 在两次调用中可能产生结构迥异的输出。这种不确定性在原型探索阶段可以接受但在生产环境中会导致以下问题布局漂移同一个功能区域一次生成为横向 Flex 布局另一次变为纵向 Grid 布局。组件不一致功能相同的按钮一次使用原生button另一次使用自定义Button组件。样式散落颜色值、间距值、字体大小在多次生成中不统一。可访问性缺失aria 标签、键盘导航等规范被忽略。约束系统的目标不是消除模型的创造性而是在结构层建立「不可逾越的边界」确保输出在框架内保持一致性。二、Schema 约束的三层设计原则约束系统从粗到细分三个层级每一层对应控制力的递增和灵活度的递减。第一层布局骨架约束。定义页面的宏观结构——有哪些区域、区域的排列方向和比例、最大嵌套深度。这一层不限制组件类型只规定「在哪里放什么」。第二层组件类型白名单。声明允许使用的组件及合法属性。模型只能从白名单中选择组件超出范围的生成结果被拒绝。第三层样式令牌约束。将所有样式值抽象为设计令牌Design Token模型不直接写颜色值或间距值而是引用令牌名称。渲染引擎负责令牌到 CSS 的映射。// ---------- Schema 约束定义 ---------- /** 设计令牌所有样式值的唯一来源 */ interface DesignTokens { colors: { primary: string; success: string; danger: string; background: string; textPrimary: string; textSecondary: string; border: string; }; spacing: { xs: number; // 4px sm: number; // 8px md: number; // 16px lg: number; // 24px xl: number; // 32px }; typography: { heading1: { size: number; weight: number; lineHeight: number }; heading2: { size: number; weight: number; lineHeight: number }; body: { size: number; weight: number; lineHeight: number }; caption: { size: number; weight: number; lineHeight: number }; }; } /** 组件类型白名单每种组件定义了允许的属性 */ type ComponentSchema | { type: Container; props: { layout: flex-row | flex-column | grid; gap: keyof DesignTokens[spacing]; padding?: keyof DesignTokens[spacing]; children: UINode[]; }; } | { type: Button; props: { label: string; variant: primary | secondary | text | danger; size: sm | md | lg; disabled?: boolean; onClick?: string; // 事件处理函数名称运行时映射 }; } | { type: Input; props: { placeholder?: string; label?: string; type: text | number | email | password; required?: boolean; errorMessage?: string; }; } | { type: Table; props: { columns: Array{ key: string; title: string; width?: number; align?: left | center | right; }; dataSource: string; // 数据源变量名 pagination?: { pageSize: number }; }; } | { type: Card; props: { title?: string; children: UINode[]; bordered?: boolean; }; } | { type: Text; props: { content: string; variant: keyof DesignTokens[typography]; color?: keyof DesignTokens[colors]; align?: left | center | right; }; }; interface UINode { id: string; // 全局唯一标识 component: ComponentSchema; } /** 页面骨架定义了布局区域和嵌套规则 */ interface PageSchema { version: 1.0; pageId: string; maxNestingDepth: number; // 最大组件嵌套深度 regions: Array{ id: string; name: string; direction: horizontal | vertical; ratio: number; // 占页面比例 0~1 allowedComponents: ArrayComponentSchema[type]; }; }三、运行时校验与自动修复流程Schema 定义完成后需要运行时校验器在 LLM 输出后立即执行验证。校验分为两个阶段阶段一结构校验。检查 JSON 结构是否符合 Schema 定义包括组件类型是否在白名单中、属性值是否合法、嵌套深度是否超限。阶段二语义校验。检查 UI 树的逻辑合理性如必填 Input 是否有关联的 label、Table 列定义是否与 dataSource 字段匹配。/** 结构校验器 */ class UISchemaValidator { private tokens: DesignTokens; private componentWhitelist: Setstring; constructor(tokens: DesignTokens) { this.tokens tokens; this.componentWhitelist new Set([ Container, Button, Input, Table, Card, Text ]); } validate(node: UINode, parentPath: string ): ValidationResult { const path ${parentPath}/${node.id}; const errors: ValidationError[] []; const comp node.component; // 规则1: 组件类型白名单检查 if (!this.componentWhitelist.has(comp.type)) { errors.push({ path, rule: component-whitelist, message: 组件类型 ${comp.type} 不在白名单中, severity: error, }); return { valid: false, errors }; } // 规则2: 按类型分发校验 switch (comp.type) { case Container: errors.push(...this.validateContainer(comp.props, path)); break; case Button: errors.push(...this.validateButton(comp.props, path)); break; case Input: errors.push(...this.validateInput(comp.props, path)); break; case Table: errors.push(...this.validateTable(comp.props, path)); break; case Text: errors.push(...this.validateText(comp.props, path)); break; } return { valid: errors.length 0, errors }; } private validateContainer( props: any, path: string ): ValidationError[] { const errors: ValidationError[] []; const validLayouts [flex-row, flex-column, grid]; if (!validLayouts.includes(props.layout)) { errors.push({ path, rule: container-layout, message: 无效布局 ${props.layout}允许值: ${validLayouts.join(, )}, severity: error, }); } // 间距引用令牌检查 if (props.gap !(props.gap in this.tokens.spacing)) { errors.push({ path, rule: token-reference, message: 间距令牌 ${props.gap} 未定义, severity: warning, }); } return errors; } private validateButton(props: any, path: string): ValidationError[] { const errors: ValidationError[] []; if (!props.label || typeof props.label ! string) { errors.push({ path, rule: button-label-required, message: Button 必须包含非空 label 属性, severity: error, }); } const validVariants [primary, secondary, text, danger]; if (!validVariants.includes(props.variant)) { errors.push({ path, rule: button-variant, message: 无效 variant ${props.variant}, severity: warning, }); } return errors; } private validateInput(props: any, path: string): ValidationError[] { const errors: ValidationError[] []; if (props.required !props.label) { // 必填输入框必须有标签可访问性要求 errors.push({ path, rule: a11y-input-label, message: 必填 Input 缺少 label 属性不满足可访问性要求, severity: warning, }); } return errors; } private validateTable(props: any, path: string): ValidationError[] { const errors: ValidationError[] []; if (!Array.isArray(props.columns) || props.columns.length 0) { errors.push({ path, rule: table-columns-required, message: Table 必须包含至少一列定义, severity: error, }); } return errors; } private validateText(props: any, path: string): ValidationError[] { const errors: ValidationError[] []; if (props.color !(props.color in this.tokens.colors)) { errors.push({ path, rule: token-reference, message: 颜色令牌 ${props.color} 未定义, severity: warning, }); } return errors; } }四、兜底策略与工程落地即使有严格的 Schema 约束模型仍有概率生成不符合规范的结果。兜底策略负责在验证失败时确保界面可用。自动修复策略对于可判定但不影响功能的问题如使用了非法颜色令牌、variant 错误校验器应自动修正。对于结构性错误如组件类型不在白名单、必填属性缺失应触发重试。/** 增强的生成流水线约束注入 校验 自动修复 兜底 */ async function generateUIWithConstraints( userPrompt: string, schema: PageSchema, tokens: DesignTokens, maxRetries: number 3 ): PromiseUINode { const validator new UISchemaValidator(tokens); for (let attempt 1; attempt maxRetries; attempt) { try { // 1. 将 Schema 注入 System Prompt const systemPrompt buildConstraintPrompt(schema, tokens); const rawOutput await callLLM(userPrompt, systemPrompt); // 2. 解析 JSON 输出 const uiNode JSON.parse(rawOutput) as UINode; // 3. 结构校验 const result validator.validate(uiNode); if (result.valid) { return uiNode; } // 4. 区分可自动修复和需要重试的错误 const fatalErrors result.errors.filter( (e) e.severity error ); const warnings result.errors.filter( (e) e.severity warning ); if (fatalErrors.length 0) { // 仅有 warning自动修复后返回 console.warn( [Constraint] 第 ${attempt} 次生成有 ${warnings.length} 条警告已自动修复 ); return uiNode; } // 将错误信息注入下一次生成的 prompt userPrompt ${userPrompt}\n\n上次生成的错误:\n${ fatalErrors.map((e) - [${e.rule}] ${e.message}).join(\n) }\n请修正以上错误后重新生成。; } catch (err) { if (attempt maxRetries) { throw err; } } } // 5. 重试耗尽返回兜底模板 console.error( [Constraint] ${maxRetries} 次重试后仍未通过校验使用兜底模板 ); return getFallbackTemplate(schema); } function buildConstraintPrompt( schema: PageSchema, tokens: DesignTokens ): string { return 你是一个 UI 生成器。严格按以下约束输出 JSON 【可用组件】Container(布局), Button, Input, Table, Card, Text 【样式令牌】颜色: ${JSON.stringify(tokens.colors)} 间距: ${JSON.stringify(tokens.spacing)} 【布局规则】最大嵌套深度: ${schema.maxNestingDepth} 【输出格式】纯 JSON不包含 markdown 标记和解释文字; } /** 兜底模板保证页面至少能渲染 */ function getFallbackTemplate(schema: PageSchema): UINode { return { id: fallback-root, component: { type: Container, props: { layout: flex-column, gap: md, padding: lg, children: [{ id: fallback-message, component: { type: Text, props: { content: 页面生成失败请联系管理员, variant: body, color: textSecondary, align: center, }, }, }], }, }, }; }五、总结Schema 约束系统的本质是将设计规范从「建议」升级为「规则」。三层约束布局骨架 → 组件白名单 → 样式令牌形成了一个递进的控制体系每一层都减少了模型可自由发挥的空间从而提升了输出的一致性。工程落地的三个关键点约束定义需要与团队的已有设计系统对齐否则会出现「规范的双重标准」校验器应区分 error 和 warning避免过严导致重试风暴兜底模板是最后的安全网没有兜底的约束系统在生产中不可上线。