LLM 기반 시스템에서 회귀 테스트는 자주 거짓 안심을 만든다. assert output == expected 라는 한 줄은 거짓말은 아니지만, 모델이 더 잘 답하기 시작했을 때 그 한 줄은 가장 먼저 빨갛게 변한다. 우리는 이 문제를 한참 동안 무시하다가, 결국 회귀 테스트 대신 회귀 평가를 쓰기로 했다.
이 글은 그 결정이 어떻게 내려졌는지, 그리고 그 결정 이후 우리 팀의 PR 리뷰 과정이 어떻게 바뀌었는지에 대한 메모다. 결론을 먼저 말하면 — 우리가 PR을 머지할지 말지를 결정하는 화면은 더 이상 GitHub Actions의 초록색 체크가 아니다. 그건 스코어보드다.
1. 왜 테스트가 깨지는가
가장 단순한 LLM 호출 — 사용자 질문을 받아 답하는 함수 — 를 생각해 보자. 이 함수에 대한 테스트는 보통 이렇게 생겼다.
def test_summarize():
out = summarize("긴 문서 ...")
assert out == "기대한 한 줄 요약"
이 테스트는 모델이 정확히 "기대한 한 줄 요약"이라는 문자열을 뱉어야만 통과한다. 모델이 더 좋아져서 같은 의미를 다른 단어로 표현하면 — 빨강이다. 모델이 더 나빠져서 같은 단어를 다른 의미로 쓰기 시작해도 — 초록일 수 있다. 둘 다 우리가 원하는 신호가 아니다.
테스트는 결과가 같음을 증명하고, eval은 결과가 더 나아졌음을 보여준다.
2. 스코어보드의 모양
우리가 쓰는 형태는 단순하다. 데이터셋에 대해 모델을 돌리고, 각 출력에 대해 판정자(judge)가 점수를 매긴다. 평균이 떨어지면 PR은 머지되지 않는다.
def evaluate(model, dataset):
scores = []
for ex in dataset:
y = model(ex.input)
scores.append(judge(y, ex.gold))
return mean(scores)
가장 중요한 건 judge다. 우리는 처음에는 사람을 썼고, 다음에는 LLM-as-a-judge를, 지금은 둘을 같이 쓴다. 사람의 점수가 LLM의 점수와 어디서 갈라지는지는 그 자체로 또 하나의 dataset이 된다 — 이 dataset이 시간이 지나면서 우리 시스템에서 가장 비싼 자산이 되어 가고 있다.
$ uv run pytest tests/ --eval
collected 47 evals · scoring…
baseline 0.71 → current 0.78 (+0.07) ✓
이 한 줄짜리 출력 — baseline → current — 가 우리 PR 본문에 자동으로 박힌다. 머지 전 마지막 사람이 보는 화면은 이거다.