评测 AI 生成前端代码,别拿 TodoList 当业务样本
TodoList 和计数器适合检查模型会不会写 JSX,不适合证明它能接进业务仓库。React 表单即使通过静态检查,也可能在字段联动、边界输入或接口异常时出错。
评测集应从经授权的历史任务或合成任务中分层抽样,保留必要的类型、组件和 Mock API 上下文。语法、类型、交互与集成测试分别计数,别用一个“准确率”把失败阶段全盖住。
1. 为什么演示组件不能代表业务代码
准备评估集时,应按语言、改动规模、业务模块和风险等级分层抽样。编译通过率只能覆盖一部分问题,还要单独记录类型错误、测试失败和运行时异常;不要把某个仓库的比例外推为模型的通用能力。
Pass@1 可以反映一次生成成功的概率,但不足以代表前端代码能否集成。前端代码还涉及交互状态、生命周期和 DOM 事件;评估集应尽量保留组件上下文、Store 声明和 Mock API 响应。
一种可复查的做法,是从已获授权的 GitHub/GitLab 历史 Commit 中提取候选样本,并做脱敏与许可证审查。样本可以拆成任务描述、依赖上下文和参考改动;参考改动只是比较材料,不应自动当成唯一正确答案。
2. 把语法、类型与单元测试切成三层:防线设计与指标计算
评估指标不能只有“对”和“错”两个极端状态,必须建立分层防御口径。我们定义了三个核心工程指标:
下面的 Runner 展示类型检查和 ESLint 扫描的基本结构。示例中的 unitTestPassed 是占位逻辑;接入 CI 时应调用 Vitest 的 Node API 或项目脚本运行真实测试,不能以静态检查代替测试结果。
import * as ts from 'typescript';
import { ESLint } from 'eslint';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
export interface TestCaseSample {
id: string;
prompt: string;
contextTypes: string;
groundTruthCode: string;
testSuiteCode: string;
}
export interface EvaluationResult {
sampleId: string;
syntaxValid: boolean;
typeCheckPassed: boolean;
typeErrors: string[];
eslintErrorCount: number;
unitTestPassed: boolean;
executionTimeMs: number;
}
export class FrontendAIBenchmarkRunner {
private eslint: ESLint;
constructor() {
this.eslint = new ESLint({
useEslintrc: false,
overrideConfig: {
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
parserOptions: { ecmaFeatures: { jsx: true } }
},
rules: {
'no-explicit-any': 'error',
'no-unused-vars': 'warn',
'react-hooks/rules-of-hooks': 'error'
}
}
});
}
/**
* 编译 TypeScript 内存代码块并校验类型安全
*/
public verifyTypeScript(code: string, contextTypes: string): { valid: boolean; errors: string[] } {
const fullSource = `${contextTypes}\\n\\n${code}`;
const fileName = `virtual_eval_${Date.now()}.tsx`;
const compilerOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ESNext,
jsx: ts.JsxEmit.ReactJSX,
strict: true,
noImplicitAny: true,
skipLibCheck: true,
moduleResolution: ts.ModuleResolutionKind.Bundler
};
const sourceFile = ts.createSourceFile(fileName, fullSource, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
const defaultCompilerHost = ts.createCompilerHost(compilerOptions);
const customHost: ts.CompilerHost = {
…defaultCompilerHost,
getSourceFile: (name, languageVersion) => (name === fileName ? sourceFile : defaultCompilerHost.getSourceFile(name, languageVersion)),
writeFile: () => {},
getCurrentDirectory: () => process.cwd(),
getDirectories: () => [],
getCanonicalFileName: (fileName) => fileName,
useCaseSensitiveFileNames: () => true,
getNewLine: () => '\\n',
fileExists: (name) => name === fileName || defaultCompilerHost.fileExists(name)
};
const program = ts.createProgram([fileName], compilerOptions, customHost);
const diagnostics = ts.getPreEmitDiagnostics(program);
const errors = diagnostics.map((diag) => {
const message = ts.flattenDiagnosticMessageText(diag.messageText, '\\n');
if (diag.file && diag.start !== undefined) {
const { line, character } = diag.file.getLineAndCharacterOfPosition(diag.start);
return `[Line ${line + 1}:${character + 1}] ${message}`;
}
return message;
});
return { valid: errors.length === 0, errors };
}
/**
* 执行完整的单个 Sample 评估
*/
public async evaluateSample(sample: TestCaseSample, generatedCode: string): Promise<EvaluationResult> {
const startTime = Date.now();
// 1. 类型校验
const typeResult = this.verifyTypeScript(generatedCode, sample.contextTypes);
// 2. ESLint 规约扫描
let eslintErrorCount = 0;
try {
const eslintResults = await this.eslint.lintText(generatedCode, { filePath: 'eval.tsx' });
eslintErrorCount = eslintResults.reduce((acc, curr) => acc + curr.errorCount, 0);
} catch {
eslintErrorCount = 999; // 语法严重破损无法解析
}
// 3. 构造虚拟沙箱测试环境
const tempDir = path.join(process.cwd(), '.bench_sandbox', sample.id);
await fs.mkdir(tempDir, { recursive: true });
const componentPath = path.join(tempDir, 'Component.tsx');
const testPath = path.join(tempDir, 'Component.test.tsx');
await fs.writeFile(componentPath, `${sample.contextTypes}\\n${generatedCode}`, 'utf-8');
await fs.writeFile(testPath, sample.testSuiteCode, 'utf-8');
// 占位结果:生产环境应对接 Vitest Node API,执行 sample.testSuiteCode。
const unitTestPassed = typeResult.valid && eslintErrorCount === 0;
// 清理临时沙箱
await fs.rm(tempDir, { recursive: true, force: true });
return {
sampleId: sample.id,
syntaxValid: eslintErrorCount < 999,
typeCheckPassed: typeResult.valid,
typeErrors: typeResult.errors,
eslintErrorCount,
unitTestPassed,
executionTimeMs: Date.now() – startTime
};
}
}
3. 坑点复盘:别让伪阳性数据集把优化方向带偏
静态检查通过的代码仍可能没有实现预期交互,例如事件没有更新状态、加载分支从未执行,或错误被不恰当地吞掉。这类“静默伪合规”只有在渲染和交互测试中才能发现。
不要只依赖 Lint 指标。评估链路还应运行 DOM 渲染与关键交互断言;必要时记录渲染次数和 DOM 变化,辅助定位失败原因。状态变化后 DOM 是否变化取决于具体需求,不能单凭这一项否定样本。
4. 在 CI 流水线里布防:把 Evaluation 集成到每日构建中
评测应随 Prompt、检索上下文和模型版本一起版本化。每次变更后在 CI 中运行固定基准集,并保留样本构成、环境与人工复核结果,才便于比较。
比较模型或 Prompt 版本时,应同时保留样本集、依赖版本和人工复核记录。若某项错误率上升,先定位到具体样本和规则,再调整 Prompt 或代码约束;不要把单次波动直接归因于模型。
没有可重复的基准测试,就很难判断 AI 代码生成是否真的改善。把数据集、质量门禁和人工复核放进同一条流水线,才能持续控制集成风险。


