当命名不再是随意涂鸦:AI 辅助 Design Token 命名的语义化与一致性自动检查
当命名不再是随意涂鸦: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,不需要查文档就能理解这是按钮主色的背景值。命名就是文档,这就是语义化命名的真正价值。
更多推荐


所有评论(0)