Skip to content
This page has been auto-translated and may contain errors.View in English

평가(Evals)

docs.scrimba.com

프롬프트를 수정하고, 한 번 시도해보니 답변이 더 나아 보여서 배포합니다. 그리고 일주일 후 사용자들은 몰래 나빠진 케이스들과 마주치게 됩니다. **평가(eval)**는 이 문제를 해결하는 방법입니다. AI 기능이 실제로 동작하는지 확인하는 반복 가능한 테스트로, 입력과 좋은 출력이 어떤 모습인지를 짝지어 놓고 매번 같은 방식으로 채점해서 어떤 변경이 실제로 도움이 되었는지 알 수 있게 합니다. 모델은 확률적인 블랙박스라서 같은 질문에도 실행마다 다르게 답하므로, 측정이야말로 유일하게 확실히 알 수 있는 방법입니다. 그리고 이 습관이 데모와 실제 제품을 가르는 핵심입니다.

눈으로 확인하는 방식이 왜 실패하는가

AI의 답변을 손으로 직접 확인하는 방식은 처음엔 괜찮아 보입니다. 프롬프트를 실행하고, 답변을 읽어보고, 괜찮아 보이니 다음으로 넘어갑니다. 문제는 세 가지가 조용히 여러분에게 불리하게 작용한다는 점인데, 여러분은 이미 이 세 가지를 모두 겪어봤습니다.

세 가지는 다음과 같습니다:

  • 출력이 매번 달라집니다. 같은 프롬프트라도 실행마다 다른 답변이 나올 수 있으므로, 한 번 결과가 좋았다고 해서 다음번에도 좋으리라 보장할 수 없습니다.
  • 변경이 회귀(regression)를 일으킵니다. 한 케이스를 고치려는 프롬프트 수정이 종종 여러분이 보지 않던 다른 케이스를 깨뜨립니다.
  • 규모가 커지면 감당이 안 됩니다. 다섯 개의 답변은 읽을 수 있습니다. 하지만 한 줄을 바꿀 때마다 오백 개의 답변을 매번 읽을 수는 없습니다.

이 중 어느 것도 여러분이 뭔가 잘못했다는 뜻은 아닙니다. 다만 눈으로 확인하는 방식은 이 정도 규모의 일에는 맞지 않는 도구라는 뜻이며, 이보다 더 마음 편한 방법이 있습니다.

Juno눈으로 확인하는 방식이 왜 실패하는가 답변을 손으로 읽는 방식이 무너지는 이유는 세 가지이며, 모두 모델이 작동하는 방식에 뿌리를 두고 있습니다. 출력이 실행마다 달라지고, 수정은 여러분이 보지 않던 케이스를 조용히 깨뜨리며, 답변이 수백 개가 되면 손으로는 도저히 따라갈 수 없습니다. 이건 성실함의 문제가 아니라, 일의 규모에 맞지 않는 도구를 쓰고 있는 문제입니다. 다행히 해결책은 그리 어렵지 않고, 바로 다음에 나옵니다.

답변 하나를 눈으로 확인하는 것은 거의 아무것도 알려주지 않는데, 그 이유는 모델이 작동하는 방식에 있습니다. 출력이 비결정적이기 때문에(같은 프롬프트라도 실행마다 다른 답변이 나올 수 있습니다), 답변 하나는 표본 크기가 1인 샘플일 뿐입니다. 이것만으로는 통과율(pass rate), 즉 제대로 나온 케이스의 비율을 알 수 없습니다. 실제로 얼마나 자주 잘 동작하는지 추정하려면 같은 케이스들에 대해 여러 번 실행해야 하기 때문입니다.

회귀가 눈에 안 보이는 것도 비슷한 이유입니다. 한 케이스를 손으로 고치면 다른 케이스들에 대한 기록이 남지 않으므로, 다음 수정이 여러분이 더 이상 지켜보지 않던 무언가를 깨뜨릴 수 있습니다. 이를 알아낼 유일한 방법은 고정된 케이스 집합을 매번 같은 방식으로 채점하는 것이며, 그러면 점수가 떨어질 때 프로덕션에서의 놀라움이 아니라 숫자로 먼저 드러납니다.

수학적인 문제 위에 사람의 문제도 더해집니다. 수동 검토는 사람마다, 그리고 기분에 따라 흔들립니다. 같은 답변도 월요일엔 괜찮아 보이고 금요일엔 부족해 보입니다. 그래서 해야 할 일은 "괜찮아 보인다"를 고정된 케이스 집합에 대한 측정된 통과율로 바꾸는 것입니다. 같은 케이스들에 대해 느낌을 숫자로 바꾸세요.

Juno눈으로 확인하는 방식이 왜 실패하는가 답변 하나는 표본 크기가 1인 샘플이라서, 모델이 한 번 잘 동작했다는 것만 알려줄 뿐 얼마나 자주 잘 동작하는지는 알려주지 않습니다. 실제 통과율을 보려면 많은 케이스를 실행해야 하고, 회귀를 잡아내려면 그 케이스 집합을 고정해두고 매번 같은 방식으로 채점해야 합니다. 수동 검토는 검토자에 따라서도 흔들리므로, 해야 할 일은 "괜찮아 보인다"를 측정된 숫자로 바꾸는 것입니다.

샘플링 검사가 실패하는 이유는 여러분이 부주의해서가 아닙니다. 선택 편향(selection bias)이 애초에 구조에 내재되어 있기 때문입니다. 여러분은 잘 될 것으로 예상되는 케이스들을 살펴보므로, 절대 확인하지 않는 그 행동이 바로 사용자 앞에서 회귀를 일으키는 행동입니다. 추적하지 않는 지표가 바로 움직이는 지표입니다.

베이스라인 점수가 없으면, 품질의 모든 변화는 여러분이 스스로에게 하는 이야기일 뿐입니다. 특정 변화를 특정 수정에 귀속시킬 수 없고, 어떤 수정이 통과율(pass rate)(케이스 중 올바르게 나온 비율)을 떨어뜨렸는지 이분 탐색으로 찾아낼 수도 없습니다. 변경 전을 측정한 적이 없기 때문입니다. "느낌이 더 좋다"는 반박할 증거가 없기 때문에 살아남습니다.

느낌에 기반한 반복 작업은 과적합도 일으킵니다. 같은 몇 가지 예시를 계속 다시 입력해보고, 그것들이 괜찮아 보일 때까지 튜닝하다 보면, 실제 트래픽은 다른 곳으로 흘러가는데 프롬프트는 조용히 그 작은 비공개 집합에 맞춰 훈련됩니다. 수동 검토로는 이런 분포 변화를 볼 수 없습니다. 새로운 입력이란 바로 여러분이 직접 입력하지 않는 것들이기 때문입니다. 버전 관리된 평가 세트와 추적된 점수만이 어떤 변화가 실제로 도움이 되었는지, 그저 느낌만 좋아진 것인지를 구분할 수 있는 유일한 방법이며, 이것이 반복(iterating)과 추측(guessing)을 가르는 차이입니다.

Juno눈으로 확인하는 방식이 왜 실패하는가 저도 느낌만으로 배포한 뒤에, 한 번도 샘플로 확인하지 않았던 그 경로가 에러 로그에서 터지는 걸 지켜본 적이 있습니다. 그게 바로 선택 편향이 제 역할을 하는 순간입니다. 베이스라인이 없으면 귀속도 이분 탐색도 불가능합니다. 숫자가 없었다면 어떤 수정이 그 숫자를 움직였는지 증명할 수 없습니다. 버전 관리된 세트와 추적된 점수를 유지하세요. 그렇지 않으면 계속 다시 입력하는 그 다섯 개 예시에 과적합하는 동안 실제 트래픽은 여러분이 볼 수 없는 곳으로 빠져나갑니다.

평가 세트 구축하기

평가 세트는 테스트 케이스의 목록입니다. 각 케이스에는 입력과, 그 입력에 대해 좋은 출력이 어떤 모습인지가 담겨 있습니다. 작게 시작하세요. 흔한 상황들을 다루는 열에서 스무 개 정도의 케이스에 까다로운 엣지 케이스 몇 개를 더하면, 아무것도 없는 것보다 훨씬 유용하며, 실제로 새로운 실패를 발견할 때마다 세트를 키워나갈 수 있습니다.

python
eval_set = [
    {"input": "음식이 차갑고 늦게 나왔어요.", "expected": "NEGATIVE"},
    {"input": "인생 최고의 식사였어요!", "expected": "POSITIVE"},
    {"input": "그냥 괜찮았어요, 특별하진 않았어요.", "expected": "NEUTRAL"},
    # ...발견하는 실제 케이스와 엣지 케이스를 여기에 추가하세요
]

사용자가 나쁜 답변을 신고하면, 그것이 어떤 출력이어야 했는지와 함께 세트에 추가하세요. 그러면 평가 세트는 여러분이 다시는 반복하고 싶지 않은 모든 실수를 기억하는 저장소로 자라나게 되며, 이는 여러분이 고친 버그가 그대로 테스트로 남는 것과 같은 방식입니다.

Juno평가 세트 구축하기 평가 세트는 테스트 케이스의 목록입니다: 입력과, 좋은 답변이 어떤 모습인지. 일상적인 상황들과 까다로운 케이스 몇 개를 담아 열에서 스무 개로 시작하세요. 사용자가 나쁜 답변을 마주할 때마다 원래 나와야 했던 답변과 함께 추가하면, 세트가 조용히 다시는 반복하고 싶지 않은 모든 실수를 기억해줍니다.

**평가 세트(eval set)**는 여러분이 모아둔 테스트 케이스들이며, 각각은 입력과 그에 대해 좋다고 인정할 출력이 짝지어져 있습니다. 중요한 것은 크기가 아니라 커버리지입니다. 잘 고른 스무 개의 케이스가 대충 모은 오십 개보다 항상 더 낫습니다.

세 가지를 다루도록 목표를 잡으세요. 사용자가 실제로 자주 마주치는 흔한 경로, 두 라벨이 가까이 있어 경계가 모호한 판단 기준선(경계에 딱 걸쳐 있는 애매한 케이스들), 그리고 이미 잘못되는 걸 본 적 있는 알려진 실패 유형입니다. 잘 나오는 입력만 골라서 넣으면 점수는 좋아 보여도 아무것도 알려주지 않으므로, 세트를 실제 트래픽을 대표하도록 유지하세요.

사용자가 신고하는 버그는 모두 세트 안에 영구적인 케이스가 됩니다. 그러면 세트는 회귀 테스트 모음처럼 작동합니다. 고친 버그를 드러낸 케이스가 이제 매번 실행되므로, 그 버그는 계속 고쳐진 상태로 유지됩니다. 기대 출력이 자유 텍스트가 아니라 라벨이나 JSON 필드처럼 구조화되어 있다면, 구조화된 출력에서 다루듯 정확 일치(exact-match) 비교로 채점할 수 있으며, 이는 깔끔하고 모호함이 없습니다.

여러분이 절대 튜닝하지 않는 별도의 보류(held-out) 부분을 유지하세요. 지켜보고 있는 정확한 케이스들을 통과할 때까지 프롬프트를 조정한다면, 이미 스스로 공부한 답을 가지고 자신을 채점하는 것이며, 그 점수는 아무 의미도 갖지 못하게 됩니다.

Juno평가 세트 구축하기 커버리지가 단순한 크기보다 중요합니다. 흔한 경로, 라벨 사이의 애매한 경계, 이미 본 적 있는 실패를 다루세요. 신고된 버그를 모두 영구적인 케이스로 만들면 세트가 회귀 테스트 모음처럼 작동합니다. 절대 튜닝하지 않는 보류 부분을 남겨두세요. 그렇지 않으면 이미 공부한 답으로 스스로를 채점하는 셈입니다.

**평가 세트(eval set)**를 프롬프트 바로 옆에서 버전 관리하는 자산으로 다루세요. 둘은 함께 움직이며, 프롬프트가 바뀌면 무엇이 좋은 답변으로 인정되는지도 조용히 바뀔 수 있기 때문입니다. 세트의 라벨 품질이 지표의 상한선이 됩니다. "expected" 값 하나가 잘못되어 있으면 점수의 한계가 되고 여러분에게 잘못된 교훈을 조용히 가르치므로, 케이스를 모으는 것만큼이나 라벨을 신중하게 정리하는 일도 중요합니다.

테스트 유출(leakage)을 경계하세요. 보고하는 그 정확한 세트를 통과할 때까지 튜닝한다면, 그 숫자는 연출일 뿐입니다. 반복 작업 중에는 들여다보지 않는 부분을 따로 떼어두고, 시간이 지나면서 그마저 튜닝 루프에 서서히 스며들지 않도록 주기적으로 교체하세요.

클래스 균형도 살펴보세요. 90퍼센트가 명백한 긍정 케이스인 세트는 보기 좋은 숫자를 보여주면서도 실제로 중요한 나머지 10퍼센트의 모든 실패를 감춰버리므로, 어렵고 드문 케이스가 실질적인 비중을 갖도록 샘플링하세요. 케이스는 상상이 아니라 프로덕션 로그에서 가져오세요. 실제 트래픽은 책상에서 절대 떠올리지 못할 표현들을 드러내기 때문입니다.

세트가 낡아간다는 사실을 받아들이세요. 제품이 변화하면서 오래된 케이스는 사용자가 보내는 것과 점점 멀어지고, 세트가 한 번도 본 적 없는 새로운 실패 유형이 나타납니다. 세트를 관리하는 일은 한 번의 구축이 아니라 계속되는 작업입니다.

Juno평가 세트 구축하기 세트는 프롬프트 옆에서 버전 관리하는 자산이고, 그 라벨 품질이 지표의 확고한 상한선입니다. expected 값 하나가 잘못되면 전체 점수가 그 한계에 갇힙니다. 부분을 떼어두고 교체하세요. 그렇지 않으면 보고하는 숫자는 연출일 뿐입니다. 클래스 균형을 살피고, 로그에서 실제 케이스를 샘플링하고, 제품이 움직이면 세트도 낡아간다고 가정하세요. 그러니 세트를 관리하는 일은 사실상 끝나지 않습니다.

채점하는 방법

작업마다 채점 방식이 다르므로, 첫 단계는 기대하는 답변의 종류에 맞는 검사 방식을 고르는 것입니다.

  • **정확 일치(exact match)**는 정답이 하나뿐일 때 적합합니다. 분류 라벨, 추출된 필드, 예 또는 아니오 같은 경우입니다. 출력을 기대값과 직접 비교합니다.
  • 기준 기반(criteria-based) 채점은 요약이나 답변처럼 하나의 정답이 없을 때 적합합니다. 출력이 조건을 만족하는지 확인합니다. 요약이 핵심 사실을 언급하는가? 50단어 이하인가? 어조가 공손한가?
  • **LLM을 심판으로 사용(LLM-as-judge)**하는 방식은 두 번째 모델 호출로 기준에 따라 출력을 채점합니다. 단순한 검사로는 잡아내기 어려운 모호한 품질 문제에까지 확장할 수 있지만, 심판 역시 하나의 모델이므로 틀릴 수 있습니다. 그래서 가끔은 사람의 판단과 비교해 점검해야 합니다.
python
# 평가 세트에 대한 정확 일치 채점
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을 심판으로 사용하는 방식이 효과가 있는 이유는, 답변이 기준을 만족하는지 판별하는 것이 답변을 만들어내는 것보다 흔히 더 쉬운 예측 문제이기 때문입니다. 에세이를 쓰는 것보다 검토하는 것이 더 쉬운 것과 같은 이치입니다. 다만 심판도 같은 결함을 가진 또 하나의 예측기일 뿐이므로, 그 점수를 유용한 추정치로 다루고, 명확한 채점 기준을 주고, 가끔은 그 평가를 여러분 자신의 평가와 비교해 확인하세요.

Juno채점하는 방법 답변에 맞는 검사를 고르세요. 정답이 하나면 정확 일치, 여러 좋은 답이 있으면 기준 기반, 모호한 품질에는 심판 역할을 하는 두 번째 모델입니다. 정확 일치가 가장 편한 출발점이고, 이미 연습한 분류와 추출 작업과도 딱 맞아떨어집니다. 하지만 심판도 하나의 모델이므로, 가끔은 직접 그 평가를 들여다보세요.

작업에 맞는 채점기를 고르고, 그러면서도 원하는 것을 여전히 포착할 수 있는 가장 저렴한 방식을 선택하세요. **채점기(grader)**는 출력이 통과했는지를 판단하는 코드나 모델 호출이며, 잘못된 선택은 실제 실패를 놓치거나 괜찮은 답변에 실패 표시를 붙이게 됩니다.

정확 일치는 라벨과 추출된 필드에 적합합니다. 출력을 기대값과 비교하며 결과가 결정적이므로 믿을 수 있습니다. 구조화된 출력과 자연스럽게 어울리는데, 모델이 고정된 스키마를 반환하면 자유 형식 텍스트 대신 깔끔하게 비교할 대상을 얻기 때문입니다.

프로그래밍 방식 기준은 중간 영역을 저렴하게 다룹니다. 길이를 확인하고, 필수 문자열이 있는지 확인하고, JSON이 파싱되는지 검증하고, 정규식을 실행하는 식입니다. 이런 검사는 모델 호출 없이 밀리초 단위로 실행되므로, 심판에게 돈을 쓰기 전에 반구조화된 것들에는 이 방식을 먼저 쓰세요.

LLM을 심판으로 쓰는 방식은 요약이나 답변처럼 좋은 형태가 여러 가지이고 정확한 목표가 없는 개방형 품질 문제에 적합합니다. 심판이 일관된 기준을 적용하도록 작성된 채점 기준(rubric)이 필요하고, 표본에 대해 심판의 채점을 여러분 자신의 라벨과 비교하는 보정(calibration) 점검도 필요합니다. 채점기 역시 여러분이 믿어야 할 코드라는 점을 기억하세요. 불안정한 채점기는 자신 있게 틀린 점수를 내놓을 뿐입니다.

Juno채점하는 방법 필요한 것을 여전히 잡아낼 수 있는 가장 저렴한 채점기를 쓰세요. 라벨과 필드에는 정확 일치, 길이나 유효한 JSON이나 필수 문자열에는 프로그래밍 방식 검사, 개방형 품질에만 모델 심판을 씁니다. 정확 일치는 구조화된 출력과 잘 맞는데, 고정된 스키마가 깔끔하게 비교할 대상을 주기 때문입니다. 무엇을 고르든 채점기도 여러분이 믿어야 할 코드이므로, 불안정한 채점기는 자신 있게 틀린 점수를 줍니다.

LLM을 심판으로 쓰는 방식은 가장 흥미로운 방식으로 문제를 일으키는 채점기이므로, 이를 활용하기 전에 그 편향을 미리 대비해야 합니다. **심판(judge)**은 더 긴 답변을 선호하고, 둘을 비교할 때 첫 번째 선택지를 고르며, 자신의 스타일과 비슷하게 읽히는 텍스트에 후한 점수를 줍니다. 그리고 절대 점수는 보정이 잘 안 되어 있어서, 한 실행에서 나온 8점이 다음 실행에서의 8점과 같지 않습니다.

심판도 여러분이 그 자체로 평가해야 할 모델이라고 생각하세요. 심판 모델을 고정하고 버전을 관리하세요. 업데이트될 때마다 흐트러지고 그에 따라 여러분의 점수도 함께 움직이기 때문입니다. 몇 가지 기준 예시(anchor examples)가 담긴 촘촘한 채점 기준을 주고, 라벨이 있는 표본에서 심판과 사람의 일치도를 측정해서 어느 정도까지 신뢰할 수 있는지 파악하세요.

가능하다면 절대적인 1-10점 채점보다는 쌍대 비교("A와 B 중 어느 것이 더 나은가?")를 요청하세요. 상대적인 판단이 모델에게 보정된 숫자를 맞히라고 요구하는 것보다 대체로 더 신뢰할 만합니다. 심판을 고정하고, 채점 기준을 촘촘하게 만들고, 사람과 비교해 점검하세요.

순환성과 비용도 신경 쓰세요. 같은 모델 계열이 스스로의 출력을 채점하면 같은 사각지대를 공유하게 되므로, 생성 모델이 만든 결함을 심판도 놓치기 쉬우며, 규모가 커지면 모든 케이스를 심판이 평가하는 것은 첫 번째 모델 비용 위에 얹히는 두 번째 모델 비용이 됩니다.

Juno채점하는 방법 심판은 여러분이 묻지 않은 취향을 가지고 있습니다. 긴 답변, 첫 번째 선택지, 자기 자신처럼 들리는 문장을 좋아하고, 절대 점수는 실행마다 흔들립니다. 심판을 고정하고 버전을 관리하고, 기준 예시가 담긴 촘촘한 채점 기준을 주고, 사람과 얼마나 자주 일치하는지 추적하세요. 이제는 채점기 자체를 채점하고 있는 셈이니까요. 외로운 1-10점 채점보다는 쌍대 비교에 의존하고, 같은 계열의 모델이 스스로를 채점하면 같은 사각지대를 공유하고 비용도 두 배가 된다는 점을 잊지 마세요.

변경할 때마다 실행하기

평가 세트는 실제로 실행할 때만 유용합니다. 프롬프트를 코드처럼 다루세요. 프롬프트나 모델, 검색(retrieval) 로직에 변경을 배포하기 전에, 평가를 실행하고 이전 점수와 비교하세요. 숫자가 내려갔다면, 손으로 한 번 해본 테스트가 마침 괜찮아 보였더라도, 여러분이 회귀를 일으킨 것입니다.

이것이 프롬프트 엔지니어링을 추측이 아니라 자신 있게 반복할 수 있는 작업으로 바꿔주는 핵심입니다. 무언가를 바꾸고, 측정하고, 점수가 더 높으면 유지하고, 더 낮으면 버립니다. 이는 평범한 소프트웨어를 신뢰할 수 있게 만드는 것과 같은 순환 과정을, AI 기능의 모호한 부분에 그대로 적용하는 것입니다.

점수가 떨어졌다면, 여러분이 회귀를 일으킨 것입니다. 운 좋게 한 번 잘된 손 테스트가 세트를 뒤엎을 수는 없으므로, 매번 숫자가 결정하게 하세요.

Juno변경할 때마다 실행하기 평가 세트는 실제로 실행해야만 도움이 되므로, 습관으로 만드세요. 어떤 변경이든 배포하기 전에 평가를 실행하고 지난번 점수와 비교하세요. 내려갔다면 무시할 우연이 아니라 고쳐야 할 회귀입니다. 바꾸고, 측정하고, 더 높은 점수를 유지하면, 추측이 아니라 반복 작업을 하고 있는 것입니다.

평가 실행을 개발 과정에 연결해서, 테스트 모음이 머지를 막듯 변경을 막는 관문으로 만드세요. 프롬프트나 모델, 검색(관련 문서를 가져오는 단계)에 대한 변경은 세트를 실행하고 이전 실행과 점수를 비교하기 전까지는 배포되지 않아야 합니다. 한 번의 손 테스트 이후 느낀 감이 아니라, 숫자가 그 변경이 개선인지를 결정해야 합니다.

한 번에 변수 하나만 바꾸세요. 같은 단계에서 프롬프트를 수정하고 동시에 모델을 바꾸면, 점수 변화의 원인이 두 가지가 되어버려서 어느 쪽 때문인지 알 수 없습니다. 변경 사항을 분리하고, 세트를 실행하고, 결과를 읽은 다음, 다음 변경으로 넘어가세요.

종합 점수(aggregate score)(모든 케이스에 걸쳐 평균 낸 단일 숫자)만 보지 말고 케이스별 차이(diff)도 확인하세요. 안정적인 총점은 한 케이스가 고쳐지고 동시에 다른 케이스가 깨진 상황을 감출 수 있습니다. 순변화가 0이 되어 버리지만, 실제로는 동작이 조용히 바뀐 것입니다. 어떤 케이스가 뒤집혔는지 살펴보면 변경이 실제로 무엇을 했는지 알 수 있습니다.

시간에 따라 점수를 추적해서 개선이 눈에 보이고 회귀가 눈에 띄게 하세요. 기록된 이력은 "이게 더 나은 것 같다"는 느낌을 가리킬 수 있는 선(line)으로 바꿔주고, 갑작스러운 하락이 어떤 변경 때문인지 빠르게 추적할 수 있게 해줍니다.

Juno변경할 때마다 실행하기 테스트가 머지를 막듯 점수로 변경을 막으세요. 실행 없으면 배포도 없습니다. 한 번에 변수 하나만 바꿔서 점수 변화의 원인을 하나로 만들고, 케이스별 차이를 읽으세요. 평평한 총점은 한 케이스가 고쳐지고 다른 케이스가 깨진 상황을 감출 수 있기 때문입니다. 시간에 따라 점수를 기록해서 성과는 눈에 보이고 회귀는 눈에 띄게 하세요.

의식(ritual)이 아니라 진짜 관문으로 만드세요. CI에 점수 임계값을 설정해서, 기준선 밑으로 떨어진 변경이 누군가 확인하는 것을 기억해내는 데 의존하지 않고 빌드 자체를 실패시키게 하세요. 그리고 모델 버전이 올라갈 때마다 세트를 다시 실행하세요. 같은 프롬프트라도 새로운 가중치에서는 다르게 반영되기 때문입니다. 테스트한 모델 버전을 고정하고, 새 버전을 채택하기 전에 다시 평가하세요.

두 가지 함정을 기억하세요. 첫째, 평가 세트(eval set)(기대 결과가 있는 고정된 테스트 케이스 모음)는 대체 지표(proxy)일 뿐 절대적인 진실이 아닙니다. 그래서 세트에 지나치게 맞춰 튜닝하면 과적합이 일어납니다. 점수는 올라가지만 실제 동작은 정체됩니다. 세트를 계속 확장하고 교체해서, 테스트를 외우는 것이 아니라 작업 자체를 측정하도록 하세요.

둘째, 오프라인 평가만으로는 전체를 볼 수 없습니다. 샘플링된 프로덕션 트래픽, 사용자 피드백, 불만 비율 같은 온라인 신호와 함께 사용하고, 실제 실패를 새로운 케이스로 세트에 다시 반영하세요.

출력이 비결정적이기 때문에, 같은 입력도 실행마다 다른 점수를 받을 수 있습니다. 케이스마다 충분한 표본을 실행하고, 단일 실행의 잡음을 쫓기보다는 허용 오차(tolerance)를 기준으로 판단하세요. 실행 간 1점 정도의 흔들림은 회귀가 아니라 측정 자체의 숨결일 뿐입니다. 그 변동을 버텨낼 수 있는 수준으로 기준선을 설정하세요. 그렇지 않으면 한 주를 유령을 쫓는 데 써버릴 것입니다. 저도 정확히 그런 경험을 한 번이 아니라 여러 번 했습니다.

Juno변경할 때마다 실행하기 CI에 임계값을 두어서 나쁜 변경이 빌드를 실패시키게 하고, 새 가중치는 같은 프롬프트를 다르게 읽으므로 모델 버전이 올라갈 때마다 다시 평가하세요. 세트는 대체 지표일 뿐이므로 계속 교체하고 온라인 신호와 함께 쓰세요. 그렇지 않으면 자기 테스트에 과적합하게 됩니다. 그리고 출력이 비결정적이므로, 유령을 쫓아다니지 말고 허용 오차를 기준으로 판단하세요. 네, 저도 그렇게 유령을 쫓아본 적이 있습니다.

실전에서

앞 절의 run_evals 함수는 작지만 완전한 평가 도구입니다. 분류기와 평가 세트를 지정해주면, 몇 개의 케이스가 통과했는지 알려주고 어떤 케이스가 실패했는지 정확히 보여줍니다.

python
run_evals(eval_set, classify_review)
# FAIL: "그냥 괜찮았어요, 특별하진 않았어요." -> got POSITIVE, wanted NEUTRAL
# 2/3 passed

그 실패한 한 줄이 열 번의 성공적인 손 테스트보다 더 가치 있는 이유는, 이제 고치고 다시 측정할 수 있는 진짜 약점을 짚어주기 때문입니다. 마지막 챕터인 안전과 한계에서는 실제 사용자 앞에 이 모든 것을 내놓기 전에 대비해야 할 위험들을 다룹니다.

Juno실전에서run_evals 같은 작은 도구만으로도 이미 제 값을 합니다. 점수와 정확히 어떤 케이스가 깨졌는지를 보여주니까요. 하나를 고치고, 다시 실행해서, 숫자가 움직이는 것을 지켜보세요. 다음으로는, 안전과 한계에서 실제 사용자가 도착하기 전에 대비해야 할 것들을 다룹니다.

작은 평가 도구(eval harness), 즉 평가 세트의 모든 케이스를 실행하고 출력을 기대값과 비교해 채점하는 코드는 "괜찮아 보인다"를 움직일 수 있는 숫자로 바꿔주는 순환 과정입니다. 세트를 실행하고, 점수와 실패 목록을 얻고, 가장 저렴하게 고칠 수 있는 실제 실패를 고치고, 다시 실행합니다.

실패한 각 케이스는 완료의 기준이 명확한 작업 단위입니다. 그 기준은 바로 케이스가 통과하는 것입니다. 이는 "프롬프트를 더 좋게 만들어라"보다 훨씬 명확한 목표입니다. 목표에 도달했는지, 회귀가 일어났는지를 정확히 볼 수 있기 때문입니다.

이 모든 것이 실제 사용자와 만나기 전, 다음 챕터인 안전과 한계에서는 미리 대비해야 할 위험들을 다룹니다.

Juno실전에서 평가 도구는 느낌을 움직일 수 있는 숫자로 바꿔줍니다. 세트를 실행하고, 점수와 실패를 확인하고, 가장 저렴하게 고칠 수 있는 실제 실패를 고치고, 다시 실행하세요. 각 실패는 완료 기준이 명확한 작업이며, 이는 "더 좋게 만들어라"보다 훨씬 명확한 목표입니다. 그다음은 실제 사용자가 보기 전에 안전과 한계입니다.

프로덕션에서는 그 작은 도구가 더 이상 손으로 실행하는 스크립트가 아니라 시스템의 일부가 됩니다. CI 관문, 즉 여러분이 정한 기준선 아래로 점수가 떨어지면 릴리스를 막는 검사로 자라나고, 여기에 릴리스마다 지켜보는 대시보드가 더해져서 한 번의 실행으로는 절대 보이지 않는 느린 변화를 잡아냅니다.

오프라인 평가(배포 전에 채점하는 고정된 세트)와 온라인 모니터링(배포 후 실제 트래픽에서 수집하는 신호)은 서로를 먹여줍니다. 프로덕션에서의 실패는 새로운 오프라인 케이스가 되고, 오프라인 세트는 그 수정이 실제로 효과가 있었는지 알려줍니다. 여기서 이기는 팀은 가장 기발한 프롬프트를 가진 팀이 아닙니다. 다른 모두가 눈으로 확인하는 동안 측정한 팀입니다.

그것이 바로 이 습관의 본질입니다. 다음 챕터인 안전과 한계에서는 이 모든 것이 실제 사용자에게 도달하기 전에 대비해야 할 위험들을 다룹니다.

Juno실전에서 이것을 성숙시키면 스크립트는 CI 관문과 대시보드가 되고, 오프라인 평가와 온라인 모니터링이 서로를 먹여줍니다. 저도 "제가 보기엔 괜찮은데요"로 배포하고 나중에 그 대가를 치른 적이 있으니, 제 경험으로 말씀드리면 이기는 팀은 다른 모두가 눈으로 확인하는 동안 측정한 팀입니다. 실제 사용자가 도착하기 전에 안전과 한계를 읽어보세요.