我在之前的文章里说过一个得罪人的观点:大多数团队的 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 审查结果:
| 安全 | 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 改一改就开始用了。
接入你团队的步骤
成本极低:一个 PR 约 5000 token,用 DeepSeek V4 Flash 约 ¥0.005。一天 20 个 PR 才 1 毛钱。
下一篇是模块五的最后一篇——把前面9篇总结成一张能力模型图。帮你判断五个方向里该选哪个深入。


