LLM 如何评审 Merge Request:机制、提示词与一个演示案例
开发者在 GitLab 上点下“创建 MR”,几秒后 MR 页面出现“🔴 代码审查中“,再过一两分钟变成“🟢 已通过,自动批准“或”🔴 存在高风险问题“。背后是一个 LLM 代码审查平台在完整跑一遍“先阻断 → 逐文件审查 → 结构化输出 → 门禁决策 → 回写结果”的闭环。本文用一条演示 MR 把整个过程拆开看。
文中所有主机地址、端口、真实部署路径、模型名称均以占位符/通用表述表示,不含内部部署信息。
一、整体流程:从 MR 事件到审查结论
Section titled “一、整体流程:从 MR 事件到审查结论”GitLab MR 事件 → 审查服务 /webhook(token 验证 + 事件过滤) ↓立即阻断合并(取消批准 + blocking discussion + 标签 + 描述标记"审查中") ↓拉取 MR 变更文件(过滤二进制/第三方依赖) ↓逐文件并行调用 LLM(默认并发 3)──────────┐ ↓ │ 每个文件:结构化 JSON 输出(summary + issues[]) │ 系统提示词 + few-shot 示例 ↓ │ + 文件路径/新增行号/文件内容/diff行号硬过滤(只保留"新增代码行"内的问题) ┘ ↓门禁决策:存在 ≥高风险 或 有文件审查失败 → blocked 否则 → approved(解阻 + 自动批准) ↓回写 MR 描述 / 发邮件 / 问题入库六个关键动作:触发 → 阻断 → 拉取 → 审查 → 过滤 → 决策。
二、LLM 怎么评:提示词的三段式设计
Section titled “二、LLM 怎么评:提示词的三段式设计”每个文件的审查请求由三段拼接而成,通过标准输入传给模型,并要求只输出 JSON(用 JSON Schema 强校验,格式不符算失败并重试):
1. 系统提示词:定义角色、流程与铁律
Section titled “1. 系统提示词:定义角色、流程与铁律”你是一个严格的代码审查专家,执行结构化审查。
**审查流程(必须逐项执行):**1. 遍历每个变更(以 + 开头的行是新增代码)2. 对每个变更,按照以下风险类型检查: - 高风险:SQL注入、XSS、命令注入、密码硬编码、空指针、内存泄漏、严重逻辑错误、性能问题 - 中风险:代码规范、可读性问题、潜在bug - 低风险:代码风格、注释建议
**输出要求(严格遵守):**- 只输出 JSON,格式见下方 schema- line字段必须是新增代码的实际行号- 只报告确定的问题,不确定时跳过- 同一个问题不要重复报告- **重要:只报告由新增代码引起的问题,不报告与新增代码无关的问题**
**判断标准(严格遵循):**- 密码/密钥/Token出现 → 高风险- 用户输入拼接到SQL/命令/HTML → 高风险- 可导致程序崩溃的空指针 → 高风险- 代码逻辑明显错误 → 高风险- 变量命名不规范 → 低风险- 缺少注释 → 低风险要点是分级(高/中/低)和边界(只审新增代码、只报确定问题)——审查范围收窄到 diff,避免模型对存量代码“指手画脚”。
2. Few-shot 示例:给模型一个标准答案样板
Section titled “2. Few-shot 示例:给模型一个标准答案样板”示例diff:+ password = "placeholder-password"+ query = f"SELECT * FROM users WHERE name='{user}'"
正确输出:{ "summary": "发现2个问题", "issues": [ {"severity": "高", "file": "config.py", "line": 10, "description": "发现密码硬编码", "suggestion": "使用环境变量"}, {"severity": "高", "file": "user.py", "line": 25, "description": "SQL注入漏洞", "suggestion": "使用参数化查询"} ]}3. 场景输入:把“审什么”讲清楚
Section titled “3. 场景输入:把“审什么”讲清楚”请结合以下文件的内容审查代码变更:
**文件路径:** src/auth/login.py**本次变更新增代码行号(必须只报告这些行的问题):** 14, 17, 18**文件新内容:**```...完整文件内容,作上下文...```**代码变更内容(diff):**```...该文件的 diff...```注意“新增代码行号“这一行——它既在提示词里约束模型,代码里还会做一次硬过滤(见 §四),双保险确保不审存量代码。
输出 Schema
Section titled “输出 Schema”模型必须返回 {"summary": "...", "issues": [...]},每个 issue 含:
{ "severity": "高|中|低", "category": "风险类别", "file": "路径", "line": 行号, "description": "描述", "suggestion": "修复建议", "code_snippet": "代码片段", "language": "语言" }三、演示案例:一个登录接口的 MR
Section titled “三、演示案例:一个登录接口的 MR”- MR:
feature/refactor-login → main,改动 1 个文件src/auth/login.py - 开发者新增了 3 行代码:管理员口令判断、拼接查询、日志打印
def handle_login(username, password): user = find_user(username) if user: return issue_token(user)+ if password == "placeholder-password": # 第 14 行(新增,示例值)+ return placeholder_demo_token # 第 15 行(新增,示例值)+ query = f"SELECT id FROM users WHERE name='{username}'" # 第 17 行(新增)+ print("login: " + username) # 第 18 行(新增)模型输出(结构化 JSON)
Section titled “模型输出(结构化 JSON)”审查服务把上面的提示词组合好后发给模型,得到:
{ "summary": "发现 3 个问题(高:2 中:1 低:0)", "issues": [ { "severity": "高", "category": "密码硬编码", "file": "src/auth/login.py", "line": 14, "description": "硬编码管理员密码,任何拿到代码的人都可以绕过认证", "suggestion": "密码应存环境变量或密钥管理服务,禁止明文写代码" }, { "severity": "高", "category": "SQL注入", "file": "src/auth/login.py", "line": 17, "description": "用户输入直接拼接进 SQL,可被注入改写查询逻辑", "suggestion": "改用参数化查询或 ORM" }, { "severity": "中", "category": "日志泄露", "file": "src/auth/login.py", "line": 18, "description": "将用户输入打印到日志,可能泄露敏感信息", "suggestion": "日志脱敏或移除该行" } ]}后处理与决策
Section titled “后处理与决策”- 行号过滤:三个问题的行号(14/17/18)都在新增行集合内 → 全部保留(若模型报了第 5 行旧代码的问题,会被过滤掉)
- 门禁判断:存在 severity=高 的问题 →
has_blocking = true - 结论 blocked:保持阻断(MR 页面显示 🔴 高风险),更新 MR 描述列出 3 个问题,作者收到高风险邮件,合并被阻止
- 开发者修复(用环境变量取密码、参数化查询、删日志行)→ 再次 push 触发新一轮审查 → 通过 → 自动批准,MR 变绿可合并
四、决策规则与设计取舍
Section titled “四、决策规则与设计取舍”| 条件 | 结果 |
|---|---|
| 存在 ≥高风险 问题 | blocked:保持阻断 + 邮件 + MR 描述回写 |
| 只有中/低风险 | approved(或 warning) |
| 有任何文件审查失败 | 视为 blocked(保守策略:没审到就当有问题) |
几个有意的设计
Section titled “几个有意的设计”- 只审新增代码(双重约束):提示词声明“只报告由新增代码引起的问题” + 代码按 diff 解析出的新增行号做硬过滤。原因:存量问题应该走全量扫描/单独工单,MR 审查的价值是“这一版引入了什么新风险”,而不是把历史债务堆在开发者脸上
- 失败即阻塞:某文件审查失败(超时/额度/解析失败)时,MR 不会“没审到就放行”,而是保守阻断并提示人工查看——避免静默漏检
- 行号真实性:模型必须给真实新增行号(schema 约束 + 过滤校验),保证问题能精准定位到代码行
- 模型容错:额度不足/限流时自动切换备用模型或等待重试,审查任务不因瞬时故障中断
边界(不是什么)
Section titled “边界(不是什么)”- 不做存量代码全量体检(那是全量扫描的职责)
- 不替代人工评审(LLM 是“先审一遍 + 卡高风险”,人仍然要 review 设计/架构)
- 不阻断无新增代码的变更(纯删除/纯移动不触发审查重点)
LLM 评审 MR 的本质,是把“代码审查”翻译成一个高度约束的生成任务:
- 范围约束:只审 diff 新增行 → 提示词声明 + 行号硬过滤
- 形式约束:只输出合法 JSON → Schema 强校验
- 质量约束:只报确定问题、风险分级 → 系统提示词 + few-shot
- 决策约束:高风险或失败即阻断 → 保守门禁
演示里那条“硬编码密码 + SQL 注入”的 MR,从触发到“🔴 blocked“全自动完成,作者无需任何人工介入就拿到带行号的修复建议——这就是这套机制最实用的价值。