欢迎光临
我们一直在努力

AI + 代码审查:搭建 Code Review Agent

我在之前的文章里说过一个得罪人的观点:大多数团队的 Code Review 形同虚设。

不是大家不认真——是人真的没时间。一个 300 行的 PR,认真审查至少要 15-20 分钟。一天 5 个 PR 就是 1.5 小时。谁能在 996 的节奏里每天挤出 1.5 小时做 CR?

于是 CR 变成了「LGTM」「+1」「先合吧后面再改」。

我的解决方案:写一个自动化的 Code Review Agent。 每次 PR 提交时自动审查,发现问题直接评论到 PR 下面。不是替代人审——是让人只审 AI 标记出来的地方。


架构

GitHub Webhook
→ PR 事件触发
→ 拉取 PR diff
→ AI 审查(安全检查 + 性能 + 风格 + 测试)
→ 生成审查报告
→ 通过 GitHub API 发布 Review Comment

核心三步:拉 diff → AI 审查 → 发评论。


第一步:拉取 PR 变更内容

package main

import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
)

type PRInfo struct {
Owner string
Repo string
PRNumber int
Title string
DiffURL string
Diff string
}

// 解析 GitHub Webhook payload
func parseWebhook(r *http.Request) (*PRInfo, error) {
eventType := r.Header.Get("X-GitHub-Event")
if eventType != "pull_request" {
return nil, fmt.Errorf("非 PR 事件: %s", eventType)
}

var payload struct {
Action string `json:"action"`
PullRequest struct {
Number int `json:"number"`
Title string `json:"title"`
DiffURL string `json:"diff_url"`
Base struct {
Repo struct {
Owner struct {
Login string `json:"login"`
} `json:"owner"`
Name string `json:"name"`
} `json:"repo"`
} `json:"base"`
} `json:"pull_request"`
}

json.NewDecoder(r.Body).Decode(&payload)

// 只审查新开和更新的 PR
if payload.Action != "opened" && payload.Action != "synchronize" {
return nil, fmt.Errorf("跳过事件: %s", payload.Action)
}

return &PRInfo{
Owner: payload.PullRequest.Base.Repo.Owner.Login,
Repo: payload.PullRequest.Base.Repo.Name,
PRNumber: payload.PullRequest.Number,
Title: payload.PullRequest.Title,
DiffURL: payload.PullRequest.DiffURL,
}, nil
}

// 拉取 PR diff
func fetchDiff(pr *PRInfo) error {
token := os.Getenv("GITHUB_TOKEN")
req, _ := http.NewRequest("GET", pr.DiffURL, nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/vnd.github.v3.diff")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

var buf strings.Builder
buf.ReadFrom(resp.Body)
pr.Diff = buf.String()
return nil
}


第二步:AI 审查引擎

这是核心。审查引擎检查四个维度:

type ReviewIssue struct {
File string `json:"file"`
Line int `json:"line"`
Severity string `json:"severity"` // high / medium / low
Category string `json:"category"` // security / performance / style / test
Title string `json:"title"`
Detail string `json:"detail"`
Suggestion string `json:"suggestion"`
}

func reviewDiff(diff string) ([]ReviewIssue, error) {
// diff 可能很长,截断到 LLM 能处理的范围
maxTokens := 40000
if len(diff) > maxTokens {
diff = diff[:maxTokens] + "\\n… (diff 太长,已截断)"
}

prompt := fmt.Sprintf(`你是资深 Go 代码审查员。审查以下 PR diff,按四个维度检查。

## 审查维度

### 1. 安全 (Security)
– SQL / NoSQL 注入风险
– 敏感信息泄露(日志输出密码、Token 等)
– 未检查的权限
– 不安全的文件操作(路径遍历)

### 2. 性能 (Performance)
– 循环中的不必要的内存分配
– 未关闭的资源(文件句柄、数据库连接、HTTP Body)
– N+1 查询
– 高并发下的锁竞争

### 3. 代码风格 (Style)
– 错误处理不一致(有的返回 error,有的 panic)
– 命名不规范
– 重复代码
– 过长的函数(超过 50 行)

### 4. 测试 (Test)
– 缺少边界条件测试
– 测试中有 hardcode 的时间/随机值
– Mock 不完整导致测试可能非确定性

## 输出格式

返回 JSON 数组(不要 Markdown 包裹):

[
{
"file": "文件路径",
"line": 行号,
"severity": "high | medium | low",
"category": "security | performance | style | test",
"title": "问题简述",
"detail": "详细说明",
"suggestion": "修复建议(代码 diff 格式)"
}
]

如果没有发现问题,返回空数组 []。

PR diff:
%s`, diff)

result, err := callLLM("你是代码审查专家,只返回 JSON。", prompt)
if err != nil {
return nil, err
}

result = cleanJSON(result)
var issues []ReviewIssue
json.Unmarshal([]byte(result), &issues)
return issues, nil
}


第三步:发 Review Comment 到 GitHub PR

func postReview(pr *PRInfo, issues []ReviewIssue) error {
token := os.Getenv("GITHUB_TOKEN")

// 构建 review body
var body strings.Builder

// 统计摘要
highCount, medCount, lowCount := 0, 0, 0
for _, iss := range issues {
switch iss.Severity {
case "high": highCount++
case "medium": medCount++
case "low": lowCount++
}
}

body.WriteString(fmt.Sprintf(`## 🤖 AI Code Review

| 高危 🔴 | 中危 🟡 | 低危 🟢 |
|———|———|———|
| %d | %d | %d |

—
`, highCount, medCount, lowCount))

// 按严重程度分组列出
severities := []string{"high", "medium", "low"}
icons := map[string]string{"high": "🔴", "medium": "🟡", "low": "🟢"}
categories := map[string]string{
"security": "安全", "performance": "性能",
"style": "风格", "test": "测试",
}

for _, sev := range severities {
for _, iss := range issues {
if iss.Severity != sev {
continue
}
body.WriteString(fmt.Sprintf(
"### %s [%s] %s\\n",
icons[sev], categories[iss.Category], iss.Title,
))
body.WriteString(fmt.Sprintf(
"**文件:** `%s:%d`\\n\\n", iss.File, iss.Line,
))
body.WriteString(fmt.Sprintf("%s\\n\\n", iss.Detail))
body.WriteString(
fmt.Sprintf("<details><summary>修复建议</summary>\\n\\n```suggestion\\n%s\\n```\\n</details>\\n\\n",
iss.Suggestion,
))
}
}

if len(issues) == 0 {
body.WriteString("✅ 未发现明显问题。\\n")
}

body.WriteString("\\n—\\n*本评论由 AI Code Review Agent 自动生成*")

// 调用 GitHub API 发布 review
url := fmt.Sprintf(
"https://api.github.com/repos/%s/%s/pulls/%d/reviews",
pr.Owner, pr.Repo, pr.PRNumber,
)

payload := map[string]interface{}{
"body": body.String(),
"event": "COMMENT",
}
jsonBody, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", url,
strings.NewReader(string(jsonBody)))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/vnd.github.v3+json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("发布 review 失败: %d %s", resp.StatusCode, string(body))
}

return nil
}


Webhook 服务入口

func main() {
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
// 1. 解析 webhook
prInfo, err := parseWebhook(r)
if err != nil {
fmt.Println("跳过:", err)
w.WriteHeader(http.StatusOK)
return
}

fmt.Printf("审查 PR #%d: %s\\n", prInfo.PRNumber, prInfo.Title)

// 2. 拉 diff
if err := fetchDiff(prInfo); err != nil {
fmt.Println("拉取 diff 失败:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// 3. AI 审查
issues, err := reviewDiff(prInfo.Diff)
if err != nil {
fmt.Println("审查失败:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// 4. 发布评论
if err := postReview(prInfo, issues); err != nil {
fmt.Println("发布评论失败:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

fmt.Printf("✅ PR #%d 审查完成, 发现 %d 个问题\\n",
prInfo.PRNumber, len(issues))
w.WriteHeader(http.StatusOK)
})

fmt.Println("Code Review Agent 启动, 监听 :9090/webhook")
http.ListenAndServe(":9090", nil)
}


实测效果

我把它接到我自己的开源项目上跑了一周。9 个 PR,AI 审查结果:

维度AI 标记人工确认后有效准确率
安全 4 个 3 个 75%
性能 11 个 9 个 82%
风格 15 个 12 个 80%
测试 7 个 5 个 71%
合计 37 个 29 个 78%

78% 的准确率意味着——10 个 AI 标记的问题里,约 8 个是真的有问题。剩下 2 个是误报。

但这不重要。 关键是人不需要逐行看代码了——只看 AI 标记出来的 37 个地方。10 分钟的 CR 变成 2 分钟。


和 daily-report-agent 的关系

你可能发现了——这个 Code Review Agent 和 daily-report-agent 的架构一模一样:

daily-report-agent:
Git API → AI 处理 → 企业微信

code-review-agent:
GitHub Webhook → AI 审查 → GitHub Comments

数据源变了,AI 的 Prompt 变了,输出渠道变了——但核心的「事件驱动 + AI 处理后分发」的结构完全一样。

这就是我为什么说 daily-report-agent 是个可复用模板。这篇的代码,你可以直接 fork 改一改就开始用了。


接入你团队的步骤

  • fork 项目 → 配好环境变量(GITHUB_TOKEN、DEEPSEEK_API_KEY)
  • 部署到服务器 → 一个 1C2G 的 VPS 就够了
  • 配置 Webhook → GitHub Repo Settings → Webhooks → 指向你的服务器
  • 先跑一周 → 收集准确率数据,调整 Prompt 降低误报
  • 全量启用 → 所有 PR 自动审查
  • 成本极低:一个 PR 约 5000 token,用 DeepSeek V4 Flash 约 ¥0.005。一天 20 个 PR 才 1 毛钱。

    下一篇是模块五的最后一篇——把前面9篇总结成一张能力模型图。帮你判断五个方向里该选哪个深入。

    赞(0)
    未经允许不得转载:171主机测评 » AI + 代码审查:搭建 Code Review Agent
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址