AI 可以在几分钟内生成一个 Python 模块,但“能运行”不等于“可以合并”。高效审查的关键不是逐行猜测代码是否正确,而是先让 Ruff、mypy、Bandit 和 pytest 清理机械问题,再把人的注意力留给业务语义、边界条件和错误处理。
把检查顺序设计成一条快速失败流水线
一套实用顺序是:
- Ruff:发现未使用导入、可疑表达式、命名和常见代码缺陷。
- mypy:检查参数、返回值、
None和容器类型是否一致。 - Bandit:扫描危险函数、硬编码密码、不安全的子进程调用等模式。
- pytest:验证代码在真实输入和边界条件下的行为。
- 人工审查:检查工具不理解的业务规则、权限边界和失败策略。
这个顺序的价值在于快速失败。没有必要在类型错误尚未解决时,花十分钟分析某个复杂测试为什么失败;也不应因为测试通过,就忽略明显的命令注入风险。
可以为项目建立一个最小检查环境:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ruff mypy bandit pytest
ruff check .
mypy src
bandit -r src
pytest
Windows PowerShell 中激活虚拟环境时,把第二行替换为:
.venv\Scripts\Activate.ps1
为了让本地与 CI 使用相同规则,可以在 pyproject.toml 中固定基础配置:
[project]
name = "ai-code-review-demo"
version = "0.1.0"
requires-python = ">=3.11"
[tool.ruff]
target-version = "py311"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.mypy]
python_version = "3.11"
strict = true
warn_unreachable = true
[tool.pytest.ini_options]
addopts = "-q"
testpaths = ["tests"]
Ruff 的 B 规则来自 bugbear 类检查,适合提前暴露一些“语法合法但容易出错”的写法。mypy 的严格模式可能会在旧项目中产生大量告警,接入时可以先限定到 AI 新增或修改的目录,再逐步扩大范围。
工具能抓住什么,又会漏掉什么
静态检查最擅长处理结构化问题。例如,AI 可能生成一个声明返回 str、实际上却在某条分支返回 None 的函数,mypy 往往可以立即指出冲突。Bandit 则能提醒审查者关注 subprocess、eval、弱随机数和硬编码凭据等敏感模式。
但这些工具无法证明业务逻辑正确。AI 生成代码常见的薄弱点包括:
- 把“折扣百分比”错误理解为小数比例;
- 只覆盖正常路径,没有处理空集合、负数、零值和上限;
- 捕获过宽的
Exception,然后静默返回默认值; - 在更新数据库后才执行权限判断;
- 使用浮点数计算金额,导致舍入规则不符合业务要求;
- 调用了名称相似但语义不同的库 API;
- 测试只是重复实现代码中的公式,因此实现和测试会一起犯错。
因此,工具全部通过只代表代码获得了进一步人工审查的资格,而不是证明它已经正确。
用边界测试审查“看起来合理”的实现
假设 AI 生成了一个价格计算函数。这里给出一套可以直接运行的微型项目,用来演示如何把业务约束写成测试。
先创建目录:
mkdir -p src/shop tests
touch src/shop/__init__.py tests/__init__.py
将下面内容保存为 src/shop/pricing.py:
from decimal import ROUND_HALF_UP, Decimal
def final_price(subtotal: Decimal, discount_percent: int) -> Decimal:
"""Apply an integer percentage discount and round to cents."""
if subtotal < 0:
raise ValueError("subtotal must not be negative")
if isinstance(discount_percent, bool):
raise TypeError("discount_percent must be an integer, not bool")
if not 0 <= discount_percent <= 100:
raise ValueError("discount_percent must be between 0 and 100")
multiplier = Decimal(100 - discount_percent) / Decimal(100)
return (subtotal * multiplier).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
再将下面内容保存为 tests/test_pricing.py:
from decimal import Decimal
import pytest
from shop.pricing import final_price
def test_applies_discount_and_uses_commercial_rounding() -> None:
assert final_price(Decimal("10.05"), 10) == Decimal("9.05")
def test_allows_boundary_percentages() -> None:
assert final_price(Decimal("20.00"), 0) == Decimal("20.00")
assert final_price(Decimal("20.00"), 100) == Decimal("0.00")
@pytest.mark.parametrize("discount", [-1, 101])
def test_rejects_out_of_range_discount(discount: int) -> None:
with pytest.raises(ValueError):
final_price(Decimal("20.00"), discount)
def test_rejects_negative_subtotal() -> None:
with pytest.raises(ValueError):
final_price(Decimal("-0.01"), 10)
def test_rejects_boolean_as_percentage() -> None:
with pytest.raises(TypeError):
final_price(Decimal("20.00"), True)
运行时需要让 Python 找到 src 目录:
PYTHONPATH=src ruff check src tests
PYTHONPATH=src mypy src
bandit -r src
PYTHONPATH=src pytest
这个例子暴露了几个自动生成代码容易忽视的问题:金额应使用 Decimal;舍入方式必须明确;折扣必须有上下界;Python 中 bool 是 int 的子类,所以只写整数类型标注并不能阻止 True 被当作 1。
测试用例也不应只从实现中反推。更可靠的做法是先从需求提取不变量,例如“最终金额不能为负”“100% 折扣结果必须为零”“金额保留两位小数”,再根据这些不变量构造输入。
人工审查要聚焦高风险差异
工具运行完毕后,不必平均审查每一行。优先查看 AI 新增或修改的高风险区域:
- 外部输入:HTTP 参数、文件路径、环境变量是否经过验证?
- 权限操作:鉴权发生在读取或写入敏感数据之前吗?
- 异常处理:错误是否被静默吞掉?重试会不会重复扣款或重复写入?
- 资源生命周期:文件、事务、网络连接是否在异常路径中释放?
- 并发状态:共享缓存、全局变量和读改写操作是否安全?
- 第三方 API:参数名称、返回结构和版本是否与真实文档一致?
- 删除与迁移:操作是否可逆,是否需要幂等性和审计记录?
审查外部调用时,最好要求 AI 生成的变更附带 API 文档依据或契约测试。不要仅凭函数名“看起来正确”就接受一个并不存在的 SDK 方法。
还可以把固定检查封装为脚本,减少每次手动输入命令造成的遗漏:
#!/usr/bin/env bash
set -euo pipefail
ruff check src tests
mypy src
bandit -r src
PYTHONPATH=src pytest
将其保存为 check.sh 后执行:
chmod +x check.sh
./check.sh
合并前的精简清单
在接受 AI 生成的 Python 代码前,可以快速确认:
- Ruff、mypy、Bandit 和 pytest 是否全部通过?
- 测试是否包含零值、空值、上下界、错误输入和异常路径?
- 测试预期是否来自需求,而不是复制实现逻辑?
- 是否核对了第三方 API、配置项和依赖版本?
- 是否检查了权限、敏感数据、命令执行和文件路径?
- 失败后能否安全重试,部分写入是否会留下脏状态?
- 这段代码是否足够清晰,使下一位工程师无需询问 AI 也能维护?
高效审查不是用更多工具替代判断,而是用工具压缩低价值工作。让 Ruff 处理风格和常见缺陷,让 mypy 检查类型契约,让 Bandit标出安全热点,让 pytest 固化行为;工程师则集中验证真正昂贵的部分:代码是否实现了正确的业务规则,以及失败时是否仍然安全。