느낌으로 모델을 바꾸면 생기는 일
한 줄 요약
평가 하네스 없이 모델이나 프롬프트를 바꾸는 것은 테스트 없이 리팩터링하는 것과 같습니다. 좋아졌다는 느낌과 실제로 좋아진 것은 다릅니다.
왜 이게 필요했나
새 모델이 나왔습니다. 몇 개 물어보니 답이 더 낫습니다. 바꿉니다. 2주 뒤 특정 유형의 질문에서 품질이 떨어졌다는 문의가 들어옵니다. 되돌릴지 판단해야 하는데, 근거가 없습니다.
이 상황이 반복되는 이유는 LLM 출력이 결정적이지 않고 평가 기준이 주관적이기 때문입니다. 단위 테스트처럼 "정확히 이 값" 을 기대할 수 없는 경우가 많습니다. 게다가 사람의 기억은 최근 몇 건에 크게 휘둘립니다. 인상 깊은 실패 하나가 전체 품질에 대한 판단을 뒤집습니다.
그래도 잴 수는 있습니다. 완벽하지 않아도 회귀를 잡을 만큼은 됩니다.
평가 하네스의 네 부분
평가셋. 실제 트래픽에서 뽑은 대표 질문들과 기대 결과입니다. 30~100건이면 회귀 감지에 충분히 시작할 수 있습니다. 중요한 것은 실제 분포를 반영하는 것입니다 — 잘 되는 케이스만 모으면 회귀를 못 잡습니다. 트래픽 유형별 비율을 맞추고, 어렵다고 알려진 케이스를 최소 20% 넣습니다.
채점기. 유형에 따라 다릅니다.
| 출력 유형 | 채점 방법 | 주의 |
|---|---|---|
| JSON·분류·추출 | 정확 일치 | 필드 순서·공백을 정규화하고 비교 |
| 짧은 사실 답 | 키워드 포함, 정규식 | 동의어를 목록으로 허용 |
| 긴 서술 | LLM 심판 | 심판 모델과 프롬프트를 버전 고정 |
| 코드 | 실행 후 테스트 통과 | 가장 신뢰할 만하다 |
LLM 심판은 편향이 있습니다. 긴 답을 짧은 답보다 높게 주고, 자기 계열 모델의 문체를 선호하며, 첫 번째로 보여 준 후보를 선호합니다. 그래서 A/B 비교를 시킬 때는 순서를 무작위로 바꿔 두 번 물어보고 결과가 뒤집히면 무승부로 셉니다.
기준선. 현재 운영 중인 조합의 점수를 저장해 둡니다. 이것 없이는 비교할 대상이 없습니다. 모델·프롬프트·온도·평가셋 버전을 함께 기록해야 나중에 재현됩니다.
게이트. 새 조합의 점수가 기준선보다 일정 폭 이상 낮으면 실패로 판정합니다.
임계를 정하는 법 — 노이즈를 먼저 잰다
임계를 감으로 정하면 안 됩니다. 같은 설정으로 세 번 돌려 흔들림 폭을 먼저 재고, 그보다 큰 값을 임계로 씁니다.
# 같은 모델·프롬프트로 3회
run 1: 0.847 run 2: 0.861 run 3: 0.839
→ 노이즈 폭 약 2.2%p → 임계는 3~4%p 로 잡는다
평가셋이 작으면 노이즈가 큽니다. 50건짜리 평가셋에서 1건이 뒤집히면 2%p 가 움직입니다. 통계적으로 말하면 50건에서 관측한 0.85 의 95% 신뢰구간은 대략 0.75–0.92 입니다. 작은 평가셋에서 1–2%p 차이로 판단하면 안 됩니다.
온도를 0 으로 두어도 완전히 결정적이지는 않습니다. 배치 크기와 부동소수점 누적 순서에 따라 결과가 달라질 수 있어, 같은 입력에 다른 출력이 나오는 일이 있습니다.
품질만 재면 절반이다
품질과 함께 지연·비용을 같은 하네스에서 재야 합니다.
| 축 | 재는 값 | 게이트 예시 |
|---|---|---|
| 품질 | 평가셋 점수 | 기준선 −3%p 이하면 실패 |
| 지연 | p50, p95, 첫 토큰까지 | p95 가 기준선의 1.5배 넘으면 실패 |
| 비용 | 1,000건당 토큰 비용 | 기준선의 2배 넘으면 사람이 승인 |
평균이 아니라 p95 를 봐야 합니다. 평균 지연은 좋은데 p95 가 세 배가 되는 변경이 실제로 있습니다. 사용자는 평균을 겪지 않고 자기 요청의 지연을 겪습니다.
첫 토큰까지의 시간(TTFT)도 따로 잽니다. 스트리밍 UI 에서는 전체 완료 시간보다 TTFT 가 체감 품질을 좌우합니다.
현장에서 만나는 모습
하네스를 CI 에 넣는 것이 결정적입니다. 프롬프트 변경 PR 에서 자동으로 돌고 회귀면 머지가 막히면, 사람이 기억할 필요가 없어집니다. 프롬프트를 코드처럼 다루는 것의 실제 의미가 이것입니다.
평가셋은 살아 있어야 합니다. 사용자 문의로 발견된 실패 케이스를 평가셋에 추가하는 루프를 만들면, 같은 실패가 두 번 나지 않습니다. 회귀 테스트의 원리와 같습니다.
다만 함정이 하나 있습니다. 평가셋에 맞춰 프롬프트를 계속 고치면 평가셋에 과적합됩니다. 실제 트래픽에서는 나빠지는데 점수만 오릅니다. 이것을 막으려면 평가셋을 둘로 나눠, 개발용(반복해서 보는 것)과 검증용(가끔만 보는 것)을 분리합니다. 검증용 점수가 개발용을 따라오지 않기 시작하면 과적합 신호입니다.
무엇을 평가셋에 넣을 것인가
평가셋을 만들 때 가장 흔한 실수는 잘 되는 것만 모으는 것 입니다. 그러면 점수가 늘 높고 회귀도 안 잡힙니다. 다섯 종류를 의도적으로 섞습니다.
- 대표 케이스 — 실제 트래픽에서 가장 많은 유형. 전체의 절반쯤.
- 경계 케이스 — 입력이 비었을 때, 아주 길 때, 다른 언어가 섞였을 때.
- 알려진 실패 — 과거에 문의로 들어온 것. 여기가 회귀 감지의 핵심입니다.
- 거부해야 하는 것 — 답하면 안 되는 질문. 모델이 얌전해졌는지 확인합니다.
- 함정 — 전제가 틀린 질문("2026년 노벨상 받은 그 사람 말이야"). 모델이 전제를 되묻는지, 아니면 지어내는지를 봅니다.
기대 결과를 쓸 때는 하나의 정답이 아니라 허용 집합 으로 씁니다.
{"id": "refund-01",
"input": "환불 언제까지 돼요?",
"must_include": ["7일", "영업일"],
"must_not_include": ["환불 불가", "죄송"],
"max_tokens": 200}
must_not_include 가 의외로 유용합니다. 품질 사고의 상당수는 "없어야 할 말이
나온 것" 이지 "있어야 할 말이 빠진 것" 이 아닙니다.
다음 실습에서 할 것
평가셋을 불러 러너를 만들고, 정확 일치와 부분 점수 채점기를 각각 구현하고, 지연 SLO 위반을 세고, 같은 설정으로 세 번 돌려 노이즈 폭을 재고, 기준선을 저장한 뒤 회귀를 판정하고, 마지막에 CI 게이트 스크립트로 만듭니다.