Prompt Engineering 实战:5 个让 AI 生成生产级业务代码的工程模式

上周 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 个核心要点:

  1. 约束前移:把 Code Review 标准写到 Prompt 里,而不是生成后再改。约束越具体,输出越可控。
  2. 参考引导:给 AI 看已有代码,比描述代码风格有效十倍。100 行以内的参考最佳。
  3. 分治生成:复杂功能拆成小任务逐个生成,每步独立可验证,出问题只需重生成单步。
  4. 测试先行:先写测试再让 AI 实现,把"代码行不行"变成客观可验证的问题。
  5. 验证闭环:建立类型检查 + Lint + 测试的自动化管道,拦截 AI 代码的常见问题。

AI 编程工具的代码生成能力已经不是瓶颈,瓶颈在于我们怎么精确地表达需求、怎么验证输出。与其抱怨 AI 生成的代码不能用,不如把工程化的思维应用到 Prompt 阶段------毕竟,垃圾进,垃圾出。

相关推荐
去伪存真1 小时前
AI时代,古法编程的码农路在何方?
前端
计算机魔术师1 小时前
从7月绕过沙盒到9月再曝违规,OpenAI的AI安全防线在漏什么
前端
我的div丢了肿么办1 小时前
element-plus表格使用:filter-method过滤导致出现无数据
前端·javascript·vue.js
倚楼听风语1 小时前
Kotlin 协程与 suspend 原理详解
java·算法·kotlin
F_Quant1 小时前
高股息+破净+PEG三因子:年化20%的背后,是一个“敢于证伪自己”的捡烟蒂策略
前端
不可能片场1 小时前
Electron 发布核验 别把没查到当成没发出去
前端·electron
开开心心就好2 小时前
文件被占用删不掉?解锁工具强制删除
java·运维·服务器·开发语言·前端·pdf·ocr
weixin_460443562 小时前
Java+Linux私有化训考平台:服务、数据库与终端部署结构解析
java·linux·数据库·私有化部署·企业培训考试系统·企业考试系统
云和数据.ChenGuang2 小时前
python大小整数范围解析
java·开发语言·python