AI 辅助 UI 生成与设计系统自动化实践先收紧输入、状态与退出边界1. 先看一眼生成结果颜色是最容易失控的地方把 Figma 节点直接交给模型生成 React 代码最先出现偏差的往往不是 DOM而是样式细节。同一个品牌蓝很容易被写成几个相近的十六进制值单看差别不大放进组件库后就会破坏 Token 的统一。模型适合协助判断节点语义不适合决定最终样式。生成代码在合并前应先经过 Token 校验禁止把临时色值和内联样式直接带入主干。# 抓取当前构建产物中未收敛的十六进制颜色数量 grep -E -o #[a-fA-F0-9]{6} src/components/generated/*.tsx | sort | uniq -c | sort -nr | head -n 10 # 校验 CSS 变量定义与 Token 映射覆盖率 npx stylelint src/**/*.css --custom-syntax stylelint-config-design-system第一版不必追求从画板直接产出可上线代码。更实际的边界是模型只参与节点语义和组件候选的判断Token、样式和值的写入交给可复现的规则。这样即使模型输出有变化结果也能被拦在管道里。flowchart TD A[Figma REST API 节点导出] -- B[AST 结构降噪与属性清理] B -- C{JSON Schema 校验器} C -- 校验失败 -- D[中断解析并抛出具体 Node ID] C -- 校验通过 -- E[LLM 语义识别与组件映射] E -- F[Token 映射闸门 strict-token-matcher] F -- G{未定义色值/尺寸?} G -- 是 -- H[强行降级为最近邻标准 Token] G -- 否 -- I[输出符合规范的 React/TypeScript 代码]2. 抓 Rest API 结构在 AST 语法树入口挂上第一道 JSON Schema 闸门Figma REST API 返回的文档树极其庞大一个稍微复杂的弹窗画板导出的 JSON 文件就能突破 3MB。如果直接把这个原始 JSON 丢给大模型不仅上下文 Token 浪费极度严重而且包含了大量绝对坐标、导出渲染缓存以及历史修改标记等噪声数据。我们必须在入口处使用 Node.js 编写预处理脚本深度剪枝后只保留absoluteBoundingBox、fills、strokes和children的核心节点。同时为防范 Figma 插件升级导致字段变更必须加上 JSON Schema 校验。import { z } from zod; // 定义 Figma 原生节点的严格过滤 Schema export const FigmaNodeSchema z.object({ id: z.string(), name: z.string(), type: z.enum([FRAME, TEXT, RECTANGLE, COMPONENT, INSTANCE]), absoluteBoundingBox: z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number(), }), fills: z.array(z.object({ type: z.string(), color: z.object({ r: z.number(), g: z.number(), b: z.number(), a: z.number(), }).optional(), })).optional(), children: z.array(z.lazy(() FigmaNodeSchema)).optional(), }); export type FigmaNode z.infertypeof FigmaNodeSchema; export function sanitizeFigmaTree(rawNode: any): FigmaNode { const result FigmaNodeSchema.safeParse(rawNode); if (!result.success) { console.error(Figma JSON 结构非法, 错误路径:, result.error.format()); throw new Error(Node ${rawNode?.id || unknown} 匹配 Schema 失败); } const node result.data; if (node.children) { node.children node.children.map(sanitizeFigmaTree); } return node; }这一步的目的不是替模型“洗白”数据而是把输入范围说清楚。进入 Prompt 的节点字段越少、结构越固定后续的映射和排错就越容易复现。3. Node.js 转换器实现把模糊语义压进确定性的 Token 词汇表大模型输出代码后绝对不能直接保存为.tsx文件。我们必须通过 Babel / TypeScript AST 解析器重新遍历代码提取出所有硬编码的 style 属性与 class 名称并在内存中与标准 Design Token 进行距离计算。对于颜色采用 CIEDE2000 色差算法计算数值接近度强行覆盖非标准色值对于 Padding 和 Margin强制按 4px 网格进行向下或向上对齐。import { parse } from babel/parser; import traverse from babel/traverse; import generate from babel/generator; import * as t from babel/types; const DESIGN_TOKENS { colors: { var(--color-primary-500): #3b82f6, var(--color-neutral-100): #f3f4f6, var(--color-neutral-900): #111827, }, spacing: [0, 4, 8, 12, 16, 24, 32, 48, 64], }; export function enforceDesignTokens(sourceCode: string): string { const ast parse(sourceCode, { sourceType: module, plugins: [jsx, typescript], }); traverse(ast, { JSXAttribute(path) { // 检查并替换 style 内联属性 if (path.node.name.name style t.isJSXExpressionContainer(path.node.value)) { const expression path.node.value.expression; if (t.isObjectExpression(expression)) { expression.properties.forEach((prop) { if (t.isObjectProperty(prop) t.isIdentifier(prop.key)) { // 处理 color 和 backgroundColor if ([color, backgroundColor].includes(prop.key.name) t.isStringLiteral(prop.value)) { const matchedToken findClosestColorToken(prop.value.value); prop.value t.stringLiteral(matchedToken); } } }); } } }, }); return generate(ast).code; } function findClosestColorToken(hexValue: string): string { // 此处简化为精准匹配与兜底降级 for (const [token, hex] of Object.entries(DESIGN_TOKENS.colors)) { if (hex.toLowerCase() hexValue.toLowerCase()) { return token; } } // 若模型幻觉产生色值默认回退至 primary-500 并记录告警 console.warn(检测到非标准色值 [${hexValue}]强制收敛至 var(--color-primary-500)); return var(--color-primary-500); }转换器把颜色和间距的决定权收回到 Token 表中。它不能保证生成结果完全正确但能让非标准值在进入仓库前暴露出来便于人工确认或回退。4. 落地剪枝策略第一版放弃复杂 flex 兜底只收敛基础组件许多团队在做 UI 自动生成的第一版时容易踩进“完美主义陷阱”——试图让 AI 一口气把最复杂的响应式表格、级联选择器和动态 Form 表单全都自动生成出来。结果是 Prompt 膨胀到上万字模型的 Edge Case 越来越多最终生成出的代码充满了庞大的 if-else 嵌套维护成本比从零手写还高。工程实践证明第一版的剪枝策略必须够狠。我们明确界定了系统的能力边界能力边界划分表 | 维度 | 第一版纳入支持范围 | 第一版严格禁止支持 | | :--- | :--- | :--- | | **组件类型** | Card, Button, Avatar, Badge, Simple List | Complex Table, Dynamic Form, Tree Select | | **布局模式** | 固定宽度与 Flex 基础排列 | 复杂 Grid 网格、跨行跨列响应式布局 | | **状态映射** | Hover, Active, Disabled 基础伪类 | 异步 Data Fetching、复杂的 Redux/Zustand 状态流 | | **代码交付** | 展示型 JSX 模版 CSS 变量 | 包含业务 validation 逻辑的交互代码 |范围缩到展示型基础组件后评估会简单很多检查生成的 DOM 骨架、Token 引用和视觉差异即可。业务状态、校验和数据请求仍由工程师接手避免把无法验证的逻辑混进生成结果。5. 线上发布闸门只要 Diff 覆盖率低于 92% 自动阻断 CI为了验证生成的 UI 组件与原始 Figma 设计稿是否保持一致我们在 Jenkins / GitHub Actions 流程里挂载了无头浏览器 Playwright 进行像素级 Visual Diff 检测。CI 阶段自动启动 Storybook 渲染生成的代码生成截图后再与 Figma Rest API 拿到的 Image Render 进行矩阵对比。# 执行像素对比诊断脚本 npx playwright test tests/visual-diff.spec.ts --reporterjson visual-diff-report.json # 检查 Diff 占比 node -e const fs require(fs); const report JSON.parse(fs.readFileSync(visual-diff-report.json)); const passRate report.stats.expected / report.stats.total; console.log(UI 像素比对通过率:, (passRate * 100).toFixed(2) %); if (passRate 0.92) { console.error(CRITICAL: 像素一致性低于 92%自动化构建已被阻断); process.exit(1); } 视觉比对是发布前的一个信号不应替代人工走查。把阈值、基准截图和豁免理由留在仓库里团队才能判断差异是预期改动还是生成误差。第一版先把输入、Token 和视觉回归这三件事做稳再逐步扩大支持范围。