资讯动态

开源工具实践:本地静态规则与LLM结合的自动化代码评审

发布时间:2026/10/19 9:50:04 来源:尧图企业网站定制
代码评审这事儿干过的都懂。平时提个 MR等 review 等到地老天荒好不容易有人回了先甩一句“有空我看看”然后就没下文了真打开代码了注意力全被格式问题带跑真正埋着的逻辑错误反而一屏屏翻过去看不见。我自己维护几个开源项目被这个问题折磨了很久最后干脆动手写了个开源工具就是标题里这个open-code-review——一个跑在本地、把静态规则和 LLM 评审结合起来的小型代码评审助手。它能自动拉取 git diff先做一轮低成本的规则检查把函数复杂度过高、魔法数字、遗留 TODO 这类“一眼假”的问题捞出来再针对变更内容生成逐行的深度 review 意见最后输出一份按严重程度分级的 Markdown 报告。这篇文章就把整个工具的设计思路、核心实现、踩过的坑一次讲清楚适合想搞自动化 review 的开发者也适合想自己动手写工具的朋友参考。1. 项目背景与整体设计思路1.1 为什么还需要一个代码评审工具很多人第一反应是现有平台都自带 review 功能GitHub、GitLab 上都能评论、能讨论为什么还要单独搞一个工具我的回答是平台解决的是“流程”问题而实际痛点出在“注意力”上。评审者打开一个 500 行的 MR一半时间是花在筛选噪音上。缩进乱了、变量名长了、注释忘删了、一个魔法数字凭空冒出来——这些东西不是不重要但让这些东西占满评审精力真正可能出问题的并发边界、状态流转、回滚风险反而没人盯着。我做过一次粗略统计一个中等规模项目的 MR 里能被静态规则自动发现的低级问题大概占三成。这三成如果用工具自动标出来评审者就能把精力全部留给那七成需要人脑判断的部分。另一个痛点是兜底。团队里总有忙到顾不上细看的时候或者接手不熟悉模块时根本不敢乱发表意见。工具虽然替代不了人但它能做到“每次提交都认真看一遍”而且永远不烦躁。把低层问题过滤掉剩下的留给人工这是我认为自动化评审工具最务实的定位。1.2 方案选型与技术栈考量定下“本地运行 静态规则 LLM 补充”这个方向后技术选型我犹豫了很久最后还是选了 Python。不是因为它性能好而是因为生态太合适了。解析 diff 有标准库就能干AST 解析有现成模块将来想接第三方规则引擎也不需要额外折腾而且 Python 写命令行脚本几乎不需要模板代码原型阶段极其关键。LLM 这块open-code-review做成可插拔设计。默认走 OpenAI 兼容接口但你可以把 base_url 指向任意本地服务。接口只约定了请求格式和返回字段所以评估、换模型、内网部署这些事都不需要改代码。我自己的实测环境里跑过 GPT 系列也跑过本地量化模型效果差异主要出在指令遵循能力上工具本身不受影响。整个工具的定位决策是diff 解析、规则检查、报告生成全部本地完成只有调用 LLM 这一步会发外部请求代码内容默认不落盘。这样设计的好处是隐私边界清晰也方便那些对代码外发有顾虑的团队做二次改造。2. 核心功能拆解与实现细节2.1 数据采集层git diff 的处理工具的第一步是从 git 历史里拿到变更内容。我第一版图省事直接调git diff --unified3拿输出后按diff --git分割文件。后来发现两个问题一是新建文件没有index行导致解析器偶尔误判文件边界二是部分文件名带空格原来的按空格切分逻辑直接趴窝。最终的实现改成用--unified5确保上下文足够用--quiet检测是否有未提交变更解析时用正则严格匹配diff --git a/xxx b/xxx再结合行确认新文件路径。def parse_diff(raw_diff: str) - list[FileChange]: files [] current None for line in raw_diff.splitlines(): if line.startswith(diff --git): if current: files.append(current) parts line.split( b/, 1) # 兼容子目录前缀 file_path parts[-1].strip() current FileChange(pathfile_path, hunks[]) elif line.startswith(): # 解析 -x,y x,y 段 match re.search(r -(\d)(?:,(\d))? \(\d)(?:,(\d))? , line) ... elif current is not None: current.hunks[-1].append(line) if current: files.append(current) return files这层最容易踩的坑是二进制文件。图片、打包产物、lockfile 这些一旦混进 diff轻则解析出乱码重则直接把整个报告撑爆。处理办法是检查变更文件后缀名以及读取前 N 字节判断是否包含\0字符是就直接标记为 binary 跳过。2.2 规则引擎哪些问题不需要大模型规则引擎是工具里性价比最高的部分。它几乎不耗算力跑得飞快并且能稳定命中一大批评审者最不爱看的问题。我不追求规则数量多而是每条规则都必须能在真实项目里站得住脚。目前默认启用的核心规则有这么几条规则名称检测目标判定方式函数扇出过大新增/修改的函数超过 8 个直接调用点基于 AST 统计调用表达式圈复杂度过高新增函数圈复杂度 10统计 if/for/while/case//魔法数字非测试代码中出现裸数字排除 0、1、-1 及常量定义场景TODO/FIXME 残留变更行中带有 TODO 或 FIXME正则匹配过深嵌套新增代码块缩进超过 5 层按行级缩进计算重复代码块同一文件内出现相似段落基于行哈希的近似匹配圈复杂度计算这里多说一句。按标准的计算方式if、for、while、case、catch、、||每种出现一次复杂度加一基准复杂度为 1。实测下来一个函数如果复杂度过了 15读起来就明显费劲了测试覆盖也难做。我通过 AST 的ast.walk去数节点类型准确度和速度都够用。规则引擎的另一个好处是“可解释”。所有规则命中后都会带上规则说明、严重级别和位置信息开发者接到报告不用猜直接知道改哪里、为什么改。这一点是大模型给不了的。2.3 LLM 评审模块prompt 设计与数据裁剪LLM 评审是工具里最灵活也最需要调教的部分。设计核心原则是“只给变更相关的信息不给整文件背景”。每次调用会构造一个两段式 prompt第一段是系统角色设定告诉模型“你是一名资深代码评审者输出必须是精准的、可行动的评论不要客套话”第二段是用户数据包含文件路径、变更 diff、以及规则引擎已经跑出来的结果。让 LLM 知道规则结果很重要可以避免它重复报同样的问题。SYSTEM_PROMPT 你是一名资深代码评审工程师。你的目标是针对给定的代码变更提出精准且可执行的评审意见。 要求 1. 只关注 diff 中新增/修改的代码行不要评论上下文里未变更的内容。 2. 每条意见必须包含文件路径、行号、问题类型、问题描述、修改建议。 3. 如果没有问题明确回答“无需修改”。不要为了凑数而提意见。 4. 优先关注逻辑错误、边界条件、并发风险、资源泄漏、安全隐患。 USER_PROMPT 以下是代码变更信息请按规则进行评审。 文件: {file_path} 语言: {language} 变更内容(diff): {diff_text} 静态规则检查结果可能为空: {rule_result} 请以 JSON 数组格式输出评审意见每个元素的字段为: file_path, line, severity, type, description, suggestion 发送之前有一个关键的数据裁剪环节。一个超大文件的完整 diff 可能几万 token直接塞进模型既费钱又容易超出上下文窗口。我的处理办法是对超过 200 行的变更只保留每个 hunk 修改行前后各 20 行上下文其余用注释占位。这样既不丢核心变量又能把 token 控制在合理范围。实测一个 1000 行的变更文件处理后通常能压到原来的一半出头评审质量基本没掉。2.4 报告输出与 CI 集成报告的最终形态是 Markdown 文件。我看了不少团队的 review 习惯发现“按文件平铺意见”不如“按严重级别折叠 文件内按行号排序”好读。所以输出时做了两层组织最上面是摘要统计新增、修改、删除文件数以及各级别问题的数量往下是问题列表按 P0/P1/P2 排序同级别内按文件路径排序。针对 CI 场景还加了一个退出码机制只要存在 P0 级别问题进程就返回 1方便接入流水线后直接阻断合并。P1 和 P2 只记录在报告里不阻塞流程。我一开始把 P1 也纳入阻断结果团队反馈太苛刻几乎每个 MR 都亮红灯后来调整成只卡 P0 才平衡了效果。3. 从零到一的实操过程3.1 项目初始化与依赖管理项目一开始就用pyproject.toml管理打包工具选的hatchling。依赖只有openai、click、pathspec和 Python 3.10 自带的标准库。之所以把依赖控制得这么精简是希望任何人pip install open-code-review之后能立刻跑起来不让环境问题消磨用户耐心。[project] name open-code-review version 0.1.0 requires-python 3.10 dependencies [ click8.0, openai1.0, pathspec0.11, ] [project.scripts] open-code-review open_code_review.cli:main命令行的交互设计上我默认子命令叫review常用参数包括--diff指定 diff 来源、--exclude排除目录、--min-severity最低输出级别、--output报告路径。还有--model指定模型名--base-url指向兼容接口的本地服务。这些参数看起来多但在 CI 里跑流水线时几乎都有用。3.2 规则检查模块的实现顺序建议先实现规则引擎再做 LLM 模块因为规则引擎可以完全离线开发方便快速验证主流程。我开发时先写了“解析 diff → 逐文件跑规则 → 输出 Markdown”这条链路用几个本地仓库做回归数据。等这条链路稳定跑通再往上接 LLM就不会出现“模型调用失败整个工具不可用”的情况。AST 分析这里有个坑Python 的ast对部分新语法兼容性有延迟。比如 3.12 的某些语法糖在 3.10 的ast下会直接抛SyntaxError。解决方式是捕获异常后回退到纯文本规则也就是只做函数长度和缩进检查不做深层 AST 分析。这样最坏情况是丢精度不会崩工具。3.3 核心代码规则引擎的骨架规则引擎整体做成注册器模式。每个规则是一个函数接收FileChange对象和源码内容返回一个或多个Issue。这种设计方便以后扩展第三方规则社区里的人不用改核心代码写个函数注册一下就行。from dataclasses import dataclass, field dataclass class Issue: file_path: str line: int severity: str # P0 / P1 / P2 rule: str message: str suggestion: str rules [] # 注册器 def register_rule(func): rules.append(func) return func register_rule def check_todo(file_change, code_lines): issues [] for idx, line in enumerate(code_lines, start1): if TODO in line or FIXME in line: issues.append(Issue( file_pathfile_change.path, lineidx, severityP2, ruletodo-left, message检测到遗留的 TODO/FIXME 标记, suggestion解决该事项或关联 issue 后再提交, )) return issues主循环里对每个变更文件按语言分发Python 文件走 AST 逻辑JavaScript/TypeScript 走简化版规则不整 AST只做正则和缩进其他文本文件走通用规则。这个分层的思路是按需提升复杂度而不是一上来就要求每个语言都有完美的 AST 解析。3.4 接入本地模型的配置示例默认配置走 OpenAI 兼容接口。比如用本地部署的模型只需要在调用时指定base_url和model参数。下面是 CLI 直接用和 Python API 调用两种方式。open-code-review review \ --diff git diff --staged \ --base-url http://127.0.0.1:8080/v1 \ --model local-qwen2.5-coder如果项目里需要内置配置可以放到pyproject.toml的[tool.open-code-review]段落里比如设置默认排除目录、最低输出级别、模型名。命令行参数优先于配置文件配置文件优先于默认值这是三层的可预期覆盖逻辑。[tool.open-code-review] exclude [vendor/, dist/, node_modules/] min-severity P2 model gpt-4o-mini base-url https://api.openai.com/v13.5 报告生成的实现细节报告生成不是简单地字符串拼接而是先构建一个ReviewReport数据类把所有意见结构化再通过 renderer 输出成 Markdown。这样可以轻轻松松再加一个 JSON 输出格式方便 CI 或其他工具消费数据。摘要部分用了几个关键指标本次评审覆盖的代码行数、新增行数、删除行数、各级别问题数量、以及一个简单的“评审评分”。评分不是玄学而是基于 P0 权重 10、P1 权重 3、P2 权重 1 计算得出分数越高代表风险越高方便一眼感知 MR 健康度。class ReviewReport: def __init__(self): self.files: list[FileChange] [] self.issues: list[Issue] [] self.model_output [] def risk_score(self) - int: score 0 for issue in self.issues: if issue.severity P0: score 10 elif issue.severity P1: score 3 else: score 1 return score3.6 在本地仓库上的实测效果搭建完工具后我花了半天时间在自己维护的开源项目上跑了几轮真实 MR验证效果。那是一个约两万行的 Python 项目改动的文件集中在核心模块。跑完一轮下来工具报出了 3 个 P1、7 个 P2以及一堆 P3 级别的格式问题P3 默认不出现在报告里。人工复核之后P1 里有两个是真问题一个是没有处理字典 key 不存在时的默认值一个是循环内重复调用了开销极大的函数。这两条恰好是评审者容易被格式噪音掩盖掉的逻辑隐患。我还做了一组对比同一批 MR让工具单独跑规则、单独跑 LLM、规则 LLM 一起跑。结果很能说明问题。单独跑规则查得出魔法数字和复杂函数但看不出“这个变量名容易引起误解”这类语义问题单独跑 LLM它能发现深层逻辑问句但对 TODO 残留这种低级问题经常漏报。两者结合之后覆盖面最完整而且 LLM 的误报率明显下降原因是规则结果给了它正确的上下文提示。4. 常见问题与排查技巧实录4.1 diff 解析相关的坑最常遇到的是 windows 换行符问题。git 默认在检出时可能把 CRLF 转成 LF所以解析 diff 时必须统一先line.rstrip(\r\n)否则行号正则会匹配失败。另一个是文件名包含空格的情况比如assets/my file.txt。遇到这类文件split( b/)之后的路径本身是能拿对的但如果你在解析时提前把路径按空格切了就废了。我的建议是永远从 b/的原始行里提取路径别用diff --git行的推断值。4.2 LLM 调用超限与稳定性处理本地模型或共享 API 服务很容易触发超时和限流。我处理的方式是给请求设置三档重试连接超时 15 秒、读超时 60 秒最大重试 3 次重试之间退避 2 秒。如果三次都失败不会直接报错退出而是把这一条记为 “LLM 评审不可用”然后输出规则检查的结果。宁可少一点建议也不能让工具因为外部依赖挂了就完全不可用。这在实际使用中几乎是必现的坑。import time from openai import OpenAI def call_llm_with_retry(client, messages, max_retries3): for attempt in range(max_retries): try: resp client.chat.completions.create( modelMODEL_NAME, messagesmessages, temperature0.2, max_tokens1200, ) return resp.choices[0].message.content except Exception as exc: if attempt max_retries - 1: return None time.sleep(2 * (attempt 1))4.3 token 消耗异常问题上线一周后有用户反馈某次评审 token 消耗大得离谱。排查后发现是文件过滤没生效打包后的二进制文件被当成文本发了过去一次顶几十个普通文件。这个问题的根因是.gitignore规则没完全覆盖所有产物目录。最终修复方案是不信任单一 ignore 规则手动维护一个SKIP_FILE_PATTERNS集合包括图片、压缩包、二进制库文件等后缀以及.lock文件默认不做评审因为 lockfile 这种自动生成文件对评审毫无意义。4.4 常见问题速查表现象可能原因解决方案diff 解析结果为空git diff 输出为空或命令执行失败先用git status确认有未提交变更再检查仓库路径是否正确AST 解析抛 SyntaxError新语法在当前 Python 版本不支持捕获异常降级为文本规则不影响主流程LLM 经常超时模型服务负载高或网络延迟大调大请求超时减少并发本地模型建议配流式输出报告里规则重复报同一个问题同一行同时命中多条规则按“文件行号规则名”做去重保留最高严重级别的记录P0 阻断没有触发CI 退出码未捕获检查是否在流水线最后一步执行工具P0 存在时显式sys.exit(1)4.5 调试工具的技巧开发这个工具的过程中我养成了一个习惯所有中间结果都支持落盘。--debug-dir参数能把解析后的 diff、规则命中明细、发送给 LLM 的 prompt 和原始响应全部存下来。这个调试手段帮我解决了不少问题。比如有用户反馈“LLM 回的格式不对”我一看保存下来的原始响应发现是模型输出里混进了解释性文字导致 JSON 解析失败。后来在 prompt 里强制要求“只输出 JSON 数组不要任何解释文字”并在解析时先用正则捞出以[开头、]结尾的部分兼容性大幅提升。5. 实际体验之后的几个建议工具写了也用了几个月我自己最大的感受是它把评审这件事从一个“拼耐心”的任务变成了“拼判断力”的任务。以前看 MR得强迫自己一行行读现在工具先把明显的问题标出来我再带着问题去读代码注意力和思考深度都提升了不止一个档次。如果你也想在自己的项目里接入这类方案我的建议是分三步走。第一步先只用规则引擎不加 LLM让团队熟悉报告格式。第二步让工具以“建议”身份出现在 CI 中只输出报告不阻断观察一段时间统计误报率。第三步确认规则噪音足够低之后再开启 P0 阻断和 LLM 补充。节奏比功能重要工具一上来太严厉团队很快就会无视报告。最后聊一个取舍。open-code-review不打算做成全自动的“AI 评审机器人”它的定位始终是辅助工具而不是决策者。规则引擎是稳定可信的底座LLM 是带点不确定性的增强模块两者结合恰好覆盖了各自的盲区。未来如果要扩展我可能会考虑加入自定义规则模板、支持更多的语言 AST 解析、以及提供 GitHub Action / GitLab CI 的现成示例。这些方向都有不少人在问了社区需求比预期来得更快。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑