如果你和我一样,经常在 PR 里被小到行尾空格、大到逻辑边界的问题反复折磨,应该会喜欢这套方案。我把 ruff、mypy 和一个本地部署的代码模型塞进同一条审查流水线,形成了双轨代码审查机制:一条轨道用规则与类型把低阶问题锁死,另一条轨道用模型去补语义和逻辑上的漏网之鱼。
这套方案并不神秘,也不是要把 Code Review 完全自动化。真正想做的是让工具在人工 review 之前,先把脏活累活干完,然后把最有价值的问题留给人类。这篇文章会把我搭建这套双轨审查的配置、踩坑、提示词和取舍全部写出来,适合正在做 Python 工程化、或者在团队里推行代码质量体系的同学参考。读完你可以直接抄作业,也能根据自己团队的情况调整规则和模型阈值。
1. 双轨审查的出发点:为什么只用工具或只用模型都不行
先说结论:静态工具和模型解决的是不同层次的问题,谁也没法完全替代谁。
1.1 规则工具能锁死什么
ruff 的强大在于确定性。它不会跟你商量,不符合规则就是不符合规则。比如未使用的导入、错误的命名风格、不必要的列表推导、可疑的断言写法,这些都可以通过规则集一次性扫出来。mypy 的处理则是另一类确定性:类型不匹配、隐式 Any、不正确的返回类型、参数缺失,它在类型层面给出严格判断。
这带来的好处是:一旦规则通过,你能对代码的“基本卫生”放心一半。团队里不会再有“忘记删 print”“变量名蛇形改成驼峰”“把 Optional 直接返回给调用方”这类低级争执。规则工具把约定变成了机器可执行的约束。
但它也有明显盲区。工具不会理解“这段业务逻辑为什么成立”,也不会判断“这个异常被吞掉是不是在掩盖问题”。它检查的是形式,不是语义。
1.2 模型能把控什么
模型审查的价值在于语义推断。给它一段代码,它能结合上下文指出:这个分支条件可能永远为真、这里缺少空值判断、异常处理范围过大导致错误被静默吞掉、这条路径的边界条件没有覆盖到。这些内容是靠“理解”得到的,不依赖固定规则。
但模型的问题是概率性。它给出的判断不保证百分百正确,有时会提出无中生有的建议,有时又会漏掉真正关键的问题。如果直接把模型输出的每一条都当成必须修复的问题,团队会比不用模型时更累。
1.3 两条轨道如何互补
双轨的核心思路就四个字:分工明确。规则轨负责确定性的检查,模型轨负责不确定性的提醒。前者没有通过就无须让模型出场,否则纯属浪费;后者则专门处理规则看不出来的逻辑问题和设计隐患。两者不是“用哪个好”,而是“在哪个阶段用哪个”。
我在实际团队里推动这套方案时,最深的感受是:工具必须“有限责任”。让模型负全责,它担不起;让规则做语义判断,它也做不了。只有把它们放在各自的位置上,整体审查体系才是可解释、可容忍、可持续的。
2. 第一轨:用 ruff 把代码风格和低阶问题锁死
这是最无趣、但收益最快的一步。我甚至建议所有新项目不管后边接不接模型,先把 ruff 跑起来。
2.1 为什么选 ruff,而不是 flake8+isort+black 的组合
以前我的方案是 black 格式化、isort 排序、flake8 检查。每个工具都要一份配置,执行顺序还要编排,速度也一般。后来切到 ruff,因为它是 Rust 实现的,单次扫描是毫秒级,而且把 lint、format、import sort 全合并了。
选 ruff 还有一层现实原因:它默认兼容 pycodestyle 和 pyflakes,绝大多数老团队迁移成本低。并且它支持自动修复,notebook 场景也覆盖。对审查流程来说,审查工具本身要快,不然开发者顺手就会删掉 pre-commit。
2.2 实操:ruff 配置、规则集和自动修复
我常用的一套 pyproject.toml 配置大致是这样:
[tool.ruff] target-version = "py311" line-length = 100 src = ["src", "tests"] extend-exclude = ["docs", "scripts/archive"] [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # bugbear "C4", # flake8-comprehensions "UP", # pyupgrade "SIM", # flake8-simplify "S", # bandit security "ARG", # unused arguments "PIE", # misc. fixes ] ignore = ["S101", "B008", "S311"] fix = true show-fix-status = true [tool.ruff.format] quote-style = "double" indent-style = "space" docstring-code-format = true选规则集的时候不要贪多。团队没在用的规则,比如某些风格要求,直接关掉。否则每条误报都在消耗信任。我的建议是先开 E、W、F、I、B、UP、SIM,等跑几天再逐步加 S 和 ARG。安全规则 S 很有用,但初始阶段容易误报,比如 assert 在某些项目里就是合理用法,S101 就可以忽略。
常用命令也记一下:
ruff check . # 只检查 ruff check . --fix # 自动修复可修复项 ruff check . --fix --diff # 先看修复内容再应用 ruff format . # 格式化在 pre-commit 里,可以直接用官方 hook:
- repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format这里需要注意的是:建议使用版本锁定。把 rev 固定到具体版本,可以保证团队所有人拿到的规则一致,CI 结果也稳定。
2.3 把 ruff 嵌入 pre-commit 和 CI
本地开发阶段,我只让 ruff 输出 error 和需要手动修改的项,因为自动修正的项直接由 hook 改掉。CI 阶段则建议运行ruff check .不加--fix,防止 CI 环境发生文件变更造成混乱。
很多团队会遇到一个问题:历史代码大量不符合 ruff 规则。我处理这类存量项目的方法是“先 baseline,再增量”。第一次跑ruff check --fix把能自动修的修掉,剩下的手动问题记录到 ignore 清单,并挂一个 issue 排期清理。不要指望一次全清完,而要让新增代码从第一天起就干净。
提示:不要一开始就开
--preview规则。预览规则经常变,今天让你改的代码,下个月新版本可能又有新建议,容易造成无意义的 churn。
3. 第二轨:用 mypy 把类型边界立起来
类型检查是双轨里“兜底”的一轨。它的目标不是消灭所有动态性,而是让你在调用函数时不用靠猜。
3.1 渐进式开启 strict:从新代码开始
老项目直接开--strict,基本上会崩出一大片历史错误。更合理的方式是先从新代码和核心模块开始。
先给 mypy 配一个基础的全局配置:
[tool.mypy] python_version = "3.11" mypy_path = ["src"] plugins = ["pydantic.mypy"] show_error_codes = true explicit_package_bases = true namespace_packages = true [[tool.mypy.overrides]] module = ["tests.*"] disallow_untyped_defs = false disallow_any_explicit = false然后在核心业务包上开启更高强度检查。mypy 的 override 机制可以针对不同模块设置不同严格度。比如:
[[tool.mypy.overrides]] module = ["src.billing.*", "src.auth.*"] strict = true这样做的原因是:核心模块改动频率最高、出事故成本最高,应该用强类型保护;边角脚本和测试代码保证基本类型正确即可,不必逼着测试里每个桩函数都写完整签名。
3.2 关键配置项与常见卡点
我在使用中最常调的几个参数:
disallow_untyped_defs:要求所有函数都有类型注解。新代码建议开启。disallow_any_explicit:禁止显式写Any。一开始会很痛苦,但它会逼你定义真正的类型。warn_return_any:返回了未注解或 Any 值时发出警告。这个很有用,防止类型信息“泄漏”。check_untyped_defs:检查未注解函数体内部逻辑。建议开,代价很小但能发现不少低级错误。no_implicit_optional:要求显式写出Optional。这个建议必开,旧项目里很多参数不做空值判断,和这个配置有关系。
常见卡点是操作第三方库没有一个类型存根。解决办法是安装types-requests、types-PyYAML之类的存根包,或者在overrides里把某个第三方模块设为ignore_missing_imports = true。我更推荐前者,因为忽略导入会连真正的问题一起跳过。
3.3 mypy 增量审查的落地经验
想让 mypy 在审查里真正发挥作用,不能只靠本地跑,还要让它在 CI 上以--strict形态出现。CI 可以比本地更严格,因为 CI 会完整跑一遍所有模块,不容易跑到一半因为某个历史遗留错误而中断。
我的做法是分两层:本地 pre-commit 只跑mypy --config-file,优先保证没有新增错误;CI 跑完整检查,并把错误数量写入一个允许的基线。如果 CI 错误数量超过基线,构建就失败。这样团队可以有条不紊地减少历史债务,同时不会让新人一上来就被老代码的错误淹没。
注意:mypy 的结果不是只看“跑不跑得通”。很多人为了通过检查,会把参数直接写成
Any,这样反而失去意义。在审查时,我会特别看 diff 里有没有出现Any或# type: ignore的滥用。type: ignore必须带错误码,比如# type: ignore[arg-type],并且默认只允许用于可解释的场合。
4. 第三轨:模型审查如何补上语义缺口
两轨静态检查解决完形式问题后,我会把本次变更的 diff 送到一个模型服务,让它从“一个资深工程师做 Code Review”的角度给意见。这一节可以说是整个方案里最有意思的部分,也是最容易翻车的部分。
4.1 模型审查到底审查什么
不要把模型当成万能裁判。它真正擅长的是识别规则和类型都覆盖不到的“语义层问题”。我在实际使用里,发现它比较能发现以下几类问题:
- 边界条件缺失。比如分了页却没查最后一条数据,循环里用了
<=导致多走一次,或者用户输入为空时直接抛异常。 - 错误处理粒度过粗。比如大范围的
except Exception把 KeyError 和 ConnectionError 一网打尽,导致很难排查线上故障。 - 明显的逻辑反转。比如
if not user.is_active配合continue写反,导致活跃用户反而被跳过。 - 资源和并发问题。连接未释放、没有重试机制、用共享可变对象做缓存等。
- 安全敏感点。虽然不是专门的安全扫描器,但模型对 SQL 拼接、日志中打印敏感字段这类问题有不错的敏感性。
但如果让它检查“代码风格是否统一”“命名是否符合规范”,它往往不如 ruff 稳定,还会给出五花八门的建议。因此我给模型的定位是“语义审查助手”,不是“通用审查工具”。
4.2 基于 diff 的模型审查流水线
模型审查的关键是上下文。只给一段孤立的新增代码,它做不了有效的判断。我采取的是 diff + 相关函数上下文的方式。
具体流程是这样的:
- 从版本控制系统拿到本次变更的文件列表和 diff。
- 解析 diff,提取每个变更文件的新增代码块。
- 对新代码块涉及的函数,向上游多拿 30 行左右的上下文。
- 对每个文件生成一个独立的提示词请求,而不是把整个仓库塞进去。
一次性把整个仓库的代码都给模型,既浪费资源,又会让模型注意力涣散。按文件拆分请求,可以保证每一个文件里的业务逻辑相对完整,模型也能集中精力理解本次变更。
我在这里还会额外提取类名、函数名、被调用的外部函数签名,拼成一段结构化描述。比如:
文件: src/services/order.py 变更函数: create_order(user_id: int, items: list[dict]) -> Order 依赖函数: get_user(user_id), check_stock(items), save_order(order) 新增代码: <diff>这么做的目的是最大程度还原你作为一名审查者会看到的背景信息,让模型不是“凭空猜”,而是“基于上下文推理”。
4.3 让模型输出结构化结果:一个可复用的提示词
模型审查如果没有稳定输出,很难接入工程流程。我在实践里把提示词写成了固定模板,并强制模型只返回 JSON。
你是一名有十年经验的高级 Python 工程师,正在 review 一段代码变更。 请基于本次 diff 和上下文信息,从语义层面指出代码中可能存在的高价值问题, 忽略风格问题与类型问题(已有其他工具处理)。 要求: 1. 只审查真实存在于 diff 中的代码,不要假设不存在的缺陷。 2. 每条问题必须给出:严重级别(error/warning/info)、涉及的函数/行号、问题描述、修改建议。 3. 不要使用模糊措辞如“注意”“建议”,直接给出判断。 请以如下 JSON 格式输出: { "issues": [ { "level": "warning", "function": "create_order", "line": 42, "message": "在 items 为空时直接调用 check_stock,可能导致数据库查询空列表。", "suggestion": "在调用 check_stock 之前增加空列表判断并返回业务错误。" } ] }这里有三点很重要。
第一,明确告诉模型忽略风格和类型问题。因为 ruff 和 mypy 已经管了,模型再提就是噪音。
第二,要求所有结论都指向 diff 中实际存在的代码。没有这条约束,模型会开始“发明问题”,比如根据想象补一个根本不存在的数据竞争。
第三,定死输出结构。这样做的好处是审查结果能在下游被自动解析,再按级别分拣到 PR comment 或邮件提醒里。
模型建议不要自动改代码。我见过很多团队试图让模型直接生成 patch,结果修了一个问题,引入了两个新问题。模型审查的价值是“发现问题并启发人类”,而不是直接取代人类决策。真要自动修改,也应该局限在 ruff 这种能百分之百确定修复方案的规则里。
4.4 模型审查和规则结果如何合并
模型和规则的输出要合并成一个报告,但不能简单拼在一起。我建议先按严重级别分类,再把模型结果里和规则重复项直接丢掉。
规则结果主要分两类:error是必须修的;warning是可选修。模型结果我给三个等级:
error:基本确定有问题,建议阻塞合并。warning:疑似有问题,需要作者回应或解释。info:优化建议,不强制。
在 PR 评论里,我会让模型输出显示为“AI 建议”,并附带对应的函数和行号。这样作者一眼就知道这是模型给的提醒,而不是某个人敲定的硬性要求。人工 reviewer 再用自己的判断决定是否采纳。
我踩过的一个坑是:把模型评论直接交给机器人自动 approve / request changes。模型幻觉不可避免,一旦出现模型误杀,开发者会对整套系统失去信任。所以当前我的流水线是模型结果只做“摘要提醒”,最终裁决权永远在人类 reviewer 手上。
5. 双轨协同的完整流水线与场景拆解
前面讲的是组件,这一节讲整个系统怎么串起来。双轨审查最大的收益,其实是把审查行为从“人肉走查”变成“人盯异常”的模式。
5.1 本地模型服务的选择思路
模型审查要落地,部署是绕不开的。出于数据安全考虑,强推本地部署或内网部署,不要把业务代码送到外部公共接口里。可选的方案有 Ollama、vLLM、内部部署的开源模型服务,还有公司自建的模型网关。
选模型的时候,不一定要上最大的参数版本。审查 diff 不需要极强的长文本能力,我用量比较多的是 7B 到 14B 规模的代码模型。代码模型在语义理解上比通用模型更敏锐,会优先关注结构性缺陷;通用模型更像是“话多但不够准”的同事,经常给出泛泛而谈的建议。
配置上要留意显存和并发。一份大规模 diff 可能拉得很长,序列长度需要尽量给高。如果显存有限,可以把 diff 按函数拆细后再送入模型,避免在长上下文里丢信息。服务层建议加一个简单的并发队列,防止 CI 同时触发多份审查时把机器压垮。
注意:不要让模型审查成为 CI 的硬阻塞。模型服务会慢,也会偶发超时。我的做法是它作为异步任务跑,结果生成后才会通知,不影响整体构建的退出码。这样即使模型服务挂了,主干流程依然能走。
5.2 完整流水线示例:pre-commit、CI、模型审查、结果回传
我目前的完整流程是这样:
- 开发者本地提交前,pre-commit 先跑 ruff 和 mypy。这两个不够快,pre-commit 反而会成为负担。
- push 后,CI 跑完整 ruff 检查、mypy strict 检查和单元测试。
- 静态检查通过后,CI 把 diff 加上下文交给模型服务。
- 模型审查结果生成一个 JSON 文件,解析后写入 PR comment。
- 人工 reviewer 在 PR 页面看到三部分内容:静态检查状态、模型建议列表、自己在 review 时需要重点确认的问题。
这个流水线里最关键的是第 3 步到第 4 步之间的“上下文管理”。你可以在 CI 工作流里选择用 vcs 提供的 diff,或直接读取以前 commit 的快照。为了减小噪声,我还会过滤掉 lock 文件、生成的代码和纯配置文件,因为这些内容模型看了也白看。
5.3 审查结果分级与人工闭环
审查流程不能只有“自动生成评论”,还要有闭环。我让模型审查结果进入一个简单的状态表:待确认、已有回应、已解决、已忽略。模型评论的作者需要在 PR 里回复一次,哪怕回复“这个场景不会发生,不加判断”,也算闭环。这样之后追溯每条模型建议的理由,行业规范上会很清晰。
分级规则的设定是很快的,难的是让人工 reviewer 愿意看模型评论。后来我找到的方法是:把模型审查结果里级别为 error 的问题直接同步到 review 负责人,warning 和 info 只在 PR 评论区展示。这一招让“模型噪声”不再干扰主线,人们对它的接受度明显提高了。
提示:建议每周或每两周回顾一次模型建议的有效率。有效率的定义是人工最终确实修改了代码。如果模型建议有效率持续低于 20%,就要考虑换模型或调整提示词了。这个指标比跑分更能反映模型在真实工程环境中的价值。
6. 实操中的高频问题与避坑清单
这套体系跑了一段时间以后,真正的问题往往不在工具本身,而在人和流程的磨合。
6.1 误报、幻觉与审查噪音
规则工具最烦人的是误报,模型工具最担心的是幻觉。误报可以通过调规则和加 ignore 来解决,但幻觉需要靠提示词和上下文约束来压住。我在 4.3 里限制了“只审查 diff 中的真实代码”,基本能去掉一半以上的幻觉问题。
另一类噪音是模型提出的问题虽然真实存在,但和本次修改无关。比如,你改了一行日志,模型却指出附近某个函数有空值风险。虽然风险可能成立,但放在这个 PR 里会让作者困惑。解决办法是给模型的指令里加一句“仅讨论本次变更引入的问题,不要评论历史代码”。这句话看着轻描淡写,实际效果非常明显。
6.2 性能与成本:如何让审查跑得快
静态工具很快,但模型服务会慢。如果一次 PR 里有 20 个文件,逐文件开请求可能要几分钟。我在实践里做了一些小优化:
- 只对新增代码超过 5 行的文件做模型审查,纯删除或纯重命名的文件直接跳过。
- 模型服务走单独的并发池,限制同时最多 3 个请求,避免机器 CPU 被打满。
- 结果做 hash 缓存:同一个 commit 的同一个文件的审查结果不要重复计算。
成本方面,模型审查的单位成本虽然不贵,但长期跑下来也是一笔预算。建议按照 PR 的大小给配额,比如单 PR 最多审查 10 个文件,超过部分只做静态检查。人不会因为有 30 个文件要 review 就真的逐行看完,模型也一样,工作量过大时的判断质量反而下降。
6.3 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| ruff 把代码改乱了 | 启用了互相冲突的规则 | 检查配置,开启show-fix-status,必要时先 diff 后应用 |
| mypy 报错但在 CI 通过 | 本地配置和 CI 配置不一致 | 保持同一个 pyproject.toml,不要在 CI 单独追加参数 |
| 模型审查频繁超时 | 并发过高或序列太长 | 减少并发数,拆分 diff 后再送模型 |
| 模型总是说些空话 | 缺少上下文 | 把函数签名和依赖函数拼接进提示词 |
| 模型提出了很多“历史问题” | 没有在提示词里限定职责 | 强调只关注本次 diff 新增/修改的代码 |
| 审查结果没人看 | 结果没有分层,噪音过多 | 把 error 单独提取,warning 和 info 收敛展示 |
有人用type: ignore绕过 mypy | 规则太松,没有错误码约束 | 强制要求带错误码,并在 review 中拒绝无注释 ignore |
| PR 作者频繁忽略模型建议 | 建议质量通常不高 | 调整提示词或模型,按有效率指标评估后再决定是否保留 |
结尾
我在实际操作中体会到,工具链本身不是壁垒,真正难的是让团队信任一套自动化审查逻辑而不觉得被冒犯。ruff、mypy 和模型这三条路线,分别对应了确定性、类型和语义三个层次。每一次多赚回十分钟,累积起来就是很大的收益。
最后再分享一个小技巧:把模型审查结果里的高频建议做成团队 wiki 里的反面案例,比如“空列表没有判断就查询”“异常细节被吞掉”“类型用 Any 逃逸检查”,每一条都配上真实 PR 的 diff。这样做不仅能训练新人,也可以反过来帮模型提示词迭代。毕竟代码审查的本质,是让人和工具一起把知识沉淀下来,而不是某一次跑完就算结束。