- 심사자 점수는 올랐는데 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 평가를 감으로 하지 않는 법
- 위 코드의 카파 계산은 표준 정의를 그대로 구현한 것이며, 예시 숫자는 설명을 위해 제가 구성한 가상의 라벨입니다. 실제 팀 데이터로 바꿔 돌려 보시기를 권합니다.
현재 단락 (1/62)
프롬프트를 한 번 손보고, 사내 평가 대시보드의 충실도 점수가 0.82에서 0.90으로 올랐습니다. 배포했습니다. 그다음 주에 상담 이관 건수가 늘었습니다. 로그를 열어 보면 답변...