AI 辅助 UI 生成:从设计意图到代码产出的工程化闭环

一、设计稿到代码的鸿沟:AI 生成 UI 的现实困境

设计稿交付到前端实现,始终存在一道难以弥合的缝隙。设计师交付的 Figma 文件中,间距是 8px 的倍数,颜色是精确的 HEX 值,圆角是 12px。但前端实现时,组件库的默认间距是 4px 倍数,颜色走 Design Token 映射,圆角需要适配不同主题。这道缝隙,传统方案靠人工对齐,耗时且易出错。

AI 辅助 UI 生成被寄予厚望——输入设计稿,输出可用代码。但真实场景中,AI 生成的代码往往"看起来对,用起来错":样式散落、Token 未对齐、响应式断裂、无障碍属性缺失。问题的核心不在于 AI 的生成能力,而在于输入给 AI 的上下文是否充分、生成结果是否被约束在设计系统的边界内。

本文将围绕 AI 辅助 UI 生成的工程化实践,从 Prompt 架构到设计系统约束,构建一个可复现的生成闭环。

二、AI 生成 UI 的上下文工程:Prompt 即架构

2.1 生成流程的完整链路

AI 生成 UI 不是一次性的"输入-输出",而是一个多阶段的上下文构建过程。

flowchart LR
    A[设计稿解析] --> B[结构化描述提取]
    B --> C[设计系统 Token 注入]
    C --> D[Prompt 组装与约束注入]
    D --> E[LLM 代码生成]
    E --> F[静态分析与规则校验]
    F --> G{通过校验?}
    G -->|是| H[代码输出]
    G -->|否| I[错误反馈回注]
    I --> D

每个阶段都有明确的输入输出规范。跳过任何一步,生成质量都会显著下降。

2.2 设计稿的结构化描述

直接将设计稿截图丢给 AI,生成结果不可控。正确做法是先将设计稿转化为结构化描述:

// 设计稿结构化描述的 TypeScript 接口定义
interface UIDescription {
  // 组件类型:button | card | modal | form | nav
  componentType: string;
  // 布局信息
  layout: {
    direction: 'row' | 'column';
    gap: number;         // 间距,单位 px
    padding: [number, number, number, number]; // 上右下左
    alignment: 'start' | 'center' | 'end' | 'stretch';
  };
  // 子元素列表
  children: UIDescription[];
  // 样式属性
  style: {
    width?: string;
    height?: string;
    borderRadius?: string;
    backgroundColor?: string;  // 引用 Token 名而非 HEX 值
    boxShadow?: string;
    typography?: {
      fontSize: string;
      fontWeight: number;
      lineHeight: string;
      color: string;           // 引用 Token 名
    };
  };
  // 交互状态
  interactions?: {
    hover?: Partial<UIDescription['style']>;
    focus?: Partial<UIDescription['style']>;
    disabled?: Partial<UIDescription['style']>;
  };
  // 无障碍属性
  accessibility: {
    role?: string;
    label: string;
    ariaDescribedBy?: string;
  };
}

2.3 Token 约束注入

将设计系统的 Token 以约束列表的形式注入 Prompt,确保 AI 不会凭空创造不存在的变量:

// 从 Design Token 文件提取约束列表
function buildTokenConstraints(tokens: DesignTokens): string {
  const constraints: string[] = [];

  // 颜色 Token
  Object.entries(tokens.color).forEach(([name, value]) => {
    constraints.push(`颜色 ${name}: var(--color-${name}) [${value}]`);
  });

  // 间距 Token
  Object.entries(tokens.spacing).forEach(([name, value]) => {
    constraints.push(`间距 ${name}: var(--spacing-${name}) [${value}]`);
  });

  // 圆角 Token
  Object.entries(tokens.radius).forEach(([name, value]) => {
    constraints.push(`圆角 ${name}: var(--radius-${name}) [${value}]`);
  });

  return [
    '## 设计系统约束(必须使用以下 Token,禁止硬编码数值)',
    ...constraints,
    '## 规则',
    '- 所有颜色必须使用 var(--color-xxx) 格式',
    '- 所有间距必须使用 var(--spacing-xxx) 格式',
    '- 所有圆角必须使用 var(--radius-xxx) 格式',
    '- 禁止使用内联 style 属性',
    '- 必须包含完整的无障碍属性(aria-label、role)',
  ].join('\n');
}

2.4 Prompt 组装模板

// 将结构化描述 + Token 约束组装为完整 Prompt
function assemblePrompt(
  desc: UIDescription,
  tokenConstraints: string,
  framework: 'react' | 'vue' | 'html'
): string {
  return `
你是一个前端组件生成器。根据以下结构化描述生成 ${framework} 组件代码。

${tokenConstraints}

## 组件描述
${JSON.stringify(desc, null, 2)}

## 输出要求
1. 使用 TypeScript(如适用)
2. 样式使用 CSS Modules 或 Tailwind CSS
3. 包含完整的 Props 类型定义
4. 包含 hover/focus/disabled 状态样式
5. 包含无障碍属性
6. 代码中添加中文注释说明核心逻辑
`.trim();
}

三、生产级生成管线:从 Prompt 到可交付代码

3.1 生成结果的静态校验

AI 生成的代码必须通过静态校验,才能进入代码库。以下是核心校验规则:

// 校验 AI 生成代码是否符合设计系统约束
interface ValidationResult {
  passed: boolean;
  errors: ValidationError[];
}

interface ValidationError {
  rule: string;
  message: string;
  line?: number;
}

function validateGeneratedCode(code: string, tokens: DesignTokens): ValidationResult {
  const errors: ValidationError[] = [];

  // 规则1:禁止硬编码颜色值
  const hardcodedColorRegex = /#[0-9a-fA-F]{3,8}|rgb\(|rgba\(/g;
  let match;
  while ((match = hardcodedColorRegex.exec(code)) !== null) {
    errors.push({
      rule: 'no-hardcoded-color',
      message: `检测到硬编码颜色: ${match[0]},应使用 Design Token`,
      line: code.substring(0, match.index).split('\n').length,
    });
  }

  // 规则2:禁止硬编码间距值(允许 0)
  const hardcodedSpacingRegex = /(?:margin|padding|gap):\s*\d+px/g;
  while ((match = hardcodedSpacingRegex.exec(code)) !== null) {
    const value = parseInt(match[0]);
    if (value !== 0 && !Object.values(tokens.spacing).includes(`${value}px`)) {
      errors.push({
        rule: 'no-hardcoded-spacing',
        message: `检测到非 Token 间距: ${match[0]}`,
        line: code.substring(0, match.index).split('\n').length,
      });
    }
  }

  // 规则3:必须包含 aria-label 或 aria-labelledby
  if (!code.includes('aria-label') && !code.includes('aria-labelledby')) {
    errors.push({
      rule: 'missing-aria-label',
      message: '交互组件必须包含无障碍标签',
    });
  }

  // 规则4:必须包含 focus 样式
  if (!code.includes(':focus') && !code.includes('focus-visible')) {
    errors.push({
      rule: 'missing-focus-style',
      message: '交互组件必须包含焦点样式',
    });
  }

  return {
    passed: errors.length === 0,
    errors,
  };
}

3.2 错误反馈回注——让 AI 自我修正

校验不通过时,将错误信息回注到 Prompt 中,让 AI 重新生成:

async function generateWithRetry(
  desc: UIDescription,
  tokens: DesignTokens,
  maxRetries: number = 3
): Promise<string> {
  const tokenConstraints = buildTokenConstraints(tokens);
  let prompt = assemblePrompt(desc, tokenConstraints, 'react');

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const generatedCode = await callLLM(prompt);
    const validation = validateGeneratedCode(generatedCode, tokens);

    if (validation.passed) {
      return generatedCode;
    }

    // 将校验错误追加到 Prompt,引导 AI 修正
    const errorFeedback = validation.errors
      .map((e) => `[${e.rule}] ${e.message}`)
      .join('\n');

    prompt += `\n\n## 上次生成的代码存在以下问题,请修正:\n${errorFeedback}`;
  }

  throw new Error(`生成失败:${maxRetries} 次重试后仍未通过校验`);
}

3.3 设计稿解析——Figma API 集成

// 从 Figma 节点提取结构化描述
async function parseFigmaNode(nodeId: string): Promise<UIDescription> {
  const response = await fetch(
    `https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/nodes?ids=${nodeId}`,
    { headers: { 'X-Figma-Token': FIGMA_TOKEN } }
  );

  const data = await response.json();
  const node = data.nodes[nodeId].document;

  return figmaNodeToDescription(node);
}

// 递归转换 Figma 节点为结构化描述
function figmaNodeToDescription(node: FigmaNode): UIDescription {
  const desc: UIDescription = {
    componentType: inferComponentType(node),
    layout: {
      direction: node.layoutMode === 'HORIZONTAL' ? 'row' : 'column',
      gap: node.itemSpacing || 0,
      padding: [
        node.paddingTop || 0,
        node.paddingRight || 0,
        node.paddingBottom || 0,
        node.paddingLeft || 0,
      ],
      alignment: mapAlign(node.primaryAxisAlignItems),
    },
    children: (node.children || []).map(figmaNodeToDescription),
    style: extractStyle(node),
    accessibility: {
      label: node.name || '未命名组件',
    },
  };

  return desc;
}

四、AI 生成 UI 的边界:不可自动化的设计决策

4.1 语义鸿沟——AI 无法理解设计意图

AI 能识别"这是一个按钮",但无法理解"这个按钮的圆角比标准大 4px,是因为品牌调性要求更柔和的视觉语言"。这种语义层面的设计决策,必须由人类设计师显式标注,无法通过视觉解析自动推断。

4.2 上下文窗口的物理限制

一个包含 50 个组件的页面,其结构化描述可能超过 10000 Token。加上设计系统约束和校验规则,总 Prompt 长度可能逼近模型的上下文上限。解决方案是分组件生成、逐个校验,而非一次性生成整个页面。

4.3 生成一致性的不可控性

同一份 Prompt 多次调用 LLM,生成的代码结构可能不同——类名命名风格、组件拆分粒度、状态管理方式都存在随机性。在生产环境中,必须通过严格的校验规则和代码模板约束,将随机性控制在可接受范围内。

4.4 无障碍的隐性缺失

AI 生成的代码往往"看起来可用",但缺少键盘导航、屏幕阅读器支持、焦点管理等隐性无障碍特性。这些无法通过视觉校验发现,必须依赖 axe-core 等自动化无障碍测试工具进行运行时检测。

五、总结

AI 辅助 UI 生成的核心价值,不在于"替代前端开发",而在于"将设计稿到代码的转化过程工程化"。结构化描述是输入的精度保障,Token 约束是输出的边界约束,静态校验是质量的最后防线。三者缺一,生成结果就不可控。

落地路线建议:

  1. 建立设计稿结构化描述规范,将 Figma 节点映射为统一的 JSON Schema。
  2. 将 Design Token 以约束列表形式注入 Prompt,禁止 AI 使用硬编码值。
  3. 实现生成代码的自动化校验管线,覆盖颜色、间距、无障碍、焦点样式。
  4. 校验不通过时自动回注错误信息,最多重试 3 次。
  5. 语义层面的设计决策由人类标注,AI 只负责机械转化。
  6. 集成 axe-core 进行运行时无障碍检测,补充静态校验的盲区。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐