AI 生成代码的关键问题不是能否通过一次编译,而是变更能否被稳定地审查、验证和回滚。本文把 Git Diff、测试选择、风险分级和 CI 门禁串成一条变更流水线,让每个阻断结果都有依据,也让人机协作留下可追溯记录。

1. 先定义“进入主干”的变更路径

AI 生成代码进入主干时,至少会经过四个阶段:提交、自动分析、人工审查和合并。不要把 AI 当作一个特殊的代码来源,单独增加一套完全不同的流程;更实用的做法是记录生成来源,再使用统一的质量规则处理变更。

一个可执行的路径如下:

  1. 开发者让 AI 生成或修改代码,并在本地运行格式化、静态检查和相关测试。
  2. 提交 Pull Request,CI 获取目标分支与当前提交之间的 Git Diff。
  3. 流水线按照变更文件、代码行数、敏感目录和测试结果计算风险等级。
  4. 风险较低的变更执行定向测试,风险较高的变更扩大测试范围,并要求额外审批。
  5. 所有门禁通过后合并;合并记录保留变更摘要、测试范围、风险信号和操作者信息。

这里的重点是“变更”而不是“作者”。AI 生成的代码可以和人工代码使用同一套编译、测试和安全规则,但应额外记录 generated_by_ai、使用的工具、提示词版本或任务编号等元数据。元数据不应替代审查,也不应该成为允许绕过测试的理由。

2. 用 Git Diff 建立变更分级

测试选择的输入应该来自结构化的 Diff 信息,而不是依赖提交消息中的主观描述。至少需要收集以下字段:文件状态、路径、增加和删除的行数、是否包含二进制文件、是否触及配置或迁移目录。

可以先采用四级模型:

等级典型变更默认测试合并要求
L0文档、注释、纯格式调整Markdown 检查或不运行业务测试一名审查者
L1单模块业务逻辑、小范围重构受影响模块测试、静态检查一名熟悉模块的审查者
L2公共接口、数据库查询、依赖升级相关模块测试、集成测试、安全检查代码所有者审批
L3认证授权、支付、数据迁移、生产配置全量测试、构建、部署前验证至少两名审查者和变更负责人

分级不是为了制造精确的“风险分数”,而是为了把默认动作写清楚。实际项目中还需要处理叠加规则:同时修改公共接口和数据库迁移时,取更高等级;修改文件数量超过阈值时,至少提升一级;删除测试文件、关闭 CI 检查或修改权限策略时,直接进入人工复核。

下面的脚本使用 Python 标准库解析 git diff --name-statusgit diff --numstat。它不依赖特定 CI 平台,可以在本地或流水线中运行。执行前需要在 Git 仓库中提供两个提交或引用,例如 main...HEAD

#!/usr/bin/env python3
import json
import subprocess
import sys
from pathlib import PurePosixPath


def git(*args):
    result = subprocess.run(
        ['git', *args],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    return result.stdout.splitlines()


def classify(base):
    names = git('diff', '--name-status', f'{base}...HEAD')
    stats = git('diff', '--numstat', f'{base}...HEAD')
    files = []
    risk = 0
    signals = []

    for line in names:
        parts = line.split('\t')
        status = parts[0]
        path = parts[-1]
        files.append({'status': status, 'path': path})
        suffix = PurePosixPath(path).suffix.lower()
        if path.startswith(('auth/', 'payments/', 'migrations/')):
            risk = max(risk, 3)
            signals.append(f'sensitive_path:{path}')
        elif suffix in {'.sql', '.tf', '.yml', '.yaml'}:
            risk = max(risk, 2)
            signals.append(f'infrastructure_or_schema:{path}')
        elif suffix in {'.py', '.go', '.java', '.js', '.ts'}:
            risk = max(risk, 1)
        if status.startswith('D') and suffix in {'.py', '.go', '.java', '.js', '.ts'}:
            risk = max(risk, 2)
            signals.append(f'deleted_source:{path}')

    added = 0
    deleted = 0
    for line in stats:
        parts = line.split('\t')
        if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit():
            added += int(parts[0])
            deleted += int(parts[1])
    if len(files) > 20 or added + deleted > 500:
        risk = min(3, max(risk, 2))
        signals.append('large_diff')

    level = f'L{risk}'
    return {
        'level': level,
        'files_changed': len(files),
        'lines_added': added,
        'lines_deleted': deleted,
        'signals': sorted(set(signals)),
        'files': files,
    }


if __name__ == '__main__':
    if len(sys.argv) != 2:
        raise SystemExit('usage: select_change.py BASE_REF')
    print(json.dumps(classify(sys.argv[1]), ensure_ascii=False, indent=2))

这个脚本只负责收集事实和给出初始等级,不负责决定是否合并。最终规则应在 CI 中明确表达,避免把隐藏在脚本中的判断变成无法解释的黑盒。

3. 根据影响范围选择测试

测试选择可以分成三层。第一层是始终运行的快速检查,例如 git diff --check、格式检查、类型检查和依赖锁文件校验。第二层是受影响模块测试,根据路径映射到测试目录。第三层是全量测试,通常由高风险信号、较大的 Diff 或定时任务触发。

路径到测试的映射应尽量靠近代码仓库的结构,并且可被审查。例如,修改 services/order/ 时运行 tests/order/;修改共享库时运行依赖它的模块;修改数据库迁移时至少运行数据库集成测试。不要仅依据文件扩展名决定测试范围,因为同一个 .py 文件可能是业务逻辑、脚本或测试夹具。

一个 GitHub Actions 的简化示例如下。假设仓库包含前面的 tools/select_change.py,Python 项目使用 pytest,并且在 pyproject.toml 中声明了测试依赖。

name: change-gate

on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: read

jobs:
  analyze:
    runs-on: ubuntu-latest
    outputs:
      level: ${{ steps.change.outputs.level }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - id: change
        shell: bash
        run: |
          python tools/select_change.py origin/main > change.json
          level=$(python -c "import json; print(json.load(open('change.json'))['level'])")
          echo "level=$level" >> "$GITHUB_OUTPUT"
          cat change.json
      - name: Diff whitespace check
        run: git diff --check origin/main...HEAD

  targeted-tests:
    needs: analyze
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: python -m pip install -e '.[test]'
      - name: Run tests
        run: pytest -q

  full-tests:
    needs: analyze
    if: needs.analyze.outputs.level == 'L2' || needs.analyze.outputs.level == 'L3'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: python -m pip install -e '.[test]'
      - run: pytest -q

示例中的 targeted-tests 为了保持完整性运行了全部 pytest。在真实仓库里,可以先根据 Diff 生成测试路径,再把路径传给 pytest;如果无法可靠建立映射,宁可扩大测试范围,也不要宣称测试覆盖了全部影响。测试选择结果应作为 CI 工件保存,包含原始 Diff 引用、选择原因和实际执行的命令。

4. 设计可解释的风险门禁

风险门禁需要回答三个问题:触发了什么信号、需要采取什么动作、谁可以解除阻断。规则应使用稳定且容易复现的输入,不要把模型的自然语言评价直接作为合并条件。

常见的阻断规则包括:

风险信号阻断或升级动作解释记录
修改认证、授权或密钥处理代码至少 L3,全量测试,要求代码所有者审批列出具体路径和规则编号
修改数据库迁移或删除字段阻止直接合并,要求迁移兼容性检查记录迁移文件和上下游版本
修改生产部署配置执行配置校验和部署预演记录配置差异及环境
删除或绕过测试、覆盖率门禁直接阻断,除非有明确例外审批记录被删除的检查项
依赖升级涉及运行时或安全补丁执行依赖扫描和集成测试记录锁文件差异
大规模新增或删除代码提升一级并要求拆分或额外审查记录行数和文件数

门禁状态最好分为 passfailmanual_review,不要只有一个布尔值。fail 表示客观检查未通过,例如测试失败或存在高危漏洞;manual_review 表示规则发现了需要人工判断的风险,例如一次性迁移脚本。人工解除也要留下审查者、理由、时间和关联工单,而不是直接重新运行流水线覆盖记录。

AI 生成代码还应增加两个审查维度:第一,是否引入了未经确认的外部依赖、网络访问或数据处理路径;第二,是否生成了看似完整但没有测试覆盖的边界逻辑。审查者可以要求 AI 给出变更摘要和假设,但摘要不能代替 Diff。真正的依据仍是代码、测试和运行时行为。

5. 审计、合并与回滚设计

审计记录应能回答“谁在什么时候,以什么上下文,生成并合并了什么”。建议将以下信息写入 Pull Request 或 CI 工件:提交 SHA、目标分支、Diff 统计、风险等级、风险信号、测试命令及结果、审查者、AI 工具名称和版本、生成任务标识,以及人工修改说明。提示词可能包含敏感信息,不应默认写入公开日志;可以保存脱敏后的任务摘要或内部引用。

合并策略同样影响回滚成本。AI 生成的批量变更应尽量拆成职责单一、可独立回滚的提交,例如先增加兼容代码,再切换调用方,最后删除旧路径。涉及数据库时采用向后兼容的展开与收缩步骤,先部署可同时识别新旧格式的代码,再迁移数据,最后删除旧逻辑。不要把代码改动、不可逆数据操作和配置切换塞进一个无法拆解的提交。

合并后还需要观察实际运行结果。对于高风险变更,保留功能开关、灰度比例或快速回滚版本,并把异常指标与发布记录关联。回滚预案至少应写明回滚提交、是否需要反向迁移、如何处理已经写入的新数据,以及谁有权限执行。没有验证过的回滚命令不能算作回滚设计。

总结

AI 生成代码进入主干,核心不是增加一层“AI 专用审查”,而是把生成来源纳入一条可验证的变更流水线:

  • 用 Git Diff 收集文件、行数和敏感路径等客观事实。
  • 根据变更范围、文件类型和风险信号划分 L0 到 L3 等级。
  • 始终运行快速检查,再按影响范围选择定向测试或全量测试。
  • 将阻断规则写成可复现的条件,并区分失败与人工复核。
  • 保存 Diff、测试、审批和生成上下文,保证后续可以审计。
  • 通过小提交、兼容迁移和功能开关降低回滚成本。

这套机制不会保证 AI 生成的代码没有缺陷,但能让缺陷更早暴露,让阻断理由可以解释,也让问题进入主干后仍然有清晰的恢复路径。