평가 프롬프트의 문장 하나를 고쳤다. 설명은 더 명확해진 것 같은데, 이전에 잡던 오류를 계속 잡을까?

평가 결과를 화면에서 몇 개 읽는 것으로는 이 질문에 답하기 어렵다. 입력과 기대 판정, 이전 결과와 새로운 결과를 같은 형태로 저장하면 변경을 비교할 수 있다.

앞선 글에서 만든 도서관 안내문 기준을 작은 회귀 테스트로 옮겨보자. 이 글의 데이터와 판정은 모두 작성한 가상 예시다. 모델 API를 호출하지 않으며, 실행 결과는 집계 코드의 동작만 보여준다.

무엇의 회귀를 검사하는지 정한다

이번 예제에서는 후보 요약과 평가 기준을 고정한다. 평가 프롬프트만 변경했다고 가정하고, 저장된 두 벌의 평가 결과를 비교한다.

생성 모델과 평가 모델, 기준을 한꺼번에 바꾸면 점수 변화의 원인을 분리하기 어렵다. 실제 실험에서는 비교 목적에 맞게 바뀌는 요소를 명시해야 한다.

기록 역할
cases.json 원문·후보 요약·기대 판정
baseline.json 이전 평가 결과를 흉내 낸 가상 데이터
candidate.json 일부 판정을 바꾼 가상 데이터
compare.py 버전·표본을 검증하고 변화 집계
test_compare.py 누락·중복·실행 실패 등의 처리 확인

전체 코드는 예제 디렉터리에 있다. Python 표준 라이브러리만 사용한다.

데이터는 입력과 기대 판정을 함께 보관한다

예제에는 정상 요약, 원문과 모순되는 요약, 예외 정보를 빠뜨린 요약, 근거 없는 조건을 추가한 요약이 한 건씩 있다. 각 입력에서 사실 충실성·핵심 정보·근거 없는 추가의 세 항목을 확인한다. 형식 검사는 이번 예제 범위에서 제외한다.

{
  "id": "missing-exception",
  "source": "도서관은 월요일에 휴관한다. 화요일부터 일요일까지 9시부터 18시까지 운영한다. 어린이 자료실은 17시에 문을 닫는다.",
  "candidate": "월요일은 휴관하며 화~일요일 9~18시에 운영한다.",
  "expected": {
    "consistency": "pass",
    "coverage": "fail",
    "grounding": "pass"
  }
}

여기서 기대 판정은 예제를 설명하기 위해 작성한 값이다. 실제 평가 데이터로 사용하려면 판정 주체·검수 절차·불일치 해결 방법을 기록해야 한다. JSON으로 저장했다는 사실이 정답의 신뢰도를 높여주지는 않는다.

각 파일에는 dataset_version과 rubric_version을 넣는다. 이번 비교기는 둘 중 하나라도 다르면 실행을 중단한다. 기준이 바뀌었다면 동일한 새 기준으로 기준선부터 다시 평가해야 한다.

평가 실패를 조용히 빼지 않는다

내용에 대한 unclear와 실행 실패는 다르다. 전자는 판단 근거 부족이고 후자는 평가 결과를 얻지 못한 상태다.

{"id": "complete", "status": "error", "error": "timeout"}

이 예제는 한 입력의 실행이 실패하면 그 입력의 세 항목을 일치 건수에 포함하지 않되, 전체 분모 12개는 유지한다. 별도로 실행 실패 건수를 표시한다. 따라서 여기서 표시하는 값은 전체 예정 항목 대비 기대 판정 일치 건수이며, 성공한 호출만의 정확도가 아니다.

평가 결과에서 ID가 빠지거나 중복되면 집계 전에 오류를 낸다. 반환된 행 수가 같더라도 다른 입력의 결과로 바뀌었다면 감지해야 하기 때문이다.

실서비스에서는 재시도를 하더라도 최초 실패와 시도 횟수, 최종 상태를 함께 보관하는 것이 좋다. 이번 코드는 저장된 최종 결과를 비교하며 API 호출과 재시도 자체는 구현하지 않는다.

총점보다 어떤 방향으로 틀렸는지를 본다

단순 일치 건수와 함께 다음 값을 집계한다.

지표 이 예제의 정의
matched 기대 판정과 같은 기준별 판정 수
unsafe_pass 기대값이 fail인데 pass로 판정한 항목 수
false_reject 기대값이 pass인데 fail로 판정한 항목 수
unclear 판단 불가로 반환한 항목 수
execution_errors 평가 실행에 실패한 입력 수

판정 단위와 입력 단위를 구분해야 한다. 잘못된 통과 2개가 한 입력에서 발생했다면 문제가 있는 입력이 2개라는 뜻은 아니다.

핵심 집계 로직은 다음과 같다.

for criterion, verdict in row["verdicts"].items():
    target = expected[row["id"]][criterion]
    stats["matched"] += verdict == target
    stats["unsafe_pass"] += target == "fail" and verdict == "pass"
    stats["false_reject"] += target == "pass" and verdict == "fail"
    stats["unclear"] += verdict == "unclear"

이 코드만으로 모든 종류의 불일치를 세분화한 것은 아니다. 예를 들어 기대값이 unclear인데 단정적인 판정을 내린 경우는 일치 건수에 반영되지만 별도 오류 항목은 없다. 실제 적용에서는 필요한 오류 유형을 추가해야 한다.

실행하면 의도한 악화가 드러난다

저장소를 받은 뒤 다음 명령을 실행한다.

cd examples/llm-judge-regression
python3 -m unittest -v
python3 compare.py

기준선은 기대값을 그대로 복사했고, 후보에는 두 가지 변화를 의도적으로 넣었다. 모순이 있는 요약의 사실 충실성을 pass로 바꾸고, 예외 누락의 핵심 정보 판정을 fail에서 unclear로 바꿨다.

항목 기준선 후보
기대 판정 일치 12/12 10/12
잘못된 통과 0 1
잘못된 탈락 0 0
판단 불가 0 1
실행 실패 0 0

프로그램은 gate: FAIL을 출력하고 종료 코드 1을 반환한다. 이는 예제 실행 오류가 아니라 의도적으로 넣은 회귀를 감지했다는 결과다. 단위 테스트 명령은 이 동작이 맞는지 확인하므로 정상적으로 통과한다.

이 예제의 통과 조건은 실행 실패가 없고, 잘못된 통과·탈락·판단 불가가 늘지 않으며, 전체 일치 건수가 줄지 않는 것이다. 설명용으로 정한 보수적인 규칙이며 모든 과제의 운영 기준은 아니다. 특히 실제로 판단 불가가 적절한 표본에서는 그 수가 늘어나는 것이 개선일 수도 있다.

CI에서는 비용이 드는 작업을 분리한다

저장된 JSON의 검증과 집계는 API 없이 실행할 수 있다. 반면 새 프롬프트가 실제로 어떤 판정을 내리는지 확인하려면 모델 호출이 필요하다.

따라서 두 단계를 분리할 수 있다. 일반 변경에서는 파일 구조·ID·버전·집계 코드를 확인하고, 평가 프롬프트 변경 시에는 정해진 검증 표본에 한해 실제 판정을 새로 수집한다. 실제 실행 기록에는 모델 ID, 프롬프트 원문과 버전, 생성 설정, 실행 시각, 토큰과 비용도 포함한다.

고정된 가상 데이터 테스트가 통과했다고 새 프롬프트의 품질이 검증된 것은 아니다. 이 예제는 평가 인프라의 기본 동작을 확인한다. 실제 평가자의 품질은 별도의 표본과 사람 판정으로 확인해야 한다.

반복 실행으로 판정이 흔들릴 수 있는 시스템에서는 한 번의 비교를 곧바로 성능 차이라고 해석하지 않는다. 기준별 변화와 원본 사례를 읽고, 필요하면 반복 측정과 표본 확대를 통해 확인한다.

평가를 코드로 관리하는 목적은 점수를 자동으로 믿기 위해서가 아니다. 무엇이 바뀌었고, 어떤 입력에서 판정이 달라졌는지 검토할 수 있도록 기록을 남기는 것이다.

참고 자료