01 为什么要做提交前AI审查

团队Code Review永远是稀缺资源。Senior忙、Junior看不出深层问题,最后Review流于形式。ESLint管语法一致性,管不了"这段逻辑绕不绕"。大模型恰恰擅长这一层。
Vite插件机制给了我们一个绝佳拦截点:transform钩子在每个模块加载时触发。我们能拿到源码、异步调API、把结果塞回构建日志,不需要重写CI。
这篇文章从0到1写一个能跑的Vite插件,在vite build时把代码喂给大模型,让它在控制台标出坏味道。完整源码可复制运行,依赖版本清晰标注。
02 环境准备+依赖清单
json
// package.json
{
"dependencies": {
"vite": "^5.4.0",
"openai": "^4.67.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^20.14.0"
}
}
环境变量配置:
ini
// .env
AI_REVIEW_API_KEY=sk-xxx
AI_REVIEW_BASE_URL=https://api.deepseek.com/v1
兼容OpenAI接口的厂商均可:OpenAI、DeepSeek、Moonshot、智谱、通义千问,换个baseURL就能切。
03 Vite插件最小骨架
Vite插件本质是带name的对象。关键字段:
name:插件唯一标识
enforce: 'pre':排在esbuild之前,拿到原始TS/JSX源码
apply: 'build':只在构建时生效
transform(code, id):核心钩子,每个模块都会触发
typescript
// vite-plugin-ai-review.ts
import type { Plugin } from 'vite';
export interface AiReviewOptions {
apiKey: string;
baseURL?: string;
model?: string;
failOnError?: boolean;
}
export default function aiReview(options: AiReviewOptions): Plugin {
return {
name: 'vite-plugin-ai-review',
enforce: 'pre',
apply: 'build',
async transform(code, id) {
const cleanId = id.split('?')[0];
if (!/.(ts|tsx|js|jsx)$/.test(cleanId)) return null;
if (/node_modules|/dist/|.d.ts$|.test./.test(cleanId)) return null;
const issues = await reviewCode(code, cleanId, options);
for (const issue of issues) {
const label = `[ai-review] ${cleanId}:${issue.line} [${issue.severity}] ${issue.message}`;
if (issue.severity === 'error' && options.failOnError) {
this.error(label);
} else {
this.warn(label);
}
}
return null;
},
};
}
this.error会让构建失败,this.warn只输出警告。两者都被Vite集成进构建日志和CI退出码,不要用console.log。
04 大模型API接入
用官方openaiSDK,支持任意兼容接口:
php
// llm.ts
import OpenAI from 'openai';
const client = new OpenAI({
apiKey=***!,
baseURL: process.env.AI_REVIEW_BASE_URL ?? 'https://api.openai.com/v1',
});
const SYSTEM_PROMPT = `You are a senior code reviewer. Identify code smells: long functions, deep nesting, duplicated logic, swallowed errors, magic numbers.
Return STRICT JSON: {"issues":[{"line":number,"severity":"error"|"warning"|"info","message":string,"suggestion":string}]}
If no issues, return {"issues":[]}`;
export async function callLLM(code: string, filePath: string, model = 'gpt-4o-mini') {
const resp = await client.chat.completions.create({
model,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: `File: ${filePath}${code}},
],
temperature: 0.2,
max_tokens: 1500,
});
const content = resp.choices[0]?.message?.content ?? '{}';
try {
const parsed = JSON.parse(content);
return Array.isArray(parsed.issues) ? parsed.issues.slice(0, 20) : [];
} catch {
return [];
}
}
注意三点:temperature: 0.2比0更稳定;max_tokens: 1500防止账单爆炸;JSON解析失败时降级为空数组,不让构建挂掉。
05 坏味道Prompt设计
Prompt是这套插件最值钱的部分。五个关键点:
强制JSON输出。早期写"请审查代码",模型返回大白话无法解析。改成Return STRICT JSON+schema贴prompt里,解析成功率从60%拉到98%。
明确severity语义。error=必须改(安全/逻辑bug)、warning=建议改(长函数/嵌套)、info=提示。在prompt里给出例子,模型才知道分类。
支持空返回。模型有"必须找出问题"的倾向,干净代码也会被编出毛病。强制{"issues":[]}非常必要。
设max_tokens。不设模型会"滔滔不绝"解释每个小问题,账单爆炸。1500 tokens够审查中等文件。
temperature用0.2而非0。0在某些模型上会出现贪心采样退化,两次审查结果不一致。0.2带一点随机性,反而让模型更敢说真话。
06 vite.config.ts完整接入
javascript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import aiReview from './vite-plugin-ai-review';
export default defineConfig({
plugins: [
aiReview({
apiKey=***!,
baseURL: 'https://api.deepseek.com/v1',
model: 'deepseek-chat',
failOnError: process.env.CI === 'true',
}),
react(),
],
});
成本数据(实战参考):
- 300个TS文件全量审查:deepseek-chat约0.04美元/次,gpt-4o-mini约0.15美元/次
- 加hash缓存后二次构建成本降至5%以下
- CI建议挂
vite build --mode production,作为PR硬门禁
性能优化建议:用git diff --cached --name-only拿暂存区文件列表,在include里精确匹配,只审查PR实际改动的部分。dev模式默认关闭,避免HMR触发频繁API调用。
07 跑通验证+常见报错
实战两周数据(15万行中后台项目):
- 平均CI审查耗时:47秒
- 误报率:8%
- 漏报率:12%
- 阻断PR:23个,其中19个确实是坏味道
常见报错与解决方案:
-
JSON.parse失败。某些兼容接口偶尔返回非JSON前缀。解决方案:用正则提取
{...}段再解析,或直接降级为空issues。 -
rate limit 429。Vite build全量并发打爆API。解决方案:加并发限流器,限到5并发。
-
HMR触发频繁调用。dev模式每次保存都调API。解决方案:
enableInServe: false,dev默认关。 -
生成代码被审查。GraphQL codegen、openapi-typescript生成的文件动辄几千行,模型编出无数"问题"。解决方案:
exclude里加掉所有codegen目录。 -
测试文件被审查。测试代码里的magic number和长函数是合理的,AI不懂这个上下文。解决方案:
exclude加.test.、.spec.。 -
CI超时。LLM调用层没设超时会卡死整个构建。解决方案:自己加
AbortController,30秒超时降级。
08 互动
你的团队目前Code Review靠人还是靠规则?
如果误报率10%是当前上限,你会怎么调prompt让它更贴合你的业务?