
基于模板的 AI 组件生成约束 Prompt 输出的一致性与可控性一、AI 生成的同一个 Button三次输出三个样子把 Figma 里的一个确认按钮截图扔给当前流行的 AI 生成工具。第一次输出button classNamebg-blue-500 text-white px-6 py-2 rounded-md hover:bg-blue-600 确认 /button第二次同样的截图button classNamebg-primary text-white px-24 py-8 rounded text-sm font-medium transition-colors duration-200 hover:opacity-90 确认 /button第三次button classNamebtn btn--primary btn--medium onClick{handleClick} 确认 /button同一个人、同一张图、同一个工具——三次生成三种 API 风格Tailwind Utility 组合、Tailwind 混搭 Token、BEM 命名。问题不是机器没看懂图而是机器没有代码规范约束。它每次都在语言模型的概率空间中独立采样生成统计学上最可能的 JSX而设计规范不是概率高的事件。这就是当前 AI 代码生成最头疼的一致性问题。LLM 能生成看起来正确的 UI 代码但它不知道你的项目里所有颜色必须走 CSS 变量、所有间距必须用 Token、所有 Button 必须从lib/ui的Button组件导出。这种局部看着都对全局放进项目就炸的输出就是模板约束要解决的问题。二、模板约束的三层架构模板约束不是简单地给 Prompt 加一句请遵守我们的样式规范。它需要三个层面的协同工作组件签名约束API Schema告诉 AI 这个组件有哪些 Props类型是什么哪些必传。这是最低层次的约束——防止 AI 自己捏造不存在的属性名。样式约束Token Map告诉 AI所有颜色使用var(--color-xxx)值所有间距使用var(--space-n)值。防止 AI 输出padding: 16px这种硬编码。模板约束Template Skeleton告诉 AI 这个组件的 HTML 结构是固定的框架你只能在指定的slot区域内填充内容和样式变体。这是最高层次的约束——保证生成的所有同类型组件共享相同的 DOM 骨架。flowchart TD A[用户输入br/(Figma截图/需求描述)] -- B[Prompt 组装器] C[组件签名约束br/(TS Interface AST)] -- B D[样式 Token 约束br/(Token Map JSON)] -- B E[模板骨架约束br/(Handlebars 模板)] -- B B -- F[LLM 推理] F -- G[生成的 JSX 代码] G -- H[后处理校验] H -- I{Props 签名br/是否匹配?} I --|否| J[降级为最接近的合法 Props] I --|是| K{样式值是否br/来自 Token?} K --|否| L[替换为 Token 引用] K --|是| M[输出合规代码] J -- K L -- M三、模板约束引擎的实现/** * AI 组件生成模板约束引擎 * * 三层约束策略 * 1. ComponentSchema - Props 类型约束防止 AI 捏造属性 * 2. TokenRegistry - 样式 Token 约束防止硬编码魔法数字 * 3. TemplateSkeleton - 骨架模板约束保证 DOM 结构一致性 */ // 第一层组件签名约束 interface PropConstraint { name: string; type: string | number | boolean | enum | ReactNode; required: boolean; enumValues?: string[]; // 枚举类型的合法值 defaultValue?: unknown; // 默认值 } interface ComponentSchema { name: string; // 组件名 Button props: PropConstraint[]; // Props 定义 importPath: string; // 导入路径 lib/ui/Button } // Button 组件的签名约束定义 const ButtonSchema: ComponentSchema { name: Button, importPath: lib/ui/Button, props: [ { name: variant, type: enum, required: false, enumValues: [primary, secondary, outline, ghost], defaultValue: primary }, { name: size, type: enum, required: false, enumValues: [sm, md, lg], defaultValue: md }, { name: disabled, type: boolean, required: false, defaultValue: false }, { name: loading, type: boolean, required: false, defaultValue: false }, { name: children, type: ReactNode, required: true }, { name: onClick, type: ReactNode, required: false }, // 事件处理函数 ], }; // 第二层Token 注册表 // 规定所有合法 TokenAI 输出中出现的任何颜色/间距都应该在这张表中 const TokenRegistry { color: [ --color-primary, --color-primary-hover, --color-text-primary, --color-text-secondary, --color-bg-primary, --color-bg-secondary, --color-border-default, --color-danger, --color-success, ], spacing: [ --space-1, --space-2, --space-3, --space-4, --space-5, --space-6, --space-8, --space-10, --space-12, ], radius: [ --radius-sm, // 4px --radius-md, // 8px --radius-lg, // 12px --radius-full, // 9999px ], }; // 第三层骨架模板 // 模板中的 slot 是 AI 可以自由发挥的填充区 // 模板外的结构是固定锁死的 const ButtonTemplate Button variantslot namevariant constraintenum valuesprimary|secondary|outline|ghost defaultprimary / sizeslot namesize constraintenum valuessm|md|lg defaultmd / disabled{slot namedisabled constraintboolean defaultfalse /} loading{slot nameloading constraintboolean defaultfalse /} onClick{slot nameonClick constraintfunction /} slot namechildren constraintReactNode / /Button ; /** * Prompt 组装器将三层约束注入 prompt * 这不是简单的字符串拼接—— * 每一层约束都以结构化的方式嵌入 prompt让 LLM 理解约束边界 */ function buildConstrainedPrompt( userRequest: string, schema: ComponentSchema, template: string ): string { return 你是一个严格遵循设计规范的 React 组件生成器。你必须遵守以下三层约束。 ## 第一层组件 API 约束 你只能使用组件: ${schema.name} 导入路径固定为: import { ${schema.name} } from ${schema.importPath}; 该组件支持的 Props: ${schema.props.map(p { const required p.required ? (必传) : (可选); const typeInfo p.type enum p.enumValues ? ${p.type}${p.enumValues.join( | )} : p.type; return - ${p.name}: ${typeInfo} ${required}; }).join(\n)} ## 第二层样式 Token 约束 生成的代码中不得出现任何硬编码的颜色值如 #3B82F6、blue。 所有颜色必须使用 CSS 变量 var(--color-xxx)。 合法颜色 Token: ${TokenRegistry.color.map(c \var(${c})\).join(, )} 合法间距 Token: ${TokenRegistry.spacing.map(s \var(${s})\).join(, )} ## 第三层骨架模板约束 你生成的组件结构必须基于以下模板。你的任务是在 slot 区域填充正确的值 \\\jsx ${template} \\\ ## 用户需求 ${userRequest} ## 输出格式 只输出 JSX 代码块不要包含任何解释文字。 .trim(); } /** * 后处理校验器检查 AI 输出是否违反约束 * 如果检测到违规自动修复到最接近的合法值 */ function validateAndFixOutput(generatedCode: string, schema: ComponentSchema): string { let fixed generatedCode; // 检查1替换硬编码颜色 // 匹配常见的硬编码颜色模式 const hexColorPattern /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/g; if (hexColorPattern.test(fixed)) { hexColorPattern.lastIndex 0; // 将硬编码颜色映射到最接近的 Token fixed fixed.replace(hexColorPattern, (match) { // 简化映射3B82F6 → --color-primary, EF4444 → --color-danger const nearestToken mapHexToToken(match); return var(${nearestToken}); }); } // 检查2替换硬编码间距 const pxPattern /(padding|margin|gap)\s*:\s*(\d)px/g; fixed fixed.replace(pxPattern, (match, prop, value) { const nearestToken mapPxToSpacingToken(parseInt(value)); return ${prop}: var(${nearestToken}); }); // 重置正则状态 pxPattern.lastIndex 0; // 检查3验证 Props 名是否在 Schema 中如果在但类型不对修正类型 for (const prop of schema.props) { if (prop.type enum prop.enumValues) { const enumPattern new RegExp(${prop.name}[]([^])[], g); fixed fixed.replace(enumPattern, (match, value) { if (!prop.enumValues!.includes(value)) { // 使用枚举的第一个值作为默认降级 const fallback prop.defaultValue as string || prop.enumValues![0]; return ${prop.name}${fallback}; } return match; }); } } return fixed; } /** * 简单的颜色映射表常见 HEX → 最接近的 Design Token * 生产环境中应使用 OKLCH 空间的距离计算实现精确映射 */ function mapHexToToken(hex: string): string { const map: Recordstring, string { #3B82F6: --color-primary, #2563EB: --color-primary-hover, #EF4444: --color-danger, #22C55E: --color-success, #0F172A: --color-text-primary, #475569: --color-text-secondary, #F8FAFC: --color-bg-primary, #F1F5F9: --color-bg-secondary, #E2E8F0: --color-border-default, }; const normalized hex.toLowerCase(); if (map[normalized]) return map[normalized]; // 如果是未知 HEX映射到语义上最中性的 Token return --color-primary; } function mapPxToSpacingToken(px: number): string { // 4px 的整数倍映射表 const spacingMap: Recordnumber, string { 4: --space-1, 8: --space-2, 12: --space-3, 16: --space-4, 20: --space-5, 24: --space-6, 32: --space-8, 40: --space-10, 48: --space-12, }; if (spacingMap[px]) return spacingMap[px]; // 找不到精确匹配时取最接近的 ≥4px 的倍数 const nearest Math.max(4, Math.round(px / 4) * 4); return spacingMap[nearest] || --space-4; } export { buildConstrainedPrompt, validateAndFixOutput, ComponentSchema, ButtonSchema, ButtonTemplate };四、模板约束的三个失效场景场景一AI 需要一个设计中不存在的变体。用户要求生成一个绿色 Button 用于成功提示。你的设计系统里没有variantsuccess而函数mapHexToToken将绿色#22C55E映射到了--color-success——但 Button 的variant不接受success值。最终后处理校验尝试验证输出发现variantsuccess不在枚举中降级为variantprimary并把绿色丢失了。这就是约束太死的反面——正确的修复是扩展 Schema 使variant枚举包含success而不是在降级时丢弃用户的意图信息。场景二Token 注册表过小。你的注册表只包含了 9 种颜色 Token。但前端设计系统的 Token 文件实际定义了 64 种包括danger-50、danger-100、danger-200这种数字色阶。AI 生成的代码中发现一个需要danger-50的背景但在注册表中找不到它——后处理逻辑不知道该映射到什么颜色只能给一个 Default。解决方案注册表应该直接从 Token JSON 文件自动生成而不是手动维护。场景三模板骨架限制了交互行为的表达空间。你把组件的 DOM 结构锁在模板里但不同的variantmodal需要一个额外的关闭按钮在模板里没有 slot。这是模板约束设计的核心权衡结构锁死得越多生成的多样性越低但一致性越高。找到平衡点的实用策略是——核心结构锁死 扩展区域开放。把按钮的 HTML 结构buttontag、className 前缀、事件绑定锁死在模板里但允许 AI 在childrenslot 中自由排列图标和文字的布局。五、总结AI 组件生成的一致性取决于三层约束API Schema → Token Registry → Template Skeleton。组件签名约束PropConstraint防止 AI 捏造不存在的 Props 和类型。Token 注册表约束所有颜色/间距必须使用 CSS 变量后处理自动替换硬编码值。骨架模板锁死 DOM 结构AI 只能在slot区域自由填充保证同类组件结构一致。HEX → Token 的颜色映射可以用 OKLCH 空间的距离计算实现精确匹配简易版可用查表法。后处理校验是一个安全网——应该先靠 Prompt 约束后处理只处理 Prompt 没拦截住的少量违规。Token 注册表应直接从设计系统的 Token JSON 自动生成手动维护会快速落后于真实 Token。核心结构锁死 扩展区域开放是模板约束的平衡点——不完全锁死也不完全放任。当约束和用户意图冲突时如要求不存在的 variant应该保留意图信息并通过 Schema 扩展来满足。这套方案的适用边界设计系统完整且有 Token 定义的团队不适用的场景是早期自由探索阶段的项目。