让 AI 生成的代码安全上线:三层约束框架与 QA 左移实践

2026-06-10 15 预计阅读时间: 1 分钟
来源: my.oschina.net 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.

预计阅读时间:14 分钟

AI 写代码已经不是"要不要用"的问题,而是"怎么管"的问题。电商搜索团队在 H1 周期里踩过足够的坑后得出一个结论:裸奔式 AI-Coding——让模型自由生成、人工事后 review——上线事故率不可接受。真正可控的做法是在输入、生成、输出三个环节各加一道硬约束,同时把 QA 从"接单测试"推到"标准制定"的前端,和 RD 形成闭环。这篇文章把这套实践拆开讲,并给出可直接改造的 CI 流水线配置。

三层约束框架:输入 / 生成 / 输出

把 AI-Coding 的质量管控分成三层,不是概念游戏,而是因为每一层的失效模式不同,拦截手段也不同。

输入层——给模型什么,它就生成什么。 输入约束的核心是规范 Prompt 和上下文。具体做法包括:

  • 为每类任务维护一份 Prompt 模板库,模板里写清楚函数签名、入参校验规则、禁止使用的依赖;
  • 上下文窗口只注入与当前任务相关的代码片段和接口文档,而不是把整个仓库丢进去——无关上下文越多,模型"幻觉"越严重;
  • 对输入做静态校验:如果 Prompt 里缺少返回值约定或异常处理要求,直接拒绝提交给模型。

生成层——模型在推理过程中可以被引导。 生成约束不是改模型权重(大多数团队做不到),而是在推理参数和后处理规则上设卡:

  • 限制 temperaturetop_p,生产代码场景建议 temperature ≤ 0.2,降低随机性;
  • 在 system prompt 中硬编码"禁止生成网络请求、文件写入、动态执行"等安全红线;
  • 对生成结果做结构化抽取:只取代码块,丢弃模型附带的自然语言解释,避免解释片段被误当代码执行。

输出层——生成结果必须过机器门才能进仓库。 输出约束是最硬的一道闸:

  • lint + type check 必须通过,零 warning 才允许合并;
  • 单测覆盖率阈值硬卡(例如新增代码行覆盖率 ≥ 80%);
  • 安全扫描(依赖漏洞、硬编码密钥、SQL 注入模式)作为必须通过的 gate;
  • 人工 review 不取消,但 scope 收窄:只审查业务逻辑正确性,格式和风格问题交给机器。

三层叠加的效果是:输入层减少模型犯错概率,生成层限制犯错幅度,输出层拦截漏网之鱼。任何一层单独用都不够,三层一起才能把 AI 代码的上线风险压到可接受区间。

Harness 工程基础设施:让约束自动执行

约束写在文档里没人看,写在 CI pipeline 里没人能绕过。电商搜索团队用 Harness 搭了一套工程基础设施,把三层约束全部落地为流水线 stage:

  • Stage 1 — Input Guard:解析提交的 Prompt 模板 ID 和上下文清单,校验必填字段,缺失则 pipeline 立即 fail;
  • Stage 2 — Generate & Extract:调用模型服务,抽取代码块,做结构化格式校验(语法树是否完整、import 是否在白名单内);
  • Stage 3 — Output Gate:跑 lint、type check、单测、安全扫描,任何一项不达标则阻断合并。

Harness 的优势在于每个 stage 可以独立配置超时、重试和通知策略,RD 和 QA 各自维护自己负责的 stage 规则,不需要互相等。这比"一条大脚本跑到底"的 Jenkins 旧模式更可控,也更容易迭代。

RD 与 QA:从线性交接到闭环共建

旧模式下,RD 写完代码扔给 QA 测,QA 测完扔回 RD 改,循环往复。AI-Coding 时代这个模式有两个致命问题:一是 AI 生成代码的缺陷密度和类型分布跟人写的不同,QA 用老套路覆盖不到;二是交接周期长,AI 代码迭代快,等 QA 测完可能已经落后两个版本。

新协作模式的核心变化:

  • 目标对齐:RD 和 QA 的共同目标不是"代码功能正确",而是"AI 代码安全上线"。安全上线 = 功能正确 + 风险可控 + 可观测;
  • QA 左移:QA 在 Prompt 模板设计阶段就介入,定义"可测试性标准"——比如要求 AI 生成的每个函数必须有纯逻辑入口(不依赖外部状态),否则单测写不了;
  • 闭环共建:QA 发现的缺陷模式被提炼成规则,回注到输入层 Prompt 模板或输出层扫描规则里,下次同类问题在生成阶段就被拦截,而不是等 QA 再测一遍。

实际操作中,QA 每周把高频缺陷做分类,输出一份"缺陷→约束映射表",RD 据此更新 Prompt 模板和 CI 规则。这张表是闭环的关键载体——没有它,QA 左移就只是"更早介入"而不是"更早拦截"。

实践:搭建三层约束的 CI 流水线

下面给出一个 GitHub Actions workflow,把三层约束落地为可运行的 stage。电商搜索团队用的是 Harness,但逻辑完全一致——读者可以根据自己团队的 CI 平台做对应改造。

name: ai-code-quality-gate

on:
  pull_request:
    paths:
      - 'src/**'          # 只对业务代码目录触发
      - 'ai-prompts/**'   # Prompt 模板变更也触发,确保输入约束同步更新

jobs:
  # ── Stage 1: 输入约束校验 ──
  input-guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 校验 Prompt 模板必填字段
        run: |
          # 检查每个 Prompt 模板是否包含必填段落
          MISSING=0
          for f in ai-prompts/*.md; do
            for field in "函数签名" "入参校验" "异常处理" "禁止依赖"; do
              if ! grep -q "$field" "$f"; then
                echo "❌ $f 缺少必填字段: $field"
                MISSING=1
              fi
            done
          done
          if [ "$MISSING" -eq 1 ]; then
            echo "::error::Prompt 模板校验失败,缺少必填字段"
            exit 1
          fi
          echo "✅ Prompt 模板校验通过"

      - name: 校验上下文注入范围
        run: |
          # 上下文文件清单不得超过 20 个,避免窗口膨胀
          COUNT=$(wc -l < ai-prompts/context-manifest.txt)
          if [ "$COUNT" -gt 20 ]; then
            echo "::error::上下文文件数 $COUNT 超过阈值 20"
            exit 1
          fi

  # ── Stage 2: 生成结果抽取与结构校验 ──
  generate-extract:
    needs: input-guard
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 抽取代码块 & 校验语法完整性
        run: |
          python3 scripts/extract_code_blocks.py \
            --input src/ \
            --output .extracted/ \
            --whitelist-deps "utils,models,api" \
            --blocklist-deps "os.system,subprocess,eval,exec"

          # 用 Python AST 检查抽取结果是否语法完整
          python3 scripts/ast_check.py --dir .extracted/ || {
            echo "::error::生成代码语法树不完整"
            exit 1
          }

  # ── Stage 3: 输出质量门 ──
  output-gate:
    needs: generate-extract
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Lint & Type Check
        run: |
          pip install ruff mypy
          ruff check src/ --strict
          mypy src/ --strict --ignore-missing-imports

      - name: 单测覆盖率卡点
        run: |
          pip install pytest pytest-cov
          pytest src/ --cov=src/ --cov-fail-under=80

      - name: 安全扫描
        uses: zizmorcore/zizmor-action@v0      # 扫描硬编码密钥等
        with:
          severity: high

      - name: 依赖漏洞检查
        run: |
          pip install pip-audit
          pip-audit --strict

改造要点:

  1. ai-prompts/ 目录和 src/ 目录并列管理,Prompt 模板跟代码一起版本控制;
  2. extract_code_blocks.pyast_check.py 是团队自维护的脚本,核心逻辑是:从 AI 输出中只取 ```python 块,白名单校验 import,AST 解析确认语法完整;
  3. 覆盖率阈值 --cov-fail-under=80 根据团队实际情况调整,但必须有一个硬数字,不能是"尽量高";
  4. 安全扫描用了 zizmor + pip-audit,读者可替换为自己团队的工具链(Semgrep、Trivy 等),逻辑不变。

下面是 extract_code_blocks.py 的最小可运行版本,可以直接放到 scripts/ 目录:

"""从 AI 生成文件中抽取代码块,过滤禁止依赖"""
import ast, re, sys, pathlib

WHITELIST = {"utils", "models", "api"}
BLOCKLIST = {"os.system", "subprocess", "eval", "exec"}

def extract_blocks(text: str) -> list[str]:
    """抽取 ```python ... ``` 代码块"""
    return re.findall(r"```python\n(.*?)```", text, re.DOTALL)

def check_imports(code: str) -> list[str]:
    """返回违规 import 列表"""
    violations = []
    tree = ast.parse(code)
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                root = alias.name.split(".")[0]
                if root not in WHITELIST:
                    violations.append(alias.name)
        if isinstance(node, ast.ImportFrom):
            if node.module and node.module.split(".")[0] not in WHITELIST:
                violations.append(node.module)
    return violations

def check_blocklist(code: str) -> list[str]:
    """返回出现的禁止调用"""
    found = [b for b in BLOCKLIST if b in code]
    return found

def main(src_dir: str, out_dir: str):
    violations_total = []
    out_path = pathlib.Path(out_dir)
    out_path.mkdir(exist_ok=True)

    for f in pathlib.Path(src_dir).rglob("*.py"):
        blocks = extract_blocks(f.read_text())
        for i, block in enumerate(blocks):
            imp_violations = check_imports(block)
            blk_violations = check_blocklist(block)
            if imp_violations or blk_violations:
                violations_total.append(
                    f"{f.name} block#{i}: imports={imp_violations} blocklist={blk_violations}"
                )
                continue
            # 语法完整性校验
            try:
                ast.parse(block)
            except SyntaxError as e:
                violations_total.append(f"{f.name} block#{i}: syntax error {e}")
                continue
            # 写出合法代码块
            (out_path / f"{f.stem}_block{i}.py").write_text(block)

    if violations_total:
        for v in violations_total:
            print(f"❌ {v}")
        sys.exit(1)
    print("✅ 所有代码块校验通过")

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

上线前的取舍与检查清单

三层约束不是零成本。几个实际取舍需要团队提前对齐:

取舍点 建议 理由
temperature 设多低 生产代码 0.1–0.2 太低会牺牲多样性(比如模型总生成同一套解法),太高则不可控
覆盖率阈值定多少 新增代码 ≥ 80%,存量逐步提升 一刀切 80% 对老代码不现实,分阶段更可执行
QA 左移到什么程度 至少覆盖 Prompt 模板评审 再往前推到模型选型阶段,多数 QA 团队能力暂不具备
人工 review 取消吗 不取消,scope 收窄 机器能拦格式和安全问题,业务逻辑正确性仍需人判断

上线检查清单(每次 AI 代码合并前逐项确认):

  • [ ] Prompt 模板包含四项必填字段(函数签名、入参校验、异常处理、禁止依赖)
  • [ ] 上下文文件数 ≤ 阈值,无无关注入
  • [ ] 生成代码 import 全部在白名单内,无 eval/exec/subprocess
  • [ ] lint + type check 零 warning
  • [ ] 新增代码单测覆盖率 ≥ 团队阈值
  • [ ] 安全扫描无 high severity 发现
  • [ ] QA 已评审 Prompt 模板的可测试性
  • [ ] 本周缺陷→约束映射表已更新到 CI 规则

最后一项容易被忽略,但它才是闭环运转的证据。如果映射表连续两周没更新,要么是缺陷已经彻底被前置拦截了(好事),要么是 QA 没在做提炼(坏事)——后者意味着闭环断了,QA 左移退化为"更早介入但没反馈"。

三层约束 + 闭环共建,本质上是把"AI 代码质量"从一个模糊的信任问题,变成一组可度量、可自动执行、可迭代的质量工程问题。约束越硬,AI-Coding 的上线信心越足。


相关推荐