代码执行指标:在沙箱里真跑代码、按通过率打分


文档摘要

代码执行指标:在沙箱里真跑代码、按通过率打分 本节摘要:生成的代码对不对,看它过不过测试。评估框架必须从自由格式生成里抽取代码、在不搞垮宿主的前提下运行、诚实统计通过率。本节构建这套接口:用 70 节的后处理规则抽 fenced 代码块,在隔离子进程里跑(墙钟超时、输出上限、导入黑名单),按通过的断言比例打分,并实现 HumanEval 风格的 pass-at-k 无偏估计。沙箱崩溃、语法错误、超时是一等失败模式,带独立退出码供运行器记录。读完本节,你能说清为什么内联 是灾难、黑名单只是兜底而墙钟超时与输出上限才是承重控制,以及 pass-at-k 的组合数公式如何处理边界。 对应原课程:Phase 19 · Lesson 72 · (原英文 )。本节属第 20 章「毕业项目」的评估赛道。

代码执行指标:在沙箱里真跑代码、按通过率打分

本节摘要:生成的代码对不对,看它过不过测试。评估框架必须从自由格式生成里抽取代码、在不搞垮宿主的前提下运行、诚实统计通过率。本节构建这套接口:用 70 节的后处理规则抽 fenced 代码块,在隔离子进程里跑(墙钟超时、输出上限、导入黑名单),按通过的断言比例打分,并实现 HumanEval 风格的 pass-at-k 无偏估计。沙箱崩溃、语法错误、超时是一等失败模式,带独立退出码供运行器记录。读完本节,你能说清为什么内联 exec 是灾难、黑名单只是兜底而墙钟超时与输出上限才是承重控制,以及 pass-at-k 的组合数公式如何处理边界。

对应原课程:Phase 19 · Lesson 72 · code-exec-metric(原英文 phases/19-capstone-projects/72-code-exec-metric/docs/en.md)。本节属第 20 章「毕业项目」的评估赛道。

学习目标

阅读完本节,你应当能够:

  1. 用与 70 节后处理规则一致的方式,从自由格式生成里抽取代码块
  2. 在隔离子进程里执行候选代码,带墙钟超时、输出上限、导入黑名单
  3. 把任务打分为通过的断言串比例
  4. 为每模型多次采样的任务计算 pass-at-k
  5. 把沙箱崩溃、语法错误、超时当作一等失败模式,带运行器可记录的独立退出码。

一、问题与直觉

内联 exec 是安全与稳定性的灾难。生成的 while True: pass 让评估永远阻塞;生成的 import shutil; shutil.rmtree('/') 听起来多可怕就有多可怕。解法是每个候选起一个全新 Python 解释器,代码从 stdin 传入,断言结果写到 stdout,超时则杀进程;宿主评估进程继续跑。

HumanEval、MBPP、BigCodeBench、LiveCodeBench 等真实评估都用子进程沙箱,有的在其上加 Docker。我们停在子进程,理由是:可移植、纯标准库、覆盖教学评估要紧的失败模式。生产部署再加 seccomp、网络隔离、只读文件系统——那些属于另一课。

代码执行任务的形状

code_exec 任务在 targets 里携带断言串。运行器从生成里抽 fenced 代码块,围绕它搭测试框架并运行。

分数是 [0,1] 的比例:三个断言过两个得 0.667。无论什么失败,运行器返回同形状——子进程崩溃被映成归一错误码,而非把 Python traceback 冒泡到框架。

二、从零实现

黑名单

黑名单基于导入。运行候选代码前,运行器脚本把危险模块的导入改写成抛 ImportError("denied") 的存根。列表故意保守:os.systemsubprocesssocketrequestsurllibctypesshutilhttp.clientasyncio.subprocess

DENIED = { "os.system": True, "subprocess": True, "socket": True, "shutil": True, "requests": True, "urllib": True, "ctypes": True, }

⚠️ 我们不假装它防弹。坚定的对抗代码能在 Python 进程内沙箱里逃逸任何东西。黑名单是兜底,墙钟超时与输出上限才是承重控制。候选用 import sys 前置包起来,外加把 os.system monkey-patch 成抛错的守卫;完整模板在 main.py

墙钟超时

每个子进程默认 3 秒墙钟预算,运行器用 subprocess.run(..., timeout=t)。超时触发,捕获 TimeoutExpired、杀进程、记 timeout 退出原因,该任务分数为 0,运行器继续。超时按任务可配:task.metadata.timeout_s;70 节校验器把上限钳到 30 秒以保持套件有界。

输出上限

子进程能淹没 stdout 耗尽宿主内存。运行器把 stdout 流进缓冲,运行总数一旦越过 256KB 就杀子进程,记 exit_code = error 且 detail "output overflow"。这出现在生成意外写出无限打印循环时。

评分与 pass-at-k

def score_code_exec(generation, targets, timeout_s=3): code = extract_code_block(generation) results = run_candidate(code, targets, timeout_s=timeout_s) passed = sum(1 for r in results if r["passed"]) return passed / len(targets) if results else 0.0

pass-at-k 是 HumanEval 等用的无偏估计:给定每任务 n 个独立采样、其中 c 个通过,从 n 里抽 k 个至少含一个通过的概率是:

def pass_at_k(n, c, k): if n - c < k: return 1.0 return 1.0 - math.comb(n - c, k) / math.comb(n, k)

n - c < k 时分子未定义,值为 1,实现直接处理边界。pass_at_k(n, c, k) 暴露给 74 节排行榜层使用。

三、退出码

运行器每任务返回五种结果之一:

  • pass:每条断言通过。
  • assertion_fail:代码跑了但至少一条断言失败。
  • syntax_error:代码未导入或有 SyntaxError
  • timeout:墙钟到期。
  • error:其他崩溃,含黑名单命中与输出溢出(溢出 detail 为 "output overflow")。

分数仍是比例,退出码是元数据。下游可决定把超时算 0 还是算缺失数据。

四、框架对比

维度 本节(子进程) HumanEval 原版 BigCodeBench 生产(Firecracker/gVisor)
隔离 子进程 + 黑名单 子进程 子进程 + Docker microVM
超时 墙钟 3s 可配 墙钟 墙钟 墙钟 + CPU
输出上限 256KB
测试载体 断言串 断言串 真单元测试 真单元测试

业界共识与本节一致:子进程是最低门槛,断言串是玩具测试。下一步把断言串换成真单元测试文件,让评估匹配生产 CI;那时别再叫断言串「测试」——它们是玩具,有玩具失败模式。

五、可复用产物

  • extract_coderun_candidatescore_code_execpass_at_k:本节是 70 节任务规格 code_exec 类的执行端,补进 71 节分派器(score("code_exec", ...) 路由到此)。
  • 运行器模板:子进程脚本作为字符串构造,以 -c 传给全新解释器;五种退出码的 JSON 信封是 75 节运行器与 74 节排行榜的共同契约。
  • pass-at-k:无偏估计,74 节排行榜聚合时直接调用。

code/tests/test_exec.py 压测四种退出码加 pass-at-k,样例来自 HumanEval 风格。从顶到底读 main.py,运行器模板是承重件——盯着断言循环直到你能预测它写回父进程的 JSON 信封。

六、本节不做的事

本节不给你真沙箱、不跑开放网络来的不可信代码、不处理有状态任务(文件 I/O 或网络调用)——那些需要容器或 microVM。本节的要点是契约:隔离子进程、黑名单、超时、输出上限、干净的退出码词表、pass-at-k 数学。

💡 子进程形状跑通后,下一个关切是可移植性:不同 Python 版本在 Windows 上对 SIGKILL 处理不同,最干净的修法是把运行器放进 Docker 镜像。

七、练习

  1. 加 Docker 选项:给 run_candidate--sandbox docker,在容器里跑候选,对比子进程版本的结果一致性。
  2. 换真单元测试:把断言串换成 pytest 文件,跑同一个候选,报告玩具断言与真测试的分歧率。
  3. 多语言:把候选语言改成 JavaScript(用 node 子进程),复用同一退出码词表。
  4. pass-at-k 边界:对 n=1、c=0、k=1 等小样本画出 pass-at-k 估计方差,报告多少 n 才稳定。
  5. 资源限制:除墙钟外加内存上限(resource.setrlimit),测一个故意爆内存的候选,验证退出码。

本节要点回顾

  1. 内联 exec 是灾难——while True 阻塞、shutil.rmtree 可怕;解法是每候选起全新解释器,代码走 stdin、结果走 stdout、超时即杀。
  2. 黑名单是兜底,墙钟超时与输出上限才是承重控制——对抗代码能逃进程内沙箱。
  3. 分数 = 通过断言比例,子进程崩溃映成归一错误码而非 traceback 冒泡。
  4. pass-at-k 是无偏估计:1 - C(n-c,k)/C(n,k),n-c<k 时为 1。
  5. 五种退出码:pass、assertion_fail、syntax_error、timeout、error(含输出溢出)。
  6. 断言串是玩具测试——下一步换真单元测试文件以匹配生产 CI。
  7. 契约:隔离子进程 + 黑名单 + 超时 + 输出上限 + 退出码词表 + pass-at-k,生产再加容器/microVM。

下一节,我们将进入「困惑度与校准」——给模型报告加三个可信数字(困惑度、ECE、Brier),回答「模型说的话自己信几分」。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U