文章主张将模型视为需要锁定和验收的依赖,在切换AI代码审查模型前使用带标准答案的版本化样本集回归测试。该方法可发现漏报、误报增加和拒绝分析等烟雾测试难以暴露的漂移。
一次看似常规的成本优化,却让我们遭遇了一次悄无声息的回归:有人把代码审查助手切换到了另一个模型。工具依然能生成流畅的审查意见,却直接放过了上传处理程序中的一个路径遍历漏洞——过去几个月里,旧模型每次都能一眼识别出这种模式。唯一触发警报的是一个纳入版本管理的 fixture,它的结果从通过变成了失败;而这声警报,直到变更发生两天后才响起。
如今,我把这条经验视为不可妥协的原则:模型是一项需要锁定版本的依赖;在没有回归证据的情况下替换模型,等同于未经审查便改变了系统的安全态势。下面是一套精简的工作流:无论要更换的是付费模型、自托管模型还是免费模型,都必须先在一组固定且具有已知答案的测试语料上通过门禁,之后才有资格进入 CI。
这类助手会接收不可信代码并给出判断。有三种模型漂移尤其值得关注,而且它们都不会在冒烟测试中暴露:
静默的能力退化——过去能够检出的风险,如今悄无声息地消失了。
噪声膨胀——模型开始把无害代码标记为问题,久而久之,审查人员会习惯性地忽略所有告警。
拒答蔓延——模型开始拒绝分析涉及安全的文件。这本质上是一次披着安全外衣的检测中断。
只有具备 ground truth 的语料库,才能将这些问题与“模型看起来没问题”区分开来。
每个测试用例由一个源文件和一份机器可读的预期结果组成。将它与门禁脚本保存在同一个仓库中:
corpus/
traversal_join/ # must flag
handler.go
verdict.yaml # verdict: flag, severity_at_least: high
traversal_sanitized/ # must pass (negative twin)
handler.go
verdict.yaml # verdict: pass
jwt_alg_none/ # must flag
auth.py
verdict.yaml
secrets_in_config/ # must flag
settings.yaml
verdict.yaml
正向用例(corpus/traversal_join/handler.go):
func serveFile(w http.ResponseWriter, r *http.Request) {
// fixture: user input joined directly into a filesystem path
name := r.URL.Query().Get("f")
http.ServeFile(w, r, "/var/data/"+name)
}
负向孪生用例——接口结构相同,但输入经过了净化处理,模型必须对它保持沉默:
func serveFile(w http.ResponseWriter, r *http.Request) {
name := filepath.Clean(r.URL.Query().Get("f"))
if strings.Contains(name, "..") || filepath.IsAbs(name) {
http.Error(w, "invalid", http.StatusBadRequest)
return
}
http.ServeFile(w, r, filepath.Join("/var/data", name))
}
孪生结构正是这里的关键。一个同时标记这两个文件的模型,只是噪声生成器;一个对两者都不告警的模型,则不过是个橡皮图章。只有两个 verdict 之间的差异才真正承载信息。
下面的脚本沿用了我在本地推理环境中使用的模式。在将它指向你自己的部署之前,请把这段代码视为尚未执行的模板。任何兼容 OpenAI API 的 endpoint 都可以使用:
#!/usr/bin/env python3
"""Model-swap gate. python >= 3.11, openai >= 1.40, pyyaml"""
import json, os, sys, pathlib, yaml
from openai import OpenAI
SYSTEM = (
"You audit code for exploitable defects. Respond with JSON only: "
'{"verdict": "flag" | "pass", "severity": "low" | "medium" | "high", "evidence": str}'
)
SEV_RANK = {"low": 1, "medium": 2, "high": 3}
api = OpenAI(base_url=os.environ["MODEL_BASE_URL"],
api_key=os.environ.get("MODEL_API_KEY", "unused"))
MODEL = os.environ["MODEL_NAME"] # exact pinned ID; "latest" is banned
broken = []
for case in sorted(pathlib.Path("corpus").iterdir()):
src = next(p for p in case.iterdir() if p.name != "verdict.yaml")
want = yaml.safe_load((case / "verdict.yaml").read_text())
reply = api.chat.completions.create(
model=MODEL, temperature=0,
messages=[{"role": "system", "content": SYSTEM},
{"role": "user", "content": src.read_text()}],
).choices[0].message.content
try:
got = json.loads(reply)
except json.JSONDecodeError:
broken.append((case.name, "non-JSON output"))
continue
ok = got.get("verdict") == want["verdict"]
if ok and want["verdict"] == "flag":
ok = SEV_RANK.get(got.get("severity"), 0) >= SEV_RANK[want["severity_at_least"]]
print(f"{'ok ' if ok else 'BAD'} {case.name}: want={want['verdict']} got={got.get('verdict')}")
if not ok:
broken.append((case.name, got.get("evidence", "")[:100]))
print(f"\nmodel={MODEL} failures={len(broken)}")
sys.exit(1 if broken else 0)
然后比较当前模型与候选模型:
MODEL_NAME=<incumbent-exact-id> python gate.py | tee incumbent.log
MODEL_NAME=<candidate-exact-id> python gate.py | tee candidate.log
diff incumbent.log candidate.log && echo "no regression on corpus"
要把这段演示代码变成真正的门禁,还需要满足以下约束:
只允许使用精确的模型标识符。浮动标签会让每次运行都变成一次碰运气。
将 Temperature 设为 0,并记录在日志头部。否则,你将无法区分结果抖动与真正的回归。
Flake 即失败。如果一个用例第一次运行失败、重试却通过,也必须拒绝这个候选模型。不确定的安全判断,根本算不上判断。
这类工作负载只包含十几个简短的 completion,因此天然适合使用免费的模型服务:无需为并行推理付费,就可以拿候选模型与当前模型进行基准对比。
利益披露:本文是 MonkeyCode 产品推广工作的一部分。MonkeyCode 目前宣传提供免费的模型访问服务和免费服务器选项(信息由运营方提供;我没有核实其配额、可用模型或优惠持续时间——请先确认最新条款)。
我认为合理的职责划分是:
免费层 → 用于实验。运行语料库、比较日志,然后判断候选模型是否值得接受更长时间的评估。
你能够掌控的基础设施 → 用于强制执行。具有阻断能力的 CI 检查应当部署在能够提供可用性保障的地方,因为一个返回 404 的门禁,要么会阻塞所有合并,要么会遭到绕过——两种结果都很糟糕。
一个包含 15 个文件的语料库,只能证明模型在这 15 种模式上没有发生回归,仅此而已。你应当根据真实发生过的漏检事故不断扩充它。
如果助手的输出从来不会阻断任何流程,而人工也只是随手扫一眼,那么一份轻量级的抽查清单或许已经足够。
如果你的工具完全隐藏了模型标识符,你就无法为一个叫不出名字的对象设置门禁——要么要求供应商提供带版本号的模型,要么接受无法监控的漂移。
任何指向免费 endpoint 的 pipeline 阶段都需要 fallback,否则你的安全检查本身就会变成一项可用性依赖。
我反复思考的边界问题是:这些不变量中,究竟哪些应该由模型层负责?上面的路径遍历案例,或许更应该交给一条确定性的 Semgrep 规则来提供保障,而模型 fixture 仅用于衡量助手的判断能力。代码审查 pipeline 越依赖一个可能在没有通知的情况下发生变化的系统,你就越有理由重新划定边界,让更多职责回归确定性工具。你会把这条边界画在哪里?
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。