第 07 章
实战:代码审查机器人
实战:AI 代码审查机器人
动真格的了 — 用前面学的所有知识,构建一个能自动审查 Git 代码变更的命令行工具。
我们要做什么
一句话:输入一个 git diff,输出一份结构化的代码审查报告。
就像公司里的 Code Review,但这次审查员是 AI,7x24 小时在线,不请假不摸鱼。
功能清单
- ✅ 读取
git diff获取代码变更 - ✅ 让 Codex 逐块分析代码质量
- ✅ 用结构化输出生成标准化审查报告
- ✅ 流式模式实时显示审查进度
- ✅ 彩色终端输出,美观易读
技术架构
git diff → 解析变更 → 构造 Prompt → Codex SDK → 结构化报告 → 彩色输出综合运用:
- 第 3 章学的流式处理
- 第 5 章学的结构化输出(Zod)
- 第 6 章学的线程配置
项目初始化
bashmkdir ai-code-reviewer && cd ai-code-reviewer
git init
npm init -y
npm install @openai/codex-sdk zod zod-to-json-schema
npm install -D typescript tsx @types/nodetsconfig.json:
json{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist"
}
}第一步:定义审查报告的 Schema
先想清楚我们希望 AI 返回什么格式的数据:
typescript// schema.ts
import { z } from "zod";
// 单个问题
export const IssueSchema = z.object({
severity: z
.enum(["info", "warning", "error", "critical"])
.describe("问题严重程度: info=建议, warning=需关注, error=应修复, critical=必须修复"),
category: z
.enum(["bug", "security", "performance", "style", "maintainability", "logic"])
.describe("问题分类"),
file: z.string().describe("涉及的文件路径"),
line: z.number().optional().describe("大致行号(如果能确定的话)"),
title: z.string().describe("问题标题,10字以内"),
description: z.string().describe("问题详细描述"),
suggestion: z.string().describe("具体的修改建议"),
code_snippet: z.string().optional().describe("有问题的代码片段"),
});
// 整体审查报告
export const ReviewSchema = z.object({
summary: z.string().describe("整体评价,2-3 句话概括代码质量"),
score: z.number().min(0).max(100).describe("代码质量评分,100分满分"),
issues: z.array(IssueSchema).describe("发现的问题列表"),
highlights: z.array(z.string()).describe("做得好的地方,每条一句话"),
risk_level: z
.enum(["low", "medium", "high"])
.describe("总体风险级别"),
});
export type Review = z.infer<typeof ReviewSchema>;
export type Issue = z.infer<typeof IssueSchema>;第二步:审查引擎
核心逻辑 — 把 diff 喂给 Codex,拿回结构化报告:
typescript// reviewer.ts
import { Codex } from "@openai/codex-sdk";
import type { ThreadEvent } from "@openai/codex-sdk";
import zodToJsonSchema from "zod-to-json-schema";
import { ReviewSchema, type Review } from "./schema.ts";
const REVIEW_PROMPT = `你是一位资深的代码审查专家,拥有 10 年以上的软件开发经验。
请仔细审查以下 git diff,从这几个维度分析:
1. **Bug 和逻辑错误** — 有没有明显的 bug、边界条件没处理、空指针等
2. **安全隐患** — SQL 注入、XSS、敏感信息泄露、不安全的依赖
3. **性能问题** — N+1 查询、不必要的循环、内存泄漏风险
4. **代码风格** — 命名规范、函数长度、注释质量
5. **可维护性** — 重复代码、过度耦合、缺少测试
审查标准:
- 不要吹毛求疵,只报告真正有意义的问题
- 每个问题都要给出具体的修改建议
- 如果代码写得好,也要在 highlights 里指出
以下是需要审查的 diff:`;
// 非流式版本 — 简单直接
export async function reviewDiff(diff: string): Promise<Review> {
const codex = new Codex();
const thread = codex.startThread({
sandboxMode: "read-only",
modelReasoningEffort: "high",
skipGitRepoCheck: true,
});
const turn = await thread.run(`${REVIEW_PROMPT}\n\n\`\`\`diff\n${diff}\n\`\`\``, {
outputSchema: zodToJsonSchema(ReviewSchema, { target: "openAi" }),
});
return ReviewSchema.parse(JSON.parse(turn.finalResponse));
}
// 流式版本 — 实时显示进度
export async function reviewDiffStreamed(
diff: string,
onProgress: (event: ThreadEvent) => void
): Promise<Review> {
const codex = new Codex();
const thread = codex.startThread({
sandboxMode: "read-only",
modelReasoningEffort: "high",
skipGitRepoCheck: true,
});
const { events } = await thread.runStreamed(
`${REVIEW_PROMPT}\n\n\`\`\`diff\n${diff}\n\`\`\``,
{ outputSchema: zodToJsonSchema(ReviewSchema, { target: "openAi" }) }
);
let finalResponse = "";
for await (const event of events) {
onProgress(event);
if (event.type === "item.completed" && event.item.type === "agent_message") {
finalResponse = event.item.text;
}
}
return ReviewSchema.parse(JSON.parse(finalResponse));
}第三步:漂亮的输出格式化
typescript// format.ts
import type { Review, Issue } from "./schema.ts";
const SEVERITY_CONFIG = {
info: { icon: "💬", color: "\x1b[36m", label: "建议" },
warning: { icon: "⚠️", color: "\x1b[33m", label: "警告" },
error: { icon: "❌", color: "\x1b[31m", label: "错误" },
critical: { icon: "🔥", color: "\x1b[91m", label: "严重" },
} as const;
const CATEGORY_LABELS: Record<string, string> = {
bug: "Bug",
security: "安全",
performance: "性能",
style: "风格",
maintainability: "可维护性",
logic: "逻辑",
};
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const DIM = "\x1b[2m";
export function formatReview(review: Review): string {
const lines: string[] = [];
// 标题栏
lines.push("");
lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
lines.push(`${BOLD} 📋 AI 代码审查报告${RESET}`);
lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
// 评分和风险
const scoreColor = review.score >= 80 ? "\x1b[32m" : review.score >= 60 ? "\x1b[33m" : "\x1b[31m";
const riskIcon = { low: "🟢", medium: "🟡", high: "🔴" }[review.risk_level];
lines.push("");
lines.push(` 📊 评分: ${scoreColor}${BOLD}${review.score}/100${RESET}`);
lines.push(` ${riskIcon} 风险: ${review.risk_level.toUpperCase()}`);
lines.push("");
lines.push(` ${review.summary}`);
// 问题列表
if (review.issues.length > 0) {
lines.push("");
lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
lines.push(`${BOLD} 🔍 发现 ${review.issues.length} 个问题${RESET}`);
lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
for (const issue of review.issues) {
const { icon, color, label } = SEVERITY_CONFIG[issue.severity];
lines.push("");
lines.push(` ${icon} ${color}[${label}]${RESET} ${BOLD}${issue.title}${RESET}`);
lines.push(` ${DIM}${CATEGORY_LABELS[issue.category] || issue.category} · ${issue.file}${issue.line ? `:${issue.line}` : ""}${RESET}`);
lines.push(` ${issue.description}`);
lines.push(` 💡 ${issue.suggestion}`);
if (issue.code_snippet) {
lines.push(` ${DIM}>>> ${issue.code_snippet}${RESET}`);
}
}
} else {
lines.push("");
lines.push(" ✅ 没有发现问题,代码质量很棒!");
}
// 亮点
if (review.highlights.length > 0) {
lines.push("");
lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
lines.push(`${BOLD} ✨ 做得好的地方${RESET}`);
lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
for (const highlight of review.highlights) {
lines.push(` • ${highlight}`);
}
}
// 统计
const counts = {
critical: review.issues.filter(i => i.severity === "critical").length,
error: review.issues.filter(i => i.severity === "error").length,
warning: review.issues.filter(i => i.severity === "warning").length,
info: review.issues.filter(i => i.severity === "info").length,
};
lines.push("");
lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
lines.push(` 🔥 ${counts.critical} 严重 ❌ ${counts.error} 错误 ⚠️ ${counts.warning} 警告 💬 ${counts.info} 建议`);
lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
lines.push("");
return lines.join("\n");
}第四步:CLI 入口
把所有东西串起来:
typescript// cli.ts
import { execSync } from "child_process";
import type { ThreadEvent } from "@openai/codex-sdk";
import { reviewDiffStreamed } from "./reviewer.ts";
import { formatReview } from "./format.ts";
async function main() {
// 读取命令行参数
const target = process.argv[2] || "HEAD~1";
console.log(`\n📝 审查目标: git diff ${target}\n`);
// 获取 diff
let diff: string;
try {
diff = execSync(`git diff ${target}`, { encoding: "utf-8" });
} catch (err) {
console.error("❌ 获取 git diff 失败。确保你在 Git 仓库里,且目标有效。");
process.exit(1);
}
if (!diff.trim()) {
console.log("ℹ️ 没有代码变更,无需审查。");
process.exit(0);
}
// 显示变更统计
const fileCount = new Set(diff.match(/^diff --git .+/gm) || []).size;
const additions = (diff.match(/^\+[^+]/gm) || []).length;
const deletions = (diff.match(/^-[^-]/gm) || []).length;
console.log(`📊 变更统计: ${fileCount} 个文件, +${additions} -${deletions} 行\n`);
// 流式审查
console.log("🤖 AI 正在审查...\n");
const onProgress = (event: ThreadEvent): void => {
switch (event.type) {
case "turn.started":
process.stdout.write(" ⏳ 分析中");
break;
case "item.started":
process.stdout.write(".");
break;
case "item.completed":
if (event.item.type === "command_execution") {
process.stdout.write(`\n 🖥️ 执行: ${event.item.command}`);
}
break;
}
};
try {
const review = await reviewDiffStreamed(diff, onProgress);
console.log(formatReview(review));
// 退出码:有严重问题返回 1(方便 CI/CD 判断)
const hasCritical = review.issues.some(i => i.severity === "critical" || i.severity === "error");
process.exit(hasCritical ? 1 : 0);
} catch (err) {
console.error("\n❌ 审查失败:", err instanceof Error ? err.message : err);
process.exit(2);
}
}
main();第五步:试一试
在任何 Git 仓库里,先做一些改动,然后运行审查:
bash# 审查最近一次提交的变更
npx tsx cli.ts HEAD~1
# 审查某个分支的所有变更
npx tsx cli.ts main...feature-branch
# 审查暂存区的变更
npx tsx cli.ts --staged可以在 package.json 里加个快捷命令:
json{
"scripts": {
"review": "tsx cli.ts"
}
}然后直接:
bashnpm run review HEAD~3输出效果展示
审查完成后,你会看到类似这样的报告:
═══════════════════════════════════════════
📋 AI 代码审查报告
═══════════════════════════════════════════
📊 评分: 72/100
🟡 风险: medium
整体来看代码功能实现正确,但有几处安全和性能问题需要处理。
───────────────────────────────────────────
🔍 发现 3 个问题
───────────────────────────────────────────
🔥 [严重] SQL 注入风险
安全 · src/db/query.ts:42
直接拼接用户输入到 SQL 查询中,存在注入风险
💡 使用参数化查询代替字符串拼接
⚠️ [警告] 缺少错误处理
Bug · src/api/handler.ts:18
async 函数没有 try-catch,未处理的 Promise rejection 会导致崩溃
💡 添加 try-catch 块,返回适当的错误响应
💬 [建议] 变量命名可以更清晰
风格 · src/utils/helpers.ts:7
变量名 'x' 含义不清,影响可读性
💡 改为更具描述性的名称,如 'retryCount'
───────────────────────────────────────────
✨ 做得好的地方
───────────────────────────────────────────
• 函数职责单一,模块化做得好
• TypeScript 类型定义完整
═══════════════════════════════════════════
🔥 1 严重 ❌ 0 错误 ⚠️ 1 警告 💬 1 建议
═══════════════════════════════════════════进一步扩展思路
这个工具的基础框架已经搭好了,你可以继续扩展:
- 集成到 GitHub Actions — 在 PR 上自动审查,评论到 PR 里
- Markdown 报告 — 输出
.md文件,可以存档或发邮件 - 多模型对比 — 用不同模型审查同一份代码,取交集
- 自定义审查规则 — 通过 AGENTS.md 或 prompt 注入项目特定的编码规范
- 增量审查 — 只审查本次新增的变更,跳过已审查过的
小结
这一章我们综合运用了:
| 技术 | 用在了哪里 |
|---|---|
Codex + Thread |
创建审查引擎 |
runStreamed() |
实时显示审查进度 |
outputSchema + Zod |
结构化审查报告 |
sandboxMode: "read-only" |
只读模式,安全审查 |
modelReasoningEffort: "high" |
深度分析,不放过细节 |
一个真正实用的 AI 代码审查工具,200 多行代码搞定。
下一章:实战:全能 AI 助手 — 终极 Boss 关,接入飞书、Skills 扩展、定时任务。