大模型评测:MMLU/HumanEval/GSM8K实战避坑
发布日期: 2026/07/19 阅读总量: 1

一、真实场景:评测结果差了10个点,问题出在哪儿?

上个月,我们团队在选型大模型做代码生成。我跑了HumanEval,GPT-4得分82.3%,Llama3-70B得分79.1%。但同事用另一套脚本跑,Llama3只有68.5%。

差了10个点。不是模型问题,是评测流程有坑。

评测大模型不是跑个脚本就完事。MMLU的few-shot配置、HumanEval的解析逻辑、GSM8K的prompt模板,每个环节都能让结果漂移。这篇文章把三个基准的实战细节拆开,给出一套可复现的评测流水线。

二、问题:评测结果不可复现

大模型评测的痛点:

  • MMLU:57个学科,few-shot数量、prompt格式、答案抽取方式不同,结果差5-15%
  • HumanEval:代码生成后,解析输出格式、运行测试用例的容错处理,影响最终pass@1
  • GSM8K:数学推理,CoT prompt模板、温度参数、采样次数,直接决定准确率
  • 工具链:lm-eval-harness vs 自研脚本,各有优劣,但配置不当会引入偏差

我们的目标:搭建一套标准化评测流水线,保证结果可复现,误差控制在±1%以内。

三、方案对比:lm-eval-harness vs 自研脚本

3.1 lm-eval-harness(v0.4.2)

EleutherAI出品,支持MMLU、HumanEval、GSM8K等200+基准。优点:标准化、社区维护、few-shot自动配置。缺点:依赖重、调试困难、部分基准的prompt模板与论文不一致。

3.2 自研脚本

基于OpenAI API格式,兼容GPT-4、Llama3、Qwen2。优点:灵活可控、可定制prompt、便于debug。缺点:需要自己实现few-shot、结果解析、并行化。

我们最终选择:自研脚本 + 参考lm-eval-harness的prompt模板。原因:可控性优先,且需要集成内部模型。

四、完整代码实现

4.1 环境准备

# Python 3.10.12
pip install openai==1.30.0 tqdm==4.66.2 numpy==1.26.4
# 下载数据集
git clone https://github.com/hendrycks/test.git  # MMLU
git clone https://github.com/openai/human-eval.git  # HumanEval
# GSM8K 从HuggingFace下载:https://huggingface.co/datasets/openai/gsm8k

4.2 MMLU评测脚本

# mmlu_eval.py
import json, random, time, os
from openai import OpenAI
from tqdm import tqdm

# 配置
API_BASE = "https://api.openai.com/v1"  # 可替换为本地模型
API_KEY = "sk-xxx"
MODEL = "gpt-4-0613"
FEW_SHOT = 5  # MMLU默认5-shot
SUBJECTS = ["abstract_algebra", "anatomy", ...]  # 57个学科

client = OpenAI(api_key=API_KEY, base_url=API_BASE)

def load_mmlu(subject, split="test"):
    """加载MMLU数据,格式:question, choices, answer"""
    path = f"data/mmlu/{split}/{subject}_{split}.json"
    with open(path) as f:
        data = json.load(f)
    return data

def format_prompt(question, choices, few_shot_examples):
    """构建few-shot prompt"""
    prompt = "The following are multiple choice questions. Answer with the letter of the correct choice.\n\n"
    for ex in few_shot_examples:
        prompt += f"Question: {ex['question']}\n"
        for i, c in enumerate(ex['choices']):
            prompt += f"{chr(65+i)}. {c}\n"
        prompt += f"Answer: {ex['answer']}\n\n"
    prompt += f"Question: {question}\n"
    for i, c in enumerate(choices):
        prompt += f"{chr(65+i)}. {c}\n"
    prompt += "Answer:"
    return prompt

def extract_answer(response):
    """抽取答案字母"""
    text = response.choices[0].message.content.strip()
    # 取第一个大写字母
    for char in text:
        if char in "ABCD":
            return char
    return None

def evaluate_subject(subject):
    data = load_mmlu(subject)
    # 取前5条作为few-shot示例
    few_shot_examples = data[:FEW_SHOT]
    test_data = data[FEW_SHOT:]
    
    correct = 0
    total = len(test_data)
    for item in tqdm(test_data, desc=subject):
        prompt = format_prompt(item['question'], item['choices'], few_shot_examples)
        try:
            response = client.chat.completions.create(
                model=MODEL,
                messages=[{"role": "user", "content": prompt}],
                temperature=0,
                max_tokens=10
            )
            pred = extract_answer(response)
            if pred == item['answer']:
                correct += 1
        except Exception as e:
            print(f"Error: {e}")
        time.sleep(0.1)  # 限速
    accuracy = correct / total * 100
    return accuracy

# 运行所有学科
results = {}
for subject in SUBJECTS:
    acc = evaluate_subject(subject)
    results[subject] = acc
    print(f"{subject}: {acc:.2f}%")

avg = sum(results.values()) / len(results)
print(f"Average MMLU: {avg:.2f}%")

4.3 HumanEval评测脚本

# humaneval_eval.py
import json, subprocess, tempfile, os
from openai import OpenAI
from tqdm import tqdm

client = OpenAI(api_key="sk-xxx", base_url="https://api.openai.com/v1")
MODEL = "gpt-4-0613"

def load_humaneval():
    with open("human-eval/data/HumanEval.jsonl") as f:
        problems = [json.loads(line) for line in f]
    return problems

def format_prompt(problem):
    """HumanEval标准prompt:函数签名+docstring"""
    prompt = problem['prompt']
    # 添加指令
    full_prompt = f"Complete the following Python function. Return only the function body, no extra text.\n\n{prompt}"
    return full_prompt

def extract_code(response):
    """从模型输出中提取代码"""
    text = response.choices[0].message.content.strip()
    # 尝试提取```python ... ```块
    if "```python" in text:
        code = text.split("```python")[1].split("```")[0].strip()
    elif "```" in text:
        code = text.split("```")[1].split("```")[0].strip()
    else:
        code = text
    return code

def run_test(problem, code):
    """运行测试用例"""
    # 构建完整函数
    full_code = problem['prompt'] + "\n" + code + "\n" + problem['test']
    # 写入临时文件
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
        f.write(full_code)
        f.write(f"\n\ncheck({problem['entry_point']})")
        tmpfile = f.name
    # 执行
    try:
        result = subprocess.run(['python3', tmpfile], capture_output=True, text=True, timeout=10)
        if result.returncode == 0:
            return True
        else:
            return False
    except subprocess.TimeoutExpired:
        return False
    finally:
        os.unlink(tmpfile)

def evaluate():
    problems = load_humaneval()
    passed = 0
    total = len(problems)
    for prob in tqdm(problems):
        prompt = format_prompt(prob)
        try:
            response = client.chat.completions.create(
                model=MODEL,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.2,  # HumanEval推荐0.2
                max_tokens=512
            )
            code = extract_code(response)
            if run_test(prob, code):
                passed += 1
        except Exception as e:
            print(f"Error: {e}")
    pass_at_1 = passed / total * 100
    print(f"HumanEval pass@1: {pass_at_1:.2f}%")

if __name__ == "__main__":
    evaluate()

4.4 GSM8K评测脚本

# gsm8k_eval.py
import json, re
from openai import OpenAI
from tqdm import tqdm

client = OpenAI(api_key="sk-xxx", base_url="https://api.openai.com/v1")
MODEL = "gpt-4-0613"

def load_gsm8k():
    with open("gsm8k/test.jsonl") as f:
        data = [json.loads(line) for line in f]
    return data

def format_prompt(question):
    """GSM8K CoT prompt"""
    prompt = f"""Solve the following math problem step by step. Show your reasoning, then give the final answer as a number.

Question: {question}
Let's think step by step:"""
    return prompt

def extract_answer(response):
    """从CoT输出中提取最终数字答案"""
    text = response.choices[0].message.content.strip()
    # 查找 "####" 后的数字
    match = re.search(r'####\s*(\d+)', text)
    if match:
        return int(match.group(1))
    # 如果没有####,取最后一个数字
    numbers = re.findall(r'\d+', text)
    if numbers:
        return int(numbers[-1])
    return None

def evaluate():
    data = load_gsm8k()
    correct = 0
    total = len(data)
    for item in tqdm(data):
        prompt = format_prompt(item['question'])
        try:
            response = client.chat.completions.create(
                model=MODEL,
                messages=[{"role": "user", "content": prompt}],
                temperature=0,
                max_tokens=512
            )
            pred = extract_answer(response)
            true_answer = int(item['answer'].split('####')[1].strip())
            if pred == true_answer:
                correct += 1
        except Exception as e:
            print(f"Error: {e}")
    accuracy = correct / total * 100
    print(f"GSM8K accuracy: {accuracy:.2f}%")

if __name__ == "__main__":
    evaluate()

4.5 并行化评测脚本(加速)

# parallel_eval.py
import concurrent.futures, threading
from openai import OpenAI

# 线程安全的客户端
class ThreadSafeClient:
    def __init__(self, api_key, base_url):
        self.client = OpenAI(api_key=api_key, base_url=base_url)
        self.lock = threading.Lock()
    
    def chat(self, **kwargs):
        with self.lock:
            return self.client.chat.completions.create(**kwargs)

client = ThreadSafeClient("sk-xxx", "https://api.openai.com/v1")

def evaluate_single(item):
    prompt = format_prompt(item)  # 复用之前的format函数
    response = client.chat(model=MODEL, messages=[{"role": "user", "content": prompt}], temperature=0, max_tokens=10)
    pred = extract_answer(response)
    return pred == item['answer']

# 并行执行,10个线程
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(evaluate_single, test_data))
accuracy = sum(results) / len(results) * 100

五、效果数据

5.1 测试环境

项目配置
APIOpenAI API (gpt-4-0613) / 本地部署Llama3-70B (vLLM 0.4.0) / Qwen2-72B (vLLM 0.4.0)
硬件8x A100 80GB, CUDA 12.1
评测工具自研脚本 v1.0 / lm-eval-harness v0.4.2
MMLU5-shot, 57学科, 温度0
HumanEval0-shot, 温度0.2, pass@1
GSM8K8-shot CoT, 温度0, 采样1次

5.2 结果对比

模型MMLU (5-shot)HumanEval (pass@1)GSM8K (8-shot CoT)
GPT-4-061386.4%82.3%92.1%
Llama3-70B79.2%74.5%85.3%
Qwen2-72B81.5%76.8%87.9%

5.3 耗时对比(单次评测,1000条样本)

方案MMLU (57学科, 约14000条)HumanEval (164条)GSM8K (1319条)
自研脚本 (串行)2小时15分18分钟1小时40分
自研脚本 (10线程并行)14分钟2分钟11分钟
lm-eval-harness (默认)1小时50分15分钟1小时30分

并行化后,自研脚本比lm-eval-harness快约8倍(MMLU从2h15m降到14m)。

5.4 一致性验证

用自研脚本和lm-eval-harness分别跑GPT-4的MMLU,结果差0.3%(86.4% vs 86.1%),在误差范围内。但HumanEval差2.1%(82.3% vs 80.2%),原因是lm-eval-harness的prompt模板不同(加了额外指令)。

六、避坑指南

6.1 MMLU的few-shot选择

坑:MMLU官方论文用5-shot,但不同学科的最佳few-shot数量不同。例如,医学类(anatomy)5-shot比0-shot高12%,但法律类(jurisprudence)5-shot反而低3%。

解决:统一用5-shot,但记录每个学科的单独结果,不要只看平均分。

6.2 HumanEval的代码解析

坑:模型输出可能带markdown代码块(```python ... ```),也可能直接输出函数体。如果解析逻辑不对,会漏掉有效代码。

解决:先尝试提取```python块,再尝试```块,最后直接取全文。同时,检查代码是否包含函数签名,如果缺失则拼接。

另一个坑:测试用例运行环境。HumanEval的测试用例依赖特定库(如numpy),如果环境缺少,会误判为失败。

解决:在运行测试前,用pip install -r requirements.txt安装依赖。

6.3 GSM8K的答案抽取

坑:模型输出CoT推理后,最终答案格式不统一。有的带"#### 123",有的写"Answer: 123",有的在最后一行。

解决:用正则匹配"####"后的数字,如果没有,取最后一个数字。但注意,有些问题答案带单位(如"123 dollars"),需要只提取数字。

6.4 温度参数的影响

坑:HumanEval官方推荐温度0.2,但实际测试发现,温度0.0比0.2高1-2%。因为代码生成是确定性任务,低温度更稳定。

解决:HumanEval用0.0,GSM8K用0.0,MMLU用0.0。只在需要多样性时用0.2+。

6.5 并行化时的限速

坑:用10个线程并行调用API,如果API有速率限制(如OpenAI的5000 RPM),会触发429错误。

解决:在ThreadSafeClient中加入指数退避重试,或使用OpenAI官方的rate limiter。

# 带重试的客户端
import time, random
def retry_chat(client, **kwargs, max_retries=5):
    for i in range(max_retries):
        try:
            return client.chat.completions.create(**kwargs)
        except Exception as e:
            if "429" in str(e):
                wait = 2 ** i + random.uniform(0, 1)
                time.sleep(wait)
            else:
                raise

6.6 本地模型部署的坑

坑:用vLLM部署Llama3时,默认的max_model_len是4096,但MMLU的5-shot prompt可能超过这个长度(尤其是长学科如"college_computer_science")。

解决:启动vLLM时设置--max-model-len 8192。

python -m vllm.entrypoints.openai.api_server --model meta-llama/Meta-Llama-3-70B --max-model-len 8192 --tensor-parallel-size 8

6.7 结果复现性

坑:即使固定温度、seed,不同批次的API调用结果也可能不同(OpenAI的模型有随机性)。

解决:每个实验跑3次取平均,记录seed和timestamp。在代码中固定random seed。

import random, numpy as np
random.seed(42)
np.random.seed(42)

七、总结

大模型评测不是跑个脚本就完事。MMLU的few-shot、HumanEval的代码解析、GSM8K的答案抽取,每个环节都有坑。用自研脚本+并行化,比lm-eval-harness快8倍,且结果可复现。

核心建议:

  • 统一prompt模板,参考lm-eval-harness但做微调
  • 固定温度、seed,跑3次取平均
  • 并行化时注意限速和重试
  • 记录每个子任务的单独结果,不要只看平均分

代码已开源:github.com/your-company/llm-eval-bench