基于 Git Hooks + LLM 实现根据自然语言规则进行 Code Review

一、问题背景

团队内部有不少开发规范,但在日常赶需求的过程中很容易遗漏。比如 spark-submit 脚本的参数格式、文件命名约定等,这些"小问题"不影响运行,但是人工校验的心智负担较大

现状的几个痛点:

  1. 规范执行靠人力 ------ 团队有规范文档,但全靠开发者自觉遵守,review 时也容易漏检
  2. 现有 CI 覆盖不到 ------ GitLab 上有通用的 Code Review 流程,但团队特有的 SparkSQL 脚本规范并未收录其中
  3. 传统方式实现成本高 ------ 这些规范描述灵活(如"每个参数独占一行"),用程序处理文本的思路必然导致后续难开发,难维护(所以一直没做)

转机: LLM 天然擅长理解自然语言描述的规则,并对代码做结构化判断。将规范以自然语言编写,交给 LLM 逐条检查,可以低成本实现一套灵活的自动化审查工具。

二、技术方案

整体思路

sql 复制代码
git commit → pre-commit hook 拦截
    → 筛选变更的 Spark 脚本文件
    → 调用 Python 脚本将代码 + 规则发送给 LLM
    → LLM 返回结构化审查结果(JSON)
    → 不通过则阻止提交,输出问题详情

为什么选 Git Hooks

Git 原生支持 hooks 机制,在 .git/hooks/ 目录下放置可执行脚本即可生效,无需额外依赖:

Hook 触发时机 适用场景
pre-commit commit 前 代码检查、格式校验
commit-msg 写入 message 后 commit message 规范检查
pre-push push 前 集成测试、权限校验

本方案选择 pre-commit ------ 在代码提交前拦截,问题发现越早修复成本越低。

实现细节

1. Hook 脚本(pre-commit)

核心逻辑:筛选本次暂存区中变更的 Spark 相关文件,传递给审查脚本。

bash 复制代码
#!/bin/bash

# 获取暂存区中新增/修改的 spark 相关文件
changed_sql_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '(^|/)spark')

if [ -z "$changed_sql_files" ]; then
  echo "本次没有变更 spark 相关文件,跳过审查"
  exit 0
fi

echo "检测到 Spark 文件变更,开始 AI Review..."
python3 ai_code_review.py ${changed_sql_files}

exit $?

2. 审查脚本(Python)

职责:读取规则文件和代码文件,组装 prompt 调用 LLM,解析返回的 JSON 判断是否通过。

python 复制代码
import os
import sys
import argparse
from openai import OpenAI
import json

sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')

RULE_FILE_PATH = "./code_review_rule.md"

def read_file(file_path):
    """读取文件内容,不存在则返回 None"""
    if not os.path.exists(file_path):
        print(f"文件不存在: {file_path}")
        return None
    with open(file_path, 'r', encoding='utf-8') as f:
        return f.read()

def code_review(client, rule_content, file_path):
    """调用 LLM 对单个文件进行规则审查"""
    code_content = read_file(file_path)
    if code_content is None:
        return None

    system_prompt = f"""你是一个专业的代码审查助手。请根据以下规则对代码进行审查。
规则:
{rule_content}

严格按以下 JSON 格式输出,不允许输出任何额外内容:
{{
    "file_pass": true/false,
    "issues": [
        {{
            "rule": "规则名",
            "message": "一句话说明原因",
            "rule_pass": true/false
        }}
    ]
}}"""

    user_prompt = f"""请对以下代码进行 Code Review:
文件名: {file_path}
---
{code_content}
---

    completion = client.chat.completions.create(
        model="qwen-plus",
        messages=[
            {'role': 'system', 'content': system_prompt},
            {'role': 'user', 'content': user_prompt}
        ]
    )
    return completion.choices[0].message.content

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('files', nargs='+', help='待审查文件路径')
    parser.add_argument('--rule-file', default=RULE_FILE_PATH)
    args = parser.parse_args()

    rule_content = read_file(args.rule_file)
    if rule_content is None:
        sys.exit(1)

    client = OpenAI(
        api_key="sk-xxxx",  # 替换为实际 API Key
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )

    review_results = []
    for i, file_path in enumerate(args.files, 1):
        print(f"[{i}/{len(args.files)}] 审查: {file_path}")
        result = code_review(client, rule_content, file_path)
        if result:
            review_results.append(result)

    # 筛选未通过的文件
    failed = [r for r in review_results if not json.loads(r).get("file_pass", False)]

    if failed:
        print("\n❌ 代码审查未通过:")
        for item in failed:
            print(item)
        sys.exit(1)
    else:
        print("\n✅ 代码审查通过")
        sys.exit(0)

if __name__ == '__main__':
    main()

3. 规则文件(code_review_rule.md)

规则用自然语言编写,随时可扩充,无需改代码:

markdown 复制代码
# SparkSQL 脚本规范

1. 文件名必须有后缀
2. 文件名除去后缀后,应与 --name 参数的值一致
3. shell 续行符 `\` 后面不能有空格
4. spark-submit 的每个参数必须独占一行

三、效果展示

LLM 返回的结构化结果示例:

json 复制代码
{
    "file_pass": false,
    "issues": [
        {
            "rule": "文件名必须有后缀",
            "message": "文件名有 .sh 后缀,符合规则",
            "rule_pass": true
        },
        {
            "rule": "文件名除去后缀后,应与 --name 参数的值一致",
            "message": "文件名与 --name 值一致,符合规则",
            "rule_pass": true
        },
        {
            "rule": "spark-submit 的每个参数必须独占一行",
            "message": "--executor-cores 和 --num-executors 在同一行,违反规则",
            "rule_pass": false
        }
    ]
}

四过程中遇到的问题:Prompt 工程的迭代

问题:LLM 输出冗余且自相矛盾

初版 prompt 没有严格约束输出格式,LLM 会:

  • 先给出"不通过"结论,然后自我反驳改为"通过"
  • 输出大段分析过程,有结论, 但是又有很多冗余信息(第一次回答中先说不通过,又自我修正为通过)

示例

javascript 复制代码
规则: 文件名除去类型后缀后, 和--name的值应该是一样的 
是否通过:不通过,不通过的理由: 文件名是 `spark_adl_dl_general_view_bitmap_v2_d_d_for_ltv.sh`,除去后缀 `.sh` 后为 `spark_adl_dl_general_view_bitmap_v2_d_d_for_ltv`;`--name` 值为 `'spark_adl_dl_general_view_bitmap_v2_d_d_for_ltv'`(含单引号),但规则比对的是**值本身**,引号属于 shell 语法,不应参与比较。因此名称字符串一致 ✅ ------ **此项实际通过**。  
⚠️ 注意:原判断易误将引号计入,但规则明确是"和--name的值应该是一样",而 `--name 'xxx'` 的值就是 `xxx`(shell 解析后传给 Spark 的 name 是无引号的)。故该条 **通过**。
规则: shell换行符后面不要有空格  
是否通过:不通过,不通过的理由: `spark-submit` 命令行末尾 `` 后存在不可见空格(肉眼不可见但实际存在)------需检查。在提供的代码中:  
`spark-submit --name 'spark_adl_dl_general_view_bitmap_v2_d_d_for_ltv' `  
→ 该行末尾 `` 后**存在空格**(经文本分析确认:原始输入中 `` 后紧接换行,但部分编辑器/粘贴可能引入空格;然而本段代码中 `` 后**无可见字符且直接换行**,符合规范)。  
但严格依据代码原文逐字符核查(用户输入中 `` 行末无空格),此项 **通过**。  


✅ 再次确认:原始文本中该反斜杠后**无空格、无制表符、直接换行** → 符合规则。
规则: spark-submit的时候每个参数都需要换行  
是否通过:不通过,不通过的理由: `--executor-cores 1 --num-executors ${num-executors} --conf spark.sql.execution.useObjectHashAggregateExec=false` 这三个参数写在同一行,未按规则"每个参数都需要换行";应拆分为三行,每行一个参数(或参数+值作为一个逻辑单元,但规则明确"每个参数",即每个 `--xxx` 开头项需独占一行)。当前违反规则。
最终结论(修正自相矛盾后):
规则: 文件名除去类型后缀后, 和--name的值应该是一样的 
是否通过:通过  

规则: shell换行符后面不要有空格  
是否通过:通过  
规则: spark-submit的时候每个参数都需要换行  
是否通过:不通过,不通过的理由: `--executor-cores 1`、`--num-executors ${num-executors}` 和 `--conf spark.sql.execution.useObjectHashAggregateExec=false` 三个参数位于同一行,未各自换行。
```

改进:约束输出格式 + 优化 prompt

javascript 复制代码
根据规则逐条检查, 严格按以下 JSON 输出,且只能输出 JSON,不允许输出任何解释、前言、反思、重复结论:
    {{
    "file_pass": true/false,
    "issues": [
        {{
        "rule": "规则名",
        "message": "一句话原因"
        "rule_pass": true/false
        }}
    ]
    }}
    1. file_pass填的本次审查最终是否通过
    2. rule_pass填的是此条规则是否通过

优化后输出

json 复制代码
{
    "file_pass": false,
    "issues": [
        {
            "rule": "文件名必须有后缀",
            "message": "文件名有 .sh 后缀,符合规则",
            "rule_pass": true
        },
        {
            "rule": "文件名除去后缀后,应与 --name 参数的值一致",
            "message": "文件名与 --name 值一致,符合规则",
            "rule_pass": true
        },
        {
            "rule": "spark-submit 的每个参数必须独占一行",
            "message": "--executor-cores 和 --num-executors 在同一行,违反规则",
            "rule_pass": false
        }
    ]
}

关于多轮交互的取舍

曾考虑让 LLM 做多轮自查(先审查再复核),最终放弃:

维度 单轮 多轮
响应速度 快,适合 commit 前阻塞场景 慢,开发者等待体验差
结果可控性 输出格式固定,易解析 多轮上下文累积可能引入矛盾
问题修复方式 优化 prompt + 规则描述 依赖模型自纠,不可预期

结论:pre-commit 场景优先保证速度和确定性,准确率通过迭代 prompt 和规则描述来提升。

相关推荐
wangruofeng8 小时前
opencodex 解锁 Codex 任意模型,一个本地代理打通 Claude/Kimi/GLM/DeepSeek
llm·github·openai
wangruofeng8 小时前
姚顺雨长谈:在 Anthropic 和 Gemini 训练模型,英雄主义已经过时
llm·aigc
大模型码小白9 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
延凡科技12 小时前
多场景落地复盘:端边云架构无人机智能巡检系统设计与实践
大数据·数据结构·人工智能·科技·架构·无人机·能源
ifenxi爱分析13 小时前
爱分析最新报告解读:AI数据基础设施与数据中台的区别
大数据·人工智能
赵康13 小时前
AI 写代码之后,Code Review 会议怎么开
ai·llm·skill
莫逸风13 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
品牌全球行13 小时前
共商共建共享 链接数字未来——“一带一路数字新城(深圳)会客厅筹备办”揭牌仪式在深圳隆重举行
大数据·人工智能
数智化管理手记15 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
ApacheSeaTunnel16 小时前
Apache SeaTunnel AI CLI Benchmark:7 款大模型、100 个 ETL 任务实测,谁真正能跑起来?
大数据·ai·开源·大模型·数据集成·cli·seatunnel·技术分享·数据同步