我爱吃西红柿~头像
AI-Code-Review的设计与实现封面图

AI-Code-Review的设计与实现

1.为什么需要AI Code Review

传统人工 Code Review 的价值不可替代,但在团队中经常遇到几个现实问题:

  • MR 数量多,人工评审时间不稳定;
  • 高风险问题容易被大量业务代码淹没;
  • 不同项目、不同 reviewer 的关注重点不一致;
  • 大 MR 很难在一次上下文中完整理解;
  • 审查结论没有统一的格式、阈值和可观测数据;
AI Code Review:快速扫描、搜索调用关系、发现高风险模式、提供证据
人工:理解业务意图、确认取舍、决定是否合并

2.项目演进

2.1 第一阶段:直接调用OpenAI兼容接口

GitLab MR Webhook
    ↓
获取 MR Diff
    ↓
过滤不支持文件和超大变更
    ↓
按文件数/字符数拆分 Diff
    ↓
调用 DeepSeek Chat Completions API
    ↓
解析 findings JSON
    ↓
多个批次去重、汇总
    ↓
发布 GitLab MR 评论

findings JSON是DeepSeek API返回内容中的结构化结果

格式大致是:

{
  "findings": [
    {
      "severity": "高",
      "file": "app/src/main/java/com/example/Login.kt",
      "location": "第 42 行",
      "problem": "这里可能发生空指针异常",
      "impact": "用户进入登录页面时可能崩溃",
      "suggestion": "调用前增加空值判断"
    }
  ]
}

大文件会被预处理拆分:

单个文件 Diff 超过字符限制
        ↓
优先按代码行拆分
        ↓
如果单行本身过长,再按字符切分
        ↓
每个分段单独调用一次模型

默认配置大致是:

单批最多约24000字符

单批最多8个文件

然后:

各批次返回 findings
        ↓
合并所有 findings
        ↓
按文件和问题内容去重
        ↓
汇总成一份结果
        ↓
发布一条 MR 评论

但这种有一个局限:

按行或字符切分可能把一个类、方法或调用链拆开,模型上下文不完整。

这种方式实现简单、调用链短,但有明显边界:

  • 模型主要看到Diff,不天然拥有完整仓库上下文;

  • 在只能把 MR Diff 发送给模型的方案中,模型看不到完整仓库。应用程序必须先找到相关文件和调用关系,再把这些上下文一起放进提示词,因此跨文件调用关系需要在应用侧预先准备;

  • 任务状态最主要在进程内存中,进程重启可能丢失任务;

  • 同一个MR更新时,旧任务执行期间新事件会被忽略

  • 大文件主要按字符和行拆分,容易破坏代码结构

2.2 第二阶段:ClaudeCode/DSH持久化审查流程

GitLab MR Webhook
        ↓
校验事件并获取 source SHA
        ↓
写入 review_jobs 任务表
        ↓
Webhook 返回 202
        ↓
Worker 获取 pending 任务
        ↓
创建独立 Worktree
        ↓
调用 Claude Code 或 DSH
        ↓
校验结构化 JSON
        ↓
重新检查 MR 当前 SHA
        ↓
发布幂等评论
        ↓
更新任务状态并清理工作区
  • Webhook只接收和入队,Worker独立消费任务

  • SQLite保存任务状态,服务重启后可以恢复

  • project_id + mr_iid + source_sha 定义一次审查快照;

  • Worktree 让每次审查都有隔离、可复现的代码目录;

  • 旧 SHA 的任务可以被新 SHA 淘汰;

  • 评论去重,避免重复发布。

  • 兼容Claude Code/DSH切换

两个常驻服务

当前生产部署包含两个独立进程:

服务入口职责运行特点
Webhook APIapp_simple.py验签、过滤事件、查询 MR、写入队列监听 0.0.0.0:5001,快速返回 202
Review Workerworker_main.py消费任务、创建 Worktree、调用 Agent、发布评论常驻轮询,也支持 --once

为什么拆分两个进程?

Webhook 请求生命周期:秒级
AI 审查生命周期:分钟级,可能失败、重试、恢复

如果把两者放在一个 Web 请求里,容易出现超时、并发阻塞和服务重启丢任务。拆开后,Webhook 只负责“可靠接单”,Worker 负责“慢任务执行”。

时序图
ReviewPublisher DSH/Claude Agent GitLab API/Git worker_main.py SQLite JobStore app_simple.py GitLab ReviewPublisher DSH/Claude Agent GitLab API/Git worker_main.py SQLite JobStore app_simple.py GitLab POST /api/gitlab/mr-webhook 校验 X-Gitlab-Token 查询 Project 和 MR enqueue(project, mr, source_sha, base_sha) 202 Accepted claim_next() pending → running 重新查询 MR 当前 SHA clone/fetch 精确 source_sha 创建 detached Worktree git diff base_sha...source_sha 在 Worktree cwd 中执行只读审查 结构化 JSON Schema/路径/行号/阈值校验 再次确认当前 SHA marker 查重并创建 MR Note running → succeeded 删除临时 Worktree
模块结构
入口层
├── app_simple.py              Webhook API
└── worker_main.py             Worker 进程入口

任务与编排
├── webhook_jobs.py            Webhook → 任务参数
├── job_store.py               SQLite 状态机和队列
└── review_worker.py           单任务端到端编排

代码工作区
└── workspace_manager.py       Bare Cache + Detached Worktree

Agent 层
├── dsh_runner.py              DSH headless 默认 Runner
└── claude_runner.py           Claude Code 回退 Runner

质量与发布
├── result_validator.py         Agent JSON 和 Finding 校验
└── review_publisher.py         MR 评论、marker、统计

策略与配置
├── review_policy.py            文件过滤、分支跳过
├── prompt_selector.py          项目 Prompt 选择
├── env_loader.py               .env 解析
├── config.json                 通用配置
└── projects.json               项目级 Prompt
Webhook:系统如何知道审查哪个项目

GitLab MR Webhook会提供:

{
  "project": {
  "id": 123,
  "name": "testai",
  "web_url": "https://gitlab.example.com/chenyiran/testai",
  "path_with_namespace": "chenyiran/testai",
  "git_ssh_url": "git@gitlab.example.com:chenyiran/testai.git",
  "git_http_url": "https://gitlab.example.com/chenyiran/testai.git",
  "namespace": "chenyiran",
  "default_branch": "develop",
  "visibility_level": 0
},
  "object_attributes": {
  "id": 456,
  "iid": 42,
  "action": "update",
  "title": "登录功能",
  "description": "增加登录功能",
  "source_branch": "feature/login",
  "target_branch": "develop",
  "source_project_id": 123,
  "target_project_id": 123,
  "state": "opened",
  "url": "https://gitlab.example.com/chenyiran/testai/-/merge_requests/42",
  "last_commit": {
    "id": "67aa3114aaebea1976057e544456e65d8689ee5c",
    "message": "add login",
    "title": "add login",
    "timestamp": "2026-10-23T10:00:00Z",
    "url": "https://gitlab.example.com/..."
  }
}
}

app_simple.py首先执行:

  1. 检查Webhook Secret;

  2. 检查object_kind == merge request;

  3. 检查action是否在open/reopen/update;

  4. 从 project.web_url 或 path_with_namespace 得到项目路径;

  5. 调用GitLab API获取项目和MR;

  6. 交给 webhook_jobs.py生成任务参数

每个任务使用以下组合标识:

project_id + mr_iid + source_sha

分别表示:

project_id  → 哪个 GitLab 项目
mr_iid      → 这个项目中的哪个 MR
source_sha  → MR 源分支的哪个具体提交

不能只使用MR编号,同一个MR也会随着push产生多个SHA

任务表
字段说明
id内部任务 ID
project_idGitLab 项目 ID
project_path项目路径
mr_iidMR IID
source_sha源分支提交 SHA
target_sha目标分支提交 SHA
base_shaMR Diff 基准 SHA
diff_version_idGitLab Diff 版本 ID
actionopen/update/reopen
statuspending/running/succeeded/failed/superseded
attempt已重试次数
workspace_path工作区路径
result_note_idGitLab 评论 ID
error_message最后一次错误
created_at创建时间
started_at开始时间
finished_at完成时间
heartbeat_atWorker 心跳时间
SQLite任务队列与状态机

状态定义:

job_store.py 中的主要状态为:

pending     已入队,等待 Worker
running     已被 Worker 原子领取
succeeded   审查完成或按策略跳过
failed      执行异常
superseded  被同一 MR 的新 SHA 淘汰
cancelled   预留的取消状态

状态图:

Webhook enqueue

claim_next()

同一 MR 出现新 SHA

校验并发布成功

Agent/GitLab/Git 异常

运行期间发现任务过期

reopen 且允许重试

Worker 重启恢复

pending

running

superseded

succeeded

failed

数据库对以下字段建立唯一约束:

UNIQUE(project_id, mr_iid, source_sha)

同一个项目、同一个MR、同一个 提交重复触发Webhook时:

第一次:创建 pending 任务
第二次:返回“任务已存在”,不重复入队

这样可以处理GitLab重试、重复reopen事件和人工重复发送Webhook

新SHA淘汰旧任务,当MR从sha-A更新到sha-B时:

数据库事务会执行类似:

UPDATE review_jobs
SET status = 'superseded'
WHERE project_id = ?
  AND mr_iid = ?
  AND source_sha <> ?
  AND status IN ('pending', 'running')
旧任务状态
sha-A pending/running → superseded
新任务状态
sha-B                → pending

Worker在以下阶段都会检查任务是否仍然有效:

  • 创建 Worktree 前;
  • Agent 调用前;
  • 结果校验前;
  • 评论发布前。

发布前还会重新从GitLab查询MR当前SHA。即使旧任务已经消耗了模型费用,也不能把旧代码的结论评论到新代码上。

原子领取与重启恢复

claim_next() 使用事务和条件更新,核心思想是:

UPDATE review_jobs
SET status = 'running', attempt = attempt + 1
WHERE id = ? AND status = 'pending'

保证只有一个worker能把任务从:

pending → running

另一个Worker会发现任务已经不是pending,因此不能再次领取。

Worker执行任务时,会保存:

heartbeat_at = 最近一次活跃时间

如果worker执行过程中崩溃或退出,任务状态仍是running,但heartbeat_at不再更新

Worker重启时计算一个截止时间:

cutoff = 当前 UTC 时间 - 超时阈值

然后执行:

UPDATE review_jobs
SET status = 'pending',
    stage = 'RECOVERED_AFTER_RESTART',
    started_at = NULL,
    heartbeat_at = CURRENT_TIME
WHERE status = 'running'
  AND (
      heartbeat_at IS NULL
      OR heartbeat_at <= cutoff
  );

重启时发现heartbeat_at<=cutoff时状态变化:

running → pending

之后Worker再次调用claim_next():

pending → running

任务会从头重新执行,而不是Agent中断的位置继续。

这里的超时阈值是可以配置的,heartbeat_at<=cutoff才认为任务已经中断。

原子领取       → 防止并发重复执行
心跳恢复       → 防止崩溃后任务丢失
Marker 幂等    → 防止重复发布评论
Git Worktree独立工作区设计

作用:同一个Git仓库缓存中,同时创建多个独立的代码目录,每个目录固定到不同的提交SHA。主要是为了保证每次审查看到的代码版本稳定、互不干扰。

它不是每次都重新完整git clone,而是复用Git对象缓存,减少磁盘和网络开销。

目录结构:

runtime/
├── repo-cache/
│   └── project-123.git          # 项目的 bare Git 缓存
└── workspaces/
    └── projects/
        └── 123/
            └── mr-456/
                ├── review-job1-aaa111/
                └── review-job2-bbb222/

第一次审查某个项目时,系统创建:

runtime/repo-cache/project-123.git

它是一个Bare Repository,共享的裸仓库缓存,没有普通工作目录,但保存Git对象和提交历史。后续同一项目的任务可以复用这份缓存,避免每个MR都重新完整clone。

创建工作区:

git -C <repo-cache> fetch origin <mr-ref-or-source-sha>

git -C <repo-cache> worktree add \
  --detach \
  <workspace-path> \
  <source-sha>

当正常完成时或发生异常时会进行清理:

git -C <repo-cache> worktree remove <workspace-path>
git -C <repo-cache> worktree prune

删除当前任务的工作区:

runtime/workspaces/projects/123/mr-456/review-xxx/

2.3 第三阶段

由GitLab Pipeline驱动的一次性审查流水线:

GitLab CI/CD基础概念

CI(Continous Integration)关注代码提交后的自动验证,在代码 Push 或 MR 更新后,自动把当前代码放进项目整体环境中编译、测试和检查。

Continuous Delivery:
自动完成构建、测试并生成可发布产物,但生产发布通常需要人工确认。

Continuous Deployment:
验证通过后自动部署到目标环境,无需人工点击。

两者都属于 CD,只是自动化程度不同。

当前项目主要使用CI能力:在MR Pipeline中执行一次AI Code Review,并把结果写回MR

Pipeline

Pipeline是一次完整的CI/CD执行实例。

MR 创建或更新
    ↓
GitLab 产生 merge_request_event
    ↓
workflow.rules 允许创建 Pipeline
    ↓
Job.rules 匹配 ai-code-review
    ↓
Runner 领取 pending Job
    ↓
Docker Executor 启动审查镜像
    ↓
checkout MR source SHA
    ↓
校验 Token、DSH 凭证和运行工具
    ↓
解析中心分支的 Runtime SHA
    ↓
按 SHA 下载审查 Runtime
    ↓
运行 ci_review.py
    ↓
调用 DSH、校验结果、发布 Note
    ↓
清理凭证和临时目录
    ↓
返回 Job 退出码

Pipeline负责:

  • 判断是否创建;

  • 选择加入的Job;

  • 管理Stage顺序

  • 管理Job依赖;

  • 记录整体状态;

  • 展示执行结果。

常见Pipeline来源:

来源说明
push分支或 Tag 被 Push
merge_request_eventMR 创建或更新
schedule定时任务
webGitLab 页面手动运行
apiGitLab API 触发
pipeline上游项目触发下游
parent_pipeline父子 Pipeline
Stage

Stage 是 Job 的阶段分组和顺序控制器,本身不执行命令,具体命令由 Job 的 script 执行。

当前中心项目 ai_code_revi 的根目录 .gitlab-ci.yml 定义了测试阶段:

stages:
  - test

unit-tests:
  stage: test

因此中心项目自己的Pipeline主要执行:

test
└── unit-tests

中心模版deploy/gitlab-mr-review.yml定义了AI审查Job:

ai-code-review:
  stage: review

业务项目通过 include 引入该模板后,会获得 ai-code-review Job,但不会自动获得中心项目根目录中的 unit-tests Job。

如果业务项目还需要先执行测试,需要在自己的 .gitlab-ci.yml 中定义测试 Job,并声明两个阶段:

stages:
  - test
  - review

unit-tests:
  stage: test
  script:
    - python -m unittest discover

ai-code-review:
  stage: review
  script:
    - python /opt/ai-review-runtime/ci_review.py

此时执行顺序为:

test
└── unit-tests
        ↓
review
└── ai-code-review
  • test:运行 Python 单元测试;
  • review:调用 DSH 执行 AI Code Review。
Job

Job是Pipeline中真正执行命令的最小单元。

简化示例:
unit-test:
  stage: test
  image: python:3.13-slim
  script:
    - python -m unittest discover

ai-code-review:
  stage: review
  script:
    - python ci_review.py

Job 可以负责:

  • 编译;
  • 测试;
  • 代码审查;
  • 构建镜像;
  • 上传制品;
  • 部署服务;
  • 触发下游 Pipeline。

真实模版会下载受控Runtime,再执行 /opt/ai-review-runtime/ci_review.py,不是直接执行 MR 中同名脚本。

Runner

Runner是实际执行Job的程序或机器。

GitLab 创建 Job
        ↓
Runner 领取 Job
        ↓
Runner 准备执行器
        ↓
执行 before_script / script / after_script
        ↓
上传日志并返回退出码

Runner常见执行器:

Executor执行位置
ShellRunner 主机 Shell
DockerDocker 容器
KubernetesKubernetes Pod
Virtual Machine虚拟机

从打印日志上,也可以看出

Running with gitlab-runner 14.6.0
on shared-runner02
Preparing the "docker" executor
  • Runner 版本为 14.6.0;
  • 任务由 shared-runner02 执行;
  • 使用 Docker executor;
当前项目如何被业务项目接入

ai_code_revi 是中心审查项目,模板位于:

deploy/gitlab-mr-review.yml

业务项目在自己的.gitlab-ci.yml中引入

include:
  - project: "chenyiran/ai_code_revi"
    ref: "codex/ci-native-review"
    file: "/deploy/gitlab-mr-review.yml"

stages:
  - test
  - review

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
      when: never
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - when: always

如果业务项目已有 build、test、deploy,只需追加 review,不要覆盖原有阶段。

两层模版的职责是:

workflow.rules → 是否创建 Pipeline

(如果这是一次分支 Push,并且该分支已经存在打开的 MR,则不创建 Push Pipeline。

如果 Pipeline 是由 MR 创建或更新触发的,就创建 Pipeline。)

Job.rules→ 是否执行ai-code-review

(必须是 MR Pipeline
必须有有效的 MR 编号
必须是 detached 类型
必须是同项目 MR)

因此普通 Push、Fork MR、merged-result Pipeline 和 merge train 默认不会执行 AI Review。

下载固定 Runtime SHA 的审查程序

Job 会通过 GitLab API 查询中心项目:

chenyiran/ai_code_revi
分支:codex/ci-native-review

得到当前 Runtime 的提交 SHA,例如:

27a14483de0bc5b0aef83444e899a8e7be84d27d

然后下载这个 SHA 对应的代码压缩包到:

/opt/ai-review-runtime

并执行:

/opt/ai-review-runtime/ci_review.py

这样可以避免直接执行 MR 中可能存在的同名脚本。

CI/CD Variables

目标项目或 Group 需要配置:

变量类型用途
GITLAB_TOKENMasked/Protected读取 MR、下载中心 Runtime、发布 Note
DSH_CREDENTIALS_FILEFile Variable提供 DSH 凭证文件
Runner和Docker Job

中心模版的关键配置:

ai-code-review:
  stage: review
  image: harbor.stary.ltd/gitlabci/ai-review-runtime:0.2.1
  tags:
    - runner02
  interruptible: true
  timeout: 40m
  variables:
    GIT_DEPTH: "0"
    GIT_STRATEGY: fetch
    DSH_PERMISSION_MODE: "read-only"
  • 使用固定的审查环境镜像;
  • 交给带 runner02 标签的 Runner;
  • 旧 MR Job 可以被新的 Pipeline 中断;
  • 单个审查最长 40 分钟;
  • 拉取完整 Git 历史;
  • DSH 以只读模式运行。

时序图:

MR Note DSH Central Runtime Docker Container Runner GitLab GitLab MR MR Note DSH Central Runtime Docker Container Runner GitLab GitLab MR 创建或更新 Merge Request 解析 workflow 和 rules 创建 ai-code-review Job 启动 ai-review-runtime 镜像 checkout MR source SHA 查询中心分支 SHA 返回 Runtime SHA 下载 Runtime archive 返回审查代码和配置 执行 ci_review.py 查询项目和 MR 在 CI checkout 中执行只读审查 返回结构化 JSON Schema、路径、行号、阈值校验 再次查询 MR head SHA 创建幂等评论 删除凭证和临时目录 上传日志和 Job 状态
Pipeline如何判断是否通过
ci_review.py 执行
    ↓
返回退出码
    ↓
GitLab Runner 判断 Job 成功或失败
    ↓
Pipeline 汇总所有 Job 状态
退出码Job 状态Pipeline 结果
0passed通常 passed
1failed通常 failed
2failed通常 failed
退出码为 0

以下情况返回 0:

if outcome in ("succeeded", "superseded", "skipped"):
    return 0

表示 Job 成功,Pipeline 通常可以通过:

  • 审查成功;
  • MR 在审查期间更新,旧任务被标记为 superseded;
  • 当前项目或文件被安全跳过。
退出码为 1

以下情况返回 1:

if outcome == "quality_failed":
    return 1

主要表示发现正式的高风险问题:

HIGH
BLOCKER

当前逻辑会:

先发布 MR 评论
    ↓
再让 Job 失败
    ↓
Pipeline 标记为 failed

这样可以把 AI 审查作为质量门禁,阻止依赖 Pipeline 成功状态的 MR 合并。

退出码为 2

CIReviewError 会返回 2,例如:

  • CI 环境变量缺失;
  • MR 或项目读取失败;
  • checkout SHA 不一致;
  • DSH 结果校验失败;
  • 批次没有完整覆盖变更文件;
  • Runtime 或配置加载失败。

这些属于执行或配置错误,不是代码质量问题。

总结

AI Code Review 的核心不只是调用模型,而是建立一套稳定、可校验、可恢复的工程流程。

本项目从直接调用 OpenAI 兼容接口,演进到 Webhook + Worker + SQLite + Worktree + DSH,最终接入 GitLab MR Pipeline。当前方案支持固定 SHA 审查、任务幂等、旧任务淘汰、只读 Agent、结构化结果校验和高风险问题质量门禁。

它不能替代人工 Code Review,但可以快速发现常见风险,减少重复检查成本,让人工 Reviewer 更专注于业务逻辑和架构决策。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/ChenYiRan123456/article/details/169345995

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--