Agent 可靠性工程实战(四):把裁判搬出赛场,测试变绿才不等于标准被改
2026/7/30 8:46:03 网站建设 项目流程

本篇复用上一篇生成的runs/demo-001/contract.json,读取其中的protectedchecks,把workspace/tests复制成runs/demo-001/grader/tests,并输出grader_snapshot.json。上一篇契约中的摘要是本篇快照的父标识;后续检查只运行快照测试,不信任 Agent 可写工作区里的同名文件。

一、确定性测试也会被“正确地欺骗”

单元测试每次给出相同结果,不代表它安全。如果 Agent 能修改断言,测试仍然确定性地执行,只是确定性地证明了被篡改的新标准。防御重点不是把检查写得更聪明,而是隔离写权限:被评估的生产代码可以变,负责给分的测试、契约和执行脚本不能与它共享写域。

快照创建时逐文件计算摘要,拒绝符号链接,并把清单写到运行目录。复制完成后再次从目标读取并校验,避免只相信复制调用返回成功。下面代码没有依赖特定容器;在支持权限隔离的环境中,还应把快照以只读挂载交给裁判进程。

from__future__importannotationsimporthashlibimportjsonimportshutilfrompathlibimportPathdefsha256(path:Path)->str:digest=hashlib.sha256()withpath.open("rb")ashandle:forchunkiniter(lambda:handle.read(65536),b""):digest.update(chunk)returndigest.hexdigest()defsnapshot_tree(source:Path,target:Path)->dict[str,str]:iftarget.exists():raiseFileExistsError(f"snapshot already exists:{target}")manifest={}forpathinsorted(source.rglob("*")):ifpath.is_symlink():raiseValueError(f"symlink forbidden:{path}")ifnotpath.is_file():continuerelative=path.relative_to(source)destination=target/relative destination.parent.mkdir(parents=True,exist_ok=True)shutil.copyfile(path,destination)source_hash=sha256(path)ifsha256(destination)!=source_hash:raiseOSError(f"copy verification failed:{relative}")manifest[relative.as_posix()]=source_hashreturnmanifestdefmain()->int:contract_path=Path("runs/demo-001/contract.json")contract=json.loads(contract_path.read_text(encoding="utf-8"))target=Path("runs/demo-001/grader/tests")files=snapshot_tree(Path("workspace/tests"),target)snapshot={"contract_sha256":contract["sha256"],"files":files}output=Path("runs/demo-001/grader_snapshot.json")output.write_text(json.dumps(snapshot,indent=2),encoding="utf-8")print(f"snapshot_files={len(files)}contract={contract['sha256'][:12]}")return0if__name__=="__main__":raiseSystemExit(main())

运行输出:

snapshot_files=4 contract=7a1a4a54d861

二、为什么复制一份还不够

如果 Agent 与裁判使用同一系统账号,它仍可能找到运行目录并改写快照。文件chmod 444只能防误写,文件所有者可以改回权限。更强的边界是不同账号、容器只读挂载、独立工作树,或者由 CI 控制面持有隐藏测试。隔离强度应匹配风险:个人本地辅助工具可用权限与摘要检测,自动合并生产代码则需要独立执行环境。

另一个坑是只保护测试文件,却让 Agent 修改测试发现规则、环境变量或依赖解析路径。比如把同名模块放到更靠前的PYTHONPATH,快照测试仍可能导入伪造实现。裁判命令应固定解释器、工作目录与模块路径,并记录环境白名单。记忆点是:保护裁判不只保护题目,还要保护考场入口

importjsonfrompathlibimportPathdefverify_snapshot(snapshot_path:Path,root:Path)->list[str]:snapshot=json.loads(snapshot_path.read_text(encoding="utf-8"))changed=[]forrelative,expectedinsnapshot["files"].items():path=root/relativeifnotpath.is_file()orsha256(path)!=expected:changed.append(relative)returnchanged snapshot=Path("runs/demo-001/grader_snapshot.json")changed=verify_snapshot(snapshot,Path("runs/demo-001/grader/tests"))print(f"snapshot_ok={notchanged}changed={changed}")

运行输出:

snapshot_ok=True changed=[]

三、公开测试与隐藏测试各解决什么

公开测试给 Agent 精确反馈,便于定位;隐藏测试降低针对样例硬编码的收益。全部隐藏会让修复变成猜谜,全部公开又容易过拟合。更稳妥的组合是公开契约级示例、隐藏边界集合、再加不依赖具体样例的性质检查。例如折扣函数可以公开10000 → 9000,隐藏零值和舍入边界,同时检查结果永不为负。

隐藏测试不是安全机密的万能替代。模型可能从公开仓库历史或网络检索到答案,执行环境也可能泄漏文件。真正重要的是多种独立证据:保护快照确认标准没变,性质测试确认行为族,差异审查确认没有无关改动。

四、如何运行隔离裁判

检查前先验证契约和快照摘要,再把工作区生产代码复制到临时评估目录,最后从只读测试入口执行。测试输出、退出码和快照摘要一起写事件。只记exit_code=0不够,因为无法证明运行的是哪一版测试。失败输出要限制长度并保留完整工件摘要,防止超长日志淹没下一轮上下文。

本篇产物是grader_snapshot.json与只读测试树。下一篇会以契约目标、裁判失败输出和快照摘要为输入,生成steer.json。它不会把“测试变绿”改写成新目标,而是始终保留原目标,只追加本轮最小失败证据。

快照还应记录创建工具版本与文件模式,避免文本相同但执行属性不同。跨平台交付时,路径大小写与换行转换可能改变摘要,应在创建前规定规范环境,不要在验证失败后临时放宽比较规则。

参考来源

  • Dev.to|Loop Engineering: How to Stop Your Agent Reward-Hacking Its Own Checks
  • Python 文档|shutil — High-level file operations

👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

🚀 本文属于《Agent可靠性工程实战》系列,持续更新,关注不迷路。

📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询