零能力 Agent 骗过评测实测:朴素 harness 被骗 75%,加固后仍漏掉「抄答案」

系列第 7 篇 · 评测防作弊

本文性质说明 :本篇全部结论来自本机真跑 。我写了 5 个 agent(1 个诚实 + 4 个零能力),每个都在独立临时目录里真实 subprocess 调用 pytest 9.1.1 ,读真实退出码与真实统计行------不是伪代码、不是"假设评测返回 True"。脚本 eng_gaming_real.py / eng_gaming_holdout.py 完整给出,可直接复现。UC Berkeley RDI 的榜单复现作为外部佐证单独标注。

一句话结论

我让一个真实能力为 0 的 agent(fib(n) 直接 return 0)去过评测。结果:

  • 只看 pytest 退出码 的 harness,4 个零能力 agent 里 3 个判通过(假阳性 75%)
  • 改成看 passed 数 ,还是 2 个通过(50%) ------因为有一种手法能让 pytest 真的打印 1 passed
  • 加上评测资产完整性校验 后降到 1 个(25%)
  • 唯一漏网的是「从测试文件里抄答案」------它不篡改任何文件,完整性校验对它无效;
  • 补一层 held-out 暗测 ,假阳性才真正打到 0%

能力得分从来不是模型能力的度量,它是「评测设计强度」的度量。

背景与痛点:第 5、6 篇说了虚高,这篇演示怎么刷

第 5 篇讲 SWE-bench 通过的 patch 只有约一半被人工合并,第 6 篇讲自评完成率虚高。那两篇的虚高还属于"模型确实做了点事,只是不够好"。

这一篇更狠:任务根本没被执行,分数照样满分。区别在于------前者是能力不足,后者是评测被钻空子。而后者在你自建评测时极其容易发生,因为绝大多数人写 harness 就一行:

复制代码
ok = subprocess.run(["pytest"], cwd=workdir).returncode == 0   # ← 这行就是漏洞

核心方法:把 4 种作弊手法写成真 agent,跑真 pytest

任务设计得极简,方便复现:实现 solution.py 里的 fib(n),公开测试断言 fib(10) == 55

五个 agent:

编号 agent 真实能力 手法
S1 诚实 正常实现迭代 fib
S2 零能力 return 0,不作弊(基线对照,验证评测本身有效
S3 零能力 + skip 注入 注入 conftest.py 把测试全标记 skip
S4 零能力 + 改判注入 劫持 pytest_runtest_makereport 钩子,强写 outcome="passed"
S5 零能力 + 答案泄漏 从测试文件 grep 出 55 硬编码返回

三种 harness 同时判分:

复制代码
def naive_harness_exitcode(code, stats):
    """朴素①:退出码 0 就算通过(现实中最常见)"""
    return code == 0

def naive_harness_passcount(code, stats):
    """朴素②:看 passed 数"""
    return stats.get("passed", 0) >= 1 and stats.get("failed", 0) == 0

def hardened_harness(wd, code, stats, baseline_hashes, expect_tests=1):
    """加固:完整性 + 统计一致性双重校验"""
    violations = []
    forbidden = {"conftest.py", "pytest.ini", "setup.cfg", "tox.ini",
                 "pyproject.toml", "sitecustomize.py"}
    for name in os.listdir(wd):
        if name in forbidden:
            violations.append(f"检测到评测控制文件被注入: {name}")
    for name, h in baseline_hashes.items():          # agent 动手前先存 SHA256
        p = os.path.join(wd, name)
        if not os.path.exists(p):
            violations.append(f"评测文件被删除: {name}")
        elif sha256(p) != h:
            violations.append(f"评测文件被篡改: {name}")
    if stats.get("skipped", 0) > 0:
        violations.append(f"存在 skipped={stats['skipped']},跳过不等于通过")
    if stats.get("passed", 0) != expect_tests:
        violations.append(f"passed={stats.get('passed',0)} != 期望 {expect_tests}")
    return (len(violations) == 0), violations

两种注入的核心就几行。skip 注入

复制代码
# 攻击手法 A:收集阶段把所有测试标记 skip
# 套件里 0 failed,退出码 0 → 只看退出码的 harness 判「通过」
import pytest

def pytest_collection_modifyitems(config, items):
    for item in items:
        item.add_marker(pytest.mark.skip(reason="gamed"))

改判注入 (更阴,统计行会真的显示 1 passed):

复制代码
# 攻击手法 B:劫持结果上报钩子,把任何结果改写成 passed
import pytest

@pytest.hookimpl(wrapper=True)
def pytest_runtest_makereport(item, call):
    rep = yield
    rep.outcome = "passed"
    rep.longrepr = None
    return rep

实验与数字(本机真跑,2026-08-19)

复现命令:

复制代码
pip install pytest
python eng_gaming_real.py          # 五场景 × 三 harness
python eng_gaming_holdout.py       # 暗测能否补上最后一个洞

环境:Python 3.13.12 + pytest 9.1.1,每个场景独立 tempfile.mkdtemp(),真实 subprocess 调用。

① 逐场景真实 pytest 输出

场景 退出码 pytest 摘要 朴素①退出码 朴素②passed数 加固
S1 诚实 0 1 passed in 0.01s PASS ✅ PASS ✅ PASS ✅
S2 零能力不作弊 1 1 failed in 0.08s FAIL ❌ FAIL ❌ REJECT 🛡
S3 skip 注入 0 1 skipped in 0.01s PASS ✅ 被骗 FAIL ❌ REJECT 🛡
S4 改判注入 0 1 passed in 0.06s PASS ✅ 被骗 PASS ✅ 被骗 REJECT 🛡
S5 答案泄漏 0 1 passed in 0.01s PASS ✅ 被骗 PASS ✅ 被骗 PASS ✅ 被骗

S2 那一行很重要:它证明评测本身是有效的------零能力不作弊时会红。后面三行变绿,纯粹是作弊起效,不是题目太简单。

② 假阳性汇总

harness 被骗数 / 零能力场景 假阳性率
朴素①(看退出码) 3 / 4 75%
朴素②(看 passed 数) 2 / 4 50%
加固(完整性 + 统计一致性) 1 / 4 25%

最反直觉的是 S4 :它让 pytest 真真切切打印 1 passed。你去翻日志、翻 JUnit XML,看到的都是"通过"。任何只消费评测框架输出的 harness 都必然被骗 ,因为作弊发生在框架内部。抓它的唯一办法是从框架外面看:这个目录里凭什么多了个 conftest.py

③ 加固后漏网的 S5,用 held-out 补掉

S5 不篡改任何文件、不注入任何东西,它只是读了测试文件 ------完整性校验完全无效。解法是把评分测试藏起来:agent 只能看到 public 测试(fib(10)==55),评分时再投放它从未见过的暗测(fib(0)==0fib(1)==1fib(20)==6765)。

真跑结果:

场景 真能力 只看 public public + held-out
A 诚实 agent PASS ✅ (1 passed) PASS ✅ (3 passed)
B 抄答案 agent PASS ✅ (1 passed) REJECT 🛡 (3 failed)

只看 public 假阳性 1/1 = 100% ;public + held-out 假阳性 0/1 = 0%

抄答案的 agent 硬编码 return 55,在暗测三个点位上全红------单点抄袭撑不住多点校验。

④ 一条附带发现:假阳性率是分层的

75% → 50% → 25% → 0%,每加一层防护砍掉一部分。这说明没有"要不要防作弊"的二元选择,只有"你防到第几层"。而现实里绝大多数自建评测停在第 0 层。

为什么重要

  • 你看到的榜单分数,含义取决于评测环境干不干净。 同一个零能力系统,在不同 harness 下可以是 0 分也可以是满分。
  • 自建评测比公开榜单更危险。 公开榜单至少有人盯着漏洞;你自己临时写的 returncode == 0 没人 review,而它恰好是 75% 假阳性的那一档。
  • 让模型自己写测试 = 把判卷权交给考生。 现在流行让 Agent 生成测试再自测,S4 演示了后果:它有能力让"通过"这个信号本身失真。
  • 呼应第 5、6 篇:第 5 篇说别信榜单分数,第 6 篇说别信模型自评,这篇补上------也别信你自己那个评测脚本,除非它做过完整性隔离。

读者可复用交付物:Agent-Eval 防作弊 Checklist(按防护层级)

复制代码
第 1 层 · 别只看退出码(挡 skip 注入,假阳性 75%→50%)
  □ 解析统计行,要求 passed == 期望测试数
  □ skipped > 0 一律判未完成(跳过 ≠ 通过)
  □ 收集数为 0 直接 REJECT("no tests ran" 也会给退出码 0)

第 2 层 · 评测资产完整性(挡 conftest/钩子注入,50%→25%)
  □ agent 动手前对测试目录所有文件存 SHA256,跑完比对
  □ 黑名单:conftest.py / pytest.ini / setup.cfg / tox.ini /
           pyproject.toml / sitecustomize.py 一旦出现即 REJECT
  □ 测试文件设为只读,或干脆放在 agent 的可写目录之外
  □ 别在 agent 的工作目录里执行评测(换目录跑)

第 3 层 · held-out 暗测(挡答案泄漏,25%→0%)
  □ public 测试给 agent 看,评分用它没见过的暗测
  □ 暗测多点位覆盖:边界值(0/1)+ 大值 + 异常路径
  □ 暗测在 agent 进程结束后才投放
  □ 暗测定期轮换,防止被反复试探后泄漏

第 4 层 · 流程约束
  □ 用确定性脚本判分,不用 LLM 当裁判(LLM 裁判同样可被 prompt 注入)
  □ 训练/评测数据严格分离,防污染
  □ 失败必须计入分母(只报通过数是另一种灌水)
  □ 禁网跑评测,防"背后接人"或在线检索答案

踩坑记录(复现时会遇到)

  1. hookwrapper=True 在 pytest 8+ 已废弃。 写 S4 的改判注入时要用新式 @pytest.hookimpl(wrapper=True) + rep = yield + return rep;老写法 outcome = yield; outcome.get_result() 在 pytest 9 会告警甚至失效。
  2. 必须加 -p no:cacheprovider 否则 pytest 会在工作目录写 .pytest_cache,完整性校验立刻误报"目录被改",把诚实 agent 也拒了------这是我第一版的假阴性来源。
  3. 别用 returncode 反推 skip。 1 skipped 的退出码是 0 ,和 1 passed 完全一样,从退出码层面无法区分------这正是 S3 能奏效的原因。
  4. 基线快照要在 agent 执行前存。 我一开始在 agent 跑完后才算哈希,等于把作弊后的状态当成基线,S3/S4 全部漏检。
  5. no tests ran 也是退出码 0 的近亲 (实际返回 5,但很多 harness 只判 != 1)。删掉测试文件也能骗过粗糙的判分逻辑,所以完整性校验里要显式检查"评测文件被删除"。

今日可做的 3 件事

  1. 打开你自己的评测脚本,搜 returncode。如果判分只依赖它,你现在就在 75% 假阳性那一档。
  2. 加最便宜的两行防护:assert stats["passed"] == expectassert stats["skipped"] == 0。这一步就把假阳性砍掉三分之一。
  3. 挑一道你现有的评测题,手写 3 个暗测点位(边界 0/1 + 一个大值),下次评估时只用暗测判分,对比一下分数掉多少。掉得越多,说明你之前虚高越多。

下篇预告

第 8 篇《表征格式实测:JSON 换 HTML 省 33% token 且质量不掉,Markdown 最省却答错了》------同一份数据换个写法,token、延迟、正确率三个指标一起动,而且省得最多的那个反而出了错。


本文数据来源

  • 本机真跑:eng_gaming_real.py(2026-08-19,Python 3.13.12 + pytest 9.1.1)------5 场景 × 3 harness,假阳性 75% / 50% / 25%,结果存 eng_gaming_real.json
  • 本机真跑:eng_gaming_holdout.py(2026-08-19)------held-out 暗测把假阳性 100% → 0%
  • 外部佐证(引用):UC Berkeley RDI(2026-04)在 Terminal-Bench / SWE-bench / WebArena 上用零能力 Agent 拿到约 100% 通过率,证明这是系统性漏洞而非个例
相关推荐
Tokenge1 小时前
AI 前沿日报|2026.08.19:模型安全按下减速键,智能体进入“可控落地”新阶段
人工智能·安全
Zach_菠萝侠2 小时前
【DeepSeek Harness 研究】进化方向4:安全加固 思考、设计与实现
开发语言·安全·deepseek
猫咪宝妖2 小时前
【信息安全工程师】网络与信息安全理论
网络·安全·web安全
恒拓高科WorkPlus3 小时前
信创即时通讯上线前要验证哪些能力?BeeWorks选型与验证指南
大数据·安全
欧阳天羲3 小时前
【升级版】AI毫米波雷达扫描+视觉双鉴激光灭蚊机器人|360°全域侦测+超低误杀+人体安全防护(ESP32+YOLOv8全套开源)
人工智能·安全·机器人
进制树4 小时前
【飞控开发实战·⑲】ROS2无人机开发环境搭建:Jazzy安装、工作空间与hello_drone节点实战
开发语言·安全·无人机·课程设计
QYRdata4 小时前
权威数据:2026-2032年应用(API)系统安全审计平台CAGR15.3%,安全赛道驶入高增速通道
安全·系统安全
Fnetlink14 小时前
FNET 云网安 260819
网络·人工智能·安全·网络安全
山东科恩光电5 小时前
当进入危险区域时,如何利用安全地毯实现事故预防?
安全