LLM에 대한 '느낌(Vibe-Checking)'을 멈추세요: 개발자를 위한 평가(Evals) 가이드
요약
LLM 배포 시 주관적인 '느낌'에 의존하는 대신, 정량적인 평가(Evals) 체계를 구축하는 실무 가이드를 제공합니다. 비결정론적이고 개방적인 LLM의 특성을 고려하여, 단순 문자열 비교가 아닌 속성 기반의 검증 방식을 제안합니다.
핵심 포인트
- 주관적인 'Vibe-checking' 대신 정량적 지표를 통한 품질 검증 필요
- LLM의 비결정론적·개방적 특성으로 인해 전통적인 assert 테스트 적용 불가
- 출력의 일치 여부가 아닌 정책 준수, 컨텍스트 근거 등 속성(Properties) 테스트 지향
- 지표 설정에 앞서 대표성을 갖춘 평가 세트(Eval set) 구축이 우선순위
시스템 프롬프트(System Prompt)를 수정하고, 항상 실행하던 동일한 두 가지 테스트 질문을 던졌고, 답변이 괜찮아 보여서 배포했습니다. 일주일 후, 고객 지원 팀은 모델이 프롬프트로 막으려 했던 바로 그 행동을 자신 있게 수행하고 있는 스크린샷을 당신에게 전달합니다. 당신은 이를 전혀 알지 못했습니다. 왜냐하면 "성능이 좋아졌는가?"라는 질문에 대한 답을 단지 '느낌(Vibes)'으로 결정했기 때문입니다.
이것은 LLM 기능을 배포할 때 발생하는 가장 흔한 실패 유형이며, 어떤 모델을 선택했느냐와는 아무런 상관이 없습니다. 만약 당신의 유일한 품질 검증(Quality Gate)이 몇 개의 출력물을 읽고 고개를 끄덕이는 것이라면, 당신이 만드는 모든 변경 사항은 동전 던지기와 같습니다. 프롬프트 수정이 도움이 되었는지, 해가 되었는지, 아니면 단지 당신이 보지 않는 곳으로 실패를 옮겨 놓았는지 알 수 없습니다. 평가(Evals)는 그 '끄덕임'을 '숫자'로 대체하는 방법입니다.
이 글은 그 숫자를 구축하기 위한 실무 가이드입니다. 오늘 오후에 바로 작성할 수 있는 30행 규모의 평가 세트(Eval set)부터, 코드 기반 검사(Code-based checks)와 LLM-as-judge 점수 산정 방식, 그리고 회귀(Regression)가 사용자에게 발견되기 전에 차단될 수 있도록 전체 과정을 CI(지속적 통합)에 연결하는 방법까지 다룹니다. 새로운 프레임워크를 채택할 필요는 없습니다. 그저 데모(Demo)와 시스템(System)을 구분 짓는 규율(Discipline)이 필요할 뿐입니다.
왜 단순히 assert output == expected를 사용할 수 없는가
전통적인 테스트는 출력 공간(Output space)이 작고 정확하기 때문에 작동합니다. add(2, 2)는 4이거나 버그입니다. LLM의 출력은 assertEqual이 작동하게 만드는 세 가지 가정을 모두 깨뜨립니다:
- 비결정론적(Non-deterministic)입니다. 동일한 프롬프트라도 두 번의 호출에서 서로 다른 텍스트를 생성할 수 있습니다. Temperature가 0일 때조차 실행 시점이나 모델 버전 간에 바이트 단위로 일치하는 출력을 보장할 수 없습니다.
- 개방형(Open-ended)입니다. "이 티켓을 요약하세요"라는 요청에는 수천 개의 정답이 존재합니다. 그 중 어떤 것도 당신의 참조값(Reference)과 문자열이 일치하지 않으며, 이는 괜찮습니다. 좋은 요약이 반드시 '유일한' 요약이어야 하는 것은 아니기 때문입니다.
- 부드럽게 실패(Fails softly)합니다. 틀린 답변은 스택 트레이스(Stack trace)를 남기지 않습니다. 대신 유창하고, 그럴듯하며, 형식이 잘 갖춰진 문단 형태로 나타나지만, 단지 내용이 틀렸을 뿐입니다. 아무것도 충돌(Crash)하지 않고, 아무런 에러 로그도 남지 않습니다.
따라서 평가(Eval)의 목표는 "출력이 기대되는 문자열과 동일한가"가 아닙니다. 그것은 "출력이 내가 중요하게 생각하는 _속성(Properties)_을 충족하는가"입니다. 즉, 제공된 컨텍스트(Context)에 근거하고 있는지, 정책(Policy)을 준수하는지, 실제로 질문에 답변하는지, 유효한 JSON인지 등을 확인하는 것입니다. 당신은 바이트(Bytes) 대 바이트를 비교하는 것이 아니라, 기준(Criteria)에 따라 동작(Behavior)을 테스트하는 것입니다. 이 개념을 이해하고 나면, 나머지는 메커니즘의 문제입니다.
지표(Metric)가 아닌 평가 세트(Eval set)부터 시작하세요
본능적으로 가장 먼저 화려한 지표(Metric)를 찾으려 할 것입니다. 하지만 순서가 틀렸습니다. 다른 모든 것을 작동하게 만드는 자산은 작지만 대표성을 갖춘 **평가 세트(Eval set)**입니다. 즉, 입력값과 좋은 출력의 모습(또는 좋은 출력이 충족해야 하는 기준)이 쌍을 이룬 고정된 컬렉션입니다. 이것이 당신의 골든 데이터셋(Golden dataset), 회귀 테스트 스위트(Regression suite), 그리고 진실의 원천(Source of truth)이 됩니다.
시작하기 위해 수천 개의 예시가 필요하지는 않습니다. **잘 선택된 30~50개의 쌍(Pairs)**만 있어도 LLM 튜닝을 '느낌(Vibes)'의 영역에서 엔지니어링의 영역으로 전환할 수 있습니다. 이제 모든 변경 사항이 동일한 고정된 기준에 따라 측정되기 때문입니다. 다음과 같이 세트를 구축하세요:
- 실제 실패 사례를 발굴하세요. 개발(Dev) 또는 운영(Prod) 환경에서 시스템이 무언가를 틀릴 때마다, 그 정확한 입력값을 올바른 동작에 대한 메모와 함께 평가 세트에 추가하세요. 당신의 버그 리포트(Bug reports)가 바로 테스트 케이스(Test cases)입니다. 이것이 당신이 가진 가장 신호(Signal)가 높은 소스입니다.
- 해피 패스(Happy path)뿐만 아니라 카테고리를 포괄하세요. 쉬운 질문, 모호한 질문, 적대적(Adversarial) 질문, 범위를 벗어난 질문(
모든 LLM 평가(Eval)의 두 가지 측면
눈으로 직접 확인하며 평가할 때 서로 뒤섞이기 쉬운 두 가지 질문을 분리하세요. 이 질문들은 해결 방법이 서로 다르기 때문입니다.
- 시스템이 올바른 컨텍스트(Context)를 검색/설정했는가? (검색(Retrieval) 또는 파이프라인(Pipeline)에 관한 질문)
- 해당 컨텍스트가 주어졌을 때, 모델이 좋은 답변을 생성했는가? (생성(Generation)에 관한 질문)
RAG를 구축하고 있다면, 첫 번째 측면은 그 자체로 하나의 전문 분야입니다. 관련 문서가 이미 알려진 질문들에 대해 recall@k와 precision@k를 측정하면, 올바른 청크(Chunk)가 프롬프트(Prompt)에 도달했는지 여부를 알 수 있습니다. 이는 별도의 다룸이 필요할 만큼 깊이 있는 주제입니다. 전용 RAG 및 검색 증강 생성(Retrieval-Augmented Generation) 코스에서는 이 부분을 심도 있게 다루며, 여기서 발생하는 실패 모드(Failure modes)는 아래의 내용과는 다릅니다. 본 가이드는 두 번째 측면인 생성된 답변의 점수 매기기(Scoring)에 집중합니다. 기술은 두 가지 계열인 **코드 기반 체크(Code-based checks)**와 **모델 기반 판사(Model-based judges)**로 나뉘며, 여러분은 이 두 가지를 모두 사용해야 합니다.
코드 기반 체크: 생각보다 저렴하고 신뢰할 수 있음
LLM을 채점하기 위해 LLM을 사용하기 전에, 놀라울 정도로 많은 품질 요소들을 일반적인 코드로 확인할 수 있습니다. 이러한 체크 방식은 결정론적(Deterministic)이며, 비용이 들지 않고, 즉각적이며, 절대 환각(Hallucination)을 일으키지 않습니다. 가능한 모든 범위에서 이를 활용하세요:
- 구조적 유효성 (Structural validity). 출력이 특정 스키마(Schema)와 일치하는 JSON이어야 한다면, 이를 검증하세요. 파싱(Parse)되지 않는 응답은 판단이 필요 없는 명백한 실패(Hard failure)입니다.
- 포함 필수 / 포함 금지 (Must-contain / must-not-contain). 14일 환불 기간에 대한 답변에는 반드시 "14"가 포함되어야 하며, "30"은 포함되지 않아야 합니다. 키워드 및 정규 표현식(Regex) 단언(Assertion)은 사실 관계의 퇴보(Regression)를 비용 없이 잡아냅니다.
- 형식 및 범위 (Format and bounds). 길이 제한, 필수 인용구 존재 여부, 시스템 프롬프트(System-prompt) 텍스트 유출 여부, 금지된 문구(예: "AI 언어 모델로서..."와 같은 관용구), 유효한 열거형(Enum) 값 등을 확인합니다.
- 의미적 유사성 (Semantic similarity). 개방형 답변의 경우, 출력값과 참조 답변을 임베딩(Embedding)한 뒤 코사인 유사도(Cosine similarity)가 임계값을 통과하는지 확인합니다. 이는 모호(Fuzzy)하지만, 판사 모델(Judge model) 없이도 "답변이 주제에서 벗어났음"을 잡아낼 수 있습니다.
# evals/checks.py
import json
...
경험 법칙(Rule of thumb): 정규 표현식이나 스키마가 잡아낼 수 있는 것이라면, 모델에게 비용을 지불하며 잡아내게 하지 마세요. 비싸고 모호한 판사 모델은 진정으로 주관적인 영역을 위해 남겨두어야 합니다.
LLM-as-judge: 강력하지만 편향되었으며, 해결 가능하다
"이 답변이 출처에 충실한가?", "이것이 도움이 되는가?", "어조가 적절한가?"와 같은 주관적인 절반의 영역을 위해서는 강력한 모델을 사용하여 출력값을 채점합니다. 이것이 바로 LLM-as-judge이며, 이는 API 호출 비용만으로 인간 수준의 판단을 수천 개의 예시에 확장할 수 있게 해줍니다. RAG(Retrieval-Augmented Generation) 스타일의 애플리케이션에서는 다음 두 가지 지표가 가장 큰 비중을 차지합니다:
- 충실도 / 근거성 (Faithfulness / Groundedness) — 답변의 모든 주장이 제공된 컨텍스트(Context)로 거슬러 올라갈 수 있는가, 아니면 모델이 무언가를 지어냈는가? 이것이 바로 환각(Hallucination) 탐지기입니다.
- 답변 관련성 (Answer relevance) — 응답이 실제로 질문에 답하고 있는가, 아니면 유창하게 회피하고 있는가?
주의할 점: LLM 판사는 잘 알려진 편향(Bias)을 가지고 있으며, 이를 무시한다면 당신의 평가(Eval) 수치는 신호(Signal)로 위장한 노이즈(Noise)에 불과합니다. 모델을 평가자로 사용하는 연구에서 보고된 주요 편향들은 다음과 같습니다:
- 위치 편향 (Position bias) — 두 답변을 비교할 때, 판정자가 품질과 상관없이 먼저 보여지는 답변(또는 고정된 슬롯의 답변)을 선호하는 현상입니다.
- 장황함 편향 (Verbosity bias) — 짧은 답변이 더 정확하더라도, 판정자가 더 길고 정교한 답변에 더 높은 점수를 주는 경향입니다.
- 자기 선호 (Self-preference) — 판정 모델이 자신의 스타일로 작성되었거나 자신의 계열(family) 모델이 작성한 텍스트를 선호할 수 있습니다.
이 기술을 포기하는 것이 아니라, 편향을 우회하도록 설계(engineer around)해야 합니다:
- '느낌(vibe)'이 아닌 루브릭(rubric)에 따라 점수를 매기세요. 각 단계별로 명시적인 기준을 포함하여 1~5점 사이의 점수를 요청하고, 판정자가 점수를 출력하기 전에 그 근거(reasoning)를 먼저 출력하도록 요구하세요. 스스로를 정당화하도록 강제된 판정자가 더 일관적입니다.
- 쌍체 비교(pairwise comparisons) 시에는 무작위화하고 순서를 바꾸세요. 각 비교를 순서를 뒤집어 두 번 실행하세요. 판정자가 두 번 모두 동일한 답변을 선택했을 때만 승리로 간주합니다. 이는 위치 편향을 직접적으로 상쇄합니다.
- 인간과 비교하여 교정(Calibrate)하세요. 20~30개의 예시를 직접 수동으로 라벨링(hand-label)한 뒤, 판정 모델을 실행하여 당신의 판단과 일치하는지 확인하세요. 일치하지 않는다면, 2,000개의 데이터를 믿기 전에 루브릭을 먼저 수정해야 합니다. 교정되지 않은 판정자는 문법만 좋은 난수 생성기에 불과합니다.
- 강력한 모델을 판정자로 사용하세요. 채점은 답변하는 것보다 더 어렵습니다. 당신의 애플리케이션이 더 작고 저렴한 모델에서 실행되더라도, 판정자로는 현재의 최첨단 모델(frontier model)을 사용하세요.
# evals/judge.py — 루브릭 기반 충실도(faithfulness) 판정기 스케치
JUDGE_PROMPT = """당신은 ANSWER가 CONTEXT에 의해 완전히 뒷받침되는지 채점하고 있습니다.
...
견고한 판정기를 설계하는 것 — 즉, 루브릭을 선정하고, 교정하며, 모델이 채점에 부적합한 도구인 시점을 파악하는 것 — 은 production 환경에서의 AI 평가 코스가 길러주는 바로 그 역량입니다. 왜냐하면 이것이 "새 프롬프트가 느낌이 더 좋아"와 "홀드아웃(holdout) 데이터에서 충실도가 0.78에서 0.91로 상승했다" 사이의 차이를 만드는 지점이기 때문입니다.
CI에 통합하세요, 그렇지 않으면 마감 기한을 견뎌내지 못할 것입니다
생각날 때마다 수동으로 실행하는 평가는 일이 바빠지는 순간 더 이상 실행하지 않게 될 평가입니다. 평가의 핵심 목적은 회귀(Regression)가 _소리 없이 배포되는 것을 불가능하게 만드는 것_이며, 이는 프롬프트(Prompt), 검색 설정(Retrieval setting), 또는 모델 버전(Model version)이 변경될 때마다 평가가 자동으로 실행되어야 함을 의미합니다.
이 패턴은 회귀 게이트(Regression gate)입니다. 평가 세트(Eval set)를 실행하고, 집계 점수(Aggregate score)를 계산한 뒤, 점수가 임계값(Threshold) 미만으로 떨어지거나(또는 마지막으로 확인된 양호한 기준점(Baseline) 미만으로 떨어지면) 빌드를 실패(Fail) 처리합니다. 이것은 일반적인 테스트 스위트(Test suite)와 유사해 보이는데, 실제로 그것이기 때문입니다.
# tests/test_evals.py
import pytest
from evals.dataset import EVAL_SET
...
CI 환경에서 이를 안정적으로 유지하기 위한 몇 가지 실무적인 참고 사항입니다:
- 모델 버전을 고정(Pin)하세요. 제공업체의 모델 ID는 업데이트되며, 버전을 고정하지 않으면 코드와 무관한 이유로 평가 기준점(Baseline)이 변하게 됩니다. 버전을 고정하고, 모델 업그레이드는 그 자체로 의도된 별도의 평가 실행으로 취급하세요.
- 비용과 불안정성(Flakiness)을 고려하세요. LLM 호출은 비용이 발생하며 때때로 타임아웃(Timeout)이 발생합니다. 가능한 곳에 캐시(Cache)를 사용하고, 필요한 경우 판정(Judge) 작업이 많은 스위트는 매 커밋마다 실행하는 대신 스케줄에 따라 실행하며, 하나의 확률적 튀는 현상(Stochastic blip) 때문에 좋은 PR(Pull Request)이 거절(Red-X)되지 않도록 임계값을 약간 너그럽게 설정하세요.
- 점수뿐만 아니라 실패 사례를 기록하세요. 게이트가 작동했을 때, 출력값은 어떤 케이스에서 회귀가 발생했는지 명시하여 수정 사항이 명확히 보이도록 해야 합니다. 단순히 "0.86 < 0.90"이라고만 표시되면 무엇을 디버깅해야 할지 알 수 없습니다.
이제 프롬프트 변경은 숫자가 첨부된 PR이 됩니다. 리뷰어는 충실도(Faithfulness)가 상승하고 거부율(Refusal rate)이 안정적으로 유지되는 것을 보거나, 혹은 점수가 폭락하여 빌드가 실패(Red)한 것을 보게 됩니다. 이것이 바로 '바라는 것(Hoping)'과 '아는 것(Knowing)' 사이의 근본적인 차이입니다.
평가를 조용히 망치는 다섯 가지 실수
평가 시스템을 구축하는 팀조차 종종 이를 스스로 망가뜨리곤 합니다. 다음 사항들을 주의하세요:
- 행복한 경로(Happy path)만 테스트하는 것. 만약 테스트 세트의 모든 사례가 시스템이 이미 잘 답변하는 질문들뿐이라면, 당신의 점수는 기분만 좋게 만드는 거짓말입니다. 신호(Signal)는 적대적 사례(Adversarial cases)와 범위를 벗어난 사례(Out-of-scope cases)에서 나타납니다.
- 테스트 세트로 튜닝하는 것. 채점 기준이 되는 동일한 예시들에 맞춰 프롬프트를 최적화하면, 해당 예시들에 과적합(Overfit)하게 됩니다. 훔쳐보지 않을 별도의 홀드아웃(Holdout) 세트를 유지하세요.
- 보정되지 않은 판사(Uncalibrated judge). 자신의 레이블(Label)과 대조하여 검증하지 않은 LLM 판사를 신뢰하는 것은, 스스로 지어낸 숫자를 믿는 것과 같습니다. 먼저 보정(Calibrate)하세요.
- 하나의 거대한 혼합 점수. 단일 평균 점수는 충실도(Faithfulness)는 향상되었으나 거절(Refusals) 기능이 망가졌다는 사실을 숨깁니다. 한 지표의 퇴보가 다른 지표의 상승에 의해 가려지지 않도록, 각 지표를 별도로 추적하세요.
- 세트가 부패하게 방치하는 것. 제품은 변합니다. 실제 사용 사례를 더 이상 반영하지 못하는 사례들은 신호를 저하시킵니다. 다른 테스트 스위트를 유지 관리하는 것과 마찬가지로, 일반적인 업무의 일부로서 세트를 가지치기하고 확장하세요.
이 중 어느 것도 생소한 것이 아닙니다. 이는 에러 경로(Error paths)를 테스트하지 않는 것과 같은 평가(Eval) 상의 실수이며, 지나고 나면 당연해 보이지만 마감 기한 압박 속에서는 건너뛰기 쉬운 일들입니다.
이것이 나머지 LLM 스택과 연결되는 방식
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기