AI Code Review实践:自动化代码审查

将LLM集成到代码审查流程中,可以在PR阶段自动发现潜在问题。本文介绍如何用GitHub Actions + LLM API搭建自动化Code Review,包括Prompt设计和误报控制策略。

LLM做Code Review的优势与边界

LLM擅长的审查场景:

  • 风格一致性:命名规范、注释质量、代码组织
  • 常见Bug模式:空指针、资源泄漏、边界条件
  • 安全问题:SQL注入、XSS、硬编码密钥
  • 文档缺失:公开API缺少文档、复杂逻辑缺少注释

LLM不擅长的:

  • 业务逻辑正确性(它不知道你的业务)
  • 性能优化(缺少运行时上下文)
  • 架构决策(需要全局视角)

明确边界很重要,否则团队会因为误报而关掉这个工具。

架构设计

整体流程:

PR创建/更新 → GitHub Actions触发 → 获取diff → 分块发给LLM → 解析响应 → 发表Review Comment

核心组件:

  1. GitHub Actions Workflow:监听PR事件,协调流程
  2. Diff解析器:将git diff拆分为有意义的审查单元
  3. LLM客户端:调用API并处理结果
  4. Comment发布器:将审查意见发布为PR inline comment

GitHub Actions集成

# .github/workflows/ai-review.yml
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    # 跳过draft PR和dependabot
    if: |
      !github.event.pull_request.draft &&
      github.actor != 'dependabot[bot]'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install openai PyGithub unidiff

      - name: Run AI Review
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: python scripts/ai_review.py

核心Review脚本

#!/usr/bin/env python3
"""ai_review.py - LLM驱动的自动代码审查"""
import os
import json
from openai import OpenAI
from github import Github
from unidiff import PatchSet

# 配置
REVIEW_MODEL = "gpt-4o"
MAX_DIFF_LINES = 500  # 单个文件最大diff行数,超过则跳过
SKIP_PATTERNS = [
    "*.lock", "*.sum", "*.min.js", "*.min.css",
    "package-lock.json", "go.sum", "Cargo.lock",
    "*.pb.go", "*_generated.*",  # 生成的代码
]

def should_skip_file(filename: str) -> bool:
    """检查文件是否应跳过审查"""
    import fnmatch
    return any(fnmatch.fnmatch(filename, p) for p in SKIP_PATTERNS)

def get_pr_diff(repo, pr_number: int) -> str:
    """获取PR的diff内容"""
    pr = repo.get_pull(pr_number)
    # 使用compare API获取完整diff
    comparison = repo.compare(pr.base.sha, pr.head.sha)
    return comparison.diff_url, pr

def parse_diff_to_chunks(diff_text: str) -> list[dict]:
    """将diff解析为审查块"""
    patch = PatchSet(diff_text)
    chunks = []
    for f in patch:
        if should_skip_file(f.path):
            continue
        # 提取变更内容
        added_lines = []
        for hunk in f:
            context = []
            for line in hunk:
                if line.is_added:
                    context.append(f"+{line.value}")
                elif line.is_removed:
                    context.append(f"-{line.value}")
                else:
                    context.append(f" {line.value}")
            added_lines.extend(context)

        if len(added_lines) > MAX_DIFF_LINES:
            continue  # 太大的diff跳过

        chunks.append({
            "file": f.path,
            "diff": "".join(added_lines),
            "is_new": f.is_added_file,
        })
    return chunks

def review_chunk(client: OpenAI, chunk: dict) -> list[dict]:
    """用LLM审查单个代码块"""
    response = client.chat.completions.create(
        model=REVIEW_MODEL,
        temperature=0.1,  # 低温度,输出更确定
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": format_review_request(chunk)},
        ],
    )
    result = json.loads(response.choices[0].message.content)
    return result.get("issues", [])

Prompt设计

Prompt是整个系统的核心。设计目标:精确、低误报、可执行的反馈。

SYSTEM_PROMPT = """你是一个严格的代码审查助手。审查规则:

## 审查范围
只关注diff中新增和修改的代码(+开头的行)。不要评论删除的代码。

## 严重程度
- critical: 安全漏洞、数据丢失风险、必定导致崩溃的bug
- warning: 潜在bug、资源泄漏、错误处理缺失
- suggestion: 代码风格、可读性改进、性能优化建议

## 输出要求
- 只报告你有高置信度的问题
- 每个问题必须引用具体的代码行
- 提供修复建议
- 如果没有发现问题,返回空数组

## 输出格式
```json
{
  "issues": [
    {
      "line": 42,
      "severity": "warning",
      "message": "问题描述",
      "suggestion": "修复建议"
    }
  ]
}

不要报告

  • 纯风格偏好(如大括号位置)
  • 已有lint工具覆盖的问题(如未使用的import)
  • 测试代码中的硬编码值
  • 对第三方库用法的主观看法"""

def format_review_request(chunk: dict) -> str:
file_type = "新文件" if chunk["is_new"] else "修改"
return f"""请审查以下{file_type}的代码变更:

文件: {chunk['file']}

{chunk['diff']}
```"""

几个关键设计决策:

  • 低temperature(0.1):审查结果需要确定性,不需要创造性
  • JSON输出:结构化输出便于后续处理,避免解析自然语言
  • 明确的"不要报告"列表:减少噪音是关键,否则开发者会无视所有审查意见
  • severity分级:让开发者可以优先处理critical,忽略suggestion

误报控制

误报是AI Code Review最大的敌人。误报率超过30%,开发者就不会看了。

策略1:置信度过滤

在Prompt中要求LLM给出置信度,只保留高置信度的结果:

# 修改输出格式,添加confidence字段
# "confidence": 0.0-1.0

def filter_issues(issues: list[dict], threshold: float = 0.7) -> list[dict]:
    return [i for i in issues if i.get("confidence", 0) >= threshold]

策略2:文件类型适配

不同文件类型关注不同方面:

FILE_RULES = {
    ".go": "关注error处理(是否忽略error返回值)、goroutine泄漏、defer使用",
    ".py": "关注类型标注、异常处理、资源管理(with语句)",
    ".js": "关注null/undefined检查、async/await错误处理、XSS风险",
    ".sql": "关注SQL注入、缺少WHERE的UPDATE/DELETE、索引使用",
}

def get_file_specific_rules(filename: str) -> str:
    ext = os.path.splitext(filename)[1]
    return FILE_RULES.get(ext, "")

策略3:反馈循环

收集开发者对AI审查意见的反应(resolved/dismissed),用来持续优化Prompt:

def collect_feedback(repo, pr_number: int) -> dict:
    """收集开发者对AI review comment的反馈"""
    pr = repo.get_pull(pr_number)
    stats = {"resolved": 0, "dismissed": 0, "total": 0}
    for comment in pr.get_review_comments():
        if comment.user.login == "github-actions[bot]":
            stats["total"] += 1
            # 通过reaction来判断反馈
            for reaction in comment.get_reactions():
                if reaction.content == "+1":
                    stats["resolved"] += 1
                elif reaction.content == "-1":
                    stats["dismissed"] += 1
    return stats

策略4:增量审查

只审查新增的变更,避免对已审查代码重复报告:

def get_incremental_diff(repo, pr, last_review_sha: str) -> str:
    """只获取自上次审查以来的新变更"""
    if last_review_sha:
        comparison = repo.compare(last_review_sha, pr.head.sha)
    else:
        comparison = repo.compare(pr.base.sha, pr.head.sha)
    return comparison

发布Review Comment

def post_review_comments(repo, pr, issues_by_file: dict):
    """将审查意见作为PR inline comment发布"""
    comments = []
    for filename, issues in issues_by_file.items():
        for issue in issues:
            severity_emoji = {
                "critical": "🔴",
                "warning": "🟡",
                "suggestion": "💡",
            }.get(issue["severity"], "💡")

            body = f"{severity_emoji} **{issue['severity'].upper()}**: {issue['message']}"
            if issue.get("suggestion"):
                body += f"\n\n**建议**: {issue['suggestion']}"

            comments.append({
                "path": filename,
                "line": issue["line"],
                "body": body,
            })

    if comments:
        pr.create_review(
            body=f"AI Code Review 发现 {len(comments)} 个问题",
            event="COMMENT",
            comments=comments,
        )
    else:
        # 没问题也留个标记
        pr.create_issue_comment("✅ AI Code Review: 未发现问题")

成本控制

LLM API调用有成本,需要控制:

# 跳过小改动(如只改了README)
MIN_CODE_CHANGES = 5  # 至少5行代码变更才触发

# 限制每次审查的token预算
MAX_TOKENS_PER_REVIEW = 50000

# 使用缓存避免重复审查
import hashlib
def diff_hash(diff_text: str) -> str:
    return hashlib.sha256(diff_text.encode()).hexdigest()

小结

AI Code Review不是要取代人工审查,而是把人从机械性检查中解放出来。关键是:

  1. 控制误报:宁可漏报也不要误报,开发者的信任很脆弱
  2. 明确边界:告诉团队AI能查什么、不能查什么
  3. 持续优化:收集反馈,迭代Prompt
  4. 渐进引入:先从suggestion级别开始,等团队信任后再提升到blocking review

实际使用下来,AI在发现错误处理缺失、安全隐患方面确实有效,但对业务逻辑的理解仍然很浅。合理定位,它是一个有价值的辅助工具。