上周 Code Review 时,同事提交了一段 AI 生成的表单组件代码。功能能跑,但类型推导全是 any,错误处理靠 try-catch 包裹整个函数,样式硬编码在一堆 className 里。我问为什么这样写,他说"Claude 给的就这样"。这不是个例------我审查过上百段 AI 生成的代码,80% 能运行,但只有 20% 能直接合入主干。问题不在 AI,在于我们给 AI 的输入太随意了。
为什么 AI 生成的代码"能跑但不能用"
先明确一点:AI 生成代码的能力已经很强了。Claude 3.5、GPT-4o 这些模型在 SWE-bench 上的通过率超过 40%,简单的 CRUD 组件几乎能一次生成。但"能跑"和"能用"之间隔着一道工程化的鸿沟。
常见的问题集中在三个方面:
类型安全缺失。AI 倾向于用 any 绕过类型检查,因为这样生成成功率最高。但任何一段包含 any 的代码都是在给后人挖坑。
错误处理粗糙。AI 生成的错误处理通常是 try-catch 包裹整个函数体,catch 里 console.error 了事。生产环境需要的分级错误处理、用户友好提示、错误上报,AI 默认不会做。
架构一致性差。AI 不了解你的项目约定。你用 Composition API,它给你 Options API;你用 Tailwind,它给你 inline style;你的组件有统一的 props 命名规范,它按自己的习惯来。
这些问题的根源不是模型能力不足,而是输入不够精确。下面是我半年实践中总结的 5 个工程模式。
模式一:约束驱动而非需求描述
最常见的错误写法是把需求丢给 AI,期望它自己推断所有细节------"帮我写一个用户注册表单组件"。AI 会生成一个能跑的表单,但大概率不符合你的项目规范。正确的做法是把约束条件显式列出:
markdown
帮我写一个用户注册表单组件,约束如下:
1. 框架:Vue 3 Composition API + <script setup>
2. 校验:使用 Zod schema,错误提示中文
3. UI:不引入第三方组件库,用原生 HTML + Tailwind CSS
4. 状态:表单数据用 reactive,提交状态用 ref
5. 边界:密码长度 8-20,邮箱格式校验,手机号支持 +86
6. 输出:只输出组件代码,不需要解释
同样是"写表单",加了约束后 AI 的输出质量天差地别。约束越具体,AI 的自由度越低,输出越可控。我把这套思路做成了约束模板生成器,将项目规范固化下来,每次生成 Prompt 时自动注入:
typescript
// prompt-constraints.ts
// 项目级约束模板,每次 AI 生成代码时自动注入
interface ConstraintTemplate {
framework: string;
uiLibrary: string;
stateManagement: string;
validation: string;
errorHandling: string;
namingConvention: string;
forbidden: string[];
}
const projectConstraints: ConstraintTemplate = {
framework: 'Vue 3 Composition API + <script setup>',
uiLibrary: 'Tailwind CSS,禁止引入第三方 UI 库',
stateManagement: '组件内用 reactive/ref,跨组件用 Pinia',
validation: 'Zod schema,所有错误提示用中文',
errorHandling: '区分用户错误(4xx)和服务端错误(5xx),用户错误展示 toast,服务端错误展示错误页',
namingConvention: '组件 PascalCase,props camelCase,事件 onXxx',
forbidden: ['Options API', 'inline style', 'any 类型', 'console.log'],
};
export function buildPrompt(task: string, extra?: Partial<ConstraintTemplate>): string {
const merged = { ...projectConstraints, ...extra };
const lines = Object.entries(merged).map(([key, value]) => {
if (Array.isArray(value)) return `${key}: 禁止 ${value.join('、')}`;
return `${key}: ${value}`;
});
return `${task}\n\n约束:\n${lines.join('\n')}`;
}
// 使用示例
const prompt = buildPrompt('写一个文件上传组件,支持拖拽和进度展示');
// 自动注入项目规范,AI 输出直接符合团队约定
这个模板的核心思路是:把 Code Review 标准前移到 Prompt 生成阶段。与其等 AI 生成完再改,不如一开始就告诉它边界在哪。我团队接入这套模板后,AI 生成代码的一次通过率从 20% 提升到了 65%。
模式二:参考代码引导
当你需要 AI 生成的代码和现有代码风格一致时,最有效的方式不是描述风格,而是直接给参考。AI 的模仿能力远强于它的推理能力------给它一个具体的例子,比给它十条规则管用得多。
typescript
// reference-guided.ts
// 提供参考组件,让 AI 匹配现有代码风格
interface PromptWithReference {
task: string;
referenceFile: string;
referenceContent: string;
}
function buildReferencePrompt({ task, referenceFile, referenceContent }: PromptWithReference): string {
return `${task}
参考以下已有组件的风格(文件:${referenceFile}):
\`\`\`vue
${referenceContent}
\`\`\`
要求:
1. 保持与参考组件相同的代码结构和命名风格
2. 使用相同的 props 定义方式和 emit 模式
3. Tailwind class 命名规则保持一致
4. 注释风格保持一致`;
}
// 实际使用:让 AI 仿照现有 DatePicker 组件生成一个 TimePicker
const prompt = buildReferencePrompt({
task: '基于已有的 DatePicker 组件,创建一个 TimePicker 组件,支持小时和分钟选择',
referenceFile: 'components/DatePicker.vue',
referenceContent: `<template>
<div class="relative">
<input
v-model="displayValue"
type="text"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
placeholder="请选择日期"
@focus="openPanel"
/>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: string;
format?: string;
disabled?: boolean;
}
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'change', value: string): void;
}
const props = withDefaults(defineProps<Props>(), {
format: 'YYYY-MM-DD',
disabled: false,
});
const emit = defineEmits<Emits>();
</script>`,
});
这个模式的效果非常显著。不给参考时,AI 可能用 Options API、可能引入 Element Plus、可能用完全不同的 class 命名。给了参考后,输出几乎可以直接合入项目。
有个细节要注意:参考代码不要给太长,100 行以内最佳。太长会占用上下文窗口,反而降低生成质量。如果参考文件很长,截取最核心的部分即可。
模式三:分治生成
复杂功能一次性丢给 AI,结果往往是 200 行的巨型函数,逻辑缠绕在一起,根本没法 Review。正确做法是拆分成小任务,逐个生成,每步独立可验证。
以"实现一个可编辑表格"为例:
typescript
// task-splitter.ts
// 将复杂需求拆分为有序的小任务,逐个生成
interface SubTask {
step: number;
task: string;
dependsOn: number[];
outputFile: string;
acceptanceCriteria: string[];
}
const editableTableTasks: SubTask[] = [
{
step: 1,
task: '定义 EditableTable 组件的 Props 和 Emits 类型',
dependsOn: [],
outputFile: 'types/editable-table.ts',
acceptanceCriteria: [
'支持 columns 配置(key, title, editable, type)',
'支持 v-model 绑定数据源',
'支持行级 edit/save/cancel 事件',
],
},
{
step: 2,
task: '实现表格渲染层,只读模式展示数据',
dependsOn: [1],
outputFile: 'components/EditableTable.vue',
acceptanceCriteria: ['根据 columns 渲染表头和行', '双击单元格触发编辑事件'],
},
{
step: 3,
task: '实现单元格编辑器,支持 text/number/select 三种类型',
dependsOn: [2],
outputFile: 'components/EditableCell.vue',
acceptanceCriteria: [
'编辑器自动聚焦',
'回车保存,ESC 取消',
'select 类型从 column.options 读取选项',
],
},
{
step: 4,
task: '实现行级编辑状态管理',
dependsOn: [3],
outputFile: 'composables/useRowEdit.ts',
acceptanceCriteria: [
'同一时间只有一行可编辑',
'保存时触发数据更新',
'取消时恢复原始数据',
],
},
];
// 逐个执行:每次只给 AI 一个任务 + 上一步的输出作为上下文
async function generateWithSplit(tasks: SubTask[]): Promise<void> {
const outputs: Record<string, string> = {};
for (const task of tasks) {
const context = task.dependsOn
.map((i) => {
const dep = tasks[i - 1];
return `上一步输出(${dep.outputFile}):\n${outputs[dep.outputFile] || ''}`;
})
.join('\n\n');
const prompt = `${task.task}
${context}
验收标准:
${task.acceptanceCriteria.map((c) => `- ${c}`).join('\n')}
只输出代码,不要解释。`;
// const result = await callAI(prompt);
// outputs[task.outputFile] = result;
console.log(`[Step ${task.step}] 生成 ${task.outputFile}`);
}
}
generateWithSplit(editableTableTasks);
分治的好处不仅是代码质量更高,还有一个隐藏优势:每一步的输出都可以单独验证。如果第三步的 EditableCell 有问题,你只需要重新生成这一步,不用重来整个组件。这比让 AI 一次性生成 500 行代码然后逐行排查高效得多。
模式四:测试先行
这是效果最好的一个模式。先写测试,再让 AI 实现代码通过测试。好处是 AI 的输出有明确的验收标准,而不是"看起来差不多就行"。
typescript
// test-first.ts
// 先写测试,让 AI 实现通过测试的代码
const testFirstPrompt = `我需要一个 formatCurrency 函数,以下是测试用例:
\`\`\`typescript
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './format-currency';
describe('formatCurrency', () => {
it('格式化正数金额', () => {
expect(formatCurrency(1234.56, 'CNY')).toBe('¥1,234.56');
expect(formatCurrency(0, 'CNY')).toBe('¥0.00');
});
it('格式化负数金额(用括号表示)', () => {
expect(formatCurrency(-100, 'CNY')).toBe('(¥100.00)');
});
it('支持多种货币', () => {
expect(formatCurrency(999.99, 'USD')).toBe('$999.99');
expect(formatCurrency(1000, 'EUR')).toBe('€1,000.00');
});
it('处理大数字加千分位', () => {
expect(formatCurrency(1000000, 'CNY')).toBe('¥1,000,000.00');
});
it('非法输入返回空字符串', () => {
expect(formatCurrency(NaN, 'CNY')).toBe('');
expect(formatCurrency(Infinity, 'CNY')).toBe('');
});
});
\`\`\`
请实现 formatCurrency 函数,要求:
1. 纯函数,无副作用
2. 完整 TypeScript 类型
3. 通过以上所有测试`;
测试先行模式把"AI 生成的代码到底行不行"这个问题变成了一个客观可验证的问题------测试通过就行,不通过就不行。比人工 Code Review 效率高一个量级。
这个模式还有一个进阶玩法:让 AI 先生成测试用例,人工 Review 测试用例是否覆盖了边界情况,再让 AI 实现代码。这样你只需要审查测试用例(通常更短、更结构化),而不需要逐行审查实现代码。
模式五:自动化验证闭环
最后一个模式不是关于怎么写 Prompt,而是关于怎么建立一个自动验证 AI 代码的流程。即使你前面四个模式都用了,AI 生成的代码仍然可能有类型错误、Lint 告警、测试失败。与其人工检查,不如让管道自动拦截。
typescript
// verify-loop.ts
// AI 代码生成后的自动化验证管道
import { execSync } from 'child_process';
interface VerificationStep {
name: string;
command: string;
fixCommand?: string;
blocking: boolean;
}
const verificationPipeline: VerificationStep[] = [
{
name: 'TypeScript 类型检查',
command: 'npx vue-tsc --noEmit',
fixCommand: 'npx vue-tsc --noEmit --fix',
blocking: true,
},
{
name: 'ESLint 代码规范',
command: 'npx eslint --max-warnings 0 src/',
fixCommand: 'npx eslint --fix src/',
blocking: true,
},
{
name: '单元测试',
command: 'npx vitest run',
blocking: true,
},
{
name: '构建检查',
command: 'npx vite build --mode test',
blocking: false,
},
];
interface VerifyResult {
step: string;
passed: boolean;
output: string;
autoFixed: boolean;
}
function runVerification(): { allPassed: boolean; results: VerifyResult[] } {
const results: VerifyResult[] = [];
for (const step of verificationPipeline) {
try {
execSync(step.command, { encoding: 'utf-8', stdio: 'pipe' });
results.push({ step: step.name, passed: true, output: '', autoFixed: false });
} catch (error: any) {
// 尝试自动修复
if (step.fixCommand) {
try {
execSync(step.fixCommand, { encoding: 'utf-8', stdio: 'pipe' });
execSync(step.command, { encoding: 'utf-8', stdio: 'pipe' }); // 修复后重新检查
results.push({ step: step.name, passed: true, output: '自动修复后通过', autoFixed: true });
continue;
} catch {
// 修复失败,走正常报错逻辑
}
}
results.push({
step: step.name,
passed: false,
output: error.stdout || error.message,
autoFixed: false,
});
if (step.blocking) break;
}
}
return { allPassed: results.every((r) => r.passed), results };
}
// 在 pre-commit hook 或 CI 中调用
const { allPassed, results } = runVerification();
if (!allPassed) {
console.log('❌ 验证失败:');
results
.filter((r) => !r.passed)
.forEach((r) => console.log(` - ${r.step}: ${r.output.slice(0, 200)}`));
process.exit(1);
} else {
console.log('✅ 所有验证通过');
}
这个管道的核心思路是:把 Code Review 的客观部分(类型、规范、测试)自动化,只把主观部分(架构设计、业务逻辑)留给人工。我们团队把这个管道接入了 Git pre-commit hook,AI 生成的代码如果没有通过验证,根本无法提交。这倒逼开发者在 Prompt 阶段就写好约束,而不是生成完丢给 Code Review 兜底。
总结
经过半年的实践,我总结了让 AI 代码达到生产级的 5 个核心要点:
- 约束前移:把 Code Review 标准写到 Prompt 里,而不是生成后再改。约束越具体,输出越可控。
- 参考引导:给 AI 看已有代码,比描述代码风格有效十倍。100 行以内的参考最佳。
- 分治生成:复杂功能拆成小任务逐个生成,每步独立可验证,出问题只需重生成单步。
- 测试先行:先写测试再让 AI 实现,把"代码行不行"变成客观可验证的问题。
- 验证闭环:建立类型检查 + Lint + 测试的自动化管道,拦截 AI 代码的常见问题。
AI 编程工具的代码生成能力已经不是瓶颈,瓶颈在于我们怎么精确地表达需求、怎么验证输出。与其抱怨 AI 生成的代码不能用,不如把工程化的思维应用到 Prompt 阶段------毕竟,垃圾进,垃圾出。