- Published on
LLM 벤치마크 도구를 뜯어보기 — 같은 MMLU가 하네스마다 다른 점수를 내는 이유
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — 같은 모델, 같은 MMLU, 세 개의 다른 점수
- 하네스가 실제로 하는 일 — 여섯 단계
- 정답을 고르는 방식이 곧 점수를 만든다
- 대표 하네스 비교
- 처음부터 끝까지 한 번 돌려 보기
- 재현에 필요한 최소 조건 목록
- 채점기가 곧 벤치마크입니다
- 마치며 — 조건 없는 숫자는 숫자가 아닙니다
- 참고 자료
들어가며 — 같은 모델, 같은 MMLU, 세 개의 다른 점수
2023년 6월, Hugging Face의 Open LLM Leaderboard 팀은 이상한 민원을 잔뜩 받았습니다. LLaMA 65B의 MMLU 점수가 원 논문의 63.4보다 한참 낮게 찍힌다는 것이었습니다. 조사 결과는 버그가 아니었습니다. 세 벌의 MMLU 구현체가 각자 다른 방식으로 같은 데이터셋을 채점하고 있었을 뿐입니다.
| 모델 | Original 구현 | HELM | lm-evaluation-harness (2023-01) |
|---|---|---|---|
| llama-65b | 0.636 | 0.637 | 0.488 |
| llama-30b | 0.584 | 0.583 | 0.457 |
| llama-13b | 0.470 | 0.471 | 0.377 |
| llama-7b | 0.351 | 0.339 | 0.342 |
| falcon-40b | 0.558 | 0.571 | 0.527 |
출처는 Hugging Face의 What's going on with the Open LLM Leaderboard?입니다. 여기서 눈여겨볼 지점은 격차의 크기가 아니라 격차가 모델마다 다르다 는 사실입니다. 65B에서는 15점 가까이 벌어지는데 7B에서는 거의 붙어 있습니다. 즉 하네스를 바꾸면 점수가 일정하게 이동하는 게 아니라 모델 사이의 순위가 뒤집힙니다. 보정 상수 하나로 환산할 수 있는 문제가 아니라는 뜻입니다.
세 구현의 차이는 이렇습니다. Original은 A/B/C/D 네 글자의 확률만 비교합니다. HELM은 모델에게 글자 하나를 생성하게 한 뒤 그것을 답으로 봅니다. 당시 harness는 선택지 문장 전체 의 로그우도를 비교했습니다. 프롬프트도 달랐습니다. Original은 과목명 줄을 넣고, HELM은 "Question:" 접두어를 붙이고, harness는 과목명을 빼고 "Choices:"를 넣었습니다.
이 글의 전제는 여기서 나옵니다. 벤치마크 점수는 모델의 속성이 아니라, 특정 조건에서 수행된 측정의 결과입니다. 그런데 발표되는 숫자의 대부분은 그 조건을 생략합니다. 그래서 우리가 계속 던져야 할 질문은 하나입니다 — 이 숫자가 겉보기의 의미를 가지려면 무엇이 참이어야 하는가.
리더보드 숫자를 얼마나 믿을 수 있는지에 대한 큰 그림은 AI 코딩 모델 평가에서 신호와 잡음 가려내기에서, 어떤 벤치마크가 무엇을 재는지의 지도는 AI 에이전트 & LLM 벤치마크 2026에서 이미 다뤘습니다. 이 글은 그 아래 층, 도구가 실제로 무엇을 하는지를 봅니다.
하네스가 실제로 하는 일 — 여섯 단계
"MMLU를 돌렸다"는 문장은 최소 여섯 단계를 압축한 표현입니다. 각 단계마다 결정해야 할 것이 있고, 그 결정이 기록되지 않으면 재현은 불가능합니다.
1. 데이터 적재 어느 split인가(test/validation), 몇 개인가, 순서는 고정인가
|
2. few-shot 구성 예시를 어느 풀에서, 몇 개, 어떤 순서로 뽑는가, 시드는
|
3. 프롬프트 조립 템플릿, 구분자, 시스템 메시지, chat template 적용 여부
|
4. 모델 호출 온도, top_p, max_tokens, 중지 문자열, 재시도, 배치 크기
|
5. 정답 파싱 로그우도 비교인가, 정규식 추출인가, 마지막 숫자인가
|
6. 채점·집계 정확 일치, 부분 점수, 모델 채점, 그리고 평균 내는 단위
각 단계가 점수를 어떻게 움직이는지 한 줄로 정리하면 다음과 같습니다.
| 단계 | 흔히 생략되는 설정 | 점수에 미치는 영향 |
|---|---|---|
| 데이터 적재 | test와 validation 중 무엇을 썼는지 | 과목별로 수 점 차이, 비교 불가능 |
| few-shot 구성 | 예시 개수, 예시 선택 시드 | 0-shot과 5-shot이 두 자릿수 벌어지는 과제가 흔함 |
| 프롬프트 조립 | chat template 적용 여부 | 지시 튜닝 모델에서 특히 큼 |
| 모델 호출 | 온도와 max_tokens | 온도 0.7이면 같은 명령도 매번 다른 점수 |
| 정답 파싱 | 추출 정규식 | GSM8K에서 strict와 flexible이 갈리는 지점 |
| 채점·집계 | 서브태스크 평균 방식 | 매크로 평균과 마이크로 평균이 다른 순위를 만듦 |
MMLU-Pro 논문(arXiv 2406.01574, NeurIPS 2024)은 이 민감도를 실제로 측정했습니다. 24가지 프롬프트 스타일로 돌렸을 때 MMLU에서는 점수 변동이 대체로 4~5%p, 최대 10.98%p였습니다. 선택지를 10개로 늘린 MMLU-Pro에서는 그 변동이 대체로 2%p, 최대 3.74%p로 줄었습니다. 뒤집어 말하면, 원래 MMLU에서 프롬프트만 바꿔도 리더보드 중위권을 관통할 만큼 점수가 움직인다 는 뜻입니다.
더 극단적인 측정도 있습니다. Sclar 등의 Quantifying Language Models' Sensitivity to Spurious Features in Prompt Design (arXiv 2310.11324)은 의미가 동일한 형식 변경만으로 LLaMA-2-13B에서 최대 76점의 정확도 차이가 났다고 보고합니다. 2025년의 A Single Character can Make or Break Your LLM Evals (arXiv 2510.05152)는 한 걸음 더 나갑니다 — 예시를 구분하는 문자 하나 를 바꾸는 것만으로 MMLU 점수가 최대 23퍼센트 움직이며, 그 조작만으로 원하는 모델을 1위에 올릴 수 있다고 보고합니다. 프롬프트 형식은 실험 설정이 아니라 측정 장치의 일부 입니다.
정답을 고르는 방식이 곧 점수를 만든다
객관식 과제에서 "모델이 답을 골랐다"를 구현하는 방법은 최소 세 가지입니다. 셋은 서로 다른 능력을 잽니다.
로그우도 비교. 모델에게 생성을 시키지 않습니다. 프롬프트 뒤에 각 선택지 문자열을 붙여 놓고 그 문자열의 로그 확률을 계산해, 가장 높은 것을 고른 답으로 봅니다. 생성이 없으므로 결정적이고 빠릅니다. 대신 지시를 따르는 능력은 전혀 재지 않습니다. 지시를 무시하는 베이스 모델도 정상 점수를 받습니다.
글자 확률 비교. 프롬프트를 "정답: " 까지 만들고 다음 토큰이 A, B, C, D일 확률만 비교합니다. 선택지 길이의 영향을 안 받지만, 모델이 실제로는 "Answer: B"처럼 답했을 상황을 무시합니다.
생성 후 파싱. 모델에게 답하게 하고 텍스트에서 답을 뽑습니다. 실제 사용에 가장 가깝습니다. 대신 파싱 규칙이 채점의 일부가 됩니다.
로그우도 방식에는 잘 알려진 함정이 하나 더 있습니다. 긴 문자열은 토큰이 많으므로 로그 확률의 합이 자연히 작아집니다. 그래서 아무 보정 없이 합을 비교하면 짧은 선택지가 구조적으로 유리 합니다. lm-evaluation-harness가 acc와 함께 acc_norm을 같이 보고하는 이유가 이것입니다. acc_norm은 로그우도를 선택지 길이로 나눠 이 편향을 상쇄합니다. 두 숫자가 크게 벌어지는 과제라면, 그 과제는 선택지 길이 분포에 민감하다는 신호입니다.
한국어로 평가할 때는 여기에 하나가 더 붙습니다. EleutherAI의 정규화 설명 글은 이 보정을 바이트 길이 정규화라고 부르고, 토큰화 방식에 무관하다는 것이 장점이라고 설명합니다. 그런데 저장소의 이슈 3278은 실제 코드가 문자열의 문자 수 로 나누고 있다고 지적합니다. 영어에서는 둘이 사실상 같지만, UTF-8에서 한 글자가 3바이트인 한국어·일본어·중국어에서는 다릅니다. 한글 선택지와 영문 선택지가 섞인 평가셋이라면 이 차이가 그대로 점수에 들어옵니다. 정규화 방식을 문서에서 읽지 말고 코드에서 확인해야 하는 이유 가 이런 것입니다.
생성 후 파싱 쪽의 대표 사례는 GSM8K입니다. lm-evaluation-harness의 gsm8k 태스크는 한 번의 실행에서 두 개의 점수를 냅니다.
| 필터 | 추출 규칙 | 놓치는 것 |
|---|---|---|
| strict-match | 지정된 형식(예를 들어 네 개의 우물 정자 뒤 숫자)에 정확히 맞는 답만 | 형식을 안 지킨 정답을 전부 오답 처리 |
| flexible-extract | 출력에서 마지막 숫자를 답으로 간주 | 계산 중간값이 마지막에 오면 오답, 통화 기호·쉼표 정규화 미흡 |
같은 실행, 같은 출력인데 두 숫자가 나옵니다. 그런데 논문과 모델 카드에 인용되는 GSM8K 점수는 대개 그중 하나뿐이고, 어느 쪽인지 밝히지 않습니다. flexible-extract 필터가 통화 기호나 쉼표를 제대로 정규화하지 못해 맞은 답을 틀렸다고 판정한다는 이슈도 저장소에 올라와 있습니다(issue 3214). 파서의 버그가 모델의 점수로 기록되는 구조입니다.
대표 하네스 비교
2026년 8월 현재 실무와 논문에서 실제로 쓰이는 도구들을 정리하면 다음과 같습니다. 버전은 확인 시점의 최신 릴리스입니다.
| 도구 | 버전 (확인일 2026-08-02) | 철학 | 채점 방식의 특징 |
|---|---|---|---|
| lm-evaluation-harness (EleutherAI) | lm-eval 0.4.12, 2026-05-11 | 태스크를 YAML로 선언, 60여 개 벤치마크 | 로그우도·생성 둘 다, 필터 체인으로 파싱 |
| HELM (Stanford CRFM) | crfm-helm 0.5.16, 2026-04-30 | 한 과제를 여러 지표로 동시에 | 정확도 외에 교정·강건성·공정성·독성·효율 함께 |
| Inspect AI (UK AISI, Meridian Labs) | inspect-ai 0.3.251, 2026-07-29 | 에이전트·도구 사용까지 1급 시민 | solver와 scorer 분리, 모델 채점과 샌드박스 내장 |
| promptfoo | npm 0.121.20, 2026-07-31 | YAML 선언형, 프롬프트·모델 격자 비교 | 어서션 기반, 레드팀 스캐너 포함 |
| DeepEval | 4.1.5, 2026-07-31 | pytest 스타일, 애플리케이션 지표 | G-Eval과 결정 트리형 판정(DAG) |
| LightEval (Hugging Face) | 0.13.0, 2025-11-24 | 백엔드 무관 경량 파이프라인 | Inspect AI를 우선 백엔드로 채택 |
| OpenCompass (Shanghai AI Lab) | 0.5.3, 2026-06-29 | 대규모 종합 평가 도구 | 모델 채점 유틸리티 포함 |
| Ragas | 0.4.3, 2026-01-13 | RAG 전용 지표 모음 | 충실도·답변 관련성·컨텍스트 정밀도 |
| EvalPlus | 0.3.1, 2024-10-20 | HumanEval·MBPP 테스트 강화 | 테스트를 대폭 늘려 약한 채점을 메움 |
| OpenAI simple-evals | 릴리스 태그 없음 | 참조 구현 공개가 목적 | 0-shot chain-of-thought 고정 |
몇 가지 짚어 둘 점이 있습니다.
HELM의 관점은 다릅니다. HELM은 "이 모델의 MMLU 점수는 몇 점인가"가 아니라 "이 시나리오에서 이 모델은 정확도·교정·강건성·공정성·독성·효율 각각에서 어떤가"를 묻습니다. 원 논문(arXiv 2211.09110)의 표현대로 16개 핵심 시나리오마다 7개 지표를 함께 재는 다지표 접근입니다. 실행은 helm-run으로 하고 helm-summarize로 집계한 뒤 helm-server로 결과를 브라우저에서 봅니다.
다만 상태 변화를 알아 두셔야 합니다. HELM은 2026년 6월 1일부터 유지보수 모드로 전환했습니다. 코드와 리더보드는 계속 공개되지만 새 기능과 새 평가는 추가되지 않습니다. 도구로 새로 채택할 때는 이 점을 감안해야 하고, 반대로 과거 HELM 점수를 인용할 때는 그 시점의 리더보드가 지금도 갱신되고 있다고 가정하면 안 됩니다.
HELM이 객관식을 다루는 방식도 알아 둘 만합니다. 선택지를 모두 붙여 주고 모델이 글자를 생성 하게 하는 방식(joint), 선택지마다 따로 점수를 매기는 방식(separate), 그리고 선택지 자체의 사전 확률로 보정하는 방식(separate-calibrated)이 있습니다. 최신 API가 토큰 확률을 노출하지 않는 경우가 늘면서 joint 쪽으로 기울었는데, 이 선택 하나가 앞의 표에서 본 15점 차이의 축이었습니다.
Inspect AI는 에이전트 평가를 전제로 설계되었습니다. 과제를 Dataset, Solver, Scorer 셋으로 나눕니다. Solver는 단순한 한 번의 생성일 수도 있고 도구를 여러 턴 쓰는 완전한 에이전트일 수도 있으며, Scorer는 문자열 비교부터 모델 채점까지 붙일 수 있습니다. 모델이 만든 코드를 안전하게 돌리기 위한 Docker·쿠버네티스 샌드박스가 기본 제공됩니다. 영국 AI Security Institute와 Meridian Labs가 함께 개발하며, 안전 평가 쪽에서 사실상 표준입니다. Hugging Face의 LightEval이 이를 우선 백엔드로 채택한 것은 2026년의 눈에 띄는 수렴 신호입니다.
OpenAI simple-evals는 도구라기보다 참조 구현입니다. 존재 이유는 OpenAI가 모델과 함께 발표하는 정확도 숫자를 어떤 프롬프트와 어떤 설정으로 냈는지 공개하는 것입니다. 0-shot chain-of-thought를 고집하는데, few-shot 프롬프트는 베이스 모델 시절의 유산이며 실사용을 덜 반영한다는 판단 때문입니다. 이 선택 자체가 다른 하네스와의 비교 불가능성을 만듭니다. 재미있는 세부도 있습니다 — 샘플러 구현마다 기본 온도가 달라서, 채팅 API용 샘플러는 온도 0.5에 최대 1024토큰, Claude용 샘플러는 온도 0.0에 최대 4096토큰이 기본값입니다. 같은 저장소 안에서도 모델에 따라 샘플링 조건이 다릅니다.
혼동하기 쉬운 두 가지를 구분해 두겠습니다. GitHub의 openai/evals 저장소는 아카이브되지 않았고 폐기 공지도 없지만, 실질적으로는 휴면 상태입니다(PyPI 패키지는 2024년 이후 갱신 없음). 반면 OpenAI가 호스팅하던 Evals 대시보드와 API는 2026년 6월 3일 폐기가 공지되었고, 2026년 10월 31일 읽기 전용, 11월 30일 종료 일정입니다. 저장소와 플랫폼은 다른 물건입니다.
promptfoo는 2026년 3월 9일 OpenAI에 인수되었습니다(OpenAI 발표). 오픈소스 라이선스는 유지된다고 공지되었습니다. 도구 선택 시 유지보수 주체가 바뀌었다는 점은 알아 둘 만합니다.
한 가지 더. Hugging Face의 Open LLM Leaderboard는 2025년 3월 13일 운영을 종료했습니다. 이 글 서두의 MMLU 표를 만든 바로 그 리더보드입니다. 리더보드는 사라져도 그 리더보드에서 나온 점수는 논문과 모델 카드에 계속 인용되므로, 인용된 점수의 출처가 지금도 살아 있는지 확인하는 습관이 필요합니다.
처음부터 끝까지 한 번 돌려 보기
설명보다 한 번 돌려 보는 편이 빠릅니다. 노트북 CPU에서도 끝나는 크기로 잡았습니다.
python3 -m venv .venv
source .venv/bin/activate
pip install "lm-eval[api]==0.4.12"
# 어떤 태스크가 있는지 먼저 확인
lm_eval --tasks list | head -40
작은 모델로 ARC-Easy를 200문항만 돌립니다. 시드와 few-shot 개수를 명시하는 것이 핵심입니다.
lm_eval \
--model hf \
--model_args pretrained=EleutherAI/pythia-160m,dtype=float32 \
--tasks arc_easy \
--num_fewshot 5 \
--batch_size 8 \
--device cpu \
--seed 1234 \
--limit 200 \
--output_path ./results/pythia-160m \
--log_samples
여기서 각 플래그가 무엇을 고정하는지가 중요합니다.
--num_fewshot 5없이 돌리면 태스크 YAML의 기본값이 쓰입니다. 태스크마다 다르고, 버전이 바뀌면 같이 바뀝니다.--seed는 few-shot 예시 추출과 셔플을 고정합니다. 빼면 실행마다 다른 프롬프트가 만들어집니다.--limit 200은 앞에서 200개만 자릅니다. 무작위 표본이 아니므로 이 숫자를 전체 점수로 인용하면 안 됩니다. 디버깅용입니다.--log_samples가 진짜 중요합니다. 프롬프트 원문, 모델 출력, 파싱 결과가 전부 저장됩니다. 이게 없으면 점수가 왜 그렇게 나왔는지 사후에 알 수 없습니다.
실행이 끝나면 결과 디렉터리에 JSON이 생깁니다. 구조는 대략 이렇습니다. 정확한 키 이름은 버전에 따라 달라질 수 있으므로 직접 열어 확인하시길 권합니다.
{
"results": {
"arc_easy": {
"alias": "arc_easy",
"acc,none": 0.395,
"acc_stderr,none": 0.0346,
"acc_norm,none": 0.365,
"acc_norm_stderr,none": 0.0341
}
},
"configs": {
"arc_easy": {
"task": "arc_easy",
"output_type": "multiple_choice",
"num_fewshot": 5,
"metric_list": [{ "metric": "acc" }, { "metric": "acc_norm" }]
}
},
"config": {
"model": "hf",
"model_args": "pretrained=EleutherAI/pythia-160m,dtype=float32",
"batch_size": 8,
"random_seed": 1234,
"limit": 200
},
"git_hash": "…",
"date": 1785000000
}
이 JSON에서 실제로 봐야 할 것은 results가 아니라 configs와 config 입니다. 점수 하나를 인용할 때 함께 붙여야 할 정보가 전부 여기 들어 있습니다. acc와 acc_norm이 3%p 벌어져 있다는 사실도 여기서만 보입니다.
지시 튜닝 모델을 API로 재는 경우는 명령이 달라집니다. vLLM이나 Ollama처럼 OpenAI 호환 엔드포인트를 띄운 뒤 이렇게 붙입니다.
lm_eval \
--model local-chat-completions \
--model_args model=qwen3-8b,base_url=http://localhost:8000/v1/chat/completions,num_concurrent=8,max_retries=3,tokenized_requests=False \
--tasks gsm8k \
--num_fewshot 5 \
--apply_chat_template \
--fewshot_as_multiturn \
--gen_kwargs temperature=0,max_gen_toks=512 \
--output_path ./results/qwen3-8b \
--log_samples
--apply_chat_template과 --fewshot_as_multiturn은 지시 튜닝 모델을 잴 때 거의 항상 필요합니다. 전자는 프롬프트를 모델이 학습한 대화 형식으로 감싸고, 후자는 few-shot 예시를 하나의 긴 텍스트가 아니라 실제 주고받은 대화 턴으로 넣습니다. 이 두 플래그의 유무만으로 지시 튜닝 모델의 점수가 크게 갈립니다. 그런데 발표된 숫자에서 이 정보를 찾을 수 있는 경우는 드뭅니다.
로그우도를 지원하지 않는 API도 있습니다. 그 경우 객관식 태스크를 로그우도로 돌릴 수 없으므로 generate_until 계열만 가능합니다. 즉 모델을 API로 재느냐 가중치로 재느냐에 따라 애초에 쓸 수 있는 채점 방식이 달라집니다. 이것만으로도 같은 벤치마크의 두 점수가 비교 불가능해질 수 있습니다.
태스크 정의를 직접 읽고 고쳐 보기
lm-evaluation-harness에서 태스크는 YAML 파일 하나입니다. 사내 데이터로 같은 하네스를 쓰고 싶다면 이 파일만 쓰면 됩니다. 객관식부터 보겠습니다.
task: internal_mcq
dataset_path: json
dataset_kwargs:
data_files:
test: ./data/internal_mcq.jsonl
output_type: multiple_choice
test_split: test
doc_to_text: "다음 질문에 답하세요.\n질문: {{question}}\n답:"
doc_to_choice: "{{choices}}"
doc_to_target: "{{label}}"
num_fewshot: 5
fewshot_config:
sampler: default
metric_list:
- metric: acc
aggregation: mean
higher_is_better: true
- metric: acc_norm
aggregation: mean
higher_is_better: true
should_decontaminate: false
metadata:
version: 1.0
여기서 doc_to_text의 문자열 한 글자만 바꿔도 점수가 움직입니다. 앞의 MMLU 사례가 정확히 그 현상이었습니다. 그래서 이 파일은 반드시 버전 관리에 들어가야 하고, metadata의 version은 정의를 바꿀 때마다 올려야 합니다. 이 필드가 존재하는 이유가 "지난달 점수와 이번 달 점수가 같은 정의에서 나왔는가"를 확인하기 위해서입니다.
생성형 과제는 필터 체인이 붙습니다. 파싱 규칙이 명시적으로 드러나는 지점입니다.
task: internal_math
dataset_path: json
dataset_kwargs:
data_files:
test: ./data/internal_math.jsonl
output_type: generate_until
test_split: test
doc_to_text: "문제: {{question}}\n풀이를 적고, 마지막 줄에 '정답: 숫자' 형식으로만 답하세요.\n"
doc_to_target: "{{answer}}"
generation_kwargs:
until:
- "\n\n"
do_sample: false
temperature: 0.0
max_gen_toks: 512
filter_list:
- name: strict-match
filter:
- function: regex
regex_pattern: "정답:\\s*(-?[0-9][0-9,]*(?:\\.[0-9]+)?)"
- function: take_first
- name: last-number
filter:
- function: regex
regex_pattern: "(-?[0-9][0-9,]*(?:\\.[0-9]+)?)"
group_select: -1
- function: take_first
metric_list:
- metric: exact_match
aggregation: mean
higher_is_better: true
ignore_case: true
ignore_punctuation: false
metadata:
version: 1.0
같은 실행에서 두 개의 필터가 각각 점수를 냅니다. 두 점수의 차이는 모델의 수학 실력이 아니라 모델이 지시한 출력 형식을 얼마나 지키는가 입니다. 이 두 숫자를 나란히 보는 습관이 중요합니다. strict가 낮고 last-number가 높다면 모델은 계산은 하는데 형식을 안 지키는 것이고, 이건 프롬프트로 고칠 수 있는 문제입니다. 둘 다 낮다면 계산을 못 하는 것이고, 이건 모델을 바꿔야 하는 문제입니다. 하나의 숫자만 보고했다면 이 구분이 사라집니다.
쉼표 처리를 보십시오. 정규식에 [0-9,]*이 들어 있습니다. 이게 없으면 모델이 "1,024"라고 답할 때 "1"만 뽑히고 오답이 됩니다. 정답 파싱 규칙은 이런 결정의 누적이고, 그 누적이 곧 점수입니다.
재현에 필요한 최소 조건 목록
점수 하나를 다시 만들어 내려면 다음이 전부 필요합니다. 하나라도 빠지면 재현이 아니라 근사입니다.
| 항목 | 왜 필요한가 | 어디에 기록하는가 |
|---|---|---|
| 모델 식별자와 리비전 | 같은 이름의 API 모델이 조용히 갱신됩니다 | 가중치는 커밋 해시, API는 스냅샷 ID |
| 하네스 버전 | 태스크 정의와 필터가 릴리스마다 바뀝니다 | lm-eval 0.4.12처럼 정확히 |
| 태스크 정의 버전 | 프롬프트 한 글자가 점수를 움직입니다 | YAML의 metadata.version과 파일 해시 |
| few-shot 개수와 시드 | 예시가 바뀌면 다른 시험지입니다 | 명령줄 플래그 원문 그대로 |
| 샘플링 파라미터 | 온도가 0이 아니면 매번 다른 점수 | temperature, top_p, max_tokens, 중지 문자열 |
| 프롬프트 조립 방식 | chat template 유무가 특히 큽니다 | apply_chat_template, 시스템 메시지 원문 |
| 정답 파싱 규칙 | 파서가 채점의 일부입니다 | 필터 이름과 정규식 원문 |
| 집계 단위 | 매크로와 마이크로가 다른 순위를 만듭니다 | 서브태스크 가중 방식 |
| 평가 대상 부분집합 | limit이나 서브샘플링은 전체가 아닙니다 | 문항 수와 선택 방식 |
| 실행 횟수와 분산 | 한 번 돌린 숫자는 폭을 숨깁니다 | 반복 횟수, 평균과 표준오차 |
| 실행 환경 | 배치 크기와 정밀도가 결과를 바꿉니다 | dtype, 배치 크기, 추론 백엔드와 버전 |
마지막 항목은 자주 무시되는데 실제로 영향이 있습니다. 같은 가중치라도 float16과 bfloat16이 다른 답을 낼 수 있고, 배치 크기에 따라 패딩과 커널 선택이 달라지며, vLLM과 transformers는 같은 프롬프트에 다른 토큰을 낼 수 있습니다. 이 층위의 차이는 대개 작지만, 리더보드에서 1등과 3등을 가르는 폭보다는 큽니다.
실무에서 이 목록을 다루는 가장 값싼 방법은 평가 실행을 사람이 치는 명령이 아니라 파일로 만드는 것입니다.
# evals/run-2026-08-02.yaml — 이 파일 자체를 커밋한다
harness: lm-eval==0.4.12
model:
provider: local-chat-completions
base_url: http://localhost:8000/v1/chat/completions
name: qwen3-8b
weights_revision: 8f2c1d9
sampling:
temperature: 0
max_gen_toks: 512
tasks:
- name: gsm8k
num_fewshot: 5
apply_chat_template: true
fewshot_as_multiturn: true
report_filters: [strict-match, flexible-extract]
repeats: 3
seed: 1234
이 파일이 있으면 "그 점수 어떻게 냈어요"라는 질문에 커밋 해시 하나로 답할 수 있습니다. 없으면 아무리 성실한 사람이라도 3개월 뒤에는 답하지 못합니다.
채점기가 곧 벤치마크입니다
여기까지 오면 결론이 하나로 모입니다. 벤치마크의 정체성은 데이터셋이 아니라 채점기 에 있습니다. 같은 문항 14,000개를 놓고도 채점기가 다르면 다른 시험입니다.
채점 방식은 크게 셋으로 나뉘고, 각각 다른 실패 양상을 갖습니다.
| 채점 방식 | 작동 | 강점 | 조용히 무너지는 지점 |
|---|---|---|---|
| 정확 일치 | 문자열 비교 | 결정적, 재현 가능 | 정답인데 표기가 달라 오답 처리 |
| 정규식 추출 | 패턴으로 답만 추출 | 형식 자유도 허용 | 패턴이 못 잡는 표현은 전부 0점 |
| 모델 채점 | 다른 LLM이 판정 | 자유 서술 채점 가능 | 심사자의 편향이 점수에 섞임 |
| 실행 기반 | 테스트를 돌려 판정 | 의미적으로 강함 | 테스트가 약하면 틀린 답도 통과 |
정규식 추출의 취약함은 앞서 GSM8K에서 봤습니다. 모델 채점의 편향은 다음 글에서 자세히 다룹니다. 실행 기반 채점의 함정 — 테스트가 약해서 틀린 패치가 통과하는 문제 — 는 SWE-bench 계열에서 실제로 측정되었고, 이것도 다음 글의 주제입니다.
여기서는 실무적 규칙 하나만 남기겠습니다. 새 벤치마크를 도입할 때 데이터셋보다 채점 코드를 먼저 읽으십시오. 문항 몇 개를 눈으로 보는 것보다, 채점 함수 30줄을 읽는 편이 그 벤치마크가 무엇을 재는지 훨씬 정확히 알려 줍니다. 그리고 자기 팀의 실패 사례 열 개를 그 채점기에 통과시켜 보십시오. 사람이 보기에 명백히 맞는 답이 0점으로 찍히는 경우가 하나라도 나오면, 그 벤치마크의 점수는 여러분이 생각하는 것과 다른 것을 재고 있습니다.
마치며 — 조건 없는 숫자는 숫자가 아닙니다
LLaMA 65B의 MMLU가 63.6이자 48.8이었던 사건은 예외적인 사고가 아니라 평가의 기본 성질이었습니다. 벤치마크 점수는 모델·하네스·프롬프트·파서·샘플링 설정이 함께 만들어 낸 하나의 관측치입니다. 그중 하나만 바뀌어도 다른 관측치가 됩니다.
그래서 실무에서 지킬 것은 두 가지뿐입니다. 첫째, 외부 점수를 인용할 때는 그 점수가 어떤 조건에서 나왔는지 확인할 수 있는지 먼저 보십시오. 확인할 수 없다면 그건 데이터가 아니라 주장입니다. 둘째, 자기 팀의 점수를 낼 때는 조건을 파일로 남기십시오. 명령줄에 친 플래그는 3개월이면 사라지지만 커밋된 YAML은 남습니다.
숫자를 신뢰하는 방법은 숫자를 더 많이 모으는 것이 아니라, 그 숫자를 만든 조건을 적어 두는 것입니다.
참고 자료
- Hugging Face — What's going on with the Open LLM Leaderboard? — 세 MMLU 구현의 점수 차이
- EleutherAI lm-evaluation-harness — 태스크 YAML, 필터 체인, CLI
- lm-eval on PyPI — 버전과 릴리스 날짜 확인용
- Stanford CRFM HELM — 다지표 평가 프레임워크
- Inspect AI (UK AI Security Institute) — solver와 scorer 분리 설계
- OpenAI simple-evals — 0-shot CoT 참조 구현
- MMLU-Pro (arXiv 2406.01574) — 프롬프트 민감도 측정
- OpenAI — promptfoo 인수 발표 (2026-03-09)
- AI 코딩 모델 평가에서 신호와 잡음 가려내기 (관련 글)
- AI 에이전트 & LLM 벤치마크 2026 (관련 글)