AI 生成 UI 的代码质量度量:从圈复杂度到可维护性指数的自动化评估
一、"AI 写的代码能跑,但你看一眼就知道是 AI 写的"
用 GPT 生成了一个后台表单页面,功能正常——输入框、下拉、日期选择器、提交按钮,全部能工作。但细看代码:一个 200 行的组件没有拆分、Props 全部用 any、useEffect 里嵌套了 3 层条件判断、硬编码了 17 个颜色值。功能正确,但质量灾难。
AI 生成代码的质量是不可信的问题。如果让 AI 直接输出代码到生产环境而不经过质量评估,三个月后你会得到一个"功能完整但无法维护"的前端项目。代码质量度量就是这道防线:对 AI 输出的代码,用机器可执行的指标做自动打分,低于阈值的退回重做。
二、代码质量的多维评估模型
该评估模型将 AI 生成的代码输入到一个多维评估体系中,主要从结构、设计规范、可维护性和性能四个维度进行拆解:
- 结构维度:重点考察圈复杂度(Cyclomatic Complexity)、组件行数(Lines per Component)以及嵌套深度(Nesting Depth)。
- 设计规范维度:关注 Token 合规率(Token Compliance)、颜色硬编码率(Hardcoded Colors)及间距标准化率(Standard Spacing)。
- 可维护性维度:涵盖可维护性指数(Maintainability Index)、代码注释率(Comment Ratio)和 Props 类型覆盖率(Type Coverage)。
- 性能维度:评估 useMemo/useCallback 使用率及重渲染风险(Re-render Risk)。
所有维度的指标经过加权评分后汇总为总分。若总分大于等于 75 分,则判定通过并合入代码库;否则退回 AI 重新生成,并附带具体的改进建议。
三、质量度量引擎实现
// code-quality/code-quality-evaluator.ts
// AI 生成代码的质量评估引擎
import ts from 'typescript';
import { ESLint } from 'eslint';
interface QualityMetrics {
/** 圈复杂度 / cyclomaticComplexity: { average: number; // 平均圈复杂度(目标 < 10) max: number; // 最大圈复杂度(目标 < 15) score: number; // 0~100 }; /* 可维护性指数 / maintainabilityIndex: { value: number; // 0~100(越高越好,> 65 为合格) score: number; }; /* Token 合规率 / tokenCompliance: { rate: number; // 0~1(1 = 所有颜色都使用 Token) violations: Array<{ file: string; line: number; value: string }>; score: number; }; /* 代码结构 / structure: { maxFunctionLines: number; // 最大函数行数(目标 < 80) maxNestingDepth: number; // 最大嵌套深度(目标 < 4) componentCount: number; // 组件数量 score: number; }; /* 综合评分(加权平均) */ overall: number;}
/**
- AI 代码质量评估器
- 对 AI 生成的代码做多维度评估,输出结构化评分和改进建议 */class CodeQualityEvaluator { private designTokens: Map<string, string>;
constructor(tokenJsonPath: string) { this.designTokens = this.loadTokens(tokenJsonPath); }
/**
- 计算代码的圈复杂度
- 圈复杂度 = 决策点数量 + 1
- 决策点:if, else if, for, while, &&, ||, ?:, ?.(), switch case */ private calcCyclomaticComplexity(source: string): { average: number; max: number } {const functions = this.extractFunctions(source);
if (functions.length === 0) {
return { average: 0, max: 0 };
}
const complexities = functions.map((fn) => {
let complexity = 1; // 基础值
// 统计决策点
const patterns = [
/\\bif\\b/g,
/\\belse\\s+if\\b/g,
/\\bfor\\b/g,
/\\bwhile\\b/g,
/\\bcase\\b/g,
/\\?\\s*:/g, // 三元运算符
/&&/g,
/\\|\\|/g,
/\\?\\./g, // 可选链
];
for (const pattern of patterns) {
const matches = fn.match(pattern);
if (matches) complexity += matches.length;
}
return complexity;
});
return {
average: complexities.reduce((a, b) => a + b, 0) / complexities.length,
max: Math.max(…complexities)
};
}
/**
- 提取所有函数体(用于圈复杂度计算) */ private extractFunctions(source: string): string[] {const functions: string[] = [];const sourceFile = ts.createSourceFile( 'temp.tsx', source, ts.ScriptTarget.Latest, true);
function visit(node: ts.Node) {
if (ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node) ||
ts.isArrowFunction(node) ||
ts.isFunctionExpression(node)) {
functions.push(node.getText(source));
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return functions;
}
/**
- 计算可维护性指数
- 公式(简化版,近似于 Visual Studio 的 MI 计算):
- MI = max(0, (171 – 5.2ln(HV) – 0.23CC – 16.2*ln(LOC)) * 100 / 171)
- 其中:
-
- HV: Halstead Volume(程序容量)
-
- CC: 圈复杂度
-
- LOC: 代码行数 */private calcMaintainabilityIndex(source: string): number { const lines = source.split('\\n').filter(l => l.trim().length > 0);
const loc = lines.length;
const cc = this.calcCyclomaticComplexity(source);
// Halstead Volume 简化计算
const operators = source.match(/[+\\-*\\/=<>!&|?:]+/g)?.length || 0;
const operands = source.match(/\\b[a-zA-Z_]\\w*\\b/g)?.length || 0;
const vocabulary = operators + operands;
const length = operators + operands;
const volume = vocabulary > 0
? length * Math.log2(vocabulary)
: 0;
const mi = Math.max(
0,
(171 – 5.2 * Math.log(Math.max(volume, 1)) –
0.23 * cc.max –
16.2 * Math.log(Math.max(loc, 1))) *
100 / 171
);
return Math.min(100, Math.round(mi));
}
/**
- 计算 Token 合规率
- 扫描代码中所有颜色/间距值,检查是否使用设计 Token */ private calcTokenCompliance(source: string): {rate: number;violations: Array<{ file: string; line: number; value: string }>; } {const violations: Array<{ file: string; line: number; value: string }> = [];
// 匹配所有硬编码的颜色值(排除 var() 和 Token 引用)
const colorRegex = /(?<!var\\(|–)(#[0-9a-fA-F]{3,8}\\b|rgba?\\([^)]+\\))/g;
let match;
const allColorValues: string[] = [];
const tokenColorValues: string[] = [];
while ((match = colorRegex.exec(source)) !== null) {
const color = match[0].toLowerCase();
// 跳过允许的基本颜色
if (['#fff', '#ffffff', '#000', '#000000', 'transparent', 'inherit'].includes(color)) {
continue;
}
allColorValues.push(color);
// 检查是否匹配设计 Token
const isToken = Array.from(this.designTokens.values())
.some(tokenValue => tokenValue.toLowerCase() === color);
if (isToken) {
tokenColorValues.push(color);
} else {
violations.push({
file: 'generated.tsx',
line: 0, // 略去行号计算
value: color
});
}
}
return {
rate: allColorValues.length > 0
? tokenColorValues.length / allColorValues.length
: 1,
violations
};
}
/**
- 综合评估——所有维度的入口 */ evaluate(source: string): QualityMetrics {const cc = this.calcCyclomaticComplexity(source);const mi = this.calcMaintainabilityIndex(source);const tokenCompliance = this.calcTokenCompliance(source);
// 圈复杂度评分
const ccScore = Math.max(0, 100 – cc.max * 5);
// 可维护性评分
const miScore = Math.min(100, mi);
// Token 合规评分
const tokenScore = tokenCompliance.rate * 100;
// 结构评分
const lines = source.split('\\n');
const maxFunctionLines = this.calcMaxFunctionLines(source);
const maxNesting = this.calcMaxNesting(source);
const structScore = Math.max(0,
100 – Math.max(0, maxFunctionLines – 80) * 0.5 – maxNesting * 10
);
// 综合评分(加权)
const overall = (
ccScore * 0.25 +
miScore * 0.25 +
tokenScore * 0.25 +
structScore * 0.25
);
return {
cyclomaticComplexity: {
average: Math.round(cc.average * 10) / 10,
max: cc.max,
score: Math.round(ccScore)
},
maintainabilityIndex: {
value: mi,
score: Math.round(miScore)
},
tokenCompliance: {
rate: Math.round(tokenCompliance.rate * 100) / 100,
violations: tokenCompliance.violations,
score: Math.round(tokenScore)
},
structure: {
maxFunctionLines,
maxNestingDepth: maxNesting,
componentCount: 0,
score: Math.round(structScore)
},
overall: Math.round(overall)
};
}
private calcMaxFunctionLines(source: string): number { const functions = this.extractFunctions(source); return Math.max(0, …functions.map(fn => fn.split('\\n').length)); }
private calcMaxNesting(source: string): number { const lines = source.split('\\n'); let currentDepth = 0; let maxDepth = 0;
for (const line of lines) {
const trimmed = line.trim();
// 统计缩进层级
const indent = line.length – line.trimStart().length;
const depth = Math.floor(indent / 2); // 假设 2 空格缩进
if (trimmed.startsWith('if ') || trimmed.startsWith('for ') ||
trimmed.startsWith('while ') || trimmed.startsWith('switch ')) {
currentDepth = depth + 1;
maxDepth = Math.max(maxDepth, currentDepth);
}
}
return maxDepth;
}
private loadTokens(path: string): Map<string, string> { // 加载设计 Token 文件 return new Map(); }}
## 四、AI 代码质量的独特挑战
**AI 倾向于"过度实现"**。人的代码倾向于"先做最小可用",AI 倾向于"把能想到的都写进去"。这导致 AI 生成的组件 Props 数量远超实际需求(平均 15+ 个 Props 而人类平均 7 个)。度量需要加入"Props 使用率"指标。
**AI 缺乏"项目上下文"**。AI 不知道项目中已有 `Button` 组件,可能会生成一个新的 `<button>` 标签。度量需要检测"重复实现"——检查 AI 生成的代码是否重复了已有组件库的功能。
**格式化不等于质量**。AI 输出的代码通常会过 Prettier 格式化——看起来工整。但格式化的整洁掩盖了结构的混乱。质量度量关注的是"代码说了什么",而不是"代码好不好看"。
## 五、总结
AI 生成 UI 代码的质量度量是一个**门禁系统**,在代码合入前自动评估:
1. **圈复杂度**(< 10 平均,< 15 最大)——控制决策分支密度
2. **可维护性指数**(> 65)——综合代码健康度的通用指标
3. **Token 合规率**(> 90%)——硬编码值的使用比例
4. **结构质量**(函数 < 80 行,嵌套 < 4 层)——代码的可读性基线
阈值不是铁律——优秀的代码可能因为"必要的复杂性"而超标。但 AI 不应该享受这个例外:当 AI 生成的代码质量不达标时,应该退回让 AI 重新生成(附带具体的改进建议),而不是人工去修。

