高效审查 AI 生成的 Python 代码:从 Ruff 到语义测试

2026-09-16 21 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

AI 可以在几分钟内生成一个 Python 模块,但“能运行”不等于“可以合并”。高效审查的关键不是逐行猜测代码是否正确,而是先让 Ruff、mypy、Bandit 和 pytest 清理机械问题,再把人的注意力留给业务语义、边界条件和错误处理。

把检查顺序设计成一条快速失败流水线

一套实用顺序是:

  1. Ruff:发现未使用导入、可疑表达式、命名和常见代码缺陷。
  2. mypy:检查参数、返回值、None 和容器类型是否一致。
  3. Bandit:扫描危险函数、硬编码密码、不安全的子进程调用等模式。
  4. pytest:验证代码在真实输入和边界条件下的行为。
  5. 人工审查:检查工具不理解的业务规则、权限边界和失败策略。

这个顺序的价值在于快速失败。没有必要在类型错误尚未解决时,花十分钟分析某个复杂测试为什么失败;也不应因为测试通过,就忽略明显的命令注入风险。

可以为项目建立一个最小检查环境:

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 则能提醒审查者关注 subprocesseval、弱随机数和硬编码凭据等敏感模式。

但这些工具无法证明业务逻辑正确。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 中 boolint 的子类,所以只写整数类型标注并不能阻止 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 固化行为;工程师则集中验证真正昂贵的部分:代码是否实现了正确的业务规则,以及失败时是否仍然安全。


相关推荐