用 Claude Code 构建自动化代码审查机器人
基于 SDK 和 Headless 模式打造 PR 自动审查流水线
项目目标
我们要构建一个自动化代码审查机器人,它能:
- 监听 GitHub PR 事件
- 自动分析代码变更
- 生成结构化的审查意见
- 将审查结果发布为 PR 评论
- 按严重程度分类问题
最终效果:每次有人提交 PR,机器人会在几分钟内给出专业的代码审查意见。
架构设计
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ GitHub │ │ Review │ │ Claude Code │
│ Webhook │────▶│ Service │────▶│ SDK │
│ │ │ │ │ │
└──────────────┘ └──────┬───────┘ └──────────────┘
│
┌──────▼───────┐
│ GitHub │
│ API │
│ (发布评论) │
└──────────────┘
技术栈
| 组件 | 技术 | 说明 |
|---|---|---|
| 运行时 | Node.js 22 + TypeScript | 主要开发语言 |
| AI 引擎 | @anthropic-ai/claude-code | 代码分析 |
| GitHub 集成 | @octokit/rest | GitHub API 客户端 |
| HTTP 服务 | Hono | 接收 Webhook |
| 部署 | GitHub Actions | 无需额外服务器 |
方案选择
我们有两种实现方案:
方案 A:GitHub Actions(推荐)
直接在 GitHub Actions 中运行,无需额外服务器:
- 优点:零运维、免费(公开仓库)、配置简单
- 缺点:冷启动慢、运行时间有限制
方案 B:Webhook 服务
部署独立的 Webhook 服务:
- 优点:响应快、可定制性强
- 缺点:需要服务器、需要运维
本文以方案 A 为主,同时展示方案 B 的核心代码。
方案 A:GitHub Actions 实现
项目结构
.github/
├── workflows/
│ └── ai-review.yml # GitHub Actions 工作流
└── scripts/
├── review.ts # 审查主逻辑
├── analyzer.ts # 代码分析器
├── formatter.ts # 结果格式化
└── github.ts # GitHub API 封装
第一步:工作流配置
# .github/workflows/ai-review.yml
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
paths:
- 'src/**'
- 'lib/**'
- 'tests/**'
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Dependencies
run: |
npm install -g @anthropic-ai/claude-code tsx
npm install @octokit/rest
- name: Run AI Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: npx tsx .github/scripts/review.ts
第二步:审查主逻辑
// .github/scripts/review.ts
import { analyzeChanges } from "./analyzer";
import { formatReview } from "./formatter";
import { postReviewComment } from "./github";
async function main() {
const prNumber = parseInt(process.env.PR_NUMBER || "0");
const baseSha = process.env.BASE_SHA || "";
const headSha = process.env.HEAD_SHA || "";
if (!prNumber || !baseSha || !headSha) {
console.error("缺少必要的环境变量");
process.exit(1);
}
console.log(`开始审查 PR #${prNumber}`);
console.log(`Base: ${baseSha.substring(0, 7)}, Head: ${headSha.substring(0, 7)}`);
// 1. 分析代码变更
const analysis = await analyzeChanges(baseSha, headSha);
// 2. 格式化审查结果
const reviewBody = formatReview(analysis);
// 3. 发布审查评论
await postReviewComment(prNumber, reviewBody);
console.log("审查完成");
}
main().catch((error) => {
console.error("审查失败:", error);
process.exit(1);
});
第三步:代码分析器
// .github/scripts/analyzer.ts
import { execSync } from "child_process";
import { claude } from "@anthropic-ai/claude-code";
export interface ReviewIssue {
file: string;
line: number;
severity: "critical" | "warning" | "suggestion";
category: string;
message: string;
suggestion: string;
}
export interface ReviewAnalysis {
issues: ReviewIssue[];
summary: string;
score: number;
stats: {
filesReviewed: number;
totalIssues: number;
critical: number;
warnings: number;
suggestions: number;
};
}
export async function analyzeChanges(
baseSha: string,
headSha: string
): Promise<ReviewAnalysis> {
// 获取变更的文件列表
const diffOutput = execSync(
`git diff --name-only ${baseSha}...${headSha}`
).toString();
const changedFiles = diffOutput
.trim()
.split("\n")
.filter((f) => f.match(/\.(ts|tsx|js|jsx)$/));
if (changedFiles.length === 0) {
return {
issues: [],
summary: "没有需要审查的代码文件变更。",
score: 10,
stats: {
filesReviewed: 0,
totalIssues: 0,
critical: 0,
warnings: 0,
suggestions: 0,
},
};
}
// 获取 diff 内容
const diffContent = execSync(
`git diff ${baseSha}...${headSha} -- ${changedFiles.join(" ")}`
).toString();
// 使用 Claude Code 分析
const response = await claude(
`你是一个资深代码审查专家。请审查以下代码变更。
变更的文件:
${changedFiles.join("\n")}
Diff 内容:
\`\`\`diff
${diffContent.substring(0, 50000)}
\`\`\`
请以严格的 JSON 格式返回审查结果(不要包含 markdown 代码块标记):
{
"issues": [
{
"file": "文件路径",
"line": 行号,
"severity": "critical|warning|suggestion",
"category": "分类(security/performance/bug/style/maintainability)",
"message": "问题描述",
"suggestion": "改进建议"
}
],
"summary": "整体评价(2-3 句话)",
"score": 1到10的评分
}
审查重点:
1. 安全漏洞(SQL 注入、XSS、敏感信息泄露)
2. 潜在 Bug(空指针、类型错误、边界条件)
3. 性能问题(不必要的计算、内存泄漏、N+1 查询)
4. 代码质量(可读性、重复代码、命名规范)
5. 最佳实践(错误处理、类型安全、测试覆盖)`,
{
cwd: process.cwd(),
allowedTools: ["Read", "Glob", "Grep"],
timeout: 120000,
}
);
try {
const result = JSON.parse(response.text);
return {
...result,
stats: {
filesReviewed: changedFiles.length,
totalIssues: result.issues.length,
critical: result.issues.filter(
(i: ReviewIssue) => i.severity === "critical"
).length,
warnings: result.issues.filter(
(i: ReviewIssue) => i.severity === "warning"
).length,
suggestions: result.issues.filter(
(i: ReviewIssue) => i.severity === "suggestion"
).length,
},
};
} catch {
console.error("解析审查结果失败,原始输出:", response.text);
return {
issues: [],
summary: "审查结果解析失败,请手动检查。",
score: 0,
stats: {
filesReviewed: changedFiles.length,
totalIssues: 0,
critical: 0,
warnings: 0,
suggestions: 0,
},
};
}
}
第四步:结果格式化
// .github/scripts/formatter.ts
import type { ReviewAnalysis, ReviewIssue } from "./analyzer";
const SEVERITY_EMOJI = {
critical: "🔴",
warning: "🟡",
suggestion: "🟢",
};
const SEVERITY_LABEL = {
critical: "严重",
warning: "警告",
suggestion: "建议",
};
const CATEGORY_LABEL: Record<string, string> = {
security: "安全",
performance: "性能",
bug: "Bug",
style: "代码风格",
maintainability: "可维护性",
};
export function formatReview(analysis: ReviewAnalysis): string {
const lines: string[] = [];
// 标题
lines.push("## AI Code Review Report");
lines.push("");
// 评分
const scoreBar = getScoreBar(analysis.score);
lines.push(`### 评分: ${analysis.score}/10 ${scoreBar}`);
lines.push("");
// 统计
lines.push("### 统计");
lines.push("");
lines.push(`| 指标 | 数量 |`);
lines.push(`|------|------|`);
lines.push(`| 审查文件数 | ${analysis.stats.filesReviewed} |`);
lines.push(`| 🔴 严重问题 | ${analysis.stats.critical} |`);
lines.push(`| 🟡 警告 | ${analysis.stats.warnings} |`);
lines.push(`| 🟢 建议 | ${analysis.stats.suggestions} |`);
lines.push("");
// 总结
lines.push("### 总结");
lines.push("");
lines.push(analysis.summary);
lines.push("");
// 详细问题
if (analysis.issues.length > 0) {
lines.push("### 详细问题");
lines.push("");
// 按严重程度分组
const grouped = groupBySeverity(analysis.issues);
for (const [severity, issues] of Object.entries(grouped)) {
const emoji = SEVERITY_EMOJI[severity as keyof typeof SEVERITY_EMOJI];
const label = SEVERITY_LABEL[severity as keyof typeof SEVERITY_LABEL];
lines.push(`#### ${emoji} ${label} (${issues.length})`);
lines.push("");
for (const issue of issues) {
const category =
CATEGORY_LABEL[issue.category] || issue.category;
lines.push(
`- **${issue.file}:${issue.line}** [${category}]`
);
lines.push(` ${issue.message}`);
if (issue.suggestion) {
lines.push(` > 建议: ${issue.suggestion}`);
}
lines.push("");
}
}
} else {
lines.push("### 没有发现问题 ✅");
lines.push("");
lines.push("代码看起来很好,没有发现需要关注的问题。");
}
// 页脚
lines.push("---");
lines.push(
"*由 AI Code Review Bot 自动生成 | Powered by Claude Code*"
);
return lines.join("\n");
}
function getScoreBar(score: number): string {
const filled = "█".repeat(score);
const empty = "░".repeat(10 - score);
return `\`${filled}${empty}\``;
}
function groupBySeverity(
issues: ReviewIssue[]
): Record<string, ReviewIssue[]> {
const order = ["critical", "warning", "suggestion"];
const grouped: Record<string, ReviewIssue[]> = {};
for (const severity of order) {
const filtered = issues.filter((i) => i.severity === severity);
if (filtered.length > 0) {
grouped[severity] = filtered;
}
}
return grouped;
}
第五步:GitHub API 封装
// .github/scripts/github.ts
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const [owner, repo] = (process.env.GITHUB_REPOSITORY || "/").split("/");
export async function postReviewComment(
prNumber: number,
body: string
): Promise<void> {
// 查找是否已有 bot 评论
const { data: comments } = await octokit.issues.listComments({
owner,
repo,
issue_number: prNumber,
});
const botComment = comments.find(
(c) =>
c.user?.login === "github-actions[bot]" &&
c.body?.includes("AI Code Review Report")
);
if (botComment) {
// 更新已有评论
await octokit.issues.updateComment({
owner,
repo,
comment_id: botComment.id,
body,
});
console.log(`更新了评论 #${botComment.id}`);
} else {
// 创建新评论
const { data } = await octokit.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
console.log(`创建了评论 #${data.id}`);
}
}
方案 B:Webhook 服务
如果需要更快的响应和更多定制,可以部署独立的 Webhook 服务:
服务端代码
// src/server.ts
import { Hono } from "hono";
import { verify } from "@octokit/webhooks-methods";
import { handlePullRequest } from "./handler";
const app = new Hono();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || "";
app.post("/webhook", async (c) => {
// 验证 Webhook 签名
const signature = c.req.header("x-hub-signature-256") || "";
const body = await c.req.text();
const isValid = await verify(WEBHOOK_SECRET, body, signature);
if (!isValid) {
return c.json({ error: "Invalid signature" }, 401);
}
const event = c.req.header("x-github-event");
const payload = JSON.parse(body);
if (event === "pull_request") {
const action = payload.action;
if (action === "opened" || action === "synchronize") {
// 异步处理,立即返回
handlePullRequest(payload).catch(console.error);
return c.json({ status: "processing" });
}
}
return c.json({ status: "ignored" });
});
export default {
port: 3000,
fetch: app.fetch,
};
处理逻辑
// src/handler.ts
import { analyzeChanges } from "./analyzer";
import { formatReview } from "./formatter";
import { postReviewComment, cloneRepo } from "./github";
export async function handlePullRequest(payload: any) {
const prNumber = payload.number;
const repoFullName = payload.repository.full_name;
const baseSha = payload.pull_request.base.sha;
const headSha = payload.pull_request.head.sha;
const cloneUrl = payload.repository.clone_url;
console.log(`处理 PR #${prNumber} from ${repoFullName}`);
// 克隆仓库到临时目录
const repoDir = await cloneRepo(cloneUrl, headSha);
try {
// 分析变更
const analysis = await analyzeChanges(baseSha, headSha);
// 格式化结果
const reviewBody = formatReview(analysis);
// 发布评论
const [owner, repo] = repoFullName.split("/");
await postReviewComment(owner, repo, prNumber, reviewBody);
console.log(`PR #${prNumber} 审查完成`);
} finally {
// 清理临时目录
const { execSync } = await import("child_process");
execSync(`rm -rf ${repoDir}`);
}
}
审查质量优化
1. 分层审查
对不同类型的文件使用不同的审查策略:
async function layeredReview(files: string[]): Promise<ReviewIssue[]> {
const layers = {
security: files.filter((f) =>
f.match(/auth|login|password|token|secret|crypto/i)
),
api: files.filter((f) => f.match(/route|controller|handler|api/i)),
ui: files.filter((f) => f.match(/\.(tsx|jsx)$/)),
logic: files.filter((f) => f.match(/service|util|helper|lib/i)),
};
const results = await Promise.all([
layers.security.length > 0
? claude(
`作为安全专家审查:${layers.security.join(", ")}
重点:认证、授权、注入、敏感信息`,
{ allowedTools: ["Read", "Grep"] }
)
: null,
layers.api.length > 0
? claude(
`作为 API 设计专家审查:${layers.api.join(", ")}
重点:RESTful 规范、错误处理、参数校验、响应格式`,
{ allowedTools: ["Read", "Grep"] }
)
: null,
layers.ui.length > 0
? claude(
`作为前端专家审查:${layers.ui.join(", ")}
重点:可访问性、性能、状态管理、组件设计`,
{ allowedTools: ["Read", "Grep"] }
)
: null,
layers.logic.length > 0
? claude(
`作为软件工程师审查:${layers.logic.join(", ")}
重点:算法效率、错误处理、边界条件、可测试性`,
{ allowedTools: ["Read", "Grep"] }
)
: null,
]);
// 合并所有结果
return results
.filter(Boolean)
.flatMap((r) => {
try {
return JSON.parse(r!.text).issues;
} catch {
return [];
}
});
}
2. 上下文感知
让审查机器人了解项目的规范:
async function contextAwareReview(
files: string[],
projectPath: string
) {
// 读取项目的 CLAUDE.md 获取上下文
const fs = await import("fs/promises");
let projectContext = "";
try {
projectContext = await fs.readFile(
`${projectPath}/CLAUDE.md`,
"utf-8"
);
} catch {
// 没有 CLAUDE.md,使用默认上下文
}
// 读取 ESLint 配置了解代码规范
let lintConfig = "";
try {
lintConfig = await fs.readFile(
`${projectPath}/eslint.config.js`,
"utf-8"
);
} catch {
// 没有 ESLint 配置
}
return claude(
`项目上下文:
${projectContext}
代码规范:
${lintConfig}
请基于以上项目上下文审查代码变更...`,
{
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep"],
}
);
}
3. 严重程度分类标准
const SEVERITY_CRITERIA = {
critical: [
"安全漏洞(SQL 注入、XSS、CSRF)",
"数据丢失风险",
"认证/授权绕过",
"敏感信息泄露",
"会导致崩溃的 Bug",
],
warning: [
"潜在的运行时错误",
"性能问题(O(n^2) 或更差)",
"缺少错误处理",
"类型安全问题",
"不当的资源管理",
],
suggestion: [
"代码风格不一致",
"可以简化的逻辑",
"缺少注释或文档",
"命名不够清晰",
"可以提取的重复代码",
],
};
监控与维护
审查质量追踪
// 记录每次审查的统计数据
interface ReviewMetrics {
prNumber: number;
timestamp: string;
filesReviewed: number;
issuesFound: number;
duration: number;
tokensUsed: number;
score: number;
}
async function trackMetrics(metrics: ReviewMetrics) {
// 追加到日志文件
const fs = await import("fs/promises");
const logLine = JSON.stringify(metrics) + "\n";
await fs.appendFile("review-metrics.jsonl", logLine);
}
成本监控
function estimateCost(inputTokens: number, outputTokens: number): number {
const inputCost = (inputTokens / 1_000_000) * 3; // $3/1M input tokens
const outputCost = (outputTokens / 1_000_000) * 15; // $15/1M output tokens
return inputCost + outputCost;
}
// 在审查完成后记录成本
const cost = estimateCost(response.usage.inputTokens, response.usage.outputTokens);
console.log(`本次审查成本: $${cost.toFixed(4)}`);
错误告警
# 在 GitHub Actions 中添加失败通知
- name: Notify on Failure
if: failure()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '⚠️ AI Code Review 执行失败,请手动审查。'
});
部署清单
GitHub Actions 方案
- 创建
.github/workflows/ai-review.yml - 创建
.github/scripts/下的脚本文件 - 在 GitHub Secrets 中添加
ANTHROPIC_API_KEY - 配置 PR 触发条件(路径过滤)
- 测试:创建一个测试 PR 验证
- 监控:检查 Actions 运行日志
Webhook 方案
- 部署 Webhook 服务
- 在 GitHub 仓库设置中配置 Webhook URL
- 配置 Webhook Secret
- 设置 ANTHROPIC_API_KEY 环境变量
- 配置 GitHub App 或 Personal Access Token
- 测试:创建一个测试 PR 验证
- 监控:设置日志和告警
自动化代码审查不是要取代人工审查,而是为它提供一个强大的”第一道防线”。AI 可以在几分钟内发现人眼容易遗漏的问题——类型不安全、缺少错误处理、潜在的性能瓶颈。把机械的检查交给机器,把创造性的判断留给人类,这才是 AI 辅助开发的正确姿势。
相关文章
🤖Claude Code
从零编写 Claude Code Autopilot 模式:从想法到代码的完全自动化实现指南
手把手教你从零开始构建一个类似 oh-my-claudecode 的 Autopilot 自动执行系统,包含需求分析、任务规划、代码执行、QA 验证等完整流程的实现
🤖Claude Codeoh-my-claudecode Autopilot 模式详解:从想法到代码的完全自动化
深入解析 oh-my-claudecode 的 Autopilot 模式,了解如何实现从模糊想法到完整产品的全自动开发
🤖Claude CodeClaude Code Computer Use:AI 自主控制电脑
深入了解 Claude Code 的 Computer Use 功能,让 AI 能够自主操作浏览器和桌面应用
评论
加载中...
评论
加载中...