模型生成的补丁可能改变函数契约、引入未授权依赖或仅适配 prompt 示例,文章给出在补丁进入工作目录前必须通过的三项检查步骤。
一个生成的补丁可能看起来正确,但仍会以 prompt 截图无法展示的三种方式失败:它改变了合约、导入了代码库其余部分不允许的模块,或者只在 prompt 中的单个示例上有效。如果你把这个补丁复制到真实仓库,你就成了集成测试,而且通常是在深夜的 pull request 中。
本文介绍了一个简短的验证循环,将模型输出视为不可信输入。该循环在候选文件接触工作树之前运行三次检查:合约、执行和回归检查。脚本是纯 Python,不依赖任何模型专用 SDK,因此同一道门禁适用于任何端点或手动文件的输出。
合约漂移是最常见的静默破坏。模型可能重命名函数、更改参数或返回不同类型,同时让 prompt 的 happy path 保持完好。静态 AST 检查可以在代码被导入之前捕获缺失的符号。
禁止Reach是第二个问题。编码模型可能决定读取文件系统、执行 git 命令或调用网络,而任务本应是纯函数的。如果允许生成的代码运行,其导入与其输出同样重要。
仅 Happy-Path 逻辑是第三个问题。候选代码可以对一个输入打印预期字符串,但对正常情况、空字符串或 Unicode slug 失败。回归检查在隔离环境中用小型测试文件对候选代码运行测试。
将以下内容保存为 gate.py。它解析候选文件、检查公共函数和导入,然后在临时目录中运行两条可选代码路径。
import ast
import json
import pathlib
import subprocess
import sys
import tempfile
from dataclasses import dataclass
@dataclass(frozen=True)
class GateSpec:
candidate_path: str
expected_functions: frozenset[str]
forbidden_modules: frozenset[str]
probe_script: str
regression_test: str
timeout_seconds: int = 10
def parse_functions(source):
tree = ast.parse(source)
return frozenset(
node.name
for node in ast.walk(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
)
def parse_modules(source):
tree = ast.parse(source)
modules = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
modules.update(alias.name.split('.')[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
modules.add(node.module.split('.')[0])
return frozenset(modules)
def run_in_temp(candidate, code, timeout):
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
(root / 'candidate.py').write_text(candidate, encoding='utf-8')
(root / 'probe.py').write_text(code, encoding='utf-8')
return subprocess.run(
[sys.executable, 'probe.py'],
capture_output=True,
text=True,
timeout=timeout,
cwd=root,
)
def evaluate(spec):
candidate = pathlib.Path(spec.candidate_path).read_text(encoding='utf-8')
failures = []
functions = parse_functions(candidate)
modules = parse_modules(candidate)
missing = sorted(spec.expected_functions - functions)
if missing:
failures.append({'gate': 'contract', 'reason': 'missing functions', 'details': missing})
forbidden = sorted(modules & spec.forbidden_modules)
if forbidden:
failures.append({'gate': 'contract', 'reason': 'forbidden import', 'details': forbidden})
if spec.probe_script:
try:
result = run_in_temp(candidate, spec.probe_script, spec.timeout_seconds)
if result.returncode != 0:
failures.append({'gate': 'execution', 'reason': 'probe failed', 'details': result.stderr.strip()[-500:]})
except subprocess.TimeoutExpired:
failures.append({'gate': 'execution', 'reason': 'probe timed out', 'details': ''})
if spec.regression_test:
test_code = '''import unittest
import candidate
''' + spec.regression_test + '''
if __name__ == '__main__':
unittest.main()
'''
try:
result = run_in_temp(candidate, test_code, spec.timeout_seconds)
if result.returncode != 0:
failures.append({'gate': 'regression', 'reason': 'test failed', 'details': result.stderr.strip()[-500:]})
except subprocess.TimeoutExpired:
failures.append({'gate': 'regression', 'reason': 'test timed out', 'details': ''})
return {'accepted': not failures, 'failures': failures, 'functions': sorted(functions), 'modules': sorted(modules)}
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--candidate', required=True)
parser.add_argument('--spec', required=True)
args = parser.parse_args()
spec_data = json.loads(pathlib.Path(args.spec).read_text())
spec = GateSpec(
candidate_path=args.candidate,
expected_functions=frozenset(spec_data.get('expected_functions', [])),
forbidden_modules=frozenset(spec_data.get('forbidden_modules', [])),
probe_script=spec_data.get('probe_script', ''),
regression_test=spec_data.get('regression_test', ''),
timeout_seconds=spec_data.get('timeout_seconds', 10),
)
print(json.dumps(evaluate(spec), indent=2))
spec 文件是一个包含以下键的小 JSON 对象:expected_functions、forbidden_modules、probe_script、regression_test 和 timeout_seconds。probe 脚本从 candidate 导入,当输出错误时应以非零退出。回归测试是 unittest.TestCase 类的 body。
python gate.py --candidate model_output.py --spec spec.json
输出是 JSON 摘要。如果 accepted 为 false,每个失败项都会命名门禁、原因和相关输出的最后部分。
在本例中生成候选文件时,可以使用任何已有的端点。如果想要零成本的路径,MonkeyCode 的免费模型访问和免费服务器选项让你无需配置本地推理即可生成和运行这些检查。披露:本文作为 MonkeyCode 产品推广的一部分编写。门禁本身不依赖 MonkeyCode,你可以用任何模型的输出替换候选源。
有用的属性是门禁与模型分开运行。你可以向多个端点发送同一 prompt,将每个响应写入 model_output.py,然后让相同的 spec 决定其中是否有任何一个值得更仔细的审查。
执行门禁不是沙箱。它在临时目录中运行生成的代码,但恶意或粗心的候选代码仍可能消耗 CPU、如果具有权限则可能写入目录外,或尝试访问网络。始终在一次性机器或容器上运行,并设置短超时。不要向其提供密钥、部署密钥或挂载的工作树。
AST 门禁只看到解析时存在的内容。它不会捕获动态导入、混淆调用或隐藏在 eval 后面的行为。它是一个快速的噪声过滤器,不是安全保证。
回归门禁的强度取决于你编写的用例。如果你编码的 happy path 与模型看到的相同,循环可能让你安心,但仍然让坏补丁通过。
如果你已有带有隔离容器、依赖扫描和人工审查的 CI 流水线,请跳过此循环。如果候选需要 GPU、长期运行的服务或凭据才能完成任务,请跳过它。这个工具最适合小型的、自包含的函数,在这些地方失败成本高且模型输出变化频繁。
重要的指标不是有多少补丁通过;而是门禁拒绝看起来干净但会破坏隐藏情况的补丁的频率。用同一 prompt 的三个候选输出尝试这个循环,并比较拒绝计数。如果一切都通过了,是时候编写更好的回归测试,而不是更信任模型。