평가(Evals)


프롬프트를 수정하고, 한 번 시도해보니 답변이 더 나아 보여서 배포합니다. 그리고 일주일 후 사용자들은 몰래 나빠진 케이스들과 마주치게 됩니다. **평가(eval)**는 이 문제를 해결하는 방법입니다. AI 기능이 실제로 동작하는지 확인하는 반복 가능한 테스트로, 입력과 좋은 출력이 어떤 모습인지를 짝지어 놓고 매번 같은 방식으로 채점해서 어떤 변경이 실제로 도움이 되었는지 알 수 있게 합니다. 모델은 확률적인 블랙박스라서 같은 질문에도 실행마다 다르게 답하므로, 측정이야말로 유일하게 확실히 알 수 있는 방법입니다. 그리고 이 습관이 데모와 실제 제품을 가르는 핵심입니다.
눈으로 확인하는 방식이 왜 실패하는가
AI의 답변을 손으로 직접 확인하는 방식은 처음엔 괜찮아 보입니다. 프롬프트를 실행하고, 답변을 읽어보고, 괜찮아 보이니 다음으로 넘어갑니다. 문제는 세 가지가 조용히 여러분에게 불리하게 작용한다는 점인데, 여러분은 이미 이 세 가지를 모두 겪어봤습니다.
세 가지는 다음과 같습니다:
- 출력이 매번 달라집니다. 같은 프롬프트라도 실행마다 다른 답변이 나올 수 있으므로, 한 번 결과가 좋았다고 해서 다음번에도 좋으리라 보장할 수 없습니다.
- 변경이 회귀(regression)를 일으킵니다. 한 케이스를 고치려는 프롬프트 수정이 종종 여러분이 보지 않던 다른 케이스를 깨뜨립니다.
- 규모가 커지면 감당이 안 됩니다. 다섯 개의 답변은 읽을 수 있습니다. 하지만 한 줄을 바꿀 때마다 오백 개의 답변을 매번 읽을 수는 없습니다.
이 중 어느 것도 여러분이 뭔가 잘못했다는 뜻은 아닙니다. 다만 눈으로 확인하는 방식은 이 정도 규모의 일에는 맞지 않는 도구라는 뜻이며, 이보다 더 마음 편한 방법이 있습니다.
평가 세트 구축하기
평가 세트는 테스트 케이스의 목록입니다. 각 케이스에는 입력과, 그 입력에 대해 좋은 출력이 어떤 모습인지가 담겨 있습니다. 작게 시작하세요. 흔한 상황들을 다루는 열에서 스무 개 정도의 케이스에 까다로운 엣지 케이스 몇 개를 더하면, 아무것도 없는 것보다 훨씬 유용하며, 실제로 새로운 실패를 발견할 때마다 세트를 키워나갈 수 있습니다.
eval_set = [
{"input": "음식이 차갑고 늦게 나왔어요.", "expected": "NEGATIVE"},
{"input": "인생 최고의 식사였어요!", "expected": "POSITIVE"},
{"input": "그냥 괜찮았어요, 특별하진 않았어요.", "expected": "NEUTRAL"},
# ...발견하는 실제 케이스와 엣지 케이스를 여기에 추가하세요
]사용자가 나쁜 답변을 신고하면, 그것이 어떤 출력이어야 했는지와 함께 세트에 추가하세요. 그러면 평가 세트는 여러분이 다시는 반복하고 싶지 않은 모든 실수를 기억하는 저장소로 자라나게 되며, 이는 여러분이 고친 버그가 그대로 테스트로 남는 것과 같은 방식입니다.
채점하는 방법
작업마다 채점 방식이 다르므로, 첫 단계는 기대하는 답변의 종류에 맞는 검사 방식을 고르는 것입니다.
- **정확 일치(exact match)**는 정답이 하나뿐일 때 적합합니다. 분류 라벨, 추출된 필드, 예 또는 아니오 같은 경우입니다. 출력을 기대값과 직접 비교합니다.
- 기준 기반(criteria-based) 채점은 요약이나 답변처럼 하나의 정답이 없을 때 적합합니다. 출력이 조건을 만족하는지 확인합니다. 요약이 핵심 사실을 언급하는가? 50단어 이하인가? 어조가 공손한가?
- **LLM을 심판으로 사용(LLM-as-judge)**하는 방식은 두 번째 모델 호출로 기준에 따라 출력을 채점합니다. 단순한 검사로는 잡아내기 어려운 모호한 품질 문제에까지 확장할 수 있지만, 심판 역시 하나의 모델이므로 틀릴 수 있습니다. 그래서 가끔은 사람의 판단과 비교해 점검해야 합니다.
# 평가 세트에 대한 정확 일치 채점
def run_evals(eval_set, classify):
passed = 0
for test in eval_set:
output = classify(test["input"])
if output == test["expected"]:
passed += 1
else:
print(f'FAIL: "{test["input"]}" -> got {output}, wanted {test["expected"]}')
print(f"{passed}/{len(eval_set)} passed")정확 일치는 가장 시작하기 편한 방법이며, 앞선 챕터들에서 다룬 분류와 추출 작업에 딱 맞습니다. LLM을 심판으로 사용하는 방식이 효과가 있는 이유는, 답변이 기준을 만족하는지 판별하는 것이 답변을 만들어내는 것보다 흔히 더 쉬운 예측 문제이기 때문입니다. 에세이를 쓰는 것보다 검토하는 것이 더 쉬운 것과 같은 이치입니다. 다만 심판도 같은 결함을 가진 또 하나의 예측기일 뿐이므로, 그 점수를 유용한 추정치로 다루고, 명확한 채점 기준을 주고, 가끔은 그 평가를 여러분 자신의 평가와 비교해 확인하세요.
변경할 때마다 실행하기
평가 세트는 실제로 실행할 때만 유용합니다. 프롬프트를 코드처럼 다루세요. 프롬프트나 모델, 검색(retrieval) 로직에 변경을 배포하기 전에, 평가를 실행하고 이전 점수와 비교하세요. 숫자가 내려갔다면, 손으로 한 번 해본 테스트가 마침 괜찮아 보였더라도, 여러분이 회귀를 일으킨 것입니다.
이것이 프롬프트 엔지니어링을 추측이 아니라 자신 있게 반복할 수 있는 작업으로 바꿔주는 핵심입니다. 무언가를 바꾸고, 측정하고, 점수가 더 높으면 유지하고, 더 낮으면 버립니다. 이는 평범한 소프트웨어를 신뢰할 수 있게 만드는 것과 같은 순환 과정을, AI 기능의 모호한 부분에 그대로 적용하는 것입니다.
점수가 떨어졌다면, 여러분이 회귀를 일으킨 것입니다. 운 좋게 한 번 잘된 손 테스트가 세트를 뒤엎을 수는 없으므로, 매번 숫자가 결정하게 하세요.
실전에서
앞 절의 run_evals 함수는 작지만 완전한 평가 도구입니다. 분류기와 평가 세트를 지정해주면, 몇 개의 케이스가 통과했는지 알려주고 어떤 케이스가 실패했는지 정확히 보여줍니다.
run_evals(eval_set, classify_review)
# FAIL: "그냥 괜찮았어요, 특별하진 않았어요." -> got POSITIVE, wanted NEUTRAL
# 2/3 passed그 실패한 한 줄이 열 번의 성공적인 손 테스트보다 더 가치 있는 이유는, 이제 고치고 다시 측정할 수 있는 진짜 약점을 짚어주기 때문입니다. 마지막 챕터인 안전과 한계에서는 실제 사용자 앞에 이 모든 것을 내놓기 전에 대비해야 할 위험들을 다룹니다.
run_evals 같은 작은 도구만으로도 이미 제 값을 합니다. 점수와 정확히 어떤 케이스가 깨졌는지를 보여주니까요. 하나를 고치고, 다시 실행해서, 숫자가 움직이는 것을 지켜보세요. 다음으로는, 안전과 한계에서 실제 사용자가 도착하기 전에 대비해야 할 것들을 다룹니다. 
