代码执行指标:在沙箱里真跑代码、按通过率打分 本节摘要:生成的代码对不对,看它过不过测试。评估框架必须从自由格式生成里抽取代码、在不搞垮宿主的前提下运行、诚实统计通过率。本节构建这套接口:用 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 章「毕业项目」的评估赛道。
阅读完本节,你应当能够:
内联 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.system、subprocess、socket、requests、urllib、ctypes、shutil、http.client、asyncio.subprocess。
DENIED = { "os.system": True, "subprocess": True, "socket": True, "shutil": True, "requests": True, "urllib": True, "ctypes": True, }
⚠️ 我们不假装它防弹。坚定的对抗代码能在 Python 进程内沙箱里逃逸任何东西。黑名单是兜底,墙钟超时与输出上限才是承重控制。候选用
import sys前置包起来,外加把os.systemmonkey-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"。这出现在生成意外写出无限打印循环时。
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_code、run_candidate、score_code_exec、pass_at_k:本节是 70 节任务规格 code_exec 类的执行端,补进 71 节分派器(score("code_exec", ...) 路由到此)。-c 传给全新解释器;五种退出码的 JSON 信封是 75 节运行器与 74 节排行榜的共同契约。code/tests/test_exec.py 压测四种退出码加 pass-at-k,样例来自 HumanEval 风格。从顶到底读 main.py,运行器模板是承重件——盯着断言循环直到你能预测它写回父进程的 JSON 信封。
本节不给你真沙箱、不跑开放网络来的不可信代码、不处理有状态任务(文件 I/O 或网络调用)——那些需要容器或 microVM。本节的要点是契约:隔离子进程、黑名单、超时、输出上限、干净的退出码词表、pass-at-k 数学。
💡 子进程形状跑通后,下一个关切是可移植性:不同 Python 版本在 Windows 上对 SIGKILL 处理不同,最干净的修法是把运行器放进 Docker 镜像。
run_candidate 加 --sandbox docker,在容器里跑候选,对比子进程版本的结果一致性。node 子进程),复用同一退出码词表。resource.setrlimit),测一个故意爆内存的候选,验证退出码。while True 阻塞、shutil.rmtree 可怕;解法是每候选起全新解释器,代码走 stdin、结果走 stdout、超时即杀。1 - C(n-c,k)/C(n,k),n-c<k 时为 1。下一节,我们将进入「困惑度与校准」——给模型报告加三个可信数字(困惑度、ECE、Brier),回答「模型说的话自己信几分」。