将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
核心组件:
- GitHub Actions Workflow:监听PR事件,协调流程
- Diff解析器:将git diff拆分为有意义的审查单元
- LLM客户端:调用API并处理结果
- 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不是要取代人工审查,而是把人从机械性检查中解放出来。关键是:
- 控制误报:宁可漏报也不要误报,开发者的信任很脆弱
- 明确边界:告诉团队AI能查什么、不能查什么
- 持续优化:收集反馈,迭代Prompt
- 渐进引入:先从suggestion级别开始,等团队信任后再提升到blocking review
实际使用下来,AI在发现错误处理缺失、安全隐患方面确实有效,但对业务逻辑的理解仍然很浅。合理定位,它是一个有价值的辅助工具。