职贝云数AI新零售门户

标题: 手把手教你完成用AI大模型做代码审查 [打印本页]

作者: 0qCf    时间: 6 小时前
标题: 手把手教你完成用AI大模型做代码审查
我头脑中有个理念,能用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
(, 下载次数: 0)
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。FROM reg.xxx.com:9088/base-images/ai-code-review/ollama-with-gpt-oss:latestCMD ["serve"]第二步编写代码审查大模型程序

简单说一下AI代码审查主流程的执行逻辑:step1 初始化与配置导入模块:requests, argparse, os, sys, time, base64, json, functools 等。重写 print 函数,使其自动刷新输入。配置GitLab API:配置Ollama 本地模型:定义系统提示 SYSTEM_PROMPT,用于引导模型执行代码审查。step2:  解析启动命令传递的Gitlab合并事情参数这些参数来自于gitlab webhook推送的合并事情,后面查询合并央求变更文件的时分要用到。运用 argparse 获取命令行参数:--project-id:GitLab 项目 ID--project-name:GitLab 项目称号--mr-iid:Merge Request IIDstep3 获取 MR 变更文件调用 GitLab API /merge_requests/{mr_iid}/changes 获取 MR 的文件变更列表。过滤文件, 下列这些文件不会被审查:前往符合条件的文件列表。step4 获取变更文件内容调用 GitLab API /merge_requests/{mr_iid} 获取源分支名,用于后续获取合并源分支改变文件残缺内容。调用 GitLab API /repository/files/{file_path} 获取改动文件残缺内容。文件内容为 Base64 编码,函数解码成 UTF-8 字符串前往。step 5 调用 gpt-oss:20b 模型评审代码构建 messages:调用 Ollama 本地模型接口 chat.completions.create 生成审查意见。前往模型输入文本。step6 提交 MR 评论调用 GitLab API /merge_requests/{mr_iid}/notes 提交评论。将 AI 模型生成的审查结果作为 MR 评论发布。import requestsimport argparseimport osimport sysimport timeimport base64from openai import OpenAIimport jsonimport functoolsprint = 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...要求:- 一直运用中文回答,假如一个成绩也检查不出来,请回复 "nice" 字样,不要输入任何多余的字符- 成绩描画和修正建议要简短,尽量控制在 100 字以内。- 假如有多个成绩,请按照成绩严重程度从高到低排序。"""# ==== GitLab API 封装 ====def get_mr_changes(project_id, mr_iid,project_name):    print(f"📂 获取项目 {project_name}  MR-{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')):            top_level_dir = normalized_path.split(os.sep)[0]            if top_level_dir in {"tests", "scripts"}:                print(f"跳过顶层目录 {top_level_dir} 中的 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_changesdef get_mr_source_branch(project_id, mr_iid):    # print(f"🔍 获取项目 {project_id} 的 MR {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,file_total,file_index):    print(f"\n\n📄 获取第 {file_index}/{file_total} 文件 {file_path} 的内容 ...")    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()        content = base64.b64decode(file_info["content"]).decode("utf-8", errors="ignore")        print(f"✅ 成功获取第 {file_index}/{file_total} 文件 {file_path} 文件内容,大小: {len(content)} 字符")        return content    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):    print("🤖 末尾调用 Ollama 模型停止代码审查...")    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:        print("⏳ 正在等待模型呼应...")        response = OLLAMA_CLIENT.chat.completions.create(            model=OLLAMA_MODEL,            messages=messages,            max_tokens=8192,            temperature=0.3        )        print("✅ 收到模型呼应")        # 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()    print(f"🎯 程序启动, {args.project_name} 项目末尾执行代码审查义务...")    # print(f"📋 参数配置 - 项目ID: {args.project_id}, MR IID: {args.mr_iid}")    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,args.project_name)        file_total=len(changes)        print(f"📋 共找到{file_total} 个符合审查条件的变更文件")        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 file_index, change in enumerate(changes, 1):        file_path = change.get("new_path", "未知文件")        try:            full_content = get_file_content(args.project_id, file_path, source_branch,file_total,file_index)        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 审查建议 - 文件 </span><span class="code-snippet__string"><span class="code-snippet__subst">{file_path}</span></span><span class="code-snippet__string">\n\n{review}"        try:            post_mr_comment(args.project_id, args.mr_iid, comment_body)            print(f"✅ 已成功写入 第 {file_index}/{file_total} 文件 {file_path} 的评审评论。")            time.sleep(1)        except Exception as e:            print(f"写评论失败: {e}", file=sys.stderr)    print("一切文件评审完成。")if name == "main":    main()这里重点说一下调用本地大模型的入参和呼应字段含义,由于这一块知识往常接触的比较少。调用本地大模型入参数阐明:
(, 下载次数: 0)
messages字段阐明
(, 下载次数: 0)
呼应字段阐明:
(, 下载次数: 0)
代码审查类义务:引荐 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 中获取。    cd /app    python ai_review.py --project-id=xxx --project-name=xxx --mr-iid=xxx3.获取脚本输入 result, 输入到 Jenkins 控制台。 4.简单检测错误: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 {              env.PROJECT_NAME = env.WEBHOOK_JSON_project_http_url.split('/')[-1].replace('.git', '')              def exitCode = sh(script: """                  cd /app                  python ai_review.py \                    --project-id=${WEBHOOK_JSON_project_id} \                    --project-name=${PROJECT_NAME} \                    --mr-iid=${WEBHOOK_JSON_object_attributes_iid}              """, returnStatus: true)              if (exitCode != 0) {                error('AI Review 脚本执行异常,检测到错误信息')              }            }          }        }      }    }  }}步骤1运用的Dockerfile.review定义如下所示:次要是安装Python,以及ai_review.py所用到的依赖,复制主文件到容器,  这里要留意一下书写顺序, 由于前三步不会变,第四步常常变化,  假如把第四步写在第三步后面,会形成每次ai_review.py内容有变更时,都重新下载Python项目依赖。
基础镜像: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_JSONToken变量,这里配置成项目名Optional filter 对webhook事情停止过滤
(, 下载次数: 0)
在Gitlab中配置webhook只所以要先在Jenkins Job中配置webhook,是由于在Gitlab中配置webhook需求的两个参数:url和Secret 令牌,来自于Jenkins Job中的设置。url 填写http://JENKINS_URL/generic-webhook-trigger/invokeSecret 令牌 填写在Jenkins Job webhook中配置的token
(, 下载次数: 0)
第五步 运转测试

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

最后

运用大模型做了一件能产生价值的事情, 内心很有充实感,这工夫花的很值。假如你的公司也打算完成AI大模型代码审查功能, 那你看我的文章, 可算是找对人了,能让你少走一些弯路,尤其是大模型的选择与下载那一块。没事多逛逛掘金,开卷有益。转载::https://juejin.cn/post/7537975830226862095




欢迎光临 职贝云数AI新零售门户 (https://www.taojin168.com/cloud/) Powered by Discuz! X3.5