tryumanshow.me
I  —  TECH  —  № 01

Eval-driven 개발: 테스트 대신 스코어보드를 쓰기 시작했나

회귀 테스트는 결과가 같음을 증명한다. LLM 시스템에서 우리에게 필요한 것은 결과가 더 나아졌다는 증거다.

PUB  2026.04.280 VIEWS14 MIN

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 본문에 자동으로 박힌다. 머지 전 마지막 사람이 보는 화면은 이거다.