「GPT-4 심판이 사람과 85% 일치한다」는 문장은 LLM 평가 글에서 가장 자주 인용되는 숫자 중 하나다. 출처는 Zheng 등의 「Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena」(arXiv 2306.05685v4, 2023)이고, 그 논문은 판정 데이터를 통째로 공개해 두었다. 그래서 이 주장은 API 호출 한 번 없이 다시 계산할 수 있다.
이 글이 재현하려는 주장은 하나다. "GPT-4 심판과 사람의 일치도는 85%로, 사람끼리의 일치도 81%보다 높다."
결론부터 적으면 셋이다.
- 재현된다. 논문과 같은 방식으로 세면 1턴 85.9%, 2턴 85.3%로 논문의 85%와 1%p 안쪽이다. 사람끼리는 82.0%·83.4%로 논문의 81%·82%와 맞는다.
- 그런데 그 85%는 무승부를 버리고 센 값이다. 같은 데이터를 무승부까지 세면 66.17%가 되고, 무승부를 절반만 인정하면 78.52%가 된다. 바뀌는 폭이 21%p인데, 논문이 다투는 차이(85% 대 81%)는 4%p다.
- 우연 보정을 걸면 「거의 완벽」이 아니다. 코헨 카파는 무승부를 버렸을 때 0.7120, 무승부를 값으로 셌을 때 0.4827이다. 논문은 카파를 보고하지 않는다.
심판 파이프라인을 어떻게 짜는지는 LLM 평가 파이프라인: 자동화된 품질 보장이, 사람에게 선호를 받아 내는 설계는 선호 데이터 수집 — 점수를 매기지 말고 둘 중 하나를 고르게 한다가 맡는다. 이 글은 공개된 판정 3,355건을 다시 세는 일만 맡는다.
판정 데이터
두 갈래
lmsys/mt_bench_human_judgments는 두 갈래로 되어 있다.
| 갈래 | 행 수 | 무엇인가 |
|---|---|---|
human |
3,355 | 사람 65명(전문가 58 + 저자 7)이 매긴 판정 |
gpt4_pair |
2,400 | 같은 응답 쌍에 대한 GPT-4 심판의 판정 |
두 갈래 모두 문항 80개, 모델 6개(alpaca-13b, claude-v1, gpt-3.5-turbo, gpt-4, llama-13b, vicuna-13b-v1.2), 턴 2개다. 판정이 데이터 안에 이미 들어 있으므로 모델을 부를 일이 없다.
구조에서 눈여겨볼 것이 둘 있다.
데이터에 남은 위치 편향
첫째, gpt4_pair의 winner에는 tie (inconsistent)라는 값이 있다. LMSYS가 GPT-4를 좌우 순서를 바꿔 두 번 돌리고, 두 판정이 어긋난 항목에 붙인 표시다. 즉 위치 편향이 데이터에 이미 기록돼 있다 — 좌우를 바꿔 물었을 때 심판이 말을 바꾸는 성질이고, 아래 위치 편향 절에서 세는 것이 바로 이 라벨이다. 따로 실험할 필요가 없다.
좌우를 없앤 키
둘째, 좌우 배치가 두 갈래에서 다르다. gpt4_pair는 모델 쌍 15개가 각각 한 방향으로만(160행씩) 들어 있는데, human은 15개 쌍이 양쪽 방향 모두 들어 있다. 그래서 사람 쪽은 「먼저 보여 준 쪽을 고르는 경향」을 직접 셀 수 있다.
이 때문에 두 갈래를 맞추려면 좌우를 없애야 한다. 모델 이름을 사전순으로 세우고 승자를 lo/hi/tie로 바꾸면 배치와 무관한 키가 된다.
재현
스크립트
pip install numpy datasets
python judge.py
import random, itertools
from collections import Counter, defaultdict
import numpy as np
from datasets import load_dataset
d = load_dataset("lmsys/mt_bench_human_judgments")
print(f"human {len(d['human'])}행 | gpt4_pair {len(d['gpt4_pair'])}행")
def canon(r): # 좌우를 없애고 (문항, 모델쌍, 턴)으로 세운다
lo, hi = sorted((r["model_a"], r["model_b"])); w = r["winner"]
key = (r["question_id"], lo, hi, r["turn"])
if w.startswith("tie"): return key, "tie"
picked = r["model_a"] if w == "model_a" else r["model_b"]
return key, ("lo" if picked == lo else "hi")
votes, who = defaultdict(list), defaultdict(list)
for r in d["human"]:
k, v = canon(r); votes[k].append(v); who[k].append(r["judge"])
major = {k: ("tie" if len(c) > 1 and c[0][1] == c[1][1] else c[0][0])
for k, vs in votes.items() for c in [Counter(vs).most_common()]}
gpt4 = dict(canon(r) for r in d["gpt4_pair"])
inc = sum(1 for r in d["gpt4_pair"] if r["winner"] == "tie (inconsistent)")
jd = Counter(r["judge"].split("_")[0] for r in d["human"])
print(f"판정자 {len(set(r['judge'] for r in d['human']))}명"
f" (expert {len({r['judge'] for r in d['human'] if r['judge'].startswith('expert')})},"
f" author {len({r['judge'] for r in d['human'] if r['judge'].startswith('author')})})"
f" | 항목 {len(major)}개 | 평균 {np.mean([len(v) for v in votes.values()]):.2f}표")
ht = sum(1 for r in d["human"] if r["winner"].startswith("tie"))
print(f"사람 판정 중 무승부 {ht}/{len(d['human'])} = {100*ht/len(d['human']):.1f}% | "
f"항목 다수결에서 무승부 {sum(1 for v in major.values() if v=='tie')}/{len(major)}")
print(f"GPT-4 좌우를 바꾸면 판정이 뒤집힌 항목: {inc}/{len(d['gpt4_pair'])} = {100*inc/len(d['gpt4_pair']):.2f}%\n")
def stats(pairs, labels):
n = len(pairs); po = sum(a == b for a, b in pairs) / n
ca, cb = Counter(a for a, _ in pairs), Counter(b for _, b in pairs)
pe = sum(ca[l]/n * cb[l]/n for l in labels)
return n, po, (po - pe) / (1 - pe)
indiv = [(v, gpt4[k]) for k, v in (canon(r) for r in d["human"]) if k in gpt4]
items = [(major[k], gpt4[k]) for k in sorted(set(major) & set(gpt4))]
print(f"{'집계 방식':<12}{'무승부 처리':<12}{'n':>6}{'일치율':>10}{'카파':>9}")
for nm, ps in (("개별 투표", indiv), ("항목 다수결", items)):
n, po, k = stats(ps, ["lo", "hi", "tie"]); print(f"{nm:<12}{'제3의 값':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
n, po, k = stats([p for p in ps if "tie" not in p], ["lo", "hi"])
print(f"{nm:<12}{'항목 제거':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
half = np.mean([1.0 if a == b else (0.5 if "tie" in (a, b) else 0.0) for a, b in ps])
print(f"{nm:<12}{'절반 인정':<12}{len(ps):>6}{half*100:>9.2f}%{'—':>9}")
hh = [(vs[i], vs[j]) for k, vs in votes.items() if len(vs) > 1
for i, j in itertools.combinations(range(len(vs)), 2) if who[k][i] != who[k][j]]
print()
n, po, k = stats(hh, ["lo", "hi", "tie"]); print(f"{'사람-사람':<12}{'제3의 값':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
n, po, k = stats([p for p in hh if "tie" not in p], ["lo", "hi"])
print(f"{'사람-사람':<12}{'항목 제거':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
for t in (1, 2): # 논문 Table 4와 같은 축(턴별·개별 투표)
p = [(v, gpt4[k]) for k, v in (canon(r) for r in d["human"]) if k in gpt4 and k[3] == t]
nt = [x for x in p if "tie" not in x]
print(f"turn {t}: S1 {100*sum(a==b for a,b in p)/len(p):.1f}% (n={len(p)}) "
f"S2 {100*sum(a==b for a,b in nt)/len(nt):.1f}% (n={len(nt)})")
rng = random.Random(0); qs = sorted({k[0] for k in major}); by = defaultdict(list)
for i, k in enumerate(sorted(set(major) & set(gpt4))): by[k[0]].append(i)
def boot(sel, lab):
out = []
for _ in range(1000):
ix = [i for q in (rng.choice(qs) for _ in qs) for i in by[q]]
ps = [items[i] for i in ix]
out.append(stats([p for p in ps if sel(p)], lab)[2])
return np.percentile(out, [2.5, 97.5])
lo3, hi3 = boot(lambda p: True, ["lo", "hi", "tie"])
lo2, hi2 = boot(lambda p: "tie" not in p, ["lo", "hi"])
print(f"\n부트스트랩 1,000회 (문항 {len(qs)}개 재표집) 카파 95% 구간")
print(f" 무승부=제3의 값 [{lo3:.4f}, {hi3:.4f}]\n 무승부 제거 [{lo2:.4f}, {hi2:.4f}]")
fp = [r["winner"] == "model_a" for r in d["human"] if not r["winner"].startswith("tie")]
print(f"\n사람이 '먼저 보여 준 쪽'을 고른 비율: {100*np.mean(fp):.2f}% (n={len(fp)}), "
f"z={(np.mean(fp)-0.5)/np.sqrt(0.25/len(fp)):.2f}")
그대로 나온 출력
돌린 결과 전문이다.
human 3355행 | gpt4_pair 2400행
판정자 65명 (expert 58, author 7) | 항목 1814개 | 평균 1.85표
사람 판정 중 무승부 780/3355 = 23.2% | 항목 다수결에서 무승부 552/1814
GPT-4 좌우를 바꾸면 판정이 뒤집힌 항목: 380/2400 = 15.83%
집계 방식 무승부 처리 n 일치율 카파
개별 투표 제3의 값 3355 66.17% 0.4827
개별 투표 항목 제거 2129 85.63% 0.7120
개별 투표 절반 인정 3355 78.52% —
항목 다수결 제3의 값 1814 67.20% 0.5047
항목 다수결 항목 제거 1078 88.40% 0.7670
항목 다수결 절반 인정 1814 80.15% —
사람-사람 제3의 값 2367 65.53% 0.4699
사람-사람 항목 제거 1550 82.71% 0.6534
turn 1: S1 66.6% (n=1689) S2 85.9% (n=1060)
turn 2: S1 65.7% (n=1666) S2 85.3% (n=1069)
부트스트랩 1,000회 (문항 80개 재표집) 카파 95% 구간
무승부=제3의 값 [0.4548, 0.5489]
무승부 제거 [0.7217, 0.8130]
사람이 '먼저 보여 준 쪽'을 고른 비율: 50.21% (n=2575), z=0.22
측정 환경
측정 환경은 Ubuntu 24.04.4 LTS, Linux 6.18 x86_64, Intel Xeon 2.10GHz 4코어, Python 3.11.15, numpy 2.4.6, datasets 5.0.1, huggingface_hub 1.28.0이다. 데이터셋은 lmsys/mt_bench_human_judgments이고 받은 날은 2026-08-26이다. 전체 실행 4.4초로, 부트스트랩 2,000회를 포함한 값이다.
논문 수치와의 대질
S1과 S2
논문 §4.2는 두 가지 셈법을 갈라 놓았다. S1은 무승부와 불일치를 모두 세고, S2는 양쪽 다 비긴 판정이 아닐 때만 센다. 논문이 정의하는 일치도는 「각 유형에서 무작위로 (서로 다른) 개인을 하나씩 뽑았을 때 둘이 같은 판정을 내릴 확률」이므로, 사람 쪽을 다수결로 뭉치지 않고 개별 투표로 세는 것이 논문의 방식이다.
| 항목 | 논문 (Table 4) | 우리 재계산 |
|---|---|---|
| GPT-4 ↔ 사람, 1턴, S2 | 85% (859표) | 85.9% (1,060) |
| GPT-4 ↔ 사람, 2턴, S2 | 85% (864표) | 85.3% (1,069) |
| GPT-4 ↔ 사람, 1턴, S1 | 70% (1,138표) | 66.6% (1,689) |
| GPT-4 ↔ 사람, 2턴, S1 | 70% (1,161표) | 65.7% (1,666) |
| 사람 ↔ 사람, 1턴, S2 | 81% (479표) | 82.0% (772) |
| 사람 ↔ 사람, 2턴, S2 | 82% (474표) | 83.4% (778) |
재현된 줄과 어긋난 줄
S2는 재현된다. 네 줄 모두 1.4%p 안쪽이고, 「GPT-4가 사람끼리보다 더 사람과 맞는다」는 핵심 대소 관계도 그대로다(85.9 대 82.0, 85.3 대 83.4).
S1은 3~4%p 낮게 나온다. 표본 수도 우리 쪽이 크다(1,689 대 1,138). 원인은 무승부·불일치를 세는 규칙일 것으로 보인다 — 논문은 GPT-4의 tie (inconsistent)를 무승부와 다르게 취급했을 수 있고, 공개된 데이터에는 그 둘이 이미 하나의 라벨로 합쳐져 들어와 있어 우리 쪽에서 되돌릴 수 없다. 이 차이는 확정하지 못했고 가설로 남긴다.
여기서 재현이 성공했다는 것보다 중요한 것은 논문 표 안에서 S1과 S2가 이미 15%p 벌어져 있다는 사실이다. 그리고 인용될 때는 대개 큰 쪽만 간다.
무승부 처리
세 가지 규칙
같은 판정 데이터를 세 가지 규칙으로 세어 보면 이렇게 갈린다.
| 무승부 처리 | 개별 투표 | 항목 다수결 |
|---|---|---|
| 제3의 값으로 센다 | 66.17% | 67.20% |
| 무승부가 낀 항목을 버린다 | 85.63% | 88.40% |
| 한쪽만 무승부면 0.5점 | 78.52% | 80.15% |
가장 낮은 66.17%와 가장 높은 88.40% 사이가 22.23%p다. 심판을 바꾼 것도, 데이터를 바꾼 것도 아니다. 집계 규칙 두 개(무승부를 어떻게 셀 것인가, 사람 여러 명을 어떻게 뭉칠 것인가)를 바꿨을 뿐이다.
버려지는 항목
이게 왜 이렇게 크냐면 무승부가 흔하기 때문이다. 사람 판정 3,355건 중 780건, 23.2%가 무승부다. 항목 다수결로 뭉쳐도 1,814개 중 552개가 무승부로 남고, 여기에 GPT-4 쪽 무승부까지 더하면 어느 한쪽이라도 무승부인 항목이 736개(1,814 − 1,078)다. 항목의 40.6%가 「버린다」에서 사라진다. 그리고 사라지는 항목은 무작위가 아니라 두 응답의 품질이 비슷해 판정이 어려운 항목이다. 어려운 문제를 빼고 채점하면 점수가 오르는 것은 당연하고, 그 오름폭이 22%p다.
집계 방식의 기여분도 따로 보인다. 개별 투표에서 항목 다수결로 바꾸면 85.63%가 88.40%로 오른다. 다수결이 개별 판정자의 흔들림을 지워 주기 때문이다. 2.77%p가 여기서 나온다.
코헨 카파
일치율만으로는 부족하다. 선택지가 셋뿐이라 아무렇게나 찍어도 상당히 맞는다. 코헨 카파는 그 우연분을 빼는 지표다.
| 비교 | 무승부 처리 | 일치율 | 카파 | 부트스트랩 95% 구간 |
|---|---|---|---|---|
| GPT-4 ↔ 사람 | 제3의 값 | 67.20% | 0.5047 | [0.4548, 0.5489] |
| GPT-4 ↔ 사람 | 항목 제거 | 88.40% | 0.7670 | [0.7217, 0.8130] |
| 사람 ↔ 사람 | 제3의 값 | 65.53% | 0.4699 | — |
| 사람 ↔ 사람 | 항목 제거 | 82.71% | 0.6534 | — |
구간은 문항 80개를 통째로 재표집하는 방식으로 냈다. 같은 문항의 판정들은 서로 독립이 아니므로 판정 하나하나를 재표집하면 구간이 실제보다 좁게 나온다.
카파를 관습적인 눈금(0.410.60 보통, 0.610.80 상당, 0.81~1.00 거의 완벽)에 대면 이렇게 읽힌다.
- 무승부를 버린 0.7670은 「상당한 일치」이고 「거의 완벽」은 아니다. 88.40%라는 숫자만 보면 후자로 읽힌다.
- 무승부를 세면 0.5047, 「보통」이다. 구간 [0.4548, 0.5489]이 0.6에 닿지도 않는다.
- 사람끼리도 같은 방향으로 낮아진다(0.6534 / 0.4699). GPT-4가 사람보다 카파가 높다는 대소 관계는 두 규칙 모두에서 유지된다.
논문의 주장 — GPT-4가 사람끼리만큼, 혹은 그 이상으로 사람과 맞는다 — 은 우연 보정 뒤에도 살아남는다. 무너지는 것은 그 수치의 절대적인 크기에 대한 인상이다.
위치 편향
심판과 사람
gpt4_pair에 이미 기록돼 있다. 2,400건 중 380건, 15.83%가 좌우를 바꾸자 판정이 뒤집혔다.
사람은 어떨까. human 쪽은 같은 모델 쌍이 양쪽 배치로 다 들어 있으니 「먼저 보여 준 쪽」을 고른 비율을 셀 수 있다. 무승부가 아닌 2,575건에서 50.21%다. 50%와의 차이가 0.21%p, 정규근사 z = 0.22 — 우연과 구별되지 않는다.
| 위치에 따른 흔들림 | |
|---|---|
| GPT-4 심판 | 15.83%의 항목에서 순서를 바꾸면 판정이 뒤집힌다 |
| 사람 65명 | 먼저 보여 준 쪽을 고른 비율 50.21% (z = 0.22) |
논문의 65%
논문 §3.1도 위치 편향을 따로 실험했고 기본 프롬프트에서 GPT-4의 일관성을 65.0%로 보고한다(첫 번째 위치 선호 30.0%, 두 번째 5.0%). 우리가 잰 15.83%와는 크게 다른데 같은 것을 잰 것이 아니다. 논문의 65%는 위치 편향만 보려고 따로 고른 소규모 집합에서 잰 값이고, 15.83%는 MT-bench 판정 2,400건 전체에서의 비율이다. 방향은 같고 크기는 설정에 달렸다.
실무에 옮길 것은 하나다. 심판을 쓸 때 좌우를 바꿔 두 번 돌리는 것은 선택이 아니다. 한 번만 돌리면 그중 6분의 1은 동전 던지기다.
꺾이는 지점과 한계
보고 규칙의 진폭
여기까지가 보고 규칙의 문제이고 여기서부터가 심판 성능의 문제다 — 무승부 처리와 집계 방식만으로 22.23%p가 움직이는데, 이 논문이 다투는 GPT-4와 사람의 차이는 3.9%p다.
숫자로 놓으면 이렇다.
| 무엇이 움직이나 | 크기 |
|---|---|
| 무승부 처리 규칙 (제3의 값 ↔ 제거, 다수결 기준) | 21.20%p |
| 사람 집계 방식 (개별 투표 ↔ 항목 다수결, 무승부 제거) | 2.77%p |
| 둘을 합친 최대 진폭 | 22.23%p |
| GPT-4 ↔ 사람과 사람 ↔ 사람의 차이 (S2, 1턴) | 3.9%p |
보고 규칙이 논점보다 5.7배 크다. 그래서 심판 일치도를 무승부 정책 없이 적은 숫자는 해석할 수가 없다. 「우리 심판은 사람과 87% 일치합니다」는 그 자체로는 아무 정보가 아니다 — 무승부를 버렸는지, 몇 %가 버려졌는지를 함께 적어야 비로소 다른 숫자와 비교된다.
표본과 셈법의 한계
축소해서 다시 풀었기 때문에 확인하지 못한 것이 넷이다.
- 한 벤치마크, 한 심판, 여섯 모델이다. 문항 80개에 2023년의 모델 6개이고 심판은 GPT-4 하나다. 카파 0.4827이 다른 심판이나 다른 과제에서도 그 근처라는 근거가 이 데이터에는 없다.
- 영어 데이터다. 한국어 응답을 심판시켰을 때 같은 값이 나올지는 이 실험이 말해 주지 않는다.
- S1의 3~4%p 불일치는 원인을 확정하지 못했다. 무승부와 불일치를 가르는 논문 쪽 규칙이 공개 데이터에서 이미 합쳐진 뒤라 되돌릴 수 없다. 가설로만 적었다.
- 논문의 표본 수와 우리 표본 수가 다르다. 사람↔사람에서 논문은 479·474표, 우리는 772·778쌍이다. 논문이 쌍을 추출하는 방식이 본문에 없어 그대로 맞추지 못했다. 비율이 1~2%p 안에서 맞으므로 같은 것을 재고 있다고 보지만, 표본 수가 다르다는 것은 셈법이 완전히 같지는 않다는 뜻이다.
지표의 한계
- 사람의 위치 편향이 0이라고 말한 것이 아니다. 2,575건에서 z = 0.22이므로 「이 표본으로는 잡히지 않는다」가 맞다. 이 크기의 표본에서 잡히려면 대략 2%p는 넘어야 한다.
- 카파도 완전하지 않다. 한쪽 라벨이 몰려 있으면 일치율이 높아도 카파가 낮게 나오는 성질이 있다. 이 데이터는 무승부가 23% 섞여 있어 심한 편은 아니지만, 카파 하나를 절대 눈금으로 쓰는 것은 일치율 하나를 쓰는 것과 같은 종류의 잘못이다.
무승부 정책
논문의 주장은 재현된다. GPT-4 심판은 이 데이터에서 사람만큼 사람과 맞고, 우연을 보정해도 그렇다.
다시 계산해서 얻은 것은 그 주장의 크기를 읽는 눈금이다. 85%는 데이터의 40%를 버리고 센 값이고, 버린 것은 하필 판정이 어려운 항목이며, 우연을 빼면 카파 0.7120이다. 그리고 그 심판은 좌우를 바꾸면 여섯 중 하나꼴로 말을 바꾼다.
심판 성능을 비교하려면 같은 눈금을 써야 한다. 그 눈금의 이름은 무승부 정책이다.
읽어주셔서 감사합니다. 😊

