代码生成模型的评测方法:从pass@k到功能正确性的多维评估体系
一、代码生成为何需要独立的评测范式
代码生成模型的评测与自然语言生成有本质区别。在文本生成中,"语义相似但措辞不同"是可接受的输出变体,BLEU和ROUGE等n-gram重叠指标被广泛使用(尽管其局限性已被充分讨论)。但在代码生成中,一个return语句中的变量名错误会导致整个函数失败——语义上的"近似正确"等同于"完全错误"。
这一特性催生了代码生成场景特有的评测指标:pass@k(Chen et al., 2021)。它不评估单次生成的代码与参考答案的表面相似度,而是评估模型在k次生成尝试中能否至少产生一个通过所有单元测试的样本。这一范式转变将评测从"生成文本vs参考文本"的比较转换为"生成代码的功能行为vs期望行为"的验证——本质上是一种基于测试的正确性度量。
二、pass@k的无偏估计与实现细节
pass@k的朴素定义是在k个生成样本中至少有一个通过测试的比例。直接估计方法(生成k个样本,检查是否有至少一个通过)需要大量的生成预算——如果pass@1=0.01,需要约100次生成才能观测到一个通过样本。
Chen等人提出了一个更高效的无偏估计器:生成n个样本(n ≥ k),统计其中通过测试的样本数为c,然后计算:
$$pass@k = 1 - \frac{\binom{n-c}{k}}{\binom{n}{k}}$$
这一估计器的精妙之处在于:它不需要为每个问题生成恰好k个样本——生成n个样本后,可以计算所有k ≤ n的pass@k值。这意味着一次评测运行可以同时报告pass@1、pass@10、pass@100,大幅降低评测计算成本。
""" pass@k的无偏估计器实现与数值稳定性优化 """ import math import numpy as np from typing import List def estimate_pass_at_k( num_samples: int, # n: 为每个问题生成的样本总数 num_correct: int, # c: 通过测试的样本数 k: int # 评估的k值 ) -> float: """计算pass@k的无偏估计。 公式: pass@k = 1 - C(n-c, k) / C(n, k) if n-c >= k else 1.0 使用对数空间计算二项式系数以避免数值溢出。 因为C(n, k)在n=200, k=100时可达10^59量级, 浮点数直接计算会溢出。 Args: num_samples: 为每个问题生成的样本数(n ≥ k) num_correct: 通过测试的样本数 k: pass@k中的k值 Returns: float: pass@k的无偏估计值 Raises: ValueError: 如果 n < k 或 num_correct > num_samples """ if num_samples < k: raise ValueError(f"n ({num_samples}) 必须 ≥ k ({k})") if num_correct > num_samples: raise ValueError(f"c ({num_correct}) 不能大于 n ({num_samples})") # 如果正确样本数 + k > 总样本数,则有很高的概率至少一个正确 # 公式中 C(n-c, k) = 0 当 n-c < k if num_samples - num_correct < k: return 1.0 # 在对数空间中计算组合数以避免溢出 # log(C(n, k)) = log(n!) - log(k!) - log((n-k)!) def log_comb(n_val: int, k_val: int) -> float: """对数空间计算组合数 C(n, k)""" if k_val < 0 or k_val > n_val: return float("-inf") # 使用 math.lgamma: log-Gamma函数,log(n!) = lgamma(n+1) return (math.lgamma(n_val + 1) - math.lgamma(k_val + 1) - math.lgamma(n_val - k_val + 1)) # log(pass@k) = log(1 - exp(log(C(n-c,k)) - log(C(n,k)))) log_numer = log_comb(num_samples - num_correct, k) log_denom = log_comb(num_samples, k) # 防止数值问题:如果分子远小于分母,比值≈0 if log_numer - log_denom < -50: # exp(-50) ≈ 1.9e-22 return 1.0 ratio = math.exp(log_numer - log_denom) return 1.0 - ratio def compute_pass_at_k_metrics( per_problem_results: List[dict], # [{"n": 200, "c": 45}, ...] k_values: List[int] = [1, 10, 100] ) -> dict: """在所有问题上聚合计算pass@k指标。 对每个问题独立计算pass@k,然后取平均值。 这种"先per-problem再average"的方式避免了 简单问题和困难问题之间的样本数量失衡问题。 Args: per_problem_results: 每个问题的 {"n": 总样本数, "c": 正确数} k_values: 要计算的k值列表 Returns: dict: {f"pass@{k}": 平均值, f"pass@{k}_per_problem": [...]} """ num_problems = len(per_problem_results) metrics = {} for k in k_values: pass_at_k_values = [] for result in per_problem_results: n = result["n"] c = result["c"] if n >= k: p = estimate_pass_at_k(n, c, k) pass_at_k_values.append(p) else: # n < k:这个问题的样本数不足以估计pass@k # 通常应设置 n ≥ max(k_values) pass_at_k_values.append(float("nan")) # 计算平均值(忽略nan) valid_values = [v for v in pass_at_k_values if not math.isnan(v)] metrics[f"pass@{k}"] = np.mean(valid_values) if valid_values else float("nan") metrics[f"pass@{k}_per_problem"] = pass_at_k_values return metrics # 使用示例 # results = [ # {"n": 200, "c": 150}, # 问题1:75%通过率 # {"n": 200, "c": 45}, # 问题2:22.5%通过率 # {"n": 200, "c": 3}, # 问题3:1.5%通过率(困难问题) # ] # metrics = compute_pass_at_k_metrics(results, k_values=[1, 10, 100]) # for k in [1, 10, 100]: # print(f"pass@{k}: {metrics[f'pass@{k}']:.4f}")三、pass@k的局限性与功能正确性的补充维度
pass@k虽然已经成为代码生成评测的事实标准,但它存在几个已知盲区。第一,pass@k对"简单问题+低k"和"困难问题+高k"没有区分对错的性质差异——两者都体现在同一个数字中,但前者反映的是模型在简单任务上的确定性,后者反映的是在困难任务上碰运气的概率。
第二,pass@k不惩罚生成过于低效但功能正确的代码。一个冒泡排序实现可以通过测试,但O(n²)的复杂度在处理大数据时不可接受。传统的单元测试通常只检查功能正确性,不检查时间和空间复杂度。
第三,pass@k不验证代码的安全性和鲁棒性。生成代码中包含SQL注入漏洞、路径遍历风险或除零错误时,只要单元测试没有覆盖这些边界情况,问题就不会被检测到。
为弥补这些盲区,出现了几个补充性的评测维度:Execution Match(不仅要求测试通过,还要求执行输出与参考实现完全一致——更严格但有时过于严格)、Code Quality Metrics(使用静态分析工具检测风格问题和潜在bug)、Security Scan(使用Bandit、Semgrep等工具检测常见安全漏洞模式)。
四、构建多维度评测的工程管线
将以上维度整合到一个统一的评测管线中,需要解决几个工程问题:
评测隔离:每个生成样本应在隔离的环境中执行(Docker容器或沙箱化进程),防止恶意代码(如os.system("rm -rf /"))影响评测基础设施。
超时管理:每个测试用例应设置独立的超时限制(如5秒)。一个无限循环的生成代码不应阻塞整个评测管线。
公平对比:不同模型的pass@k应在相同的n和相同的温度参数下计算。温度越高(越随机),pass@1越低但pass@100越高——如果不同模型使用不同的温度,评测结果不可比。
五、总结
代码生成模型的评测正在从"表面匹配"(BLEU)向"功能正确性"(pass@k)范式迁移。pass@k的无偏估计器通过一次生成n个样本(n≥k)来计算所有k值的pass@k,在统计效率和计算成本之间取得了良好平衡。但pass@k不应被视为完整的评测方案——它检验的是"代码能否工作",而非"代码是否高效、安全、可维护"。一个完整的代码生成评测体系应当在功能正确性的基础上,补充执行效率(复杂度分析)、代码质量(静态分析)和安全性(漏洞扫描)三个维度。评测管线的工程实现中,隔离执行、超时管理和公平对比是三个不可妥协的基础要求——没有这些保障,评测结果的可信度是无从谈起的。