开发者构建GitLab CI job,利用AI将大型Merge Request按风险分级,自动过滤生成的lockfile和配置文件,并内置敏感信息脱敏。提示词短小精炼,仅在大MR场景触发,调用免费模型。
一个 1,200 行的 MR 在下午 4:50 进入了我的队列。
11 个文件被修改。其中 3 个是生成的 lockfile,5 个是配置更新,2 个是实际的源码文件。在那两个源码文件中,有一个删除了一个重试块。
我没有时间去逐行阅读。于是我构建了一个 GitLab CI 作业,在人工审查之前将 diff 分成不同的风险桶。
目标不是取代审查,而是让审查的前五分钟变得更智能。
大多数 AI 审查工具会将整个分支发送给模型。
这会产生三个问题:
长 diff 会被截断,因此有风险的代码行往往是模型看不到的那一行。
生成的文件和 lockfile 会淹没真正的变更。
你可能会意外地将 token、密钥或客户数据泄露在 diff 中。
我想要的是相反的效果:对于大型 MR,使用一个小 prompt,内置脱敏功能。
对于在线方案,我使用 MonkeyCode 的免费模型访问。这样可以在调优 prompt 的同时将实验成本保持在零。
免费服务器选项也很重要。我不需要配置 runner,不需要保持 GPU 运行,也不需要维护一个本地端点。CI 作业只需调用一个简短的托管端点然后停止。
披露:本文是 MonkeyCode 产品推广的一部分。
其余的工作流是普通的 GitLab CI 和 Python。
该作业仅在合并请求上运行。它不会发布评论或合并任何内容。
它生成一个名为 diff-triage.md 的 Markdown 产物。
文件有三个部分:
跳过检查 — 如果 diff 少于 200 行新增或删除,则不调用模型。
离线分类 — 基于路径的启发式规则立即产生一个粗略的分类桶。
在线分类 — 选定的源码片段被发送到免费端点以获取语义风险标签。
每个文件获得一个标签:
mechanical — 生成的、lockfile 或仅格式变更。test-only — 只有测试变更,没有源码变更。infrastructure — CI、Docker、配置。behavior-change — 可能影响运行时的源码。needs-human-eye — 模型不确定或文件匹配危险模式。如果在线调用失败或超时,脚本会回退到离线模式。这样作业仍然可以完成。
这是 .gitlab-ci.yml:
stages:
- triage
diff-triage:
stage: triage
image: python:3.12-slim
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
variables:
TARGET: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
before_script:
- git fetch origin $TARGET --depth=1
- pip install --quiet requests
script:
- python diff_triage.py --target origin/$TARGET --mode ${TRIAGE_MODE:-offline}
artifacts:
paths:
- diff-triage.md
when: always
一旦准备好使用模型路径,在项目 CI/CD 变量中设置 TRIAGE_MODE=online。
脚本特意做得很小。离线分类器是确定性的且可运行的。在线传输是一个 stub,你可以替换为 MonkeyCode 的文档化请求格式,因为该格式可能会发生变化。
import os
import re
import subprocess
import sys
from pathlib import Path
DANGER_PATTERNS = [
(r'(?i)(password|api[_-]?key|secret|token|bearer)', 'sensitive'),
(r'(?i)(timeout|retry|rate[_-]?limit|circuit)', 'resilience'),
(r'(?i)(auth|session|permission|role)', 'access-control'),
]
def run_git(target):
diff = subprocess.run(
['git', 'diff', '--name-status', target + '...HEAD'],
check=True,
capture_output=True,
text=True,
).stdout
files = []
for line in diff.splitlines():
if not line.strip():
continue
parts = line.split('\t')
status = parts[0]
path = parts[1] if len(parts) > 1 else parts[0]
files.append((status, path))
return files
def total_changed(target):
stat = subprocess.run(
['git', 'diff', '--numstat', target + '...HEAD'],
check=True,
capture_output=True,
text=True,
).stdout
added = deleted = 0
for line in stat.splitlines():
if not line.strip():
continue
try:
a, d, _ = line.split('\t', 2)
added += int(a) if a.isdigit() else 0
deleted += int(d) if d.isdigit() else 0
except ValueError:
continue
return added, deleted
def offline_label(status, path):
p = path.lower()
lockfiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'poetry.lock']
if any(seg in p for seg in lockfiles):
return ('mechanical', 'lockfile')
if p.endswith(('.min.js', '.min.css', '.map')):
return ('mechanical', 'generated')
if any(seg in p for seg in ['.github/', 'dockerfile', '.gitlab-ci.yml', 'renovate']):
return ('infrastructure', 'build or CI config')
if p.startswith('test') or 'test' in p or p.endswith('.spec.js') or p.endswith('.test.py'):
return ('test-only', 'test change')
if p.endswith(('.yml', '.yaml', '.toml', '.json', '.ini', '.env')):
return ('infrastructure', 'config file')
return ('behavior-change', 'source file')
def redact(text):
return re.sub(
r'(?i)(password|api[_-]?key|secret|token|bearer)\s*[:=]\s*\S+',
r'\1=<redacted>',
text,
)
def online_label(path, text):
# Replace this stub with MonkeyCode's free endpoint request.
# Return JSON with keys: kind, risk, reason.
return {
'kind': offline_label('M', path)[0],
'risk': 'medium',
'reason': 'online transport not configured; offline fallback used'
}
def main():
target = sys.argv[sys.argv.index('--target') + 1]
mode = sys.argv[sys.argv.index('--mode') + 1]
files = run_git(target)
added, deleted = total_changed(target)
output = ['# Diff triage', '']
output.append(f'- files: {len(files)}')
output.append(f'- added: {added}')
output.append(f'- deleted: {deleted}')
output.append('')
if added + deleted < 200:
output.append('Small diff. Skipping model triage. Review normally.')
Path('diff-triage.md').write_text('\n'.join(output))
return
for status, path in files:
kind, reason = offline_label(status, path)
if any(re.search(pattern, path, re.IGNORECASE) for pattern, _ in DANGER_PATTERNS):
kind = 'needs-human-eye'
reason = 'danger pattern in path'
risk = 'low'
if kind == 'behavior-change':
risk = 'high'
if mode == 'online':
text = subprocess.run(
['git', 'diff', 'HEAD', '--', path],
check=True,
capture_output=True,
text=True,
).stdout
redacted = redact(text)
result = online_label(path, redacted[:4000])
kind = result.get('kind', kind)
risk = result.get('risk', risk)
reason = result.get('reason', reason)
output.append(f'## {path}')
output.append(f'- st
最大的弱点是在线分类器。免费模型端点可能会受到速率限制或不可用。作业会回退,但回退的效果只和路径模式一样智能。
第二个弱点是截断。我在发送前将每个文件的 diff 限制在 4,000 个字符。如果一个文件有一个巨大的函数,这个限制比经典的 1,200 行 MR 更糟糕。设置这个限制是为了成本和隐私,而非为了准确性。
第三个弱点是标签不等于决策。risk: low 仍然可能隐藏一个 subtle bug。产物帮助确定优先级,但不表示批准。
如果满足以下情况,请跳过此工作流:
对于只需要区分机械变更和行为变更的团队来说,即使离线路径也可以节省真正的审查时间。
先试用离线模式。如果你已经有 MonkeyCode 免费账户,将一个合并请求切换到在线模式,并将标签与你的审查笔记对比。diff 产物变成了一张审查地图,而非一个神谕。