欢迎光临
我们一直在努力

评测 AI 生成前端代码,别拿 TodoList 当业务样本

评测 AI 生成前端代码,别拿 TodoList 当业务样本

TodoList 和计数器适合检查模型会不会写 JSX,不适合证明它能接进业务仓库。React 表单即使通过静态检查,也可能在字段联动、边界输入或接口异常时出错。

评测集应从经授权的历史任务或合成任务中分层抽样,保留必要的类型、组件和 Mock API 上下文。语法、类型、交互与集成测试分别计数,别用一个“准确率”把失败阶段全盖住。

1. 为什么演示组件不能代表业务代码

准备评估集时,应按语言、改动规模、业务模块和风险等级分层抽样。编译通过率只能覆盖一部分问题,还要单独记录类型错误、测试失败和运行时异常;不要把某个仓库的比例外推为模型的通用能力。

Pass@1 可以反映一次生成成功的概率,但不足以代表前端代码能否集成。前端代码还涉及交互状态、生命周期和 DOM 事件;评估集应尽量保留组件上下文、Store 声明和 Mock API 响应。

一种可复查的做法,是从已获授权的 GitHub/GitLab 历史 Commit 中提取候选样本,并做脱敏与许可证审查。样本可以拆成任务描述、依赖上下文和参考改动;参考改动只是比较材料,不应自动当成唯一正确答案。

2. 把语法、类型与单元测试切成三层:防线设计与指标计算

评估指标不能只有“对”和“错”两个极端状态,必须建立分层防御口径。我们定义了三个核心工程指标:

  • 语法合法率(Syntax Pass Rate, SPR):生成代码是否符合 ES2024 与 JSX/TSX 词法语法规则,能否无报错解析为标准 AST。
  • 类型严谨度(Type Strictness Score, TSS):在 strict 模式下,TypeScript 编译器抛出的 Error 数量,以及隐式 any 的逃逸比例。
  • 单元测试断言通过率(Unit Test Pass Rate, UPR):将生成代码植入预设的 Vitest 测试用例容器中,断言组件属性渲染、用户交互事件响应是否符合预期。
  • 下面的 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 代码生成是否真的改善。把数据集、质量门禁和人工复核放进同一条流水线,才能持续控制集成风险。

    赞(0)
    未经允许不得转载:171主机测评 » 评测 AI 生成前端代码,别拿 TodoList 当业务样本
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址