第1842篇:AI代码审查工具的工程化集成——在GitHub Actions中自动评审PR
第1842篇:AI代码审查工具的工程化集成——在GitHub Actions中自动评审PR
说一件让我有点尴尬的事。
去年年初,我们团队做了一次代码质量回顾,统计了过去六个月合并到主干的PR里,有多少是"通过了Code Review但上线后出问题的"。结果是31%。三成多的PR,人工审查没问题,但到了生产环境就出了各种麻烦——性能问题、并发bug、空指针,什么都有。
问题出在哪?人工Review的精力是有限的,大家盯着逻辑看,很容易忽略那些"不那么明显的模式问题"。比如一个方法里不必要地创建了一个大对象、一个循环里做了数据库查询、一个没加超时设置的HTTP调用。
这些问题,AI很擅长发现。
这篇文章,我来讲讲怎么把AI代码审查集成进GitHub Actions,构建一个真正能在工程上跑起来的自动化PR评审流水线。
先讲清楚AI Code Review能做什么、不能做什么
很多团队对AI Code Review有误解,要么期望太高,要么完全不信任。我的观点是:
AI擅长的:
- 发现常见代码模式问题(N+1查询、资源未释放、NPE风险)
- 识别潜在的安全漏洞(SQL注入、不安全的反序列化)
- 检查代码风格和命名规范
- 发现遗漏的边界条件处理
- 对复杂逻辑给出可读性建议
AI不擅长的:
- 判断业务逻辑是否正确(它不知道你的业务规则)
- 评估架构层面的设计决策
- 理解跨文件、跨服务的复杂依赖关系
- 说"这个PR影响了XX功能,但你没测试那个分支"
明白了边界,就不会对AI Code Review产生不切实际的期望。
整体架构设计
这套流程里有几个关键决策点需要讲清楚:
- 发送整个文件还是只发diff:建议只发diff加上适量上下文(前后各5-10行),原因是Token限制和成本控制
- 同步还是异步:Review可以是异步的,不阻塞PR合并,只作为建议
- 评论发在哪里:可以发行内评论(inline comment)或者PR级别的总结评论
GitHub Actions配置
先看整体的workflow文件:
# .github/workflows/ai-code-review.yml
name: AI Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- '**.txt'
- '.gitignore'
jobs:
ai-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install anthropic PyGithub python-dotenv
- name: Run AI Code Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_NAME: ${{ github.repository }}
run: |
python .github/scripts/ai_review.py核心审查脚本
这是真正做事的Python脚本,我会详细解释每个关键部分:
# .github/scripts/ai_review.py
import os
import sys
import json
import anthropic
from github import Github
from typing import List, Dict, Optional
class CodeReviewConfig:
"""审查配置,可以根据项目定制"""
# 单个文件diff超过这个token数,就跳过(避免成本爆炸)
MAX_DIFF_TOKENS = 3000
# 这些文件不需要AI审查
SKIP_PATTERNS = [
'test/',
'src/test/',
'.xml',
'pom.xml',
'application.yml',
'application.properties',
'Dockerfile',
'docker-compose'
]
# Java特定的审查重点
JAVA_REVIEW_FOCUS = """
重点检查以下Java常见问题:
1. 资源管理:Stream、Connection、Reader等是否正确关闭(try-with-resources)
2. 并发安全:共享状态的线程安全性,是否存在竞争条件
3. 空指针风险:Optional使用是否正确,入参是否需要null检查
4. 集合操作:是否存在ConcurrentModificationException风险
5. 异常处理:catch块是否吞了异常,是否有必要的日志
6. 数据库操作:是否存在N+1查询,事务边界是否合理
7. 字符串操作:循环内是否使用了+拼接(应用StringBuilder)
8. equals/hashCode:重写了equals是否也重写了hashCode
"""
class PullRequestReviewer:
def __init__(self):
self.anthropic_client = anthropic.Anthropic(
api_key=os.environ['ANTHROPIC_API_KEY']
)
self.github_client = Github(os.environ['GITHUB_TOKEN'])
self.repo = self.github_client.get_repo(os.environ['REPO_NAME'])
self.pr_number = int(os.environ['PR_NUMBER'])
self.pr = self.repo.get_pull(self.pr_number)
self.config = CodeReviewConfig()
def should_skip_file(self, filename: str) -> bool:
"""判断文件是否需要跳过审查"""
for pattern in self.config.SKIP_PATTERNS:
if pattern in filename:
return True
return False
def get_pr_files(self) -> List[Dict]:
"""获取PR中变更的文件列表"""
files = []
for file in self.pr.get_files():
if self.should_skip_file(file.filename):
print(f"跳过文件: {file.filename}")
continue
# 只处理Java文件
if not file.filename.endswith('.java'):
continue
if file.status == 'removed':
continue
files.append({
'filename': file.filename,
'patch': file.patch or '',
'status': file.status,
'additions': file.additions,
'deletions': file.deletions
})
return files
def estimate_tokens(self, text: str) -> int:
"""粗略估算token数(英文约4字符=1token,中文约1.5字符=1token)"""
return len(text) // 3
def review_file(self, file_info: Dict) -> Optional[str]:
"""对单个文件进行AI审查"""
patch = file_info['patch']
if not patch:
return None
# Token超限保护
if self.estimate_tokens(patch) > self.config.MAX_DIFF_TOKENS:
return f"⚠️ 文件变更量过大({file_info['additions']}行新增),建议人工重点审查。"
prompt = f"""你是一个资深Java工程师,正在做代码审查。
文件路径:{file_info['filename']}
变更类型:{file_info['status']}
以下是代码变更的diff(+号开头是新增行,-号开头是删除行):
```diff
{patch}请按以下格式给出审查意见(只指出实际存在的问题,没有问题就直接说"代码质量良好"):
严重问题(可能导致bug或安全漏洞):
- [问题描述,附具体行号]
建议优化(影响性能或可维护性):
- [建议描述,附具体行号]
代码风格(不影响功能但值得注意):
- [风格建议]
注意:
只评论diff中新增的代码(+开头的行),不评论被删除的代码
给出具体的行号引用,不要泛泛而谈
如果代码质量良好,不要硬找问题 """
try: response = self.anthropic_client.messages.create( model="claude-opus-4-5", max_tokens=1500, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text except Exception as e: print(f"AI审查出错: {e}") return Nonedef post_review_comment(self, review_results: List[Dict]): """发布审查结果评论到PR"""
if not review_results: return # 构建总结评论 comment_body = "## 🤖 AI代码审查报告\n\n" comment_body += f"> 自动审查时间:{self._get_current_time()}\n" comment_body += f"> 审查文件数:{len(review_results)} 个\n\n" comment_body += "---\n\n" has_serious_issues = False for result in review_results: comment_body += f"### 📄 `{result['filename']}`\n\n" comment_body += result['review'] + "\n\n" comment_body += "---\n\n" if "严重问题" in result['review'] and "无" not in result['review']: has_serious_issues = True # 末尾加免责说明 comment_body += "\n> ⚠️ **注意**:AI审查作为辅助参考,不能替代人工审查。\n" comment_body += "> 对于业务逻辑正确性的判断,仍需要人工确认。\n" # 删除旧的AI审查评论(避免重复) self._delete_old_ai_comments() # 发布新评论 self.pr.create_issue_comment(comment_body) # 如果有严重问题,添加标签 if has_serious_issues: try: self.pr.add_to_labels("ai-review: needs-attention") except Exception: pass # 标签不存在就算了def _delete_old_ai_comments(self): """删除之前的AI审查评论,避免刷屏""" for comment in self.pr.get_issue_comments(): if comment.body.startswith("## 🤖 AI代码审查报告"): try: comment.delete() except Exception: pass
def _get_current_time(self) -> str: from datetime import datetime, timezone return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
def run(self): """主流程""" print(f"开始审查PR #{self.pr_number}: {self.pr.title}")
files = self.get_pr_files() print(f"需要审查的文件数: {len(files)}") if not files: print("没有需要审查的Java文件") return review_results = [] for file_info in files: print(f"正在审查: {file_info['filename']}") review = self.review_file(file_info) if review: review_results.append({ 'filename': file_info['filename'], 'review': review }) self.post_review_comment(review_results) print("审查完成!")
if name == "main": reviewer = PullRequestReviewer() reviewer.run()
---
## 进阶:差异化审查策略
不是所有PR都需要同等程度的审查。我们可以根据PR的特征动态调整策略:
```python
class SmartReviewStrategy:
"""根据PR特征选择不同的审查策略"""
def determine_strategy(self, pr) -> str:
labels = [l.name for l in pr.labels]
title = pr.title.lower()
# 标记了hotfix的,需要更严格的审查
if 'hotfix' in labels or 'hotfix' in title:
return 'strict'
# 只改了测试的PR,用轻量审查
files = list(pr.get_files())
all_test_files = all('test' in f.filename.lower() for f in files)
if all_test_files:
return 'lightweight'
# 变更超过500行,给警告但不逐行审查
total_changes = sum(f.additions + f.deletions for f in files)
if total_changes > 500:
return 'summary_only'
return 'standard'
def get_prompt_for_strategy(self, strategy: str, file_info: dict) -> str:
prompts = {
'strict': """
这是一个紧急修复(hotfix),请进行最严格的代码审查。
除了常规问题外,特别关注:
1. 这个修改是否可能引入新的边界问题
2. 是否有足够的错误处理
3. 是否需要回滚方案
""",
'lightweight': """
这是测试代码,关注:
1. 测试覆盖是否全面
2. 测试用例命名是否清晰
3. Mock是否合理
""",
'standard': """
标准代码审查,关注常见Java问题。
"""
}
return prompts.get(strategy, prompts['standard'])成本控制的实战经验
这个话题很现实,AI API调用是有成本的。如果每个PR都全量审查,一个活跃的团队每个月API费用可能让你吃一惊。
我总结了几个控制成本的方法:
方法一:只审查高风险文件
HIGH_RISK_PATTERNS = [
'Service.java', # 业务逻辑层
'Repository.java', # 数据访问层
'Controller.java', # 接口层
'Security', # 安全相关
'Payment', # 支付相关
'Auth', # 认证相关
]
def is_high_risk(filename: str) -> bool:
return any(pattern in filename for pattern in HIGH_RISK_PATTERNS)方法二:diff长度过滤
超过一定行数的diff,很可能是机械性的重构(比如包名修改、格式化),不需要AI逐行审查:
def should_use_ai(file_info: dict) -> bool:
# 超过300行变更,AI审查意义不大
if file_info['additions'] + file_info['deletions'] > 300:
return False
# 新增行太少,通常是删除代码,不需要详细审查
if file_info['additions'] < 5:
return False
return True方法三:限流保护
import time
class RateLimitedReviewer:
def __init__(self, requests_per_minute: int = 10):
self.requests_per_minute = requests_per_minute
self.request_times = []
def wait_if_needed(self):
now = time.time()
# 清理1分钟前的记录
self.request_times = [t for t in self.request_times if now - t < 60]
if len(self.request_times) >= self.requests_per_minute:
wait_time = 60 - (now - self.request_times[0])
if wait_time > 0:
print(f"达到速率限制,等待{wait_time:.1f}秒")
time.sleep(wait_time)
self.request_times.append(now)一个真实的踩坑故事
集成这套系统的时候,有个问题我折腾了挺久:AI审查结果的稳定性。
同样的代码,不同时间问AI,有时候给出的建议完全不同。这在CI/CD环境里是个麻烦事——可能上午Review通过了,下午重新触发了流水线,又出现了不同的意见,让开发者不知道该听哪个。
解决方案是两个:
温度设置为0(对于Claude来说,通过系统提示约束输出格式):让输出更确定性
结果缓存:对相同的文件内容(通过hash判断),缓存审查结果:
import hashlib
import json
import os
class ReviewCache:
def __init__(self, cache_dir: str = "/tmp/ai_review_cache"):
self.cache_dir = cache_dir
os.makedirs(cache_dir, exist_ok=True)
def get_cache_key(self, filename: str, patch: str) -> str:
content = f"{filename}:{patch}"
return hashlib.md5(content.encode()).hexdigest()
def get(self, filename: str, patch: str) -> Optional[str]:
key = self.get_cache_key(filename, patch)
cache_file = os.path.join(self.cache_dir, f"{key}.json")
if os.path.exists(cache_file):
# 缓存1小时内有效
if time.time() - os.path.getmtime(cache_file) < 3600:
with open(cache_file) as f:
return json.load(f)['review']
return None
def set(self, filename: str, patch: str, review: str):
key = self.get_cache_key(filename, patch)
cache_file = os.path.join(self.cache_dir, f"{key}.json")
with open(cache_file, 'w') as f:
json.dump({'review': review}, f)效果评估
集成三个月后,我们做了效果统计:
- AI发现的问题中,约65%被开发者认为"有价值"
- 约20%的问题是"有道理但不在本次PR的修改范围"
- 约15%是"过度担心"或"不适用于我们的场景"
- 合并后出现问题的PR比例从31%降到了19%
65%的有效率,说明AI Code Review确实在发挥作用。但别被这个数字迷惑——真正有价值的不是那些被发现的bug,而是整个团队因为知道"AI在看着"而写代码更谨慎了。
这种无形的约束效应,比任何具体发现的bug都更值钱。
