MMLU·HumanEval 점수, 이제 그대로 믿으면 안 된다: 2026년 8월 기준 LLM 벤치마크 해석과 실무 적용 가이드
들어가며
LLM을 프로젝트에 붙일 때 “어떤 모델이 더 좋나?”를 빠르게 결정하려면 결국 평가(evaluation) 가 필요합니다. 문제는 2026년 현재, MMLU/HumanEval 같은 고전 벤치마크 점수는 그 자체로 제품 성능을 대표하지 못하는 경우가 점점 늘었다는 점입니다. 이유는 크게 세 가지입니다.
- 데이터셋 결함/정답 오류: MMLU는 정답 오류가 유의미하게 존재한다는 대규모 수작업 분석이 있었고, 그 결과물이 MMLU-Redux로 이어졌습니다. (arxiv.org)
- 벤치마크 “오염(contamination)”과 과적합: 모델이 학습 과정에서 문제/해설을 “봤을” 가능성이 커질수록 점수는 더 이상 일반화 성능을 의미하지 않습니다. 코드 벤치마크는 특히 취약해서, HumanEval을 더 빡센 테스트로 확장한 HumanEval+(EvalPlus)가 널리 인용됩니다. (arxiv.org)
- 평가 구현체/프롬프트/런타임 차이로 점수가 흔들림: MMLU-Pro처럼 프롬프트 민감도를 낮추려는 시도도 있지만, 여전히 “어떤 구현으로 돌렸는지”가 점수를 좌우합니다. (arxiv.org)
언제 쓰면 좋나?
- 모델 후보군을 1차로 좁힐 때(cheap filter): “완전 탈락” 모델 걸러내기
- 동일 조건으로 반복 측정 가능한 사내 리그(내 파이프라인) 구축할 때
언제 쓰면 안 되나?
- MMLU/HumanEval 리더보드 숫자만 보고 “우리 서비스에 적합” 판단
- 프롬프트/샘플링/코드 실행 환경이 다른데 점수를 직접 비교하는 것
- 제품 요구(툴 사용, 장기 컨텍스트, 에이전트 워크플로우)가 있는데 단일 샷 QA/코딩 문제로 대표시키는 것
🔧 핵심 개념
1) MMLU: “지식+추론”처럼 보이지만, 사실은 “MCQ 채점 파이프라인”이다
MMLU는 57개 과목의 multiple-choice 문제에서 Exact Match(정답 선택) 로 채점합니다. 구현체 관점에서 핵심은 다음 흐름입니다.
- (입력)
question + options(A/B/C/D...) - (프롬프트) zero-shot 또는 few-shot 컨텍스트 구성
- (모델 출력) 선택지 토큰(예: “C”) 또는 해설+선택
- (정규화) 출력에서 최종 선택지 추출
- (스코어) 정답 레이블과 비교
이 구조의 함정:
- 모델이 “정답을 아는데” 출력 파싱이 실패하면 0점
- 반대로, 운 좋게 찍어 맞추면 1점
- 정답 레이블 자체가 틀리면, 모델이 맞는 답을 해도 0점
→ 실제로 MMLU는 정답/문항 품질 이슈가 지적되었고, 이를 정제/재주석한 MMLU-Redux가 제안되었습니다. (arxiv.org)
2) MMLU-Pro / Redux: “난이도/신뢰도”를 올리려는 갈래
- MMLU-Redux: 원본 MMLU의 오류를 체계적으로 분류/주석해 “점수의 신뢰도”를 올리려는 접근 (arxiv.org)
- MMLU-Pro: 더 어려운 문제·더 많은 보기(10-way) 등으로 “찍기 운”을 줄이고, 프롬프트 민감도를 낮추려는 접근 (arxiv.org)
실무적으로는 “원본 MMLU 점수 vs Pro/Redux 점수”가 다르면,
- 모델이 암기/오염에 강했는지
- 추론/문항 이해가 실제로 좋은지
- 평가 파이프라인이 파싱/프롬프트에 의존하는지
를 의심할 근거가 됩니다.
3) HumanEval: 코드 생성 평가는 “pass@k + 실행 환경”이 전부다
HumanEval은 164개 Python 문제에서 테스트 통과(pass@k) 로 봅니다. 실행 기반이라 좋아 보이지만, 여기서도 구조를 뜯어보면:
- (입력) 함수 시그니처 + docstring 스펙
- (모델 출력) 함수 구현 코드
- (샌드박스 실행) unit test 실행
- (집계) pass@1, pass@10 등 계산
핵심 차이:
- MMLU는 “문자 선택” 파싱이 중요
- HumanEval은 “실행/테스트”가 중요(의존성, 타임아웃, 샌드박스, 난수 고정 등)
그리고 HumanEval의 약점(현업 관점)은 명확합니다:
- “테스트가 약하면” 허술한 코드도 통과 가능
→ 그래서 HumanEval의 테스트를 대폭 확장한 HumanEval+ (EvalPlus) 같은 변형이 널리 쓰입니다. (arxiv.org) - 오염 방지 측면에서 “최신 문제”를 쓰려면 LiveCodeBench 같은 접근이 등장했습니다. (arxiv.org)
4) 2026년식 결론: 벤치마크는 “점수”가 아니라 “측정 시스템”이다
요즘 커뮤니티/프레임워크는 “canonical implementation”을 강조합니다. 예를 들어 OpenAI는 simple-evals에서 MMLU/HumanEval 등을 정리해 제공합니다. (github.com)
또한 EleutherAI lm-evaluation-harness 쪽도 태스크/설정이 계속 업데이트되어, 동일 벤치라도 설정 차이로 결과가 달라질 수 있음을 시사합니다. (github.com)
💻 실전 코드
목표: “리더보드 점수”가 아니라 내 제품 시나리오에 가까운 사내 평가 리그를 만드는 예제를 제공합니다.
- MMLU 스타일: 우리 도메인 MCQ(정답 1개) 를 동일 파이프라인으로 측정
- HumanEval 스타일: 우리 코드베이스 패턴에 가까운 ‘패치/함수 구현’ 을 테스트로 검증
- 공통: 결과를 JSONL로 남기고, 재현 가능한 설정(temperature=0, seed, timeout)을 고정
아래 예제는 Python 기반 “미니 eval runner”입니다. (벤치마크 원본을 그대로 긁어오는 게 아니라, 우리 문제를 MMLU/HumanEval 방식으로 측정하는 게 핵심입니다.)
1) 초기 셋업
python -m venv .venv
source .venv/bin/activate
pip install openai pytest jsonlines pydantic
export OPENAI_API_KEY="..."
2) 평가 러너 (MCQ + 코드 실행)
# eval_runner.py
from __future__ import annotations
import json
import os
import subprocess
import tempfile
from dataclasses import dataclass
from typing import Literal, Optional, List, Dict, Any
import jsonlines
from openai import OpenAI
# ---------- MMLU-style MCQ ----------
@dataclass
class MCQItem:
id: str
subject: str
question: str
choices: Dict[str, str] # {"A": "...", "B": "...", ...}
answer: str # e.g., "C"
def format_mcq_prompt(item: MCQItem) -> str:
# 파싱 흔들림을 줄이기 위해 출력 포맷을 강제
choices = "\n".join([f"{k}. {v}" for k, v in item.choices.items()])
return f"""You are taking a closed-book multiple-choice test.
Subject: {item.subject}
Question:
{item.question}
Choices:
{choices}
Return ONLY a JSON object like: {{"final": "A"}}.
"""
def extract_final_letter(text: str) -> Optional[str]:
# 모델이 지시를 어겨도 최대한 회수
try:
obj = json.loads(text)
val = obj.get("final")
if isinstance(val, str) and val.strip() in ["A","B","C","D","E","F","G","H","I","J"]:
return val.strip()
except Exception:
pass
for ch in ["A","B","C","D","E","F","G","H","I","J"]:
if f'"{ch}"' in text or f" {ch}" == text.strip():
return ch
return None
# ---------- HumanEval-style code execution ----------
@dataclass
class CodeItem:
id: str
prompt: str # function stub + docstring spec
tests_py: str # pytest test file content
def run_pytest(code: str, tests_py: str, timeout_s: int = 20) -> Dict[str, Any]:
with tempfile.TemporaryDirectory() as d:
code_path = os.path.join(d, "solution.py")
test_path = os.path.join(d, "test_solution.py")
with open(code_path, "w", encoding="utf-8") as f:
f.write(code)
with open(test_path, "w", encoding="utf-8") as f:
f.write(tests_py)
try:
p = subprocess.run(
["pytest", "-q", test_path],
cwd=d,
capture_output=True,
text=True,
timeout=timeout_s,
)
return {
"ok": p.returncode == 0,
"returncode": p.returncode,
"stdout": p.stdout[-2000:],
"stderr": p.stderr[-2000:],
}
except subprocess.TimeoutExpired:
return {"ok": False, "returncode": None, "stdout": "", "stderr": "TIMEOUT"}
# ---------- Model call ----------
def call_model_json(client: OpenAI, model: str, prompt: str) -> str:
# 재현성을 위해 temperature=0 고정(가능한 범위 내)
resp = client.responses.create(
model=model,
input=prompt,
temperature=0,
)
return resp.output_text
def main():
client = OpenAI()
model = os.environ.get("EVAL_MODEL", "gpt-4.1-mini")
mcq_items: List[MCQItem] = [
MCQItem(
id="ops-001",
subject="Backend/Operations",
question="In Kubernetes, what is the most appropriate primitive to ensure a pod is automatically restarted on failure?",
choices={"A": "Service", "B": "Deployment", "C": "ConfigMap", "D": "Ingress"},
answer="B",
),
MCQItem(
id="sec-002",
subject="AppSec",
question="Which HTTP response header is most directly related to mitigating XSS via restricting script sources?",
choices={"A": "CSP", "B": "ETag", "C": "Accept-Ranges", "D": "Server"},
answer="A",
),
]
code_items: List[CodeItem] = [
CodeItem(
id="code-redis-ttl-001",
prompt="""# solution.py
def normalize_cache_ttl(seconds: int) -> int:
\"\"\"Normalize TTL for cache writes.
- For negative input: raise ValueError
- For 0..30: return 30 (avoid stampede)
- For 31..3600: return as-is
- For >3600: cap to 3600 (avoid stale data)
\"\"\"
# TODO: implement
""",
tests_py="""# test_solution.py
import pytest
from solution import normalize_cache_ttl
def test_negative():
with pytest.raises(ValueError):
normalize_cache_ttl(-1)
def test_floor():
assert normalize_cache_ttl(0) == 30
assert normalize_cache_ttl(15) == 30
assert normalize_cache_ttl(30) == 30
def test_mid():
assert normalize_cache_ttl(31) == 31
assert normalize_cache_ttl(600) == 600
def test_cap():
assert normalize_cache_ttl(3601) == 3600
assert normalize_cache_ttl(999999) == 3600
""",
)
]
out_path = "eval_results.jsonl"
with jsonlines.open(out_path, mode="w") as w:
# MCQ eval
mcq_ok = 0
for it in mcq_items:
prompt = format_mcq_prompt(it)
raw = call_model_json(client, model, prompt)
pred = extract_final_letter(raw)
ok = (pred == it.answer)
mcq_ok += 1 if ok else 0
w.write({
"type": "mcq",
"id": it.id,
"subject": it.subject,
"answer": it.answer,
"pred": pred,
"ok": ok,
"raw": raw,
})
# Code eval
code_ok = 0
for it in code_items:
raw = call_model_json(client, model, it.prompt)
# 모델이 prompt 전체를 그대로 출력할 수 있으니, 최소한 solution.py로 저장 가능한 형태로 정리
code = raw if "def normalize_cache_ttl" in raw else (it.prompt + "\n" + raw)
r = run_pytest(code=code, tests_py=it.tests_py, timeout_s=20)
ok = bool(r["ok"])
code_ok += 1 if ok else 0
w.write({
"type": "code",
"id": it.id,
"ok": ok,
"runner": r,
"raw": raw,
})
print(f"[MCQ] {mcq_ok}/{len(mcq_items)}")
print(f"[CODE] {code_ok}/{len(code_items)}")
print(f"wrote: {out_path}")
if __name__ == "__main__":
main()
예상 출력(예시):
[MCQ] 2/2
[CODE] 1/1
wrote: eval_results.jsonl
이게 왜 “MMLU/HumanEval 실전 적용”이냐?
- MMLU/HumanEval을 그대로 돌리기보다, 같은 채점 원리(정답 선택 / 테스트 통과) 를 내 문제로 이식해야 점수가 의미를 갖습니다.
- OpenAI simple-evals도 “구현체(benchmark runner)”가 중요하다는 방향으로 정리되어 있습니다. (github.com)
⚡ 실전 팁 & 함정
Best Practice 1) “리더보드 점수” 대신 “내 파이프라인의 canonical run”을 만들기
- 프롬프트 템플릿, temperature, max tokens, 파서, 타임아웃, 샌드박스 옵션을 고정하고 버전 관리하세요.
- lm-evaluation-harness처럼 설정 업데이트로 결과가 바뀔 수 있다는 점을 전제로, eval 스냅샷(코드+데이터+설정) 을 남기는 게 필수입니다. (github.com)
Best Practice 2) MMLU는 “정답률”만 보지 말고, “무효 문항/불확실 문항”을 분리
MMLU류는 정답 오류/문항 품질 문제가 실제로 보고되었습니다. (arxiv.org)
실무 대응:
- 사내 MCQ 세트에도 “애매한 문항”이 섞이기 쉽습니다(특히 트렌드/버전 이슈).
- 문항별 disagreement(모델들/사람들 간 불일치) 가 큰 문제는 “평가에서 제외”하거나 “근거 제출형(LLM judge + human audit)”으로 전환하세요.
Best Practice 3) HumanEval은 반드시 HumanEval+ 또는 사내 강화 테스트로 보강
HumanEval은 테스트가 상대적으로 약해 “그럴듯한 코드”가 통과할 수 있습니다. 이를 보완하는 대표 흐름이 HumanEval+ (EvalPlus)입니다. (arxiv.org)
실무 대응:
- “단위 테스트 추가(엣지/랜덤/프로퍼티 기반)”가 가장 비용 대비 효과가 큽니다.
- 가능하면 LiveCodeBench 같은 “오염에 강한 최신 문제” 계열을 참고해, 신규 문제를 주기적으로 추가하세요. (arxiv.org)
흔한 함정/안티패턴
- pass@k를 pass@1처럼 해석: 제품은 대부분 첫 출력이 중요합니다. k를 올리면 “샘플링 비용”을 성능으로 착각하기 쉽습니다.
- MMLU에서 CoT(Chain-of-Thought) 강제 후 최종 선택 파싱이 불안정해져 오히려 점수/안정성이 떨어짐
- 코드 실행 평가에서 의존성/네트워크/시간 제한을 느슨하게 둬서 “현실에서 불가능한 풀이”가 통과
비용/성능/안정성 트레이드오프
- MMLU류: cheap(토큰만)하지만 “측정 신뢰도”를 확보하려면 문항 정제가 필요
- HumanEval류: 실행 비용+샌드박스 비용이 들지만 “실제 동작”에 더 가까움
- 둘 다 공통으로, 평가 세트가 작으면 분산이 커지고(우연), 크면 비용이 증가합니다.
→ 그래서 “소형 smoke eval + 주간 풀 eval” 같은 계층화가 현실적입니다.
🚀 마무리
정리하면, 2026년 8월 기준 MMLU/HumanEval은 여전히 유용하지만 ‘점수’가 아니라 ‘측정 시스템’ 으로 다뤄야 합니다.
도입 판단 기준:
- 우리 요구가 “지식형 QA”에 가깝다면: MMLU 스타일 MCQ를 쓰되, Redux/Pro 계열의 문제의식(오류/난이도/프롬프트 민감도) 을 내 평가에도 반영 (arxiv.org)
- 우리 요구가 “코드 생성/수정”이라면: HumanEval pass@1을 기본으로, HumanEval+ 수준으로 테스트를 강화하고 실행 환경을 제품과 가깝게 (arxiv.org)
- 그리고 무엇보다: simple-evals/lm-eval-harness처럼 구현체를 고정하고 재현 가능한 리그를 운영하는 것이 “진짜 성능 비교”의 출발점입니다. (github.com)
다음 학습 추천:
- MMLU-Redux 논문/데이터셋로 “정답 오류가 점수를 어떻게 왜곡하는지” 체감하기 (arxiv.org)
- HumanEval+ (EvalPlus) 논문로 “테스트 강화가 모델 순위를 어떻게 바꾸는지” 확인 (arxiv.org)
- LiveCodeBench로 “오염에 강한 코드 평가” 설계를 참고 (arxiv.org)
원하면, 당신의 서비스(예: RAG QA / 코드리뷰 봇 / 데이터 파이프라인 생성기)에 맞춰 평가 항목 설계(문항 타입, 채점 방식, 비용 상한, 실패 분석 리포트 포맷) 까지 이어서 템플릿으로 잡아드릴 수 있습니다.