Split View: 평가 주도 개발에서 가장 먼저 보정해야 하는 것은 심사자입니다
평가 주도 개발에서 가장 먼저 보정해야 하는 것은 심사자입니다
- 심사자 점수는 올랐는데 CS 문의도 같이 늘었다
- 평가 주도 개발이 TDD와 결정적으로 다른 지점
- 심사자는 코드가 아니라 배포된 모델입니다
- 골든셋 50~100개로 심사자를 보정하는 절차
- 일치율 88퍼센트라는 숫자를 그대로 믿으면 안 되는 이유
- 심사자 3~5개가 20~30개보다 낫다
- 에이전트는 최종 답만으로 채점할 수 없습니다
- 무엇을 게이트로 걸고 무엇을 대시보드에만 둘까
- 참고 자료
심사자 점수는 올랐는데 CS 문의도 같이 늘었다
프롬프트를 한 번 손보고, 사내 평가 대시보드의 충실도 점수가 0.82에서 0.90으로 올랐습니다. 배포했습니다. 그다음 주에 상담 이관 건수가 늘었습니다. 로그를 열어 보면 답변은 더 길고, 더 공손하고, 더 자신 있게 틀려 있습니다.
이 상황에서 대부분의 팀은 "평가가 부족했다"는 결론을 내고 평가셋을 키웁니다. 100개를 500개로 늘리고, 항목을 다섯 개에서 스무 개로 쪼갭니다. 그런데 실제 원인은 평가셋의 크기가 아닙니다. 채점을 맡긴 모델이 길고 공손한 답변에 점수를 더 주고 있었고, 아무도 그 사실을 확인한 적이 없다는 것입니다. 평가셋을 다섯 배로 키우면 그 편향도 다섯 배 더 정밀하게 재게 됩니다.
평가 주도 개발이 TDD와 결정적으로 다른 지점
Airbnb 엔지니어링이 2026년 7월 28일에 공개한 Eval-driven development는 평가를 부가 작업이 아니라 일급 엔지니어링 분야로 다뤄야 한다고 말합니다. 여기까지는 TDD와 같은 구호입니다.
다른 점은 판정자입니다. TDD의 판정자는 assert이고, 이 판정자는 틀리지 않습니다. 평가 주도 개발의 판정자 상당 부분은 LLM이고, 이 판정자는 틀립니다. 그것도 무작위로 틀리는 게 아니라 특정 방향으로 일관되게 틀립니다. 길이, 형식, 자신감, 자기 계열 모델 선호 같은 축입니다.
그래서 순서가 하나 늘어납니다. 테스트를 먼저 쓰는 것으로는 부족하고, 테스트가 자를 제대로 재는지부터 확인해야 합니다.
심사자는 코드가 아니라 배포된 모델입니다
이 관점을 받아들이면 심사자를 다루는 방식이 바뀝니다. 심사자 프롬프트는 설정 파일이 아니라 배포물입니다. 버전이 붙어야 하고, 바뀔 때 회귀 검사를 받아야 하고, 어떤 모델과 어떤 온도로 돌았는지가 결과와 함께 저장되어야 합니다.
Airbnb 글이 제시하는 세 층 구조가 여기서 의미를 가집니다. 결정적 코드 검사가 1층, 심사자 모델이 2층, 사람이 3층입니다. 핵심은 층을 나눴다는 것 자체가 아니라, 3층이 2층을 지속적으로 감사한다는 관계입니다. 사람은 모든 출력을 보지 않습니다. 대신 심사자가 사람과 얼마나 어긋나는지를 봅니다.
1층에 무엇을 넣을지도 중요합니다. JSON 스키마 위반, 금칙어, 인용 없는 단정, 응답 길이 상한처럼 규칙으로 표현할 수 있는 것은 전부 1층에서 걸러야 합니다. 이건 비용 문제가 아니라 신뢰 문제입니다. 규칙으로 잡을 수 있는 실패를 심사자에게 맡기면, 심사자가 틀렸을 때 그 실패가 통과합니다. 결정적으로 판정 가능한 것을 확률적 판정자에게 넘기는 건 언제나 손해입니다.
골든셋 50~100개로 심사자를 보정하는 절차
Airbnb 글은 심사자 보정을 다섯 단계로 정리합니다. 골든셋을 만들고, 심사자를 그 위에 돌리고, 일치도를 재고, 불일치를 뜯어보고, 주기적으로 재보정합니다. 권장 규모는 "50~100개의 골든셋"이고, 라벨링은 "주제 전문가가 라벨한 20~100행"에서 시작하라고 되어 있습니다.
이 숫자가 작아 보이는 데는 이유가 있습니다. 골든셋은 모델의 성능을 재는 자가 아니라 심사자의 성능을 재는 자입니다. 자를 검사하는 데는 표본이 많이 필요하지 않고, 대신 실패 사례가 반드시 섞여 있어야 합니다. 좋은 답만 모은 골든셋에서는 아무것도 못 걸러내는 심사자도 만점을 받습니다.
같은 글의 실제 사례에서, 처음 만든 충실도 심사자는 사람과 78퍼센트 일치했습니다. 루브릭을 다듬고 예시를 몇 개 넣은 뒤 88퍼센트로 올라갔고, 그제서야 대규모 실행에 투입됐습니다.
일치율 88퍼센트라는 숫자를 그대로 믿으면 안 되는 이유
여기서 한 단계 더 필요합니다. 단순 일치율은 라벨 분포가 치우치면 부풀려집니다. 합격이 90퍼센트인 평가에서 무조건 합격을 찍는 심사자도 일치율 90퍼센트를 받습니다. 그래서 우연 일치를 걷어낸 지표를 봐야 하고, Airbnb 글도 코헨 카파나 크리펜도르프 알파를 쓰라고 명시합니다.
"""심사자 보정: 일치율만 보지 말고 카파를 함께 본다."""
from collections import Counter
from typing import Sequence
def cohens_kappa(human: Sequence[str], judge: Sequence[str]) -> float:
assert len(human) == len(judge) and human, "짝지은 라벨이 필요합니다"
n = len(human)
po = sum(h == j for h, j in zip(human, judge)) / n # 관측 일치율
hc, jc = Counter(human), Counter(judge)
pe = sum((hc[k] / n) * (jc[k] / n) for k in set(hc) | set(jc)) # 우연 일치율
return 1.0 if pe == 1 else (po - pe) / (1 - pe)
def calibration_report(rows):
"""rows: [(input_id, human_label, judge_label), ...]"""
human = [r[1] for r in rows]
judge = [r[2] for r in rows]
k = cohens_kappa(human, judge)
po = sum(h == j for h, j in zip(human, judge)) / len(rows)
# 불일치 방향까지 봐야 편향을 잡을 수 있다
lenient = sum(h == "fail" and j == "pass" for h, j in zip(human, judge))
strict = sum(h == "pass" and j == "fail" for h, j in zip(human, judge))
return {
"agreement": round(po, 3),
"kappa": round(k, 3),
"judge_too_lenient": lenient, # 놓친 실패 — 가장 비싼 오류
"judge_too_strict": strict,
"verdict": "ship" if k >= 0.6 and lenient == 0 else "recalibrate",
}
rows = (
[(f"g{i}", "pass", "pass") for i in range(80)]
+ [(f"b{i}", "fail", "pass") for i in range(8)] # 심사자가 놓친 실패
+ [(f"c{i}", "fail", "fail") for i in range(12)]
)
print(calibration_report(rows))
# {'agreement': 0.92, 'kappa': 0.694, 'judge_too_lenient': 8, ..., 'verdict': 'recalibrate'}
일치율 92퍼센트인데도 판정이 재보정인 이유는 마지막 줄에 있습니다. 사람이 실패라고 본 20건 중 8건을 심사자가 통과시켰습니다. 이 심사자를 그대로 CI에 걸면, 팀은 앞으로 그 8건 유형의 실패를 영영 보지 못합니다.
심사자 3~5개가 20~30개보다 낫다
Airbnb 글의 표현을 그대로 옮기면, 잘 보정된 심사자 3~5개가 잡음 섞인 심사자 20~30개를 이깁니다. 이 문장은 평가 항목을 줄이라는 뜻이 아니라, 보정 비용을 감당할 수 있는 만큼만 항목을 만들라는 뜻으로 읽는 편이 정확합니다.
심사자 하나를 유지하는 비용은 프롬프트 한 덩이가 아닙니다. 골든셋 50~100건, 라벨링에 들어간 전문가 시간, 실패 유형이 바뀔 때마다의 재보정, 그리고 심사자 프롬프트가 바뀔 때 회귀를 확인할 파이프라인까지가 한 세트입니다. 이 세트를 감당할 수 없는 항목은 만들지 않는 편이 낫습니다. 보정되지 않은 심사자는 없느니만 못한 게 아니라, 잘못된 방향을 가리키는 나침반이기 때문입니다.
에이전트는 최종 답만으로 채점할 수 없습니다
에이전트로 넘어가면 축이 하나 더 생깁니다. 같은 글은 에이전트 평가가 최종 출력, 중간 추론 단계, 도구 호출과 인자라는 세 차원에 걸친다고 정리합니다.
이유는 단순합니다. 최종 답이 맞았다는 사실만으로는 그 실행이 재현될지 알 수 없습니다. 검색을 세 번 헛돌고 우연히 캐시된 답을 맞힌 실행과, 첫 호출에서 올바른 인자로 올바른 도구를 부른 실행은 같은 점수를 받지만 완전히 다른 시스템입니다. 앞의 것은 다음 주에 무너집니다.
실무에서는 도구 호출 이름과 인자를 정규화해 궤적으로 저장하고, 최소한 세 가지를 따로 셉니다. 불필요한 도구 호출 수, 잘못된 인자로 인한 재시도 수, 그리고 같은 입력을 다시 넣었을 때 궤적이 얼마나 달라지는지입니다. 세 번째가 특히 중요합니다. 같은 질문에 대해 궤적이 매번 크게 달라진다면 그 에이전트는 아직 방법을 모르는 상태이고, 지금의 성공률은 다음 배포에서 유지된다는 보장이 없습니다.
궤적 채점에 심사자를 쓸 때는 루브릭을 결과가 아니라 절차로 씁니다. "좋은 답인가"가 아니라 "이 단계에서 이 도구를 부른 것이 필요했는가"를 묻는 식입니다. 절차 질문은 답이 짧고 근거가 로그에 남아 있어서, 같은 심사자라도 결과 질문보다 사람과의 일치도가 눈에 띄게 높게 나옵니다.
무엇을 게이트로 걸고 무엇을 대시보드에만 둘까
마지막은 배포 연결입니다. 모든 지표를 CI 게이트로 걸면 파이프라인이 상시 빨간불이 되고, 아무것도 걸지 않으면 평가는 장식이 됩니다. 구분선은 이렇게 긋는 편이 실용적입니다.
| 성격 | 예 | 배치 |
|---|---|---|
| 결정적이고 위반이 곧 사고 | 금칙 표현, 개인정보 노출, 스키마 위반 | CI 게이트, 1건이라도 실패면 차단 |
| 심사자 기반이고 보정 완료 | 충실도, 지시 준수 | CI 게이트, 기준선 대비 하락 폭으로 판정 |
| 심사자 기반이고 보정 미완 | 어조, 도움됨 정도 | 대시보드 관찰용, 차단 금지 |
| 분포에 민감 | 응답 길이, 도구 호출 수 | 경보만, 추세로 판단 |
그리고 프로덕션에서 표본을 계속 끌어와야 합니다. Airbnb 글은 비식별화된 실 트래픽의 5퍼센트를 매일 샘플링한다고 밝히고 있습니다. 평가셋은 만든 날부터 낡기 시작하므로, 새 실패 유형이 골든셋으로 흘러 들어오는 경로가 없으면 몇 달 뒤에는 예전 문제만 반복해서 재게 됩니다.
평가 주도 개발의 첫 작업은 평가셋을 만드는 것이 아닙니다. 지금 쓰고 있는 심사자가 사람과 얼마나 어긋나는지를 숫자로 아는 것입니다. 그 숫자를 모른다면, 대시보드에 찍히는 모든 점수는 아직 아무 의미가 없습니다.
참고 자료
- Eval-driven development: Lessons from evaluating GenAI at scale — Airbnb Tech Blog, 2026-07-28 — 세 층 구조, 골든셋 50~100건, 78퍼센트에서 88퍼센트로의 보정 사례, 5퍼센트 일일 샘플링이 모두 이 글에서 나옵니다.
- 이 블로그의 관련 글: 우리 서비스용 평가셋 만들기, LLM 평가를 감으로 하지 않는 법
- 위 코드의 카파 계산은 표준 정의를 그대로 구현한 것이며, 예시 숫자는 설명을 위해 제가 구성한 가상의 라벨입니다. 실제 팀 데이터로 바꿔 돌려 보시기를 권합니다.
In Eval-Driven Development, the First Thing to Calibrate Is the Judge
- The Judge Score Went Up and So Did the Support Tickets
- Where Eval-Driven Development Decisively Differs from TDD
- A Judge Is Not Code, It Is a Deployed Model
- Calibrating the Judge with a Golden Set of 50 to 100 Examples
- Why You Should Not Take 88 Percent Agreement at Face Value
- Three to Five Judges Beat Twenty to Thirty
- You Cannot Grade an Agent on the Final Answer Alone
- What to Gate On and What to Leave on the Dashboard
- References
The Judge Score Went Up and So Did the Support Tickets
You touch the prompt once, and the faithfulness score on the internal eval dashboard climbs from 0.82 to 0.90. You ship. The next week, escalations to human agents go up. Open the logs and the answers are longer, more polite, and more confidently wrong.
At this point most teams conclude "our evals were not thorough enough" and grow the eval set. A hundred cases become five hundred, five criteria are split into twenty. But the real cause is not the size of the eval set. The model you handed the grading to was awarding more points to long, polite answers, and nobody had ever checked that. Grow the eval set fivefold and you simply measure that bias five times more precisely.
Where Eval-Driven Development Decisively Differs from TDD
Eval-driven development, published by Airbnb Engineering on July 28, 2026, argues that evaluation should be treated as a first-class engineering discipline rather than a side task. Up to this point the slogan is the same as TDD.
The difference is the arbiter. In TDD the arbiter is assert, and this arbiter does not get things wrong. In eval-driven development a large share of the arbitration is done by an LLM, and this arbiter does get things wrong. Not randomly, either — it gets things wrong consistently in particular directions: length, formatting, confidence, a preference for models from its own family.
So one step gets added to the order. Writing the test first is not enough. You have to first confirm that the test measures with a straight ruler.
A Judge Is Not Code, It Is a Deployed Model
Once you accept this framing, the way you handle a judge changes. A judge prompt is not a config file, it is something you deploy. It needs a version, it needs a regression check when it changes, and which model and which temperature it ran with has to be stored alongside the results.
The three-layer structure the Airbnb post lays out takes on meaning here. Deterministic code checks are layer one, the judge model is layer two, humans are layer three. The point is not that the layers were separated but the relationship: layer three continuously audits layer two. Humans do not look at every output. They look at how far the judge diverges from humans.
What you put in layer one matters too. JSON schema violations, banned terms, assertions without citations, response length caps — everything expressible as a rule should be filtered out at layer one. This is not a cost question but a trust question. If you hand a failure that a rule could catch to the judge, then when the judge is wrong that failure passes. Handing something deterministically decidable to a probabilistic decider is always a losing trade.
Calibrating the Judge with a Golden Set of 50 to 100 Examples
The Airbnb post organizes judge calibration into five steps. Build a golden set, run the judge over it, measure agreement, dig into the disagreements, and recalibrate periodically. The recommended scale is "a golden set of 50 to 100," and it says to start labeling from "20 to 100 rows labeled by subject-matter experts."
There is a reason these numbers look small. A golden set is not a ruler for the model's performance, it is a ruler for the judge's performance. Checking a ruler does not take many samples. What it does take is failure cases mixed in. On a golden set assembled only from good answers, even a judge that catches nothing scores full marks.
In the real example from the same post, the first faithfulness judge agreed with humans 78 percent of the time. After the rubric was refined and a few examples were added it rose to 88 percent, and only then was it put to work at scale.
Why You Should Not Take 88 Percent Agreement at Face Value
One more step is needed here. Plain agreement inflates when the label distribution is skewed. On an eval where 90 percent pass, a judge that stamps "pass" unconditionally also earns 90 percent agreement. So you have to look at a metric with chance agreement stripped out, and the Airbnb post explicitly says to use Cohen's kappa or Krippendorff's alpha.
"""Judge calibration: never look at agreement alone, look at kappa alongside it."""
from collections import Counter
from typing import Sequence
def cohens_kappa(human: Sequence[str], judge: Sequence[str]) -> float:
assert len(human) == len(judge) and human, "paired labels are required"
n = len(human)
po = sum(h == j for h, j in zip(human, judge)) / n # observed agreement
hc, jc = Counter(human), Counter(judge)
pe = sum((hc[k] / n) * (jc[k] / n) for k in set(hc) | set(jc)) # chance agreement
return 1.0 if pe == 1 else (po - pe) / (1 - pe)
def calibration_report(rows):
"""rows: [(input_id, human_label, judge_label), ...]"""
human = [r[1] for r in rows]
judge = [r[2] for r in rows]
k = cohens_kappa(human, judge)
po = sum(h == j for h, j in zip(human, judge)) / len(rows)
# you only catch the bias if you look at the direction of the disagreement too
lenient = sum(h == "fail" and j == "pass" for h, j in zip(human, judge))
strict = sum(h == "pass" and j == "fail" for h, j in zip(human, judge))
return {
"agreement": round(po, 3),
"kappa": round(k, 3),
"judge_too_lenient": lenient, # missed failures — the most expensive error
"judge_too_strict": strict,
"verdict": "ship" if k >= 0.6 and lenient == 0 else "recalibrate",
}
rows = (
[(f"g{i}", "pass", "pass") for i in range(80)]
+ [(f"b{i}", "fail", "pass") for i in range(8)] # failures the judge missed
+ [(f"c{i}", "fail", "fail") for i in range(12)]
)
print(calibration_report(rows))
# {'agreement': 0.92, 'kappa': 0.694, 'judge_too_lenient': 8, ..., 'verdict': 'recalibrate'}
The reason the verdict is "recalibrate" despite 92 percent agreement is in the last line. Of the 20 cases humans judged as failures, the judge passed 8. Wire this judge into CI as it stands and the team will never again see that class of failure.
Three to Five Judges Beat Twenty to Thirty
To carry over the Airbnb post's phrasing directly, three to five well-calibrated judges beat twenty to thirty noisy ones. It is more accurate to read that sentence not as "have fewer criteria" but as "create only as many criteria as you can afford to calibrate."
The cost of maintaining a single judge is not one blob of prompt. It is a set: 50 to 100 golden cases, the expert hours that went into labeling, recalibration every time the failure modes shift, and a pipeline that checks for regressions when the judge prompt changes. A criterion whose set you cannot afford is better left uncreated. An uncalibrated judge is not merely worse than nothing, it is a compass pointing the wrong way.
You Cannot Grade an Agent on the Final Answer Alone
Move to agents and another axis appears. The same post frames agent evaluation as spanning three dimensions: the final output, the intermediate reasoning steps, and the tool calls with their arguments.
The reason is simple. The fact that the final answer was right tells you nothing about whether that run will reproduce. A run that spun through three fruitless searches and happened to land on a cached answer and a run that called the right tool with the right arguments on the first call get the same score but are completely different systems. The first one collapses next week.
In practice you normalize tool call names and arguments, store them as a trajectory, and count at least three things separately: the number of unnecessary tool calls, the number of retries caused by wrong arguments, and how much the trajectory changes when you feed the same input again. The third matters most. If the trajectory varies wildly for the same question every time, that agent does not yet know the method, and there is no guarantee today's success rate survives the next deploy.
When you use a judge to grade trajectories, write the rubric about procedure rather than outcome. Not "is this a good answer" but "was calling this tool at this step necessary." Procedural questions have short answers whose evidence remains in the log, so the same judge lands noticeably higher agreement with humans than it does on outcome questions.
What to Gate On and What to Leave on the Dashboard
The last piece is the connection to deployment. Gate on every metric in CI and the pipeline stays permanently red; gate on nothing and evaluation becomes decoration. Drawing the line this way is practical.
| Character | Example | Placement |
|---|---|---|
| Deterministic, and a violation is an incident | Banned phrasing, PII exposure, schema violations | CI gate; block on even one failure |
| Judge-based and calibrated | Faithfulness, instruction following | CI gate; judge by the size of the drop against baseline |
| Judge-based and not yet calibrated | Tone, helpfulness | Dashboard observation only; never block |
| Sensitive to distribution | Response length, tool call count | Alerts only; judge by trend |
And you have to keep pulling samples from production. The Airbnb post states that it samples 5 percent of de-identified live traffic daily. An eval set starts aging the day you build it, so without a path for new failure modes to flow into the golden set, a few months later you are repeatedly measuring only the old problems.
The first task of eval-driven development is not building an eval set. It is knowing, as a number, how far the judge you are using right now diverges from humans. Without that number, every score on the dashboard means nothing yet.
References
- Eval-driven development: Lessons from evaluating GenAI at scale — Airbnb Tech Blog, 2026-07-28 — the three-layer structure, the golden set of 50 to 100, the calibration example going from 78 percent to 88 percent, and the 5 percent daily sampling all come from this post.
- Related posts on this blog: Building an Eval Set for Your Own Service, How to Stop Evaluating LLMs on Vibes
- The kappa computation above implements the standard definition directly, and the example numbers are fictional labels I put together for illustration. I recommend swapping in your own team data and running it.