当命名不再是随意涂鸦:AI 辅助 Design Token 命名的语义化与一致性自动检查

一、深度引言与场景痛点

Design Token 的命名像给花园里的每棵植物贴标签——标签准确,所有人都能快速找到需要的植物;标签混乱,整个花园变成迷宫。现实中,Token 命名混乱的根源不是设计师不认真,而是命名本身就是一件需要语义推理的脑力活:--color-blue-500 是原始色值命名,--color-primary-action 是语义命名,同一个 Token 到底用哪种命名?

语义化命名的核心要求是"名字传达用途而非外观"——--color-primary-action 告诉你这是主要操作按钮的颜色,而 --color-blue-500 只告诉你这是某个蓝色。当设计系统从 20 个 Token 增长到 200 个,语义命名的一致性变得至关重要:一个 Token 叫 --spacing-card-gap,另一个叫 --gap-card,第三个叫 --card-spacing-gap——三种命名传达同一个意思,但开发者要找三次才能确定。

AI 辅助命名检查解决的核心问题:自动扫描 Token 命名的一致性和语义合规性,发现命名风格不统一、语义层级缺失、命名重复的三类问题,并给出标准化建议。

二、底层机制与原理深度剖析

Token 命名检查的三个维度:格式一致性、语义层级完整性、语义重复检测。每个维度用不同的检查策略,逐层收拢命名混乱。

flowchart TD
    A[Token 命名清单] --> B[格式一致性检查]
    B --> B1[命名风格检测:kebab-case vs camelCase vs snake_case]
    B --> B2[层级分隔符检测:/ vs - vs _ vs .]
    B --> B3[前缀规范检测:是否统一使用 -- 前缀]

    B1 & B2 & B3 --> C[语义层级完整性检查]
    C --> C1[三层命名覆盖:primitive / semantic / component]
    C --> C2[层级跳过检测:缺少中间层级的引用]
    C --> C3[语义粒度检测:过于笼统 vs 过于具体]

    C1 & C2 & C3 --> D[语义重复检测]
    D --> D1[同义命名聚类:名字不同但语义相同]
    D --> D2[别名链检测:同一 Token 有多个入口]
    D --> D3[AI 语义相似度判定:近义词命名归并]

    D1 & D2 & D3 --> E[标准化建议]
    E --> E1[命名风格统一方案]
    E --> E2[缺失层级补全方案]
    E --> E3[重复命名归并方案]

格式一致性检查是最基础的层面。Token 命名必须统一使用一种风格——CSS Variable 用 kebab-case--color-primary-action),Figma Variable 用 / 分隔层级(Colors/Primary/Action),JSON Token 用 . 分隔(color.primary.action)。如果项目中三种格式混杂,转换管道就无法可靠运行。检查器扫描所有 Token 命名,统计格式分布,当格式不一致率超过 10% 时报警。

语义层级完整性检查是更深层的结构问题。命名应该覆盖三层语义:原始层(--color-blue-500)、语义层(--color-primary-action)、组件层(--button-primary-background)。如果某个组件直接引用原始层 Token(background: var(--color-blue-500)),跳过了语义层,设计意图就无法传递——当品牌色从蓝色改为红色时,需要逐一修改所有硬编码引用。

语义重复检测是最需要 AI 能力的层面。--spacing-card-gap--gap-card 是同义命名,但代码层面的字符串匹配无法发现这种重复——它们不是同一个字符串,却是同一个意思。AI 用语义理解判定两个命名是否表达相同含义,把同义命名聚类后建议归并为一个标准命名。

三、生产级代码实现与最佳实践

格式一致性检查器:

// scripts/token-naming-check/format-consistency.ts
interface TokenName {
  raw: string;              // 原始命名
  segments: string[];       // 分段后的命名层级
  separator: string;        // 分隔符类型
  casing: string;           // 命名风格
}

interface FormatViolation {
  token: string;
  expectedFormat: string;
  actualFormat: string;
  suggestion: string;
}

function checkFormatConsistency(tokens: string[]): FormatViolation[] {
  const violations: FormatViolation[] = [];

  // 统计项目中主要的命名风格
  const styleDistribution = new Map<string, number>();
  tokens.forEach(t => {
    const style = detectNamingStyle(t);
    styleDistribution.set(style, (styleDistribution.get(style) || 0) + 1);
  });

  // 确定主流风格(出现最多的)
  const dominantStyle = [...styleDistribution.entries()]
    .sort((a, b) => b[1] - a[1])[0][0];

  // 检查每个 Token 是否符合主流风格
  tokens.forEach(t => {
    const style = detectNamingStyle(t);
    if (style !== dominantStyle) {
      violations.push({
        token: t,
        expectedFormat: dominantStyle,
        actualFormat: style,
        suggestion: convertToStyle(t, dominantStyle)
      });
    }
  });

  return violations;
}

// 检测命名风格
function detectNamingStyle(name: string): string {
  // CSS Variable 以 -- 开头
  if (name.startsWith('--')) {
    // 检查是否使用 kebab-case(标准 CSS Variable 风格)
    if (/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name.slice(2))) return 'kebab-case';
    // 检查是否使用 camelCase(不规范但常见)
    if (/[a-z][A-Z]/.test(name.slice(2))) return 'camelCase';
    return 'mixed';
  }
  // JSON / JS 风格
  if (name.includes('.')) return 'dot-notation';
  if (name.includes('/')) return 'slash-notation';
  if (name.includes('_')) return 'snake_case';
  return 'unknown';
}

// 将命名转换为目标风格
function convertToStyle(name: string, targetStyle: string): string {
  // 提取语义片段
  const segments = name.replace(/^--/, '').split(/[-._/]/);
  const words = segments.map(s =>
    s.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(' ')
  ).flat();

  switch (targetStyle) {
    case 'kebab-case':
      return `--${words.join('-')}`;
    case 'camelCase':
      return `--${words[0]}${words.slice(1).map(w =>
        w.charAt(0).toUpperCase() + w.slice(1)
      ).join('')}`;
    case 'snake_case':
      return `--${words.join('_')}`;
    default:
      return name;
  }
}

语义层级完整性检查器:

// scripts/token-naming-check/semantic-hierarchy.ts
interface HierarchyViolation {
  token: string;
  level: 'primitive' | 'semantic' | 'component';
  missingLevel: string;     // 缺失的中间层级
  suggestion: string;       // 补全建议
}

function checkSemanticHierarchy(
  tokens: string[],
  componentReferences: Map<string, string[]>  // 组件 → 引用的 Token 列表
): HierarchyViolation[] {
  const violations: HierarchyViolation[] = [];

  // 分类每个 Token 的语义层级
  const tokenLevels = new Map<string, string>();
  tokens.forEach(t => {
    const segments = t.replace(/^--/, '').split('-');
    // 层级判定规则:
    // 包含具体色值名称(blue, red, 500)→ primitive
    // 包含语义用途(primary, action, background)→ semantic
    // 包含组件名(button, card, input)→ component
    if (segments.some(s => /^(blue|red|green|gray|500|400|300|200)$/.test(s))) {
      tokenLevels.set(t, 'primitive');
    } else if (segments.some(s => /^(button|card|input|modal|nav|header)$/.test(s))) {
      tokenLevels.set(t, 'component');
    } else {
      tokenLevels.set(t, 'semantic');
    }
  });

  // 检查组件是否直接引用 primitive Token(跳过语义层)
  for (const [component, refs] of componentReferences) {
    for (const ref of refs) {
      const level = tokenLevels.get(ref);
      if (level === 'primitive') {
        violations.push({
          token: ref,
          level: 'primitive',
          missingLevel: 'semantic',
          suggestion: `组件 "${component}" 直接引用原始 Token "${ref}",应创建语义 Token 如 --color-${component}-background 引用 ${ref}`
        });
      }
    }
  }

  // 检查语义层级是否缺少 component 层级的细化
  const semanticTokens = tokens.filter(t => tokenLevels.get(t) === 'semantic');
  for (const semToken of semanticTokens) {
    // 语义 Token 是否被至少一个组件引用?
    const isReferenced = [...componentReferences.values()]
      .some(refs => refs.includes(semToken));
    if (!isReferenced) {
      violations.push({
        token: semToken,
        level: 'semantic',
        missingLevel: 'component',
        suggestion: `语义 Token "${semToken}" 没有被任何组件引用,考虑创建组件级 Token 细化用途`
      });
    }
  }

  return violations;
}

AI 语义重复检测:

// scripts/token-naming-check/semantic-duplicate.ts
import { OpenAI } from 'openai';

interface DuplicateGroup {
  standardName: string;     // 建议的标准化命名
  duplicates: string[];     // 同义命名列表
  reason: string;           // 为什么判定为同义
}

async function checkSemanticDuplicates(
  tokens: string[]
): Promise<DuplicateGroup[]> {
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: `你是 Design Token 语义分析专家。分析以下 Token 命名列表,找出表达相同语义但命名不同的 Token 组。
同义判定规则:
1. 表达相同设计用途(如"卡片间距"和"卡片内部间距")
2. 使用不同词汇但指向同一概念(如 gap 和 spacing)
3. 命名层级不同但最终含义相同(如 color-primary 和 color-brand-primary)

为每个同义组建议一个标准化命名,遵循 kebab-case 和三层语义规则。`
      },
      {
        role: 'user',
        content: `Token 命名列表:\n${tokens.join('\n')}`
      }
    ],
    response_format: { type: 'json_object' },
    max_tokens: 4000
  });

  const result = JSON.parse(response.choices[0].message.content || '{}');
  return result.duplicateGroups || [];
}

综合检查流程编排:

// scripts/token-naming-check/run-check.ts
async function runTokenNamingCheck() {
  // 从 tokens.css 中提取所有 Token 命名
  const tokens = extractTokenNames('src/styles/tokens.css');
  // 从组件代码中提取 Token 引用关系
  const componentRefs = extractComponentReferences('src/components');

  // 三层检查并行执行
  const formatViolations = checkFormatConsistency(tokens);
  const hierarchyViolations = checkSemanticHierarchy(tokens, componentRefs);
  const duplicateGroups = await checkSemanticDuplicates(tokens);

  // 输出综合报告
  const report = {
    summary: {
      totalTokens: tokens.length,
      formatIssues: formatViolations.length,
      hierarchyIssues: hierarchyViolations.length,
      duplicateGroups: duplicateGroups.length
    },
    format: formatViolations,
    hierarchy: hierarchyViolations,
    duplicates: duplicateGroups,
    timestamp: new Date().toISOString()
  };

  fs.writeFileSync('token-naming-report.json', JSON.stringify(report, null, 2));

  // 有严重问题时阻断 CI
  if (hierarchyViolations.some(v => v.level === 'primitive')) {
    console.error('❌ Token 命名检查失败:存在原始层直接引用');
    process.exit(1);
  }
}

四、边界分析与架构权衡

语义层级的三层划分不够精确。 实际项目中,Token 的语义层级可能是四层甚至五层——原始层(--blue-500)、语义层(--primary-action)、场景层(--primary-action-dark)、组件层(--button-primary-bg)、状态层(--button-primary-bg-hover)。三层规则过于简化,无法覆盖所有场景。解决方案:允许项目自定义层级规则,检查器根据项目定义的层级配置验证完整性,而非强制三层。

AI 语义重复判定的边界。 --spacing-card-gap--gap-card 确实是同义命名,但 --color-primary--color-brand-primary 可能不是——前者是全局主色,后者是品牌主色,品牌更换时两者可能指向不同颜色。AI 的语义理解可能过度归并——把本应区分的 Token 合为一组。解决方案:AI 输出的同义组只作为"疑似重复"标记,不自动归并。开发者逐一确认后决定是否合并,避免过度归并。

命名检查的时机选择。 检查可以在 Token 创建时(实时拦截)、在 CI 中(定期扫描)、在 PR 合并前(变更审查)三个时机执行。实时拦截最理想但实现成本高——需要 IDE 插件或 Figma 插件集成。CI 定期扫描最实用但反馈周期长——开发者提交后几小时才知道命名不规范。PR 合并前审查是平衡点——在代码入库前拦截问题,反馈及时且不需要 IDE 集成。

Token 命名的文化差异。 不同团队有不同的命名文化——中文团队可能倾向中英混合命名(--color-zhuti),英文团队可能倾向纯英文缩写(--clr-pri)。AI 检查器的标准规则无法覆盖所有文化偏好。解决方案:团队定义自己的命名规范文件(包含命名风格、层级规则、词汇映射表),检查器基于团队规范而非通用规则验证。命名标准化不是全球统一,而是团队内部统一。

五、总结

Token 命名的语义化和一致性不是装饰性问题,而是设计系统可维护性的基础设施。命名传达用途而非外观,让设计意图在代码中无损传递;格式一致让管道可靠运行;层级完整让变更的影响范围可控。

AI 辅助命名的核心价值在语义重复检测——代码层面的字符串匹配无法发现"名字不同但意思相同"的命名混乱,AI 的语义理解能力恰好填补了这个缺口。但 AI 的判定不是终审,疑似重复需要开发者确认后再归并,避免语义过度归并。

命名检查的终极目标是让 Token 命名成为设计系统的导航图——任何新成员看到 --button-primary-background,不需要查文档就能理解这是按钮主色的背景值。命名就是文档,这就是语义化命名的真正价值。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐