GitLab 中接入 AI Code Review 完整指南
GitLab 中接入 AI Code Review 完整指南
本文目标是为一个示例项目接入自动化 AI Code Review,效果是:
- 开发者发起 Merge Request
- GitLab 自动触发 MR Pipeline
- Pipeline 调用 GLM-5
- AI 自动把总评和可选的行内评论写回 MR
GitLab 官方说明,Merge request pipelines 可以在创建或更新 MR 时运行专门的 CI/CD 任务;CI/CD 任务由仓库根目录的 .gitlab-ci.yml 配置。(GitLab 文档)
一、最终效果与示例命名
为了避免和你的真实项目混淆,本文统一使用这些示例名称:
- GitLab Group:
example_team - GitLab Project:
demo_ai_platform - GitLab 测试分支:
test/ai-code-review-smoke - GitLab Runner 名称:
ai-review-runner - GitLab 项目访问令牌名称:
ai-review-bot - 测试文件:
quicksort_demo.py
最终链路是:
开发者提交 MR → GitLab MR Pipeline 触发 → Runner 执行 Python 脚本 → 脚本读取 MR diff → 调用 GLM-5 → 通过 GitLab Discussions API 写回评论。 GitLab 的 Merge requests API 和 Discussions API 都支持这种自动化集成。(GitLab 文档)
二、开始前你需要准备什么
先准备 4 样东西:
- 一个 GitLab 项目,本文用
example_team/demo_ai_platform - 一个可用的 GitLab Runner
- 一个 GitLab 项目访问令牌
- 一个智谱
GLM-5的 API Key
GitLab 官方说明,项目访问令牌是项目级身份凭证,适合给自动化程序访问当前项目 API;GitLab Runner 负责执行 CI/CD job;而 GitLab CI/CD 变量会以环境变量的形式注入 job 运行环境。(GitLab 文档)
三、推荐的整体方案
对新手来说,最稳的方案不是先做 Webhook 服务,而是先做:
MR Pipeline + Python 脚本 + GitLab API + GLM-5
原因很简单:
- 不需要公网回调地址
- 不需要自建 Web 服务
- 日志全部在 GitLab 里
- 出问题更容易排查
- 跑通后再升级成 webhook 方案也不难
GitLab 官方把 pipeline 作为 CI/CD 的基础机制,允许在创建 MR 等事件时自动运行。(GitLab 文档)
四、第一步:创建 GitLab 项目访问令牌
进入项目:
Settings → Access tokens
按下面填写:
- Token name:
ai-review-bot - Description:
AI Code Review bot for Merge Request comments - Expiration date:先用 30 天或 90 天
- Role:
Developer - Scopes:只勾选
api
为什么这么选:
- 这个 bot 要读取 MR、读取 diff、写 MR discussion,所以需要 API 权限。(GitLab 文档)
- 用
Developer通常够用,不建议一开始就给更高权限。 - 令牌值只会在创建后展示一次,应该立即保存。(GitLab 文档)
建议把这个 token 记为:
GITLAB_API_TOKEN
五、第二步:在 GitLab 中配置 CI/CD Variables
进入:
Settings → CI/CD → Variables
需要创建这些变量。
1. 敏感变量
创建:
GITLAB_API_TOKENZHIPU_API_KEY
建议设置为:
- Type:
Variable - Environment:
All (default) - Visibility:
Masked and hidden - Protect variable:不要勾选
- Expand variable reference:不要勾选
GitLab 官方说明,变量可以设置为 Masked 或 Masked and hidden;隐藏后变量仍可在流水线中使用,但不能再在 UI 中显示原值。官方也说明,Protected variable 只会在受保护分支或标签的流水线中可用。(GitLab 文档)
2. 普通配置变量
创建:
REVIEW_MODEL=glm-5REVIEW_INLINE=falseREVIEW_MAX_FILES=20REVIEW_MAX_DIFF_CHARS=50000REVIEW_MAX_COMMENTS=8
第一轮测试把 REVIEW_INLINE 设成 false,这样先只测AI 总评,不测更容易出错的行内评论。
如果你使用智谱默认通用端点,就不用创建 ZHIPU_BASE_URL。
如果你确实使用特殊编码端点,再补一个变量:
ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4
智谱官方当前文档给出的对话补全接口是:
POST https://open.bigmodel.cn/api/paas/v4/chat/completions
并使用:
Authorization: Bearer <token>
认证。(智谱AI开放文档)
六、第三步:准备 GitLab Runner
GitLab 官方建议:Runner 最好安装在与 GitLab 实例不同的机器上,这是出于安全和性能考虑。(GitLab 文档)
但如果你只是测试或小规模使用,也可以在同一台 Ubuntu 22.04 机器上通过 Docker 运行 Runner。GitLab 官方支持把 Runner 作为 Docker 容器运行,并支持 Docker executor。(GitLab 文档)
1. 在 GitLab 中创建 Project Runner
进入:
Settings → CI/CD → Runners → Create project runner
建议这样填写:
- Tags:留空
- Run untagged jobs:勾选
- Runner description:
ai-review-runner - Paused:不勾
- Protected:不勾
- Lock to current projects:勾选
- Maximum job timeout:留空
这样做的原因是:
- 你的 job 默认没有写
tags:,所以 Runner 需要允许执行 untagged job。 - MR 测试通常来自普通开发分支,
Protected容易导致任务跑不起来。 - 锁定到当前项目更符合最小权限原则。
2. 在 Linux 机器上用 Docker 启动 Runner
在 Ubuntu 22.04 主机上执行:
sudomkdir-p /srv/gitlab-runner/config sudodocker pull gitlab/gitlab-runner:latest sudodocker run -d--name gitlab-runner --restart always \-v /srv/gitlab-runner/config:/etc/gitlab-runner \-v /var/run/docker.sock:/var/run/docker.sock \ gitlab/gitlab-runner:latest GitLab 官方提供了这种容器化运行 Runner 的方式。(GitLab 文档)
3. 注册 Runner
在 GitLab 页面创建 Runner 后,会得到一个 glrt-... 开头的认证 token。
执行:
sudodockerexec-it gitlab-runner gitlab-runner register \ --non-interactive \--url"http://gitlab.example.local"\--token"<YOUR_RUNNER_TOKEN>"\--executor"docker"\ --docker-image "python:3.11-slim"\--description"ai-review-runner"GitLab 官方文档说明,Runner 可以使用 register --executor "docker" 注册为 Docker executor。(GitLab 文档)
4. 建议把并发限制为 1
如果 Runner 和 GitLab 在同一台主机上运行,建议把 /srv/gitlab-runner/config/config.toml 里的并发改成:
concurrent = 1 这样更稳,避免 CI 作业和 GitLab 服务争抢资源。GitLab 官方强调 Runner 的安装与资源规划应考虑安全和性能。(GitLab 文档)
七、第四步:在仓库中放置两个关键文件
这两个文件必须放在Git 仓库中,不是放在 Runner 主机的系统目录里。
目录结构应为:
demo_ai_platform/ ├── .gitlab-ci.yml └── scripts/ └── ai_review.py GitLab 官方说明,pipeline 由仓库根目录的 .gitlab-ci.yml 配置。(GitLab 文档)
八、.gitlab-ci.yml 完整示例
把下面内容保存为仓库根目录的 .gitlab-ci.yml:
workflow:rules:-if: $CI_PIPELINE_SOURCE == "merge_request_event" -when: never stages:- ai_review default:image: python:3.11-slim interruptible:truevariables:PIP_DISABLE_PIP_VERSION_CHECK:"1"PIP_NO_CACHE_DIR:"1"PYTHONUNBUFFERED:"1"ai_code_review:stage: ai_review before_script:- python -V - pip install requests script:- python scripts/ai_review.py rules:-if: $CI_PIPELINE_SOURCE == "merge_request_event" allow_failure:trueretry:1这里最关键的是:
workflow: rules限制只在 MR pipeline 中运行- job 名字叫
ai_code_review - 通过
python scripts/ai_review.py执行审查逻辑
GitLab 官方文档说明,Merge request pipelines 需要使用 merge_request_event 规则来匹配 MR 事件。(GitLab 文档)
九、scripts/ai_review.py 完整示例
把下面内容保存为 scripts/ai_review.py:
import json import os import re import sys import time import textwrap from pathlib import Path from typing import Dict, List, Optional, Set import requests CI_API_V4_URL = os.environ["CI_API_V4_URL"].rstrip("/") PROJECT_ID = os.environ["CI_PROJECT_ID"] MR_IID = os.environ["CI_MERGE_REQUEST_IID"] GITLAB_TOKEN = os.environ["GITLAB_API_TOKEN"] ZHIPU_API_KEY = os.environ["ZHIPU_API_KEY"] MODEL = os.getenv("REVIEW_MODEL","glm-5") ZHIPU_BASE_URL = os.getenv("ZHIPU_BASE_URL","https://open.bigmodel.cn/api/paas/v4").rstrip("/") REVIEW_INLINE = os.getenv("REVIEW_INLINE","true").lower()=="true" MAX_FILES =int(os.getenv("REVIEW_MAX_FILES","20")) MAX_DIFF_CHARS =int(os.getenv("REVIEW_MAX_DIFF_CHARS","50000")) MAX_COMMENTS =int(os.getenv("REVIEW_MAX_COMMENTS","8")) SUMMARY_MARKER_PREFIX ="ai-review" CODE_EXTS ={ ".py",".js",".jsx",".ts",".tsx",".java",".go",".rb",".php",".c",".cc",".cpp",".h",".hpp",".cs",".kt",".rs",".swift",".scala",".sql",".yaml",".yml",".json",".sh",".bash",".zsh",".ps1",".vue",".css",".scss",".sass",".less",".xml",".toml",".ini",".conf",".proto",".gradle"} CODE_FILENAMES ={ "Dockerfile","dockerfile","Makefile","makefile","Jenkinsfile","pom.xml"} HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")deflog(msg:str)->None:print(msg, flush=True)defrequest_with_retry( method:str, url:str,*, params: Optional[dict]=None, data: Optional[