fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/AI/LLM 평가 프레임워크: 골든셋, 회귀 테스…
ai

LLM 평가 프레임워크: 골든셋, 회귀 테스트, LLM-as-a-judge, 사람 피드백 루프

LLM 기반 기능은 한 가지 곤란한 성질을 가진다. 같은 입력에도 출력이 매번 달라지고, "좋아졌다"가 숫자로 잘 잡히지 않는다. 프롬프트 한 줄을 고치거나 모델 버전을 올렸을 때, 그게 정말 개선인지 아니면 어떤 케이스를 조용히 망가뜨린 퇴행인지 눈으로는 알 수 없다. 그래서 LLM 제품의 신뢰성은 모델 자체보다 평가 체계(evaluation) 에서 갈린...

2026.09.08·8 min read·44 views

왜 이 주제가 중요한가

LLM 기반 기능은 한 가지 곤란한 성질을 가진다. 같은 입력에도 출력이 매번 달라지고, "좋아졌다"가 숫자로 잘 잡히지 않는다. 프롬프트 한 줄을 고치거나 모델 버전을 올렸을 때, 그게 정말 개선인지 아니면 어떤 케이스를 조용히 망가뜨린 퇴행인지 눈으로는 알 수 없다.

그래서 LLM 제품의 신뢰성은 모델 자체보다 평가 체계(evaluation) 에서 갈린다. 평가가 없으면 모든 변경이 "느낌상 더 나아진 것 같다"로 머물고, 평가가 있으면 변경을 측정 가능한 의사결정으로 바꿀 수 있다.

이 문서는 운영 무용담이 아니라 평가를 어떻게 설계하는가의 관점에서 네 가지 축을 한 흐름으로 엮는다.

  • 골든셋(golden set) — 무엇을 정답으로 둘 것인가
  • 회귀 테스트(regression test) — 변경이 기존 품질을 깨지 않았는지 어떻게 자동으로 잡는가
  • LLM-as-a-judge — 정답이 고정되지 않는 출력을 모델로 어떻게 채점하는가
  • 사람 피드백 루프(human feedback loop) — 사람을 어디에, 얼마의 비용으로 끼워 넣는가

여기서는 단일 호출·단일 응답 수준의 평가 기반을 다룬다.

전체 그림과 네 축의 관계

평가 프레임워크는 따로 노는 도구 묶음이 아니라 하나의 순환이다.

핵심은 데이터(golden set)와 채점(scorer)을 분리하고, 회귀 통과 조건을 자동화하며, 사람(human)을 전수가 아니라 선택 지점에만 넣는다는 점이다. 이 분리가 안 되면 평가가 매번 즉흥적인 수동 검수로 회귀한다.

1. 골든셋의 정답 정의

골든셋은 "이 입력에는 이런 출력이 나와야 한다"를 고정해 둔 평가용 데이터셋이다. 학습 데이터와 다르다. 학습은 모델을 바꾸지만, 골든셋은 모델을 바꾸지 않고 판정만 한다.

좋은 골든셋의 조건은 크기보다 분포다.

  • 대표성 — 실제 트래픽의 주요 의도/카테고리 분포를 반영한다. 쉬운 케이스만 모으면 항상 100점이 나와 변별력이 없다.
  • 난이도 혼합 — 평이한 케이스, 경계 케이스, 알려진 실패 케이스를 의도적으로 섞는다.
  • 부정 케이스 포함 — "거절해야 하는 입력", "모른다고 답해야 하는 입력"을 반드시 넣는다. 정상 응답만 평가하면 환각·과잉응답을 못 잡는다.
  • 라벨의 종류 명시 — 정답이 한 개로 고정되는지(분류·추출), 여러 정답이 허용되는지(요약·생성)에 따라 채점 방식이 달라진다.

각 케이스는 단순 입력/정답 쌍을 넘어, 무엇을 기준으로 통과로 볼지를 함께 적는다.

json
{
  "id": "refund-policy-001",
  "category": "policy_qa",
  "input": "결제 후 3일 지났는데 환불 되나요?",
  "reference": "기간 조건에 따라 다르며, 구체 조건은 정책 문서를 인용해 안내해야 함",
  "rubric": [
    "환불 가능 여부를 단정하지 않고 조건을 설명한다",
    "근거가 되는 정책 출처를 인용한다",
    "모르는 부분은 추측하지 않는다"
  ],
  "must_not": ["무조건 환불 가능하다고 단정", "정책에 없는 기간 임의 생성"],
  "difficulty": "boundary"
}

흔한 실수:

  • 데이터 오염(leakage) — 골든셋 입력이 프롬프트 예시(few-shot)에 그대로 들어가면 점수가 부풀려진다. 평가셋과 프롬프트 예시는 분리한다.
  • 버전 관리 부재 — 골든셋도 코드처럼 버전을 찍는다. 데이터가 조용히 바뀌면 어제와 오늘의 점수를 비교할 수 없다.
  • 정답을 한 표현으로 못박기 — 생성 과제에서 정답 문자열 하나만 두면, 의미가 같아도 표현이 다르면 다 틀리게 나온다. 이 경우 rubric(채점 기준표) 기반 채점으로 넘어가야 한다.

2. 변경으로 발생한 품질 저하 자동 감지

회귀 테스트의 목적은 "프롬프트/모델/검색 파이프라인을 바꿨을 때, 기존에 잘 되던 케이스가 깨지지 않았는가"를 자동으로 확인하는 것이다. 일반 단위 테스트와 다른 점은 출력이 비결정적이라 정확 일치(exact match)만으로는 부족하다는 데 있다.

설계 포인트는 세 가지다.

  • 메트릭 기준 점검 — 케이스별 pass/fail이 아니라 골든셋 전체의 집계 지표(정확도, rubric 통과율 등)에 임계값을 둔다. 단건 흔들림에 CI가 과민 반응하지 않게 한다.
  • 카테고리별 분해 — 전체 점수만 보면 "환불 카테고리가 무너졌는데 다른 카테고리가 올라가서 총점은 유지"되는 퇴행을 놓친다. 카테고리별로 따로 본다.
  • 비결정성 흡수 — temperature가 0이 아니면 같은 입력도 점수가 흔들린다. 케이스를 여러 번 돌려 평균을 쓰거나, 평가 시점에는 temperature를 낮춰 분산을 줄인다.

CI에 붙일 때는 "이전 버전 대비 임계 이상 하락하면 차단"이 기본형이다.

yaml
# 예: PR마다 평가 실행 후 임계값 통과 조건 확인
eval-check:
  run: python run_eval.py --golden golden_v3.json --out report.json
  rules:
    - metric: overall_pass_rate
      min: 0.85                 # 절대 하한
    - metric: overall_pass_rate
      max_drop_vs_main: 0.03    # main 대비 3%p 이상 하락 시 실패
    - metric: policy_qa_pass_rate
      max_drop_vs_main: 0.0     # 핵심 카테고리는 하락 불허

스냅샷 비교(이전 출력과 텍스트 diff)도 보조로 쓸 수 있지만, 생성 과제에서는 표현만 바뀌어도 diff가 터지므로 메트릭 기준 점검을 주로 두고 스냅샷은 참고용으로 둔다.

3. 정답이 고정되지 않는 출력을 채점하는 LLM-as-a-judge

요약·설명·대화처럼 정답이 한 개로 안 떨어지는 과제는, 사람이 매번 채점하면 느리고 비싸다. 그래서 모델이 모델의 출력을 채점하는 LLM-as-a-judge를 쓴다. 다만 judge도 LLM이라 같은 약점을 공유한다는 점을 전제로 설계해야 한다.

채점 방식은 크게 둘이다.

  • Pointwise — 출력 하나에 rubric 기준으로 점수/통과 여부를 매긴다. 기준이 명확할 때 안정적이다.
  • Pairwise — 두 출력 중 어느 쪽이 더 나은지 고른다. 절대 점수보다 상대 비교가 사람과 더 잘 맞는 경우가 많다. A/B 비교나 모델 교체 판단에 적합하다.

judge가 가진 알려진 편향:

  • Position bias — pairwise에서 먼저 제시된 답을 선호한다. 순서를 바꿔 두 번 물어 일관성을 확인한다.
  • Verbosity bias — 길고 장황한 답을 더 좋게 본다. rubric에 "간결성"을 명시하거나 길이를 통제한다.
  • Self-preference — 같은 계열 모델이 만든 답을 선호하는 경향이 있다. 가능하면 채점 모델과 생성 모델 계열을 분리한다.

가장 중요한 원칙은 judge 자체를 검증해야 한다는 것이다. judge를 믿기 전에, 사람이 라벨링한 소규모 셋에서 judge 판정과 사람 판정의 일치도(agreement)를 측정한다. 일치도가 낮으면 점수가 아니라 rubric을 고친다.

text
judge 신뢰성 점검 절차
1. 사람이 라벨링한 케이스 50~100건 준비
2. 동일 케이스를 judge로 채점
3. 사람 vs judge 일치율 / 상관 측정
4. 불일치 케이스 원인 분석 → rubric 문장 구체화
5. 목표 일치율 도달 후에야 judge를 대규모로 사용

rubric은 추상어("좋은 답인가")를 피하고, 통과 조건을 검증 가능한 문장으로 쪼갠다. 앞의 골든셋 예시에 넣은 rubric 배열이 그대로 judge 입력이 된다. 골든셋과 judge가 같은 기준을 공유하게 설계하는 것이 핵심이다.

4. 사람 피드백을 넣는 위치

사람 검수는 가장 정확하지만 가장 비싸다. 그래서 설계의 핵심은 "사람을 전수 검수에 쓰지 않고, 자동 평가가 불확실한 지점에만 배치"하는 것이다.

사람을 끼우면 좋은 지점:

  • judge가 낮은 신뢰도로 판정한 구간 — 통과/실패 경계 점수, judge가 순서에 따라 답이 바뀐 케이스.
  • 새 실패 유형 발굴 — 자동 평가가 통과시켰지만 실제로는 이상한 출력. 여기서 나온 케이스를 골든셋에 환류한다.
  • rubric 보정 — 사람과 judge가 자주 엇갈리는 카테고리의 기준을 다시 쓴다.

피드백을 받는 방식도 비용 차이가 크다.

  • 명시적 피드백 — 사람이 직접 점수/라벨을 단다. 정확하지만 느리다.
  • 암묵적 피드백 — 사용자의 재질문, 복사, 이탈 같은 행동 신호. 싸지만 노이즈가 많고 해석이 필요하다.

가장 중요한 건 루프가 닫혀야 한다는 것이다. 사람이 찾은 실패가 골든셋의 새 케이스로 들어가고, 그 케이스가 다음 회귀 테스트에서 다시 검사되어야 같은 실패가 재발하지 않는다. 피드백을 모으기만 하고 골든셋에 환류하지 않으면, 평가는 점점 현실과 멀어진다.

나쁜 설계와 개선된 설계

나쁜 설계:

  • 출시 직전 팀원이 손으로 20개쯤 돌려 보고 "괜찮네" 하고 배포한다.
  • 정답을 문자열 하나로 고정해 의미가 같아도 틀렸다고 나온다.
  • judge 점수를 검증 없이 그대로 믿는다.
  • 프롬프트 예시에 평가 케이스가 섞여 점수가 부풀려진다.

개선된 설계:

  • 카테고리·난이도가 분포된 버전 관리되는 골든셋을 둔다.
  • 생성 과제는 rubric 기반 judge로 채점하고, judge는 사람 라벨과의 일치도로 먼저 검증한다.
  • 변경마다 CI에서 카테고리별 회귀 점검을 통과해야 머지된다.
  • 사람 검수에서 나온 실패를 골든셋에 환류해 루프를 닫는다.

작은 실습 환경

거창한 플랫폼 없이도 로컬에서 평가 골격을 만들 수 있다. JSON 골든셋, 러너와 간단한 채점으로 시작한 뒤 judge를 붙이는 순서가 좋다.

bash
# 1. 골든셋과 러너만으로 시작
mkdir llm-eval && cd llm-eval
python -m venv .venv && source .venv/bin/activate
pip install pytest
# golden.json 작성 후
pytest -q test_eval.py

가장 단순한 형태의 평가 러너는 다음과 같다. 실제 모델 호출부는 call_model로 추상화해 두고, 채점은 rubric 통과 비율을 집계한다.

python
import json
 
def call_model(prompt: str) -> str:
    # 실제로는 LLM API 호출. 테스트에서는 stub로 대체.
    ...
 
def judge(output: str, rubric: list[str]) -> float:
    # 실제로는 judge 모델 호출. 여기서는 rubric 통과 개수 비율을 반환한다고 가정.
    ...
 
def run_eval(golden_path: str) -> dict:
    cases = json.load(open(golden_path, encoding="utf-8"))
    by_category: dict[str, list[float]] = {}
    for c in cases:
        out = call_model(c["input"])
        score = judge(out, c["rubric"])
        by_category.setdefault(c["category"], []).append(score)
    return {
        cat: sum(scores) / len(scores)
        for cat, scores in by_category.items()
    }
 
if __name__ == "__main__":
    report = run_eval("golden.json")
    for cat, avg in report.items():
        print(f"{cat}: {avg:.2f}")

출력은 카테고리별 평균으로 떨어진다.

text
policy_qa: 0.88
refund: 0.79
small_talk: 0.95

이 숫자를 이전 버전과 비교하는 한 줄을 CI에 추가하면, 그 순간부터 "느낌"이 아니라 "지표"로 변경을 판단하게 된다.

면접 답변 프레임

평가 설계 질문은 화려한 운영 규모가 아니라 사고의 분리를 본다. 다음 골격으로 답하면 운영 경험을 과장하지 않고도 설계 감각을 보여줄 수 있다.

  • "LLM 기능은 비결정적이라 회귀를 눈으로 못 잡습니다. 그래서 먼저 분포가 잡힌 골든셋을 만들고, 변경마다 그 셋으로 회귀를 봅니다."
  • "정답이 고정되는 과제는 정확 매칭, 생성 과제는 rubric 기반 judge로 나눠 채점합니다. judge는 그대로 믿지 않고 사람 라벨과의 일치도로 먼저 검증합니다."
  • "사람은 전수가 아니라 judge가 불확실한 구간에만 넣고, 거기서 나온 실패를 골든셋에 환류해 루프를 닫습니다."
  • "에이전트처럼 단계가 여러 개면 결과만이 아니라 궤적과 위험 행동도 따로 평가해야 합니다." (여기서 위험 점검 주제로 자연스럽게 확장)

면접에서 함정은 "정확도 몇 %를 봤냐"가 아니라 "그 정확도를 어떻게 신뢰하느냐"를 되물을 때다. 이때 judge 검증과 데이터 오염 방지를 언급하면 한 단계 위의 답이 된다.

체크리스트

  • 골든셋이 실제 트래픽 분포를 반영하고, 부정 케이스(거절·모름)를 포함하는가
  • 골든셋이 버전 관리되고, 프롬프트 예시와 분리되어 데이터 오염이 없는가
  • 정답 고정 과제와 생성 과제의 채점 방식을 구분했는가
  • 회귀 점검이 전체 점수만이 아니라 카테고리별로 하락을 감지하는가
  • 비결정성(temperature)으로 인한 점수 분산을 통제했는가
  • LLM-as-a-judge를 사람 라벨과의 일치도로 먼저 검증했는가
  • judge의 position/verbosity/self-preference 편향을 완화하는 장치가 있는가
  • 사람 검수가 전수가 아니라 불확실 구간에 집중되는가
  • 사람이 찾은 실패가 골든셋으로 환류되어 루프가 닫히는가
on this page
  • 01왜 이 주제가 중요한가
  • 02전체 그림과 네 축의 관계
  • 031. 골든셋의 정답 정의
  • 042. 변경으로 발생한 품질 저하 자동 감지
  • 053. 정답이 고정되지 않는 출력을 채점하는 LLM-as-a-judge
  • 064. 사람 피드백을 넣는 위치
  • 07나쁜 설계와 개선된 설계
  • 08작은 실습 환경
  • 09면접 답변 프레임
  • 10체크리스트
tags
#study

이런 글도

  • ai

    하네스 회고는 새 문서로 쌓지 않고 종류에 맞는 기존 단일 소스에 환원한다

    2026.10.02
  • ai

    에이전트가 배운 회피 패턴은 조건을 통과한 것만 파일로 쌓고 주기적으로 지운다

    2026.10.02
  • ai

    ADR 은 되돌리기 어려운 결정의 이유만 남기고, 대안 기각은 옵션마다 줄을 나눈다

    2026.10.02
  • ai

    스킬 문서는 반복 실행을 스크립트로 내리고 같은 지시를 한 곳에서만 소유하게 쓴다

    2026.10.02

댓글 (0)