AI 代码审查实战——在 CI/CD 里加一道 AI 质量门禁
AI 代码审查实战——在 CI/CD 里加一道 AI 质量门禁
适读人群:关心代码质量的工程师和技术负责人 | 阅读时长:约15分钟 | 核心价值:完整的AI代码审查CI/CD集成方案,包括Prompt设计、阻断策略和误报控制
去年年底,我们团队出了一个线上事故。
一个同事在处理一个紧急需求时,写了一段代码,绕过了正常的Review流程直接合并进了main分支。这段代码里有一个SQL注入漏洞——不是什么复杂的变种,就是直接把用户输入拼接进了查询语句,任何人认真看一眼都能发现。
代码在生产跑了三天,被安全扫描发现。修复成本倒不大,但那三天的暴露窗口让我们非常不舒服。
事后我们复盘,结论是:不是流程有问题,是执行有时效漏洞。紧急情况下人容易出错,审查质量下降,门禁形同虚设。
我就想:能不能在CI/CD里加一道AI审查,作为额外的安全网?不是替代人工Review,是在人工Review之前先跑一遍,把明显的问题拦住。
这篇文章把我这套方案完整地写出来。
方案架构
整个方案的逻辑是这样的:
开发者提交PR
|
v
CI流水线触发
|
v
获取本次PR的diff
|
v
调用AI进行代码审查
|
v
解析审查结果
|
[严重问题?]
/ \
是 否
| |
阻断PR 添加审查评论(不阻断)关键决策点:什么问题阻断PR,什么问题只留评论。
我的策略:
- 阻断:安全漏洞(注入、认证绕过、敏感信息泄露)、明显的数据安全问题
- 评论不阻断:代码质量问题、性能建议、风格问题、最佳实践建议
这样设计是为了控制误报带来的工程师负担。AI发现的"问题"有相当一部分是误报或者是有争议的判断。如果把所有AI建议都设成阻断,会让工程师很快就开始忽视这套机制。
GitHub Actions 完整配置
# .github/workflows/ai-code-review.yml
name: AI Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
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 Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: pip install anthropic PyGithub
- name: Get PR diff
id: get-diff
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git fetch origin ${{ github.base_ref }}
git diff origin/${{ github.base_ref }}...HEAD -- '*.py' '*.java' '*.js' '*.ts' '*.go' > /tmp/pr_diff.txt
echo "diff_size=$(wc -c < /tmp/pr_diff.txt)" >> $GITHUB_OUTPUT
- name: Run AI Code Review
id: ai-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
- name: Block PR if critical issues found
if: steps.ai-review.outputs.has_critical == 'true'
run: |
echo "Critical issues found by AI review. PR blocked."
exit 1核心审查脚本
# .github/scripts/ai_review.py
import os
import json
import sys
from anthropic import Anthropic
from github import Github
def load_diff(diff_path: str) -> str:
with open(diff_path, 'r', encoding='utf-8', errors='replace') as f:
return f.read()
def truncate_diff(diff: str, max_chars: int = 60000) -> str:
"""超大diff需要截断,避免超出上下文限制"""
if len(diff) <= max_chars:
return diff
return diff[:max_chars] + "\n\n[注意:diff过大,已截断显示前60000字符]"
REVIEW_SYSTEM_PROMPT = """你是一个专业的代码安全和质量审查专家。你的任务是审查代码diff,识别问题。
**输出格式**:必须严格输出JSON,不要有任何额外文字。格式如下:
{
"summary": "本次变更的简要描述",
"critical_issues": [
{
"severity": "CRITICAL",
"category": "安全漏洞类型",
"description": "问题描述",
"location": "文件名:行号范围(如果能确定)",
"suggestion": "修复建议"
}
],
"warnings": [
{
"severity": "WARNING",
"category": "问题类型",
"description": "问题描述",
"location": "文件名:行号范围",
"suggestion": "改进建议"
}
],
"positive_notes": ["做得好的地方"],
"overall_assessment": "APPROVE / REQUEST_CHANGES / BLOCK"
}
**CRITICAL问题的标准**(只有以下情况才标记为CRITICAL):
- SQL注入、命令注入、代码注入漏洞
- 认证绕过或权限校验缺失
- 硬编码的密码、API密钥、私钥等敏感信息
- 路径遍历漏洞
- 不安全的反序列化
- 明显的数据泄露风险(如将用户隐私数据写入日志)
**不要**把以下问题标记为CRITICAL:
- 代码风格问题
- 性能优化建议
- 可以争论的最佳实践
- 缺少注释
如果没有CRITICAL问题,critical_issues返回空数组。"""
REVIEW_USER_PROMPT = """请审查以下代码diff:
\`\`\`diff
{diff}
\`\`\`
记住:只把真正的安全漏洞和严重问题标记为CRITICAL。代码质量问题放在warnings里。"""
def run_ai_review(diff: str) -> dict:
client = Anthropic()
truncated_diff = truncate_diff(diff)
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
system=REVIEW_SYSTEM_PROMPT,
messages=[
{
"role": "user",
"content": REVIEW_USER_PROMPT.format(diff=truncated_diff)
}
]
)
raw_output = response.content[0].text.strip()
# 提取JSON(有时模型会输出```json ... ```格式)
if raw_output.startswith("```"):
lines = raw_output.split("\n")
json_lines = [l for l in lines if not l.startswith("```")]
raw_output = "\n".join(json_lines)
return json.loads(raw_output)
def format_pr_comment(review_result: dict) -> str:
lines = ["## AI代码审查报告\n"]
lines.append(f"**变更摘要**:{review_result.get('summary', '无')}\n")
critical = review_result.get("critical_issues", [])
warnings = review_result.get("warnings", [])
positives = review_result.get("positive_notes", [])
if critical:
lines.append("### 🔴 严重问题(需要修复才能合并)\n")
for i, issue in enumerate(critical, 1):
lines.append(f"**{i}. {issue['category']}**")
lines.append(f"- **位置**:{issue.get('location', '未知')}")
lines.append(f"- **描述**:{issue['description']}")
lines.append(f"- **修复建议**:{issue['suggestion']}\n")
if warnings:
lines.append("### 🟡 建议改进(不阻断合并)\n")
for i, warn in enumerate(warnings, 1):
lines.append(f"**{i}. {warn['category']}**")
lines.append(f"- **位置**:{warn.get('location', '未知')}")
lines.append(f"- **描述**:{warn['description']}")
lines.append(f"- **建议**:{warn['suggestion']}\n")
if positives:
lines.append("### 做得好的地方\n")
for p in positives:
lines.append(f"- {p}")
if not critical and not warnings:
lines.append("\n代码审查通过,未发现明显问题。")
lines.append("\n---")
lines.append("*此审查由AI自动生成,不能替代人工Review。如对AI判断有异议,请在评论中说明。*")
return "\n".join(lines)
def main():
diff = load_diff("/tmp/pr_diff.txt")
if len(diff.strip()) < 10:
print("Diff太小,跳过审查")
# 写入GitHub Actions output
with open(os.environ.get("GITHUB_OUTPUT", "/dev/null"), "a") as f:
f.write("has_critical=false\n")
return
print("运行AI代码审查...")
try:
review_result = run_ai_review(diff)
except json.JSONDecodeError as e:
print(f"AI输出解析失败:{e}")
# 解析失败不阻断PR,记录错误
with open(os.environ.get("GITHUB_OUTPUT", "/dev/null"), "a") as f:
f.write("has_critical=false\n")
return
# 发布PR评论
gh = Github(os.environ["GITHUB_TOKEN"])
repo = gh.get_repo(os.environ["REPO_NAME"])
pr = repo.get_pull(int(os.environ["PR_NUMBER"]))
comment_body = format_pr_comment(review_result)
pr.create_issue_comment(comment_body)
# 决定是否阻断
has_critical = len(review_result.get("critical_issues", [])) > 0
with open(os.environ.get("GITHUB_OUTPUT", "/dev/null"), "a") as f:
f.write(f"has_critical={'true' if has_critical else 'false'}\n")
if has_critical:
print(f"发现{len(review_result['critical_issues'])}个严重问题,将阻断PR")
else:
print(f"未发现严重问题,PR可以继续")
if __name__ == "__main__":
main()Prompt设计的几个关键决策
决策1:强制JSON输出。
如果让模型自由格式输出,解析起来很麻烦,而且格式会飘。强制JSON输出,配合Claude在结构化输出上的稳定性,解析成功率很高。
决策2:精确定义CRITICAL的标准。
这是误报控制的核心。我在Prompt里把CRITICAL的判断标准写得非常具体,同时明确列出"不要标记为CRITICAL"的情况。早期版本没有这部分,模型会把"这个函数太长了"这种问题标成CRITICAL,导致大量误报,工程师很快就不信任这套机制了。
决策3:加了"不要把争议性最佳实践标为CRITICAL"。
AI对最佳实践有自己的偏好,比如它很喜欢让你加错误处理,这本身是对的,但如果每次都阻断PR要求你加更多错误处理,会让人崩溃。
决策4:AI输出解析失败不阻断PR。
这是兜底策略。AI偶尔会输出格式不对的内容,不能因为AI自己的问题去影响正常的开发流程。
误报率控制
上线三个月以来的实际数据:
- 总审查PR数:约380个
- 触发阻断的:23个
- 其中真正有问题的:19个(82.6%准确率)
- 误报4个(占全部PR的1%)
这个数字我觉得在可接受范围内。4个误报里,3个是AI把一些特殊的字符串常量认成了"硬编码密码",1个是把一个测试用的mock签名认成了真实的API密钥。
处理误报的流程:工程师在PR评论里回复说明,由另一个工程师确认后可以手动绕过。绕过记录会被记录,定期回顾用来改进Prompt。
Jenkins的替代方案
如果你用Jenkins而不是GitHub Actions,核心脚本是一样的,只是触发方式不同:
// Jenkinsfile片段
stage('AI Code Review') {
steps {
script {
// 获取diff
sh 'git diff origin/${env.CHANGE_TARGET}...HEAD -- "*.py" "*.java" "*.js" > /tmp/pr_diff.txt'
// 运行审查脚本
def result = sh(
script: 'python .jenkins/scripts/ai_review.py',
returnStatus: true
)
// 读取输出
def hasCritical = readFile('/tmp/ai_review_critical.txt').trim()
if (hasCritical == 'true') {
error('AI代码审查发现严重问题,请查看PR评论')
}
}
}
}还没解决的问题
说实话,这套方案目前还有两个没完全解决的问题:
大diff问题。 如果一个PR改动了几千行,截断处理会漏掉一部分代码。目前的workaround是把大PR拆文件分批审查,但逻辑比较复杂,还没加进去。
跨文件安全问题检测弱。 AI只能看到diff本身,如果一个安全问题需要理解两个文件之间的关系才能发现,它很容易漏掉。这类问题还是要靠人工Review。
这套方案定位很明确:捕获明显的、单文件可见的安全问题,作为人工Review的前置补充,不是替代。在这个定位下,它确实有用,我们已经用它抓出过3个真实的安全漏洞,都是在正式Review之前被拦下来的。
