想象一所烹饪学院的毕业大考:考官端出 164 道菜式,每道菜只给你一张写着“菜名、食材清单和风味描述”的卡片,你要在倒计时内自己写出烹饪步骤。考场上,一位神秘的“味觉考官”在你出锅后开始品尝——他每道菜只尝特定几口(输入),然后核对卡片上标注的该有的味道(期望输出)。尝一口,对了,画个勾;不对,直接判负。这位“味觉考官”,就是本文的主角——测试用例(Test Case)。 在 AI 世界里,“学徒”叫大语言模型(LLM),“菜谱卡片”叫题目描述与函数签名,而“味觉考官”则是一段段写好的小程序:它把输入喂给模型生成的代码,比较实际输出与期望输出,决定通过还是失败。这套“考官”体系,凝结在 HumanEval 中——2021 年 OpenAI 发布、如今最著名的代码生成能力评测基准。本文会用讲故事的方式,带你认识这位考官的工作机制、评分规则(pass@k)、考卷设计智慧、先天不足,以及在实际工程中如何用好它。

岗位关联

掌握 HumanEval 测试用例的原理与评估方法,是 AI 从业者判断“模型写代码到底行不行”的基本功。无论训练代码模型、优化提示词还是做模型选型,这套评测思维都会成为你手中最可靠的“味觉评分卡”。

目录
  1. 可视化演示
  2. 测试用例基础
  3. HumanEval构成
  4. pass@k指标
  5. 测试执行流程
  6. 用例设计技巧
  7. 局限性分析
  8. 工程应用优化
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示HumanEval基准测试用例执行判定、pass@k无偏估计计算与变异测试增强覆盖的过程,帮助理解代码生成评测机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

测试执行:逐条判定 正常值/边界值/特殊值三类覆盖 通过显示绿色,失败显示红色 通过率=通过数/用例总数 平均每程序7-10个测试用例

测试用例基础

味觉考官的评分卡上,写着“这一口菜该是什么味道”。他夹起盘子里的第一口菜(输入),送进嘴里(运行),然后心里默念卡片上标注的标准味道(期望输出),一对比便知对错。这张评分卡,就是测试用例——它是评测里最微小、也最关键的单元:一个输入、一个期望输出,外加一个“断言”动作。

在 LLM 评测体系中,测试用例的定义完全一致。被测对象是模型生成的代码函数 f,测试用例则是一个二元组 (x, y):x 是喂给函数的输入数据,y 是函数应当返回的期望结果。评测时把 x 送进 f,得到实际输出 f(x),再用“断言”(assert)将它与 y 比对。相等则通过,不相等则失败。简单,但不容置疑。

一个合格的测试用例集不会只测普通情况。想象考官不光尝盘子中央,还要尝菜边缘烧焦的部分——这就是边界条件:空列表、单个元素、极大极小值、负数、重复元素。很多程序在“正常”输入下表现完美,一到空输入就崩溃、遇到极端值就溢出。边界用例正是用来揪出这些“藏在菜帮子里的问题”的。断言机制则是执行比对的裁判,它把“应该相等”翻译成计算机能执行的指令,一票否决。

你看,味觉考官的评分卡设计得极其克制:一张卡只检验一件事。但正因为每张卡都简单、明确、可自动执行,它们才能组成一场公平的大考——机器可以在一秒内品尝成千上万口“菜”,而不会疲倦,也不会偏心。

断言(f(x)=y)\text{断言}(f(x) = y)
公式通俗说明:一个测试用例的本质就是一句话——“把 x 送进函数 f,得到的结果必须等于 y”,断言就是那个铁面无私的裁判。
  1. x 输入样本味觉考官端起的“那一口菜”,即函数要处理的数据。
    xx
  2. f(x) 实际输出学徒做出的菜入口后的真实味道,也就是代码跑出来的结果。
    f(x)f(x)
  3. y 期望输出评分卡上预先写好的标准味道,即题目期望的正确结果。
    yy
  4. 断言比对裁判把实际味道与标准味道对照,相同判通过,不同判失败。
    断言(⋅=⋅)\text{断言}( \cdot = \cdot )

HumanEval构成

烹饪学院的毕业考卷不是从美食杂志上随手剪下来的,而是考官亲手写的 164 道原创菜式,答案严格保密。这样做的道理很朴素:如果考卷是现成菜谱抄来的,学徒们在训练时早把答案背得滚瓜烂熟,考试就失去了意义。HumanEval 正是这样一张“手写考卷”——OpenAI 的研究者手工编写了 164 道 Python 编程题,从未公开在互联网上,确保模型在训练时“没见过答案”。

每一道题都配齐了“四件套”,缺一不可。第一件是函数签名(s),它规定了函数叫什么名字、接收什么参数,相当于告诉学徒“你的操作台长什么样”;第二件是文档字符串(d),用自然语言描述这道菜该做什么、返回什么,相当于菜谱的风味说明;第三件是测试用例集(T),就是考官的味觉评分卡,每个用例给出具体输入和期望输出;第四件是参考解法(r),考官自己写的标准做法,用来验证题目确实可解、评分卡没有出错。

这四件套的分工值得细品:函数签名和文档字符串是“发给学徒的题目”,测试用例是“判分的标准”,参考解法是“出题人的自检”。四者合在一起,才构成一道完整、可自动评分的考题。164 道题全部由人手工完成,因而题目描述自然、贴近真实编程任务,也避免了自动采集题目时常见的“题目本身有错”或“和训练数据高度重合”的问题。

这份手写考卷凝聚了出题人的心血,也正因为如此,它成了业界公认的“标准考卷”。直到今天,几乎所有代码大模型的论文里,都会骄傲地晒出在 HumanEval 上考出的分数——就像烹饪学院的毕业证,成了学徒们求职的硬通货。

DH={(si,di,Ti,ri)}i=1164\mathcal{D}_H = \{(s_i, d_i, T_i, r_i)\}_{i=1}^{164}
公式通俗说明:HumanEval 数据集就是 164 份“题目材料包”的集合,每份包含函数签名、文档字符串、测试用例和参考解法。
  1. D_H 数据集整个毕业考卷,即 HumanEval 基准本体。
    DH\mathcal{D}_H
  2. s_i 函数签名每题规定的函数名与参数列表,相当于厨房的操作台。
    sis_i
  3. d_i 文档字符串对菜式风味的文字描述,告诉模型“要做什么菜”。
    did_i
  4. T_i 测试用例集考官手里那张味觉评分卡,记载着多个输入与期望输出。
    TiT_i
  5. r_i 参考解法考官自己的标准做法,用于验证题目可解。
    rir_i
  6. i 与 164从第 1 题到第 164 题逐一编号,每道题的材料缺一不可。
    i=1,2,…,164i = 1, 2, \ldots, 164

pass@k指标

一次机会太苛刻了。学徒做菜有状态波动:火候差一秒、手抖多放一撮盐,菜就废了。于是考官改了一个规则:给你 k 次机会,只要 k 道菜里有一道味道全对,就算你通过。这个“给机会”的考核方式,对应着评测中的 pass@k 指标——k 越大,考察的越是“模型有没有能力做对”,而非“模型这一次恰好做对”。

实际操作中,模型对每道题独立生成 n 份候选代码(相当于学徒做 n 遍同一道菜),每份代码都要跑完所有测试用例,只有全部通过才记作“正确”。pass@k 回答的问题是:如果从这 n 份答案里随机抽取 k 份提交,至少有一份能全部通过测试的概率是多少?比如 pass@1 就是“只给一次机会”的正确率,而 pass@5 则是“允许试 5 次”的成功率。

这里藏着一个容易踩坑的细节:很多人想当然地生成 n 份答案、取其中最好的结果来汇报,但这是“开卷作弊”——因为真实场景里你只会提交一份答案,取最好会让分数虚高。HumanEval 论文给出的方案是用组合数做无偏估计:先算出“随机抽 k 份全是错的概率”,再用 1 减去它。分母 C(n, k) 表示从 n 份里抽 k 份的所有可能,分子 C(n−c, k) 表示抽到的 k 份全部来自错误答案的可能。这样算出来的 pass@k,才是“真的只给 k 次机会”的诚实分数。

多给几次机会,及格率自然上升,所以论文里 typically 报告 pass@1 和 pass@k 两种分数,让读者既看到“一次成功率”,也看到“潜力上限”。这就像招聘时既看重面试表现,也保留一次复试机会——k 次机会背后,测的是模型真实能力的“上界”。

pass@k=1N∑i=1N[1−(n−cik)(nk)]\mathrm{pass}@k = \frac{1}{N}\sum_{i=1}^{N}\left[1 - \frac{\binom{n-c_i}{k}}{\binom{n}{k}}\right]
公式通俗说明:对每道题,先算“抽 k 次全是坏菜”的概率,用 1 减掉就得到“至少尝到一次好菜”的概率,最后对所有题目取平均。
  1. N 个问题取平均整张考卷共 N 道题,把每道题的通过概率汇总取平均,得到整体得分。
    1N∑i=1N\frac{1}{N}\sum_{i=1}^{N}
  2. n 次采样针对每道题,让模型独立生成 n 份候选答案,就像学徒把同一道菜做 n 遍。
    nn
  3. c_i 份全对n 份答案里,恰好有 c_i 份通过了全部测试用例,味道完全正确。
    cic_i
  4. C(n, k) 抽法总数从 n 份答案中任意抽取 k 份交给考官,一共有多少种抽法。
    (nk)\binom{n}{k}
  5. C(n−c, k) 全错抽法若抽到的 k 份全部来自错误答案,说明这次机会全浪费了。
    (n−cik)\binom{n-c_i}{k}
  6. 1 减比值用 1 减去“全错”的概率,得到“至少有一份全对”的概率,即 pass@k。
    [1−(n−cik)(nk)]\left[1 - \frac{\binom{n-c_i}{k}}{\binom{n}{k}}\right]

测试执行流程

考场的监考流程是严格到近乎苛刻的。每位学徒被分到独立的灶台——这是为了防止他偷看邻座,也防止他的油烟飘到别人锅里。在评测里,这个“独立灶台”叫环境隔离(sandbox):每个测试用例都在干净的命名空间里运行,函数之间互不干扰,任何全局变量的改动、临时文件的创建都限制在沙箱内,杜绝“作弊”和“串味”。

流程可分为四步。第一步,隔离:为被测代码创建独立的执行环境,清理命名空间。第二步,运行:把模型生成的代码编译进来,依次喂入测试用例的输入,执行函数调用。第三步,断言:将实际输出与期望输出比对,同时给每个用例设定超时上限 τ——如果代码陷入死循环,超过时限直接判负,就像做菜超时一样,菜再香也算失败。第四步,判定与记录:所有用例都通过才算通过,否则记录失败类型,供后续分析。

失败分析同样讲究。失败可以分为几类:编译错误是“原料都没下锅就熄火了”,说明代码有语法问题;运行异常是“做菜途中炸了锅”,代码在执行时抛异常;断言不匹配是“菜做出来了但味道不对”,程序能跑但结果错误。三类失败的整改难度逐级递增:编译错误和运行异常往往是小问题,断言不匹配则意味着模型对题目语义的理解出了偏差。

这套流程看似繁琐,却保证了评测的公平与可复现。同一个模型在不同的机器、不同的时间跑同一套测试,得到的结果应当完全一致——这正是“味觉考官”最宝贵的品质:它没有情绪波动,不会因为今天心情好就多给半勺盐的容忍度。每一个 PASS 和 FAIL,都经得起推敲。

通过(f,T)={1,若 ∀t∈T:f(ta)=tb 且 time(f)<τ0,否则\text{通过}(f, T) = \begin{cases} 1, & \text{若 } \forall t \in T: f(t_a) = t_b \text{ 且 } \text{time}(f) < \tau \\[4pt] 0, & \text{否则} \end{cases}
公式通俗说明:一段代码能否通过评测,取决于两件事——所有测试用例的输出都匹配,并且运行时间没有超时;两者缺一,直接判负。
  1. f 被测函数模型生成的那份代码,即学徒端出的成品菜。
    ff
  2. T 测试用例集需要逐口品尝的所有检查点,每个用例 t 含输入 t_a 与期望输出 t_b。
    T={t1,t2,…,tm}T = \{t_1, t_2, \ldots, t_m\}
  3. ∀ 全部用例要求每一口味道都正确,一个用例都不许放水。
    ∀t∈T\forall t \in T
  4. time(f) < τ 超时限制做菜超时同样判负,防止死循环拖垮整个考场。
    time(f)<τ\text{time}(f) < \tau
  5. 结果 1 或 0全部满足判 1 通过,任一不满足判 0 失败。
    通过(f,T)∈{0,1}\text{通过}(f, T) \in \{0, 1\}

用例设计技巧

怎样才能设计出一张“尝不出漏网之鱼”的评分卡?只尝盘子正中间显然不够,聪明的考官会布下四类检验点。第一类是功能用例,专挑最常规的输入,验证函数在“正常情况”下是否工作——相当于先尝一口菜的主体味道;第二类是边界用例,瞄准空输入、单元素、极值、负数等“危险地带”,专门测试程序的健壮性——这是菜盘边缘烧焦的部分;第三类是随机用例,用随机生成的数据反复轰炸函数,扩大覆盖范围,相当于让考官蒙着眼睛随便夹菜,尝到哪算哪;第四类是隐藏用例,由考官私藏、不对外公开,专门用来防止模型“背题”或针对公开用例过拟合。

每类用例的价值,可以用一个概率模型来理解。假设每个用例捕获缺陷的概率是 p_t,那么它漏掉缺陷的概率就是 1 − p_t。所有用例都漏掉,缺陷才能蒙混过关。由于各用例相互独立,总漏检概率就是各个“漏检概率”的连乘。这意味着什么?增加一个用例,哪怕它的捕获率只有 0.5,也能让漏检概率直接打对折——这就是为什么“用例越多、越多样,评测越可信”。

功能用例保证基本盘,边界用例防守极端,随机用例扩大覆盖面,隐藏用例锁定公正性。四者各司其职,就像一组优秀的品鉴师团队:有人尝主体、有人啃边缘、有人闭眼盲测、有人突击抽查。HumanEval 的每道题就配备了 5 到 10 个这样的用例,把模型生成的代码反复“品鉴”,直到它原形毕露或实至名归。

设计用例的智慧,本质上是“用有限的品尝次数,覆盖最大的风险空间”。一个测试用例集的好坏,不在于数量多寡,而在于能否在关键位置埋下足够的“味觉探针”。这提醒我们:在给模型写评测用例时,别只盯着“正常情况”心满意足——真正决定成败的,往往是那些看起来刁钻、却直击要害的边界与隐藏用例。

P(漏检)=∏t∈T(1−pt)P(\text{漏检}) = \prod_{t \in T} (1 - p_t)
公式通俗说明:一个缺陷要想骗过所有测试用例,必须每个用例都恰好漏掉它。用例越多、越多样,这个“集体漏检”的概率就越低。
  1. p_t 单用例检出率每个测试用例像一次品尝,单独捕获缺陷的概率为 p_t。
    ptp_t
  2. 1 − p_t 单用例漏检这一口没尝出问题的概率。
    1−pt1 - p_t
  3. ∏ 连乘所有用例都漏掉,缺陷才真正蒙混过关,概率相乘。
    ∏t∈T\prod_{t \in T}
  4. 结论用例越多、越多样,漏检概率指数级下降,评测越可信。
    P(漏检)→0P(\text{漏检}) \to 0

局限性分析

毕业考毕竟只是毕业考。这张手写考卷只有 164 道菜,而且绝大多数是家常小炒(基础算法题),既没有满汉全席(大型工程),也没有地方菜系(多语言多框架)。一位学徒在毕业考上拿到满分,只能说明他基础扎实,却不能证明他能管理一家餐厅的后厨。HumanEval 的局限,正是从“考卷覆盖度”开始的。

第一个局限是题目数量少。164 道题对于评估一个模型的能力来说,样本量小得可怜。统计学告诉我们,基于 164 道题估算的 pass@1,其波动范围(标准误)并不小——假设模型真实通过率是 p,那么 164 道题测出来的分数,大约会有 sqrt(p(1−p)/164) 的标准误差。两个模型分数相差 1 个百分点,很可能只是随机波动,而非实力差距。第二个局限是难度分布不均,164 道题中简单和中等难度的题目偏多,难题稀少,导致分数“通胀”——模型的真实下界可能被高估。第三个局限是数据污染风险:虽然题目当初是手写的,但模型预训练语料不断膨胀,难保后来训练的大模型没有在某个角落“偷看过”这些答案。

更让人警惕的是泛化能力问题。HumanEval 中的题目都是“输入—输出”型的独立函数,不涉及真实仓库中的类依赖、模块导入、异步调用、版本兼容等复杂工程约束。一个模型在 HumanEval 上取得高分,面对真实的代码补全、Bug 修复、跨文件重构时,表现可能大打折扣。分数漂亮,不等于工程可用。

理解了局限性,我们才能正确地使用这份考卷:它适合做“门槛检查”,不适合做“能力定级”。就像厨师证只能证明你通过了基础资格考试,不能证明你是米其林主厨。HumanEval 上的高分是必要的,但远远不是充分的。

σ(pass@1)=p(1−p)164\sigma(\mathrm{pass}@1) = \sqrt{\frac{p(1-p)}{164}}
公式通俗说明:用 164 道题估算通过率,结果本身有波动。题目越少,波动越大;这个公式就是衡量波动幅度的“误差尺”。
  1. p 通过率模型在 164 道题上测得的 pass@1 得分。
    pp
  2. p(1−p) 方差每道题“通过与不通过”带来的不确定性程度,在 p=0.5 时最大。
    p(1−p)p(1-p)
  3. 除以 164题目越多,平均值的波动越小,这是大数定律的作用。
    p(1−p)164\frac{p(1-p)}{164}
  4. 开根号把方差换算成标准误,即分数围绕真实水平的大致波动范围。
    p(1−p)164\sqrt{\frac{p(1-p)}{164}}

工程应用优化

既然发现了考卷的不足,学院自然要改革。改革方案有三条线:加菜、换菜系、加评分维度。所谓“加菜”,是给每道题增加更多测试用例——EvalPlus 项目将 HumanEval 的每个题目用变异测试和等价变换扩增出约 80 倍的用例,用“海量品尝”来堵住“背题过关”的漏洞;所谓“换菜系”,是引入 MBPP、MultiPL-E 等多语言、多风格的基准,把 Python 之外的 Java、C++、JavaScript 也纳入考核;所谓“加评分维度”,是在“对错”之外,进一步评估代码的效率、可读性和可维护性。

在具体工程实践中,最实用的优化是构建“持续评测”流水线。代码模型团队每次训练出新版本,都自动跑一遍 HumanEval 测试集,把 pass@1、pass@k 记录在案,对比历史版本。分数上升,可以放心发版;分数下降,立即回滚排查。这条流水线就像一个不知疲倦的“常驻味觉考官”,每次模型更新都帮你把关,防止能力悄悄退化。

数据增强方面,除了依赖 EvalPlus 这样的扩增工具,团队还可以针对自身业务场景自定义测试用例,把经常出错的边界情况沉淀成“回归测试集”。代码质量评估则更进一步:一个函数即使输出全对,也可能运行效率极低或难以维护,因此可以结合代码复杂度分析、静态检查工具、人工抽评来综合打分。这就像考官不仅尝味道,还要看摆盘、量用时、评创意。

学院的改革告诉我们一个朴素的道理:评测不是一锤子买卖,而是一个需要持续经营的基础设施。测试用例会过时,分数会通胀,模型会进化——唯有不断“加菜、换菜系、加评分维度”,让味觉考官始终保持敏锐,我们才能真正守住 AI 编程能力的质量底线。

∣Ti′∣≈80×∣Ti∣|T'_i| \approx 80 \times |T_i|
公式通俗说明:EvalPlus 把每道题的测试用例数量扩充到原来的约 80 倍,用更密集的“品尝点”大幅提升评测的区分度。
  1. |T_i| 原始用例数第 i 题自带的测试用例数量,通常只有 5 到 10 个。
    ∣Ti∣|T_i|
  2. 80 倍扩增通过变异测试与等价变换,为每个原始用例生成约 79 个变体。
    8080
  3. |T′_i| 增强后数量用例总数膨胀到原来的约 80 倍,蒙混过关的难度陡增。
    ∣Ti′∣|T'_i|
  4. 评测意义每道题的“品尝点”从几个变成几百个,分数的区分度和可信度都显著上升。
    ∣Ti′∣≈80×∣Ti∣|T'_i| \approx 80 \times |T_i|

AI 应用场景

代码模型发版前的回归评测

阶段:模型评测与迭代

解决的问题:训练大语言模型时,每隔几天就产生一个新 checkpoint,如何快速判断代码能力是进步了还是退步了?传统做法依赖人工写样例测试,既慢又主观;AI 评测工程师把 HumanEval 的 164 道题接入自动化流水线,每次训练完自动跑一遍测试用例,分数变化一目了然。

具体案例:某 AI 编程助手团队在发布新模型前,流水线自动跑完 164 道题,pass@1 从 0.72 提升到 0.78,才获准上线;一次实验性训练导致 pass@1 跌到 0.70,评测系统立即报警,团队回滚了该版本。这就是“味觉考官”在日常研发中站岗放哨的样子。

提示词方案的“厨艺比拼”

阶段:提示词工程与模型选型

解决的问题:同一个模型,用不同的提示词模板生成代码,效果差异很大。如何客观比较哪套提示词更好?把关键测试用例当作裁判,让不同提示词方案生成的代码分别“过堂”,用 pass@1 和 pass@k 说话。

具体案例:一位提示词工程师为内部代码生成工具设计了两版提示词,一版强调“先写伪代码再翻译”,另一版直接要求“输出完整函数”。他抽取 30 道 HumanEval 题做对比评测,发现第一版 pass@1 高出 11 个百分点,于是果断切换方案。测试用例成了两套方案之间最公正的仲裁者。

企业私有代码助手的基线考核

阶段:应用部署与质量保障

解决的问题:企业内部要部署一个懂私有代码库的编程助手,选型时如何筛选?光看厂商自报的指标不够,需要用统一的“考卷”横向对比不同模型。HumanEval 测试用例因其题目原创、答案保密,成为最常用的第三方标尺。

具体案例:某金融科技公司在选型时,用 HumanEval 测试集对三款大模型各采样 20 次,分别计算 pass@1 与 pass@5,还叠加了 5 道内部定制的边界用例。最终选定的一款模型虽然 pass@1 不是最高,但边界用例全过、运行耗时最短,上线后代码建议采纳率提升了 20%。

Python 演示

python66 行
from math import comb # HumanEval 风格的问题集: 每个问题包含函数名与配套测试用例PROBLEMS = [    {"name": "sum_list", "tests": [([1, 2, 3], 6), ([], 0), ([-2, 2, 5], 5)]},    {"name": "find_max", "tests": [([1, 9, 3], 9), ([-1, -5], -1), ([4], 4)]},] # 模拟 LLM 采样生成的候选代码, 真实场景由模型多次采样得到SAMPLED = {    "sum_list": [        "def solve(nums):\n    return sum(nums)",        "def solve(nums):\n    t = 0\n    for x in nums:\n        t += x\n    return t",        "def solve(nums):\n    return sorted(nums)[0] if nums else None",        "def solve(nums):\n    return sum(nums) + 1",        "def solve(nums):\n    return max(nums)",    ],    "find_max": [        "def solve(nums):\n    return max(nums)",        "def solve(nums):\n    m = nums[0]\n    for x in nums:\n        if x > m:\n            m = x\n    return m",        "def solve(nums):\n    return sorted(nums)[-1]",        "def solve(nums):\n    return min(nums)",        "def solve(nums):\n    return nums[0]",    ],}  def run_tests(code, tests):    """在隔离命名空间中执行候选代码, 逐个用例断言"""    ns = {}    try:        exec(code, ns)    except Exception:        return False    for data, expected in tests:        try:            if ns["solve"](data) != expected:                return False        except Exception:            return False    return True  def pass_at_k(n, c, k):    """无偏 pass@k: n 次采样 c 次全对, 任取 k 份至少一份全对的概率"""    if n - c < k:        return 1.0    return 1.0 - comb(n - c, k) / comb(n, k)  def main():    n = 5  # 每个问题采样 n 份候选代码    p1 = 0.0    pk = 0.0    for p in PROBLEMS:        codes = SAMPLED[p["name"]]        c = sum(1 for code in codes if run_tests(code, p["tests"]))        p1 += c / n        pk += pass_at_k(n, c, 2)        print("problem %s: %d/%d candidates passed" % (p["name"], c, n))    print("pass@1 = %.2f" % (p1 / len(PROBLEMS)))    print("pass@2 = %.2f" % (pk / len(PROBLEMS)))  if __name__ == "__main__":    main()
目的:实现一个迷你版 HumanEval 评测器——用测试用例逐份验证候选代码,并用无偏公式计算 pass@1 与 pass@2。
  1. def run_tests(code, tests):把一段候选代码送进“考场”:先编译执行,再逐条测试用例断言。
  2. exec(code, ns)在独立命名空间里执行代码,模拟真实评测中的环境隔离。
  3. if ns["solve"](data) != expected:这就是“味道比对”:实际输出与期望输出不一致,立即判失败。
  4. from math import comb导入组合数函数 C(n, k),这是无偏估计的核心工具。
  5. def pass_at_k(n, c, k):实现无偏 pass@k 估计器,避免“取最优答案”带来的虚高分。
  6. if n - c < k:当错误答案数少于 k 时,任意抽取 k 份必然至少含一份正确答案,概率为 1。
  7. p1 += c / npass@1 的朴素估计:随机抽一份恰好是全对的概率。
  8. pk += pass_at_k(n, c, 2)用无偏公式累加每题的 pass@2,最后取平均。

常见误区

误区我觉得只要模型生成的代码把测试用例全跑通了,这段代码就是 100% 正确的,可以直接放心用。

正解测试用例只能证明“你尝过的那几口没问题”,不能证明整道菜无懈可击。软件工程里有一句名言:测试只能证明缺陷存在,不能证明缺陷不存在。HumanEval 每道题只有 5 到 10 个用例,它们覆盖了常见情况,却可能在某个你没尝到的角落藏着 bug。这也是 EvalPlus 要把用例扩增 80 倍的原因——用更多的“品尝点”逼近正确性,但永远无法抵达绝对正确。

为何常见:因为测试用例的判定机制是“全对即通过”,给人一种“通过=正确”的直观错觉。人们容易忽略一个事实:用例集本身是有限的、不完整的,它只是真实世界的抽样。

误区我在报告里看到 pass@5 比 pass@1 高很多,就认为这个模型“写代码很厉害”。

正解pass@k 允许 k 次机会取最好的一次,它衡量的是“模型能力的上限”——只要抽样次数足够多,总有一次能蒙对。pass@1 才是“一次成功”的真实水平。比如一个模型 pass@1 只有 0.2,但 pass@100 接近 1.0,说明它并非不会写,而是“不稳定”。这种模型在实际使用中价值有限,因为用户不会容忍你生成 100 遍才给一个对的答案。两个指标要配合着看,不能只看大的那个。

为何常见:因为 k 越大分数越高,人的攀比心理天然喜欢“更好看”的数字。而且很多人没意识到“取最好”和“只给一次机会”之间的本质区别,把检索能力的上限误当成了生成能力的常态。

误区某模型在 HumanEval 上拿了 85 分,我觉得它可以直接帮我写生产环境的代码了。

正解HumanEval 的 164 道题都是独立的函数级算法题,不涉及真实仓库里的类继承、模块依赖、异步调用、异常处理、版本兼容等复杂工程问题。一个在 HumanEval 上表现优异的模型,面对真实项目时可能连“读懂现有代码风格”都做不到。分数高只能说明它基础算法扎实,不能推导出工程能力强。把它当作“基础门槛”而非“能力天花板”,才是最理性的使用姿势。

为何常见:因为 HumanEval 是公开报道里最常见的代码评测基准,媒体和厂商反复引用,容易让人对它产生“权威=全面”的错觉,忽略了它覆盖范围的先天局限。

误区我生成 100 份答案,只要有一份通过测试,就宣布“模型通过了这道题”,然后报一个很高的 pass@100。

正解这样做是“开卷作弊”,高估了模型的真实表现。真实场景中你只会提交一份答案,如果这一份恰好是错的,前面的 99 次成功毫无意义。HumanEval 的无偏估计公式正是为了修正这种虚高:它从 n 份答案中“随机抽取 k 份”而不是“精心挑选最好的 k 份”,算出来的才是诚实的通过率。如果你真想用大 k 的分数,必须坦诚说明这是“多次机会下的成功率”,并搭配 pass@1 一起报告。

为何常见:因为“取最优”在工程上是成本最低的做法——反正都生成了,挑最好的交卷不就是人之常情吗?但评测的公平性恰恰要求我们用“模拟真实使用方式”的规则,随机抽取才不会给模型开绿灯。

面试题

Q1. HumanEval 基准共包含多少道手写编程题?

单选题 · easy

  • A.64 道经典算法题
  • B.164 道手写原创题← 正确答案
  • C.1000 道自动爬取题
  • D.500 道竞赛压轴题

答案:B。HumanEval 由 OpenAI 研究者手工编写 164 道题,刻意避开互联网公开内容以防止模型“背答案”。64 是混淆项,1000 和 500 都远超实际数量。记住"164"是最核心的数字锚点。

Q2. 一个测试用例最基本的两个要素是什么?

单选题 · easy

  • A.函数名称和注释文本
  • B.输入数据与期望输出← 正确答案
  • C.超时阈值与内存上限
  • D.代码风格与缩进规范

答案:B。测试用例的本质就是“喂什么、该吐出什么”:输入 x 送进函数,再断言输出等于期望 y。函数名、超时、代码风格都是外围信息,不是用例的本体。就像味觉考官关注的是“尝哪一口”和“该是什么味”。

Q3. pass@k 中的参数 k 表示什么?

单选题 · mid

  • A.测试用例的总数量
  • B.模型参数的规模大小
  • C.允许提交答案的机会次数← 正确答案
  • D.数据集中题目的数量

答案:C。k 代表“给模型几次机会”:从 n 份采样中抽 k 份提交,只要至少一份全部通过测试即算成功。k 越大机会越多,分数通常越高。它跟测试用例数量、参数规模、题目数量都无关。

Q4. 为什么 HumanEval 的 pass@k 需要用无偏估计而不是直接取最好结果?

单选题 · mid

  • A.因为测试用例本身可能存在错误
  • B.因为直接取最好结果会高估模型能力← 正确答案
  • C.因为模型生成代码的速度太慢
  • D.因为题目数量超过一万不便于穷举

答案:B。如果生成 n 份答案后“挑最好的报”,相当于开了 k=n 的绿灯,分数必然虚高。无偏估计模拟“随机抽取 k 份”的真实使用方式,用组合数剔除侥幸成分。这就像考试不允许你写十遍答案再挑一份上交。

Q5. 边界测试用例主要用于发现哪类问题?

单选题 · mid

  • A.代码缩进和格式不规范
  • B.空输入、极值等特殊输入的缺陷← 正确答案
  • C.函数命名不符合命名规范
  • D.文档字符串中的拼写错误

答案:B。边界用例专攻“危险地带”:空列表、单元素、负数、极大极小值。程序在常规输入下表现良好,往往在边界处崩溃或算错。这就像考官专尝菜盘边缘烧焦的部分,专抓藏在帮子里的问题。

Q6. 执行测试用例时,为什么要在隔离环境中运行被测代码?

单选题 · mid

  • A.让模型生成代码的速度更快
  • B.防止命名空间污染和意外副作用← 正确答案
  • C.使测试用例数量自动增加
  • D.便于考官直接修改测试数据

答案:B。模型生成的代码不可控,可能定义全局变量、读写临时文件、甚至死循环。隔离环境(沙箱)确保每次运行互不干扰、可复现,超时机制防止死循环拖垮评测。这就像每位学徒用独立灶台,油烟不串味。

Q7. 某问题采样 n=5 份答案,其中 c=2 份全部通过测试,则 pass@5 的值是多少?

单选题 · hard

  • A.0.4
  • B.1.0← 正确答案
  • C.0.8
  • D.0.5

答案:B。pass@5 意味着“5 份答案全部提交”,只要其中至少一份正确(c≥1),必然通过,所以是 1.0。代入公式:1 − C(3,5)/C(5,5) = 1 − 0/1 = 1.0。0.4 是 c/n 的结果,那是 pass@1 的朴素估计。k=n 时分数恒为 1(只要有正确解)。

Q8. 下列哪些属于测试用例设计的常用技巧?

多选题 · mid

  • A.功能用例验证常规输入← 正确答案
  • B.边界用例覆盖空值与极值← 正确答案
  • C.随机用例增强覆盖范围← 正确答案
  • D.删除用例加快评测速度

答案:ABC。功能、边界、随机三类用例分别对应“常规、极端、盲测”,是设计用例集的核心手法。删除用例只会降低漏检门槛,与评测目标背道而驰。用例设计的本质是用有限品尝次数覆盖最大风险空间。

Q9. HumanEval 基准存在哪些公认的不足?

多选题 · hard

  • A.题目数量少,统计波动大← 正确答案
  • B.难度分布偏向基础与中等← 正确答案
  • C.存在预训练数据污染风险← 正确答案
  • D.每题测试用例超过数百个

答案:ABC。164 题的样本量带来不小的标准误;难度偏易导致分数通胀;预训练语料膨胀带来污染隐患。D 说反了——原始 HumanEval 每题只有 5 到 10 个用例,数百个是 EvalPlus 扩增后的效果。

Q10. 关于测试用例的“断言”机制,下列说法正确的是?

单选题 · hard

  • A.断言通过即可证明代码无任何缺陷
  • B.断言是将实际输出与期望输出进行比对← 正确答案
  • C.断言主要用于提高代码的运行速度
  • D.断言结果只能由人工进行主观判断

答案:B。断言是自动化的“味道比对”:把 f(x) 与期望 y 用等号判断,相等通过、不等失败。它既不能证明无缺陷(测试只能证伪不能证真),也不涉及提速或人工。断言的价值在于客观、可重复、可大规模自动执行。