背景
我头脑中有个理念,能用AI大模型做的事情,能自动化的事情,就不要让人做。这一类事情,人与AI大模型相比,几无优势。AI大模型不知疲倦,会强化学习,会越来越聪明,质量不断提高。随着技术的进步,代码审查已经进入可以自动化事情之列,一直都很想学习一下如何用AI大模型做代码审查,经常说就是没有行动。这次下定决心,要排除各种借口和理由,告别拖延症,现在我们进入正题。
AI大模型审查的流程是:
- 代码编写者在Gitlab UI界面发起合并代码申请
- 触发配置好的gitlab webhook事件
- webhook将代码合并信息推送给Jenkins
- 在Jenkins上运行AI代码审查主流程逻辑,将文件发送给大模型,将审查结果写入到Gitlab 合并请求评论区
实现步骤
第一步 制作大模型镜像
在代码审查方面, 下面四个大模型表现相对较好
1. GPT-OSS 系列
推荐指数:⭐⭐⭐⭐⭐
模型 :gpt-oss:20B(开源大模型)
部署 :Docker 容器,本地部署,支持 Ollama 或 vLLM
优势 :中文代码理解能力强,多语言支持(Python、Java、JS/TS 等常见语言),上下文处理能力大(20B 模型约 32k token 上下文)
特点:适合代码审查、生成、优化任务,尤其在大型项目中能提供详细分析
2. CodeLlama 系列
推荐指数:⭐⭐⭐⭐
- 模型:CodeLlama-7B/13B/34B-Instruct
- 部署:支持Docker,可用Ollama或vLLM部署
- 优势:Meta开源,专门针对代码优化,支持多种编程语言
- Python调用:通过OpenAI兼容API或直接调用
3. DeepSeek Coder
推荐指数:⭐⭐⭐⭐
- 模型:DeepSeek-Coder-6.7B/33B-Instruct
- 部署:Docker + vLLM/Ollama
- 优势:代码能力强,中文支持好,推理速度快
- 特点:在代码理解和生成方面表现优异
4. StarCoder2
推荐指数:⭐⭐⭐
- 模型:StarCoder2-7B/15B
- 部署:Docker + Transformers/vLLM
- 优势:BigCode项目,训练数据质量高
- 特点:支持80+编程语言
刚开始选择大模型选择的是Meta的 CodeLlama-7B-Instruct,因为它的占用资源比较低,后面发现上下文长度偏低,不够用,如下图所示

决定升级模型参数到CodeLlama-13B-Instruct,CodeLlama-13B-Instruct模型支持的输入长度是4096 token,比CodeLlama-7B-Instruct模型多了一倍, 满足大多数场景。查了一下CodeLlama-13B-Instruct模型的硬件要求:至少32GB RAM,推荐RTX 4090或A100,公司的GPU是A100, 符合这一条,跑起来之后发现不支持中文,于是只能无奈放弃。
接着将大模型又换成DeepSeek-Coder-6.7B-Instruct, 发现同样的文件, 评审质量不如CodeLlama系列, 最后换成gpt-oss:20b,发现响应速度比CodeLlama和DeepSeek-Coder都要快一些, 而且代码评审质量也很高。
到底该使用Docker镜像部署大模型好,还是使用裸机部署大模型好,这个要看场景
对于大多数场景,推荐从Docker开始:
- 快速验证可行性
- 建立标准化流程
- 积累运维经验
- 根据性能瓶颈决定是否迁移到裸机
性能关键场景才考虑裸机:
- 已经明确性能瓶颈在容器层
- 有足够的运维能力
- 对性能的要求超过了便捷性
- 资源充足,追求极致优化
AI代码审核,相比性能,部署便捷性,环境一致性,扩展性可能才是更应关注的方面。所以这里选择了Docker镜像部署方案。
写一个制作大模型镜像的Jenkins任务,流程是:
- 设置环境变量和凭证。
- 进入大模型镜像 Dockerfile 所在目录
- 检查 远程 Docker Registry 是否已有该镜像, 如果存在,跳过构建和推送, 如果不存在,构建镜像、推送到 远程 Docker Registry、更新 Kubernetes deployment

js
pipeline {
agent { label 'jenkins-runner-1' }
environment {
REGISTRY_HOST = 'reg.xxx.com:9088'
MODEL_NAME = 'gpt-oss-20b'
CONTAINER_NAME = 'codellama-review'
}
stage('制作大模型服务镜像') {
steps {
script {
try {
withCredentials([
usernamePassword(
credentialsId: 'REGISTRY',
usernameVariable: 'REGISTRY_USERNAME',
passwordVariable: 'REGISTRY_PASSWORD'
)
]) {
env.IMAGE_TAG_MODEL = "base-images/ai-code-review:${env.MODEL_NAME}"
dir("common-tools/ai-code-review/${env.MODEL_NAME}") {
def imageModelExists = sh(
script: "curl -s -o /dev/null -w \"%{http_code}\" -u $REGISTRY_USERNAME:$REGISTRY_PASSWORD https://$REGISTRY_HOST/v2/${IMAGE_TAG_MODEL.split(':')[0]}/manifests/${IMAGE_TAG_MODEL.split(':')[1]}",
returnStdout: true
).trim() == '200'
// 如果镜像不存在,则构建并推送,并启动容器,运行镜像服务
if (!imageModelExists) {
sh """
set -e
echo "\$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "\$REGISTRY_USERNAME" --password-stdin
docker build -f Dockerfile -t $REGISTRY_HOST/$IMAGE_TAG_MODEL .
docker push $REGISTRY_HOST/$IMAGE_TAG_MODEL
kubectl --kubeconfig=/etc/deploy/kubegpu set image deployment/$CONTAINER_NAME $CONTAINER_NAME=$REGISTRY_HOST/$IMAGE_TAG_MODEL
kubectl --kubeconfig=/etc/deploy/kubegpu rollout status deployment/$CONTAINER_NAME
"""
}
}
}
} catch (Exception e) {
throw e
}
}
}
}
}
}
构建镜像时使用的Dockerfile文件如图所示, 引用的基础镜像ollama-with-gpt-oss
的制作方法参见笔者的这篇文章下载体验了一下OpenAI号称手机也能运行起来的开源大模型gpt-oss-20b。
js
FROM reg.xxx.com:9088/base-images/ai-code-review/ollama-with-gpt-oss:latest
CMD ["serve"]
第二步编写代码审查大模型程序
简单说一下AI代码审查主流程的执行逻辑:
step1 初始化与配置
-
导入模块:
requests
,argparse
,os
,sys
,time
,base64
,json
,functools
等。 -
重写
print
函数,使其自动刷新输出。 -
配置 GitLab API:
GITLAB_API
:GitLab API 基础地址。PRIVATE_TOKEN
:私有 Token,用于访问 GitLab。
-
配置 Ollama 本地模型:
OLLAMA_MODEL
:模型名称(如gpt-oss:20b
)。OLLAMA_CLIENT
:使用OpenAI
SDK 指向 Ollama 本地服务。
-
定义系统提示
SYSTEM_PROMPT
,用于引导模型执行代码审查。
step2: 解析启动命令传递的Gitlab合并事件参数
这些参数来自于gitlab webhook推送的合并事件,后面查询合并请求变更文件的时候要用到。使用 argparse
获取命令行参数:
--project-id
:GitLab 项目 ID--project-name
:GitLab 项目名称--mr-iid
:Merge Request IID
step3 获取 MR 变更文件
-
调用 GitLab API
/merge_requests/{mr_iid}/changes
获取 MR 的文件变更列表。 -
过滤文件, 下列这些文件不会被审查:
- 删除的文件。
- 自动生成文件。
- 仅重命名但内容未变的文件。
- 非允许的文件扩展名。
- 前端项目只检查在
src/
下的文件。 - Python项目只检查在
app/
下的文件。 - 只修改空格、缩进、空行或注释的文件。
- 大文件(diff 超过 10 KB)被忽略。
-
返回符合条件的文件列表。
step4 获取变更文件内容
- 调用 GitLab API
/merge_requests/{mr_iid}
获取源分支名,用于后续获取合并源分支改变文件完整内容。 - 调用 GitLab API
/repository/files/{file_path}
获取改动文件完整内容。 - 文件内容为 Base64 编码,函数解码成 UTF-8 字符串返回。
step 5 调用 gpt-oss:20b 模型评审代码
-
构建
messages
:-
系统角色消息:包含
SYSTEM_PROMPT
。 -
用户角色消息:可选:上一次审查反馈。 当前待审查代码。
-
-
调用 Ollama 本地模型接口
chat.completions.create
生成审查意见。 -
返回模型输出文本。
step6 提交 MR 评论
- 调用 GitLab API
/merge_requests/{mr_iid}/notes
提交评论。 - 将 AI 模型生成的审查结果作为 MR 评论发布。
python
import requests
import argparse
import os
import sys
import time
import base64
from openai import OpenAI
import json
import functools
print = functools.partial(print, flush=True)
# ==== GitLab API 配置 ====
GITLAB_API = "https://git.xxx.com/api/v4"
PRIVATE_TOKEN = "gitlab-token"
# ==== Ollama 本地 API 配置 ====
OLLAMA_MODEL = "gpt-oss:20b"
OLLAMA_CLIENT = OpenAI(
api_key="ollama",
base_url="http://大模型服务对外暴露的IP:端口/v1"
)
SYSTEM_PROMPT = """
请你扮演一个资深的中文代码审查专家,
精通 Web 前端、Python 和 Java 编程语言,
请用简体中文帮我分析下面代码中可能存在的逻辑错误、
性能问题或不好的编码风格。
如果有问题,请用如下的格式输出审查结果:
##### 问题1
- 问题描述: xxx
- 修改建议: xxx
##### 问题2
- 问题描述: xxx
- 修改建议: xxx
...
要求:
- 问题描述和修改建议要简短,尽量控制在 100 字以内。
- 如果没有问题,请回复 "nice" 字样,不要输出任何多余的字符。
"""
# ==== GitLab API 封装 ====
def get_mr_changes(project_id, mr_iid):
url = f"{GITLAB_API}/projects/{project_id}/merge_requests/{mr_iid}/changes"
headers = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
resp = requests.get(url, headers=headers)
resp.raise_for_status()
changes = resp.json()["changes"]
# 允许审查的文件类型
allowed_extensions = (
'.vue', '.tsx', '.ts', '.js', '.css', '.less', '.scss',
'.py', '.pyi', '.ipynb', '.yaml', '.yml', '.ini', '.toml', '.json', '.env',
'.java', '.kt', '.xml', '.properties', '.gradle'
)
def is_only_whitespace_changes(diff_text: str) -> bool:
"""判断 diff 是否只包含空格、缩进、空行或注释变动"""
if not diff_text.strip():
return True
diff_lines = [
line[1:] # 去掉前缀 '+' 或 '-'
for line in diff_text.splitlines()
if line.startswith(('+', '-')) and not line.startswith(('+++', '---'))
]
for line in diff_lines:
stripped = line.strip()
# 如果有非空、非注释的代码,则返回 False
if stripped and not (stripped.startswith("//") or stripped.startswith("#") or stripped.startswith("/*") or stripped.startswith("*") or stripped.startswith("*/")):
return False
return True
filtered_changes = []
for change in changes:
file_path = change.get("new_path") or change.get("old_path", "未知文件")
normalized_path = file_path.replace("\\", "/")
# 1. 删除文件
if change.get("deleted_file", False):
print(f"跳过删除文件: {file_path}")
continue
# 2. 自动生成文件
if change.get("generated_file", False):
print(f"跳过自动生成文件: {file_path}")
continue
# 3. 重命名但内容没变
if change.get("renamed_file", False) and not change.get("diff", "").strip():
print(f"跳过仅重命名无内容变动的文件: {file_path}")
continue
# 4. 非允许扩展名
if not file_path.endswith(allowed_extensions):
print(f"跳过非代码文件: {file_path}")
continue
# 5. 前端文件需在 src/ 下
if file_path.endswith(('.ts', '.tsx', '.js', '.css', '.less', '.scss', '.vue')):
if not normalized_path.startswith("src/"):
print(f"跳过 src 目录外的前端文件: {file_path}")
continue
# 6. Python 文件需在 app/ 下
elif file_path.endswith(('.py', '.pyi', '.ipynb')):
if not normalized_path.startswith("app/"):
print(f"跳过 app 目录外的 Python 文件: {file_path}")
continue
# 7. 只改空格/缩进的修改
if is_only_whitespace_changes(change.get("diff", "")):
print(f"跳过仅格式调整的文件: {file_path}")
continue
# 8. 忽略大文件,这里假设 diff 字符串长度超过 10KB 的文件被认为是大文件
if change.get("diff") and len(change["diff"].encode("utf-8")) > 10 * 1024:
print(f"跳过大文件(>{10}KB): {file_path}")
continue
filtered_changes.append(change)
return filtered_changes
def get_mr_source_branch(project_id, mr_iid):
url = f"{GITLAB_API}/projects/{project_id}/merge_requests/{mr_iid}"
headers = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
resp = requests.get(url, headers=headers)
resp.raise_for_status()
return resp.json()["source_branch"]
def get_file_content(project_id, file_path, ref):
url = f"{GITLAB_API}/projects/{project_id}/repository/files/{requests.utils.quote(file_path, safe='')}"
headers = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
params = {"ref": ref}
resp = requests.get(url, headers=headers, params=params)
if resp.status_code == 200:
file_info = resp.json()
return base64.b64decode(file_info["content"]).decode("utf-8", errors="ignore")
else:
raise Exception(f"获取文件内容失败: {file_path} (status={resp.status_code})")
def post_mr_comment(project_id, mr_iid, body):
url = f"{GITLAB_API}/projects/{project_id}/merge_requests/{mr_iid}/notes"
headers = {"PRIVATE-TOKEN": PRIVATE_TOKEN, "Content-Type": "application/json"}
resp = requests.post(url, headers=headers, json={"body": body})
resp.raise_for_status()
return resp.json()
# ==== 直接调用 Ollama 模型 ====
def call_ollama_model(code_text, last_review=None):
messages = [{"role": "system", "content": SYSTEM_PROMPT.strip()}]
if last_review:
messages.append({
"role": "user",
"content": f"上一次代码审查的结果反馈是:\n{last_review}\n请根据这些反馈改进这次代码审查。"
})
messages.append({
"role": "user",
"content": f"以下是待评审代码内容: \n{code_text} \n请给出代码审查结果。"
})
# print("开始代码评审, messages 内容如下:")
# print(json.dumps(messages, ensure_ascii=False, indent=2))
try:
response = OLLAMA_CLIENT.chat.completions.create(
model=OLLAMA_MODEL,
messages=messages,
max_tokens=8192,
temperature=0.3
)
# print("接口完整响应:", response)
# print(f"评审意见...{response.choices[0].message.content}")
return response.choices[0].message.content.strip()
except Exception as e:
print(f"调用模型异常: {e}")
return f"模型响应失败: {e}"
# ==== 主流程 ====
def main():
parser = argparse.ArgumentParser(description="AI 代码评审 for GitLab MR(Ollama直连版)")
parser.add_argument("--project-id", required=True, help="GitLab 项目ID")
parser.add_argument("--project-name", required=True, help="GitLab 项目名称")
parser.add_argument("--mr-iid", required=True, help="Merge Request IID")
args = parser.parse_args()
if not PRIVATE_TOKEN:
print("请设置环境变量 GITLAB_PRIVATE_TOKEN", file=sys.stderr)
sys.exit(1)
try:
changes = get_mr_changes(args.project_id, args.mr_iid)
source_branch = get_mr_source_branch(args.project_id, args.mr_iid)
except Exception as e:
print(f"获取信息失败: {e}", file=sys.stderr)
sys.exit(1)
if not changes:
print("无变更内容,退出。")
sys.exit(0)
for change in changes:
file_path = change.get("new_path", "未知文件")
try:
full_content = get_file_content(args.project_id, file_path, source_branch)
except Exception as e:
print(f"获取完整内容失败: {e}", file=sys.stderr)
continue
prompt = f"{file_path} 文件的完整代码内容如下:\n\n{full_content}"
review = call_ollama_model(prompt)
if not review.strip() or review.strip() == "nice" or review.startswith("模型响应失败"):
continue
comment_body = f"🔍 **AI 审查建议 - 文件 `{file_path}`**\n\n{review}"
try:
post_mr_comment(args.project_id, args.mr_iid, comment_body)
print(f"✅ 已成功写入 `{file_path}` 的评审评论。")
time.sleep(1)
except Exception as e:
print(f"写评论失败: {e}", file=sys.stderr)
print("所有文件评审完成。")
if __name__ == "__main__":
main()
这里重点说一下调用本地大模型的入参和响应字段含义,因为这一块知识平常接触的比较少。
调用本地大模型入参数说明:
参数 | 类型 | 必填 | 默认 | 说明 |
---|---|---|---|---|
model |
str | ✅ | --- | 模型名称,如 "gpt-oss:20b" |
messages |
list[dict] | ✅ | --- | 消息列表,每条消息为 { "role": "system/user/assistant", "content": "..." } |
temperature |
float | ❌ | 1.0 | 控制随机性,0 越确定性,1 越自由 |
max_tokens |
int | ❌ | 2048 | 最大生成 token 数 |
top_p |
float | ❌ | 1.0 | 核采样概率阈值,通常和 temperature 配合使用 |
frequency_penalty |
float | ❌ | 0.0 | 对重复内容的惩罚系数 |
presence_penalty |
float | ❌ | 0.0 | 对新话题的奖励系数 |
stop |
str / list | ❌ | --- | 生成停止符或字符串列表 |
stream |
bool | ❌ | False | 是否启用流式输出 |
n |
int | ❌ | 1 | 返回的候选结果数量 |
logit_bias |
dict | ❌ | --- | 对指定 token 概率加权,形式 {token_id: bias} |
messages
字段说明
字段 | 类型 | 必填 | 说明 |
---|---|---|---|
role |
str | ✅ | "system" 、"user" 、"assistant" |
content |
str | ✅ | 消息内容文本 |
响应字段说明:
字段 | 类型 | 说明 |
---|---|---|
id |
str | 唯一请求 ID |
object |
str | 对象类型,通常 "chat.completion" |
created |
int | UNIX 时间戳 |
model |
str | 使用的模型 |
usage.prompt_tokens |
int | 输入 prompt token 数 |
usage.completion_tokens |
int | 输出生成 token 数 |
usage.total_tokens |
int | 总 token 数 |
choices |
list | 生成的候选结果数组 |
choices[i].index |
int | 候选序号 |
choices[i].message.role |
str | 消息角色,通常 "assistant" |
choices[i].message.content |
str | 模型生成文本 |
choices[i].finish_reason |
str | 生成结束原因,如 "stop" 、"length" |
- 代码审查类任务:推荐
temperature=0.1~0.3
,更严谨、更少幻想; - 自由生成任务:可以调高
temperature
和top_p
;
第三步 编写Jenkins Job运行大模型逻辑
Stage1:制作代码审查主流程镜像
目的:构建并推送 AI 代码审查主流程的 Docker 镜像。
执行流程:
-
使用
withCredentials
获取 Docker 仓库登录账号和密码。 -
设置镜像标签:
REVIEW_IMAGE_TAG = "base-images/ai-code-review:review-${CODE_VERSION}"
。 -
进入项目目录
common-tools/ai-code-review/ai-review-main
。 -
检查镜像是否已存在:用
curl
请求 Docker Registry 的 manifests API,HTTP 返回码200
表示镜像存在。 -
如果镜像不存在:(1) 登录 Docker Registry; (2) 构建镜像
Dockerfile.review
; (3)推送镜像到仓库。
Stage 2:AI 本地大模型代码审查
目的:运行 AI 代码审查脚本对 MR 内容进行自动审查。
执行流程:
- 使用
docker.image(...).inside
启动容器环境,运行之前构建的REVIEW_IMAGE_TAG
镜像。 - 在容器内执行脚本: 参数从 webhook JSON 中获取。
bash
cd /app
python ai_review.py --project-id=xxx --project-name=xxx --mr-iid=xxx
- 获取脚本输出
result
, 输出到 Jenkins 控制台。 4.简单检测错误:
js
pipeline {
agent { label 'jenkins-runner-1' }
environment {
REGISTRY_HOST = 'reg.xxx.com:9088'
CONTAINER_NAME = 'codellama-review'
}
stage('制作代码审查主流程镜像') {
steps {
script {
try {
withCredentials([
usernamePassword(
credentialsId: 'REGISTRY',
usernameVariable: 'REGISTRY_USERNAME',
passwordVariable: 'REGISTRY_PASSWORD'
)
]) {
env.REVIEW_IMAGE_TAG = "base-images/ai-code-review:review-${env.CODE_VERSION}"
dir('common-tools/ai-code-review/ai-review-main') {
def imageReviewExists = sh(
script: "curl -s -o /dev/null -w \"%{http_code}\" -u $REGISTRY_USERNAME:$REGISTRY_PASSWORD https://$REGISTRY_HOST/v2/${REVIEW_IMAGE_TAG.split(':')[0]}/manifests/${REVIEW_IMAGE_TAG.split(':')[1]}",
returnStdout: true
).trim() == '200'
// 如果镜像不存在,则构建并推送
if (!imageReviewExists) {
sh """
echo "\$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "\$REGISTRY_USERNAME" --password-stdin
docker build -f Dockerfile.review -t $REGISTRY_HOST/$REVIEW_IMAGE_TAG .
docker push $REGISTRY_HOST/$REVIEW_IMAGE_TAG
"""
}
}
}
} catch (Exception e) {
throw e
}
}
}
}
stage('AI本地大模型代码审查') {
// when() {
// expression { return env.WEBHOOK_JSON_object_attributes_iid == '218' }
// }
steps {
script {
docker.image("${REGISTRY_HOST}/${REVIEW_IMAGE_TAG}").inside {
script {
def result = sh(script: """
cd /app
python ai_review.py \
--project-id=${WEBHOOK_JSON_project_id} \
--project-name=${WEBHOOK_JSON_object_attributes_source_name} \
--mr-iid=${WEBHOOK_JSON_object_attributes_iid}
""", returnStdout: true).trim()
echo "脚本输出:\n${result}"
def safeResult = result ?: ''
if (safeResult.contains('Traceback') || safeResult.contains('Error')) {
error('AI Review 脚本执行异常,检测到错误信息')
}
}
}
}
}
}
}
}
步骤1使用的Dockerfile.review定义如下所示:主要是安装Python,以及ai_review.py所用到的依赖,复制主文件到容器, 这里要注意一下书写顺序, 因为前三步不会变,第四步经常变化, 如果把第四步写在第三步前面,会造成每次ai_review.py内容有变更时,都重新下载Python项目依赖。
js
# 基础镜像:Python 3.10 精简版
FROM python:3.10-slim
# 设置工作目录
WORKDIR /app
# 先安装依赖,利用缓存
RUN pip install --no-cache-dir openai requests
# 复制项目文件到镜像
COPY ai_review.py /app/
第四步 配置Webhook
要同时在Jenkins Job和Gitlab中配置webhook。Gitlab中的webhook会将各种git操作事件数据推送过来, Jenkins Job会对推送过来的事件进行监听处理
在Jenkins中设置webhook
重点配:
- Post content parameters变量merge_state,WEBHOOK_JSON
- Token变量,这里配置成项目名
- Optional filter 对webhook事件进行过滤

在Gitlab中配置webhook
只所以要先在Jenkins Job中配置webhook,是因为在Gitlab中配置webhook需要的两个参数:url和Secret 令牌,来自于Jenkins Job中的设置。
- url 填写 http://JENKINS_URL/generic-webhook-trigger/invoke
- Secret 令牌 填写在Jenkins Job webhook中配置的token

第五步 运行测试
测试方法: 基于dev分支创建一个feat分支, 故意复制一个函数, 改个名字,在Gitlab 界面发起向dev分支的合并请求,看看大模型能否评审出代码重复。
结果令人满意,大模型果然检查出了重复的代码。我看了一下大模型的评审建议, 比大多数人工审查更专业,更细致。

最后
使用大模型做了一件能产生价值的事情, 内心很有充实感,这时间花的很值。如果你的公司也打算实现AI大模型代码审查功能, 那你看我的文章, 可算是找对人了,能让你少走一些弯路,尤其是大模型的选择与下载那一块。没事多逛逛掘金,开卷有益。