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 API | app_simple.py | 验签、过滤事件、查询 MR、写入队列 | 监听 0.0.0.0:5001,快速返回 202 |
| Review Worker | worker_main.py | 消费任务、创建 Worktree、调用 Agent、发布评论 | 常驻轮询,也支持 --once |
为什么拆分两个进程?
Webhook 请求生命周期:秒级
AI 审查生命周期:分钟级,可能失败、重试、恢复
如果把两者放在一个 Web 请求里,容易出现超时、并发阻塞和服务重启丢任务。拆开后,Webhook 只负责“可靠接单”,Worker 负责“慢任务执行”。
时序图
模块结构
入口层
├── 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首先执行:
-
检查Webhook Secret;
-
检查object_kind == merge request;
-
检查action是否在open/reopen/update;
-
从
project.web_url或path_with_namespace得到项目路径; -
调用GitLab API获取项目和MR;
-
交给 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_id | GitLab 项目 ID |
project_path | 项目路径 |
mr_iid | MR IID |
source_sha | 源分支提交 SHA |
target_sha | 目标分支提交 SHA |
base_sha | MR Diff 基准 SHA |
diff_version_id | GitLab Diff 版本 ID |
action | open/update/reopen |
status | pending/running/succeeded/failed/superseded |
attempt | 已重试次数 |
workspace_path | 工作区路径 |
result_note_id | GitLab 评论 ID |
error_message | 最后一次错误 |
created_at | 创建时间 |
started_at | 开始时间 |
finished_at | 完成时间 |
heartbeat_at | Worker 心跳时间 |
SQLite任务队列与状态机
状态定义:
job_store.py 中的主要状态为:
pending 已入队,等待 Worker
running 已被 Worker 原子领取
succeeded 审查完成或按策略跳过
failed 执行异常
superseded 被同一 MR 的新 SHA 淘汰
cancelled 预留的取消状态
状态图:
数据库对以下字段建立唯一约束:
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_event | MR 创建或更新 |
| schedule | 定时任务 |
| web | GitLab 页面手动运行 |
| api | GitLab 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 | 执行位置 |
|---|---|
| Shell | Runner 主机 Shell |
| Docker | Docker 容器 |
| Kubernetes | Kubernetes 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_TOKEN | Masked/Protected | 读取 MR、下载中心 Runtime、发布 Note |
DSH_CREDENTIALS_FILE | File 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 以只读模式运行。
时序图:
Pipeline如何判断是否通过
ci_review.py 执行
↓
返回退出码
↓
GitLab Runner 判断 Job 成功或失败
↓
Pipeline 汇总所有 Job 状态
| 退出码 | Job 状态 | Pipeline 结果 |
|---|---|---|
0 | passed | 通常 passed |
1 | failed | 通常 failed |
2 | failed | 通常 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




