← 목록으로 돌아가기

LLM 평가 파이프라인 실전: Evals-as-Code로 품질 회귀를 CI에서 막고 토큰 비용까지 줄이는 법

AI

LLM 평가 파이프라인을 CI 단계에서 자동 실행하는 다이어그램

한 달 만에 터진 품질 회귀, 그리고 우리가 내린 결론

지난해 11월, 우리 팀은 고객 응대 자동화 서비스에서 예상치 못한 품질 회귀를 경험했습니다. 프롬프트 템플릿을 일부 수정하고 모델을 gpt-4o-2024-08-06에서 gpt-4o-mini로 전환한 PR이 메인 브랜치에 머지된 지 나흘째, 운영팀에서 "챗봇이 환불 정책을 엉뚱하게 답변한다"는 에스컬레이션이 올라왔습니다. QA를 거쳤고 스모크 테스트도 통과했지만, 열네 개의 엣지 케이스 중 세 개에서 답변 품질이 기준 이하로 떨어져 있었습니다. 그 세 케이스가 자동 테스트에 포함되어 있지 않았기 때문입니다.

이 사고에서 얻은 교훈은 명확했습니다. LLM의 품질은 유닛 테스트가 잡는 "옳고 그름"과 다른 차원에 있습니다. 모델 버전, 시스템 프롬프트 한 줄, 온도 파라미터 0.1의 차이가 사용자 체감 품질을 바꿀 수 있고, 이 변화는 기존 소프트웨어 테스트 패러다임으로는 포착되지 않습니다. 우리 팀이 그 이후 석 달 동안 구축한 것이 바로 Evals-as-Code 기반 LLM 평가 파이프라인입니다. PR마다 골든 데이터셋을 자동으로 돌리고, 점수가 기준치를 밑돌면 머지를 차단하며, 평가에 드는 토큰 비용까지 예산 안에서 관리하는 시스템입니다.


1. LLM 평가가 단위 테스트가 아닌 이유: 결정성 부재와 분포 이동

소프트웨어 테스트의 기본 전제는 결정성(determinism)입니다. 같은 입력에 같은 출력이 보장되어야 테스트가 의미를 가집니다. LLM은 이 전제를 처음부터 위반합니다. temperature=0으로 설정해도 OpenAI API는 내부 샘플링 과정에서 미세한 부동소수점 차이가 발생할 수 있으며, 동일한 프롬프트를 10번 실행하면 표현이 미묘하게 달라지는 응답을 볼 수 있습니다.

더 근본적인 문제는 **롤링 분포 이동(rolling distribution shift)**입니다. 모델 제공자가 동일한 모델 ID로 내부 가중치나 RLHF 데이터를 업데이트할 경우, 여러분의 코드는 변경되지 않았는데 LLM 응답 분포가 조용히 바뀝니다. OpenAI는 모델 스냅샷 ID(예: gpt-4o-2024-11-20)로 고정할 수 있지만, 이 스냅샷 자체도 일정 기간이 지나면 deprecated됩니다. 결국 모델을 유지하든 업그레이드하든, 품질을 지속적으로 측정하는 체계 없이는 눈을 감고 운전하는 것과 다름없습니다.

단위 테스트가 "함수가 올바른 값을 반환하는가"를 묻는다면, LLM 평가는 "이 응답이 우리가 정의한 품질 기준을 충족하는가"를 묻습니다. 전자는 이진(binary)이고 후자는 연속적(continuous)입니다. 또한 LLM 출력은 텍스트의 의미론적 품질을 판단해야 하므로, 평가 자체에 또 다른 LLM이 개입하는 LLM-as-Judge 패턴이 필요합니다. 이 순환적 구조가 LLM 평가를 독립된 공학 영역으로 만드는 이유입니다.

항목전통 단위 테스트LLM 평가
결정성완전 보장확률적 (반복 실행 시 분산 존재)
판정 기준이진 (pass/fail)연속 점수 (0.0~1.0)
기준값하드코딩된 expected 값골든 데이터셋 + 루브릭
실패 원인 추적코드 변경 → 재현 가능모델·프롬프트·데이터 복합 요인
평가 비용거의 무료 (CPU)토큰 비용 발생
평가자테스트 프레임워크Rule / Similarity / LLM-as-Judge

2. Evals-as-Code: 평가 케이스를 코드 리포에 두는 이점

Evals-as-Code는 평가 케이스, 루브릭, 임계값 설정을 스프레드시트나 별도 대시보드가 아닌 코드 리포지터리에서 버전 관리하는 방식입니다. 표면적으로는 단순해 보이지만, 이 결정이 팀 운영에 미치는 영향은 상당합니다.

첫째, 평가 케이스가 코드와 함께 PR에 포함되면 변경 추적이 자동으로 이루어집니다. "2026년 1월 15일에 환불 시나리오 케이스 7개를 추가했다"는 사실이 git 히스토리에 그대로 남습니다. 이전에 스프레드시트로 관리할 때는 누가 어떤 케이스를 추가·삭제했는지 추적이 불가능했습니다.

둘째, CI 파이프라인과의 연결이 자연스러워집니다. 평가 설정 파일(promptfooconfig.yaml 또는 eval_config.py)이 리포에 있으면, GitHub Actions가 파일 변경을 감지하고 필요한 케이스만 재실행하는 조건부 실행 로직을 구현하기 쉽습니다.

셋째, 새 팀원이 평가 체계를 이해하는 온보딩 비용이 낮아집니다. 코드를 클론하면 평가 케이스, 루브릭, 임계값이 모두 포함되어 있어 "어디서 평가 기준을 확인하나요?"라는 질문이 사라집니다.

이 접근법은 OpenAI Evals 프레임워크가 처음 제안한 방식이기도 합니다. OpenAI는 내부적으로 모든 모델 개선의 회귀를 이 evals 리포지터리로 추적합니다. 우리 팀은 이 철학을 차용하여 애플리케이션 레이어의 평가 케이스를 evals/ 디렉터리 아래에 도메인별로 분리해 관리합니다.


3. 골든 데이터셋 설계: 케이스 라벨링·난이도 슬라이스·시나리오 커버리지

골든 데이터셋은 평가 파이프라인의 토대입니다. 케이스가 부실하면 아무리 정교한 LLM-as-Judge 루브릭을 설계해도 파이프라인 전체가 공허해집니다. 우리 팀이 수렴한 설계 원칙은 세 가지입니다.

난이도 슬라이싱: 케이스를 Easy / Medium / Hard 세 등급으로 구분합니다. Easy는 모델이 확실히 맞혀야 하는 기본 케이스(명확한 의도, 짧은 컨텍스트), Medium은 애매한 표현이나 다중 의도가 섞인 케이스, Hard는 도메인 전문 지식이 필요하거나 프롬프트 인젝션 시도처럼 안전성이 걸린 케이스입니다. 임계값을 설정할 때 등급별로 다른 기준을 적용하면 false positive를 줄일 수 있습니다.

시나리오 커버리지 매트릭스: 기능 X 엣지 케이스 조합으로 커버리지를 시각화합니다. 예를 들어 고객 응대 챗봇이라면 "환불 요청 × 영수증 없음", "교환 요청 × 해외 배송 상품", "배송 조회 × 주문 48시간 미경과" 같은 조합 케이스가 들어가야 합니다.

라벨러 동의율 관리: 내부 도메인 전문가 2명이 각 케이스에 대해 독립적으로 "이상적인 응답"을 평가한 뒤, Cohen's Kappa 지수가 0.7 미만인 케이스는 기준이 모호하다는 신호로 보고 루브릭을 명확히 하거나 케이스를 분리합니다. 이 과정을 건너뛰면 나중에 LLM-as-Judge가 인간 평가자와 다른 결론을 내릴 때 원인을 찾기 어려워집니다.

실제 규모 기준으로, 처음 시작하는 팀이라면 도메인당 50~80개 케이스가 현실적인 목표입니다. 너무 많으면 CI 실행 시간이 길어지고, 너무 적으면 분포를 대표하지 못합니다. RAG 시스템의 청킹 전략과 평가 케이스 설계의 관계는 RAG 검색 평가와 청킹 전략에서 더 자세히 다룹니다.


4. 평가 지표 분류: Rule-based·Similarity·LLM-as-Judge 각각의 함정

세 가지 평가 방식은 각기 다른 강점과 약점을 가지며, 실무에서는 이를 레이어링하는 것이 정석입니다.

Rule-based 평가는 가장 빠르고 비용이 없습니다. JSON 스키마 검증, 금지어 포함 여부, 최소/최대 길이 체크, 정규식 매칭 등이 여기에 속합니다. 함정은 "통과했다고 품질이 좋다"는 착각입니다. 응답이 JSON 스키마를 완벽히 따르면서도 사실이 틀릴 수 있고, 금지어가 없으면서도 의도를 완전히 놓칠 수 있습니다.

Similarity 기반 평가는 ROUGE, BERTScore, 코사인 유사도 등으로 기준 응답과의 유사도를 측정합니다. 가장 큰 함정은 표현 방식이 다를 뿐 의미가 같은 응답을 낮게 평가한다는 점입니다. "환불은 7일 이내에 가능합니다"와 "구매일로부터 일주일 안에 환불 신청을 하시면 됩니다"는 의미가 동일하지만 BERTScore는 이를 완전히 동일하게 보지 않습니다.

LLM-as-Judge는 Liu et al.의 G-Eval(EMNLP 2023, arxiv.org/abs/2303.16634)이 제안한 방식으로, GPT-4 급 모델이 Chain-of-Thought로 평가 단계를 생성한 뒤 점수를 매깁니다. 인간 판단과의 상관관계(Spearman correlation 0.514)가 기존 자동 지표를 크게 상회합니다. 함정은 두 가지입니다. 첫째, LLM 생성 텍스트를 LLM이 더 높게 평가하는 자기 편향(self-preference bias)이 존재합니다. 둘째, 평가 자체가 토큰 비용을 소모합니다.


5. promptfoo·Inspect AI·LangSmith·OpenAI Evals 비교

2026년 현재 주류 LLM 평가 프레임워크 네 가지를 비교합니다.

항목promptfooInspect AILangSmithOpenAI Evals
설정 방식YAML 선언형Python 코드UI + SDKPython + YAML
CI/CD 연동공식 지원 (CLI)GitHub Actions 호환API 기반CLI
LLM-as-Judge내장내장 (scorer)내장내장
비용 추적기본 제공수동 구현 필요기본 제공제한적
오픈소스MITMIT (UK AISI)부분 오픈MIT
다중 모델 비교탁월가능가능가능
학습 난이도낮음중간낮음 (UI)중간

promptfoo 공식 문서에 따르면 단일 YAML 파일로 평가 케이스, 프로바이더, assertion을 모두 정의할 수 있어 CI 연동이 가장 빠릅니다. Inspect AI는 영국 AI Safety Institute가 개발한 프레임워크로, safety evaluation에 특화된 scorer 아키텍처가 강점입니다. LangSmith는 LangChain 생태계 내에서 트레이싱과 평가를 통합 관리할 때 유리합니다.

아래는 promptfoo 스타일의 실제 평가 케이스 YAML 파일 예시입니다. 고객 응대 챗봇의 환불 정책 도메인을 기준으로 작성했습니다.

# evals/customer-support/refund-policy.yaml
description: "환불 정책 응답 품질 평가 — 고객 응대 챗봇"

providers:
  - id: openai:gpt-4o-mini
    config:
      temperature: 0
      systemPrompt: |
        당신은 쇼핑몰 고객 응대 챗봇입니다.
        환불 정책: 구매일로부터 7일 이내 영수증 지참 시 전액 환불 가능.
        단, 해외 배송 상품 및 주문 제작 상품은 환불 불가.
  - id: openai:gpt-4o
    config:
      temperature: 0

defaultTest:
  assert:
    - type: not-contains
      value: "죄송합니다만 저는 답변드리기 어렵"
    - type: llm-rubric
      value: |
        응답이 환불 정책을 정확히 안내하는가?
        다음 기준으로 1-10점 평가하라:
        10점: 조건(7일, 영수증), 예외(해외/주문제작)를 모두 명시
        7점: 핵심 조건은 있으나 예외 중 하나 누락
        4점: 일부만 언급
        1점: 잘못된 정보 또는 회피

tests:
  - description: "7일 이내 일반 상품 환불 가능 여부"
    vars:
      query: "어제 산 티셔츠 환불하고 싶어요. 어떻게 하면 되나요?"
    assert:
      - type: contains-any
        value: ["7일", "7 일", "일주일"]
      - type: contains
        value: "영수증"
    metadata:
      tags: ["easy", "refund"]

  - description: "해외 배송 상품 환불 불가 엣지케이스"
    vars:
      query: "해외에서 직배송된 스니커즈인데 사이즈가 안 맞아요. 교환되나요?"
    assert:
      - type: llm-rubric
        value: |
          해외 배송 상품은 환불/교환 불가 정책을 명확히 안내했는가?
          대안을 제시했다면 추가 점수.
      - type: not-contains
        value: "네, 교환 가능합니다"
    metadata:
      tags: ["hard", "refund", "safety"]

  - description: "구매 9일 경과 환불 요청"
    vars:
      query: "9일 전에 구매한 바지를 환불받을 수 있을까요?"
    assert:
      - type: llm-rubric
        value: "7일 기한 초과로 환불 불가임을 안내하고, 고객에게 공감을 표했는가?"
    metadata:
      tags: ["medium", "refund"]

우리 팀은 promptfoo를 기본으로 채택하고, safety 관련 케이스에 한해 Inspect AI 스코어러를 별도로 연동합니다.


6. GitHub Actions에서 PR마다 Evals를 돌리는 CI 설계

아래는 우리 팀이 실제 운영 중인 GitHub Actions 워크플로의 핵심 부분입니다. 전체 파이프라인은 두 단계로 나뉩니다. 첫 번째는 경량 rule-based 검증(항상 실행), 두 번째는 LLM-as-Judge를 포함한 전체 평가(프롬프트 또는 모델 설정 파일 변경 시에만 실행)입니다.

# .github/workflows/llm-eval.yml
name: LLM Evaluation Pipeline

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'evals/**'
      - 'config/model.yaml'

jobs:
  eval-fast:
    name: Rule-based Evaluation (Fast Gate)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install promptfoo
        run: npm install -g promptfoo@latest

      - name: Run rule-based assertions
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          promptfoo eval \
            --config evals/promptfooconfig.yaml \
            --filter-pattern "tag:rule-based" \
            --output results/fast-eval.json \
            --no-cache

      - name: Check fast gate threshold
        run: |
          python scripts/check_threshold.py \
            --results results/fast-eval.json \
            --min-pass-rate 0.95 \
            --label "rule-based"

  eval-full:
    name: Full LLM-as-Judge Evaluation
    needs: eval-fast
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Fetch baseline results from main
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh api repos/${{ github.repository }}/actions/artifacts \
            --jq '.artifacts[] | select(.name=="eval-baseline") | .archive_download_url' \
            | head -1 | xargs -I{} curl -L -H "Authorization: Bearer $GH_TOKEN" {} -o baseline.zip
          unzip -o baseline.zip -d results/baseline/ || echo "No baseline found, skipping diff"

      - name: Install promptfoo
        run: npm install -g promptfoo@latest

      - name: Run full evaluation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          promptfoo eval \
            --config evals/promptfooconfig.yaml \
            --output results/full-eval.json \
            --max-concurrency 5

      - name: Compare with baseline and block regression
        run: |
          python scripts/check_regression.py \
            --current results/full-eval.json \
            --baseline results/baseline/full-eval.json \
            --max-regression 0.03 \
            --critical-tags "safety,refund"

      - name: Upload results as artifact
        uses: actions/upload-artifact@v4
        with:
          name: eval-results-${{ github.sha }}
          path: results/full-eval.json

이 설계에서 핵심은 paths 트리거로 불필요한 실행을 방지하는 것입니다. 프롬프트 파일이나 모델 설정이 바뀌지 않은 PR에서는 평가 파이프라인이 아예 실행되지 않아 평가 비용을 절약합니다. 에이전트 기반 CI 자동화를 더 깊이 다루고 싶다면 에이전트 하네스 엔지니어링을 참고하세요.


7. 회귀 차단 임계값: Diff 기반 가드와 False Positive 줄이기

절대 임계값(예: "pass rate 90% 미만이면 차단")은 직관적이지만 실무에서 문제가 많습니다. 골든 데이터셋이 업데이트되거나 새 케이스가 추가되면 절대값이 흔들리고, 정상 범위의 변동으로 인해 불필요한 CI 실패가 발생합니다.

우리 팀이 채택한 방식은 diff 기반 가드입니다. 현재 PR의 점수를 main 브랜치 최신 커밋의 점수와 비교하여, 허용 오차 이상으로 떨어졌을 때만 차단합니다. 경험상 LLM-as-Judge의 자연 변동 범위는 ±2~3%p이므로, 회귀 임계값은 3%p를 기준점으로 설정하고 데이터를 쌓으면서 조정합니다.

false positive를 줄이는 핵심 전략은 세 가지입니다. 첫째, LLM-as-Judge는 동일한 케이스를 여러 번 실행해 평균을 취합니다(우리 팀 기본값: 3회). 둘째, 케이스 수가 적을수록 샘플링 분산이 크므로 최소 30개 이상일 때만 통계적으로 유의미한 회귀를 선언합니다. 셋째, safetyrefund 같은 크리티컬 태그에는 허용 회귀 폭을 절반으로 설정하여 비즈니스 임팩트가 큰 케이스에 더 엄격한 기준을 적용합니다.

한 가지 주의할 안티패턴이 있습니다. 회귀가 감지되었을 때 케이스를 삭제하거나 임계값을 완화하는 방식으로 CI를 통과시키는 것입니다. 이는 평가 파이프라인의 신뢰성을 스스로 무너뜨리는 행위입니다. 회귀가 감지되면 원인(모델 변경인지, 프롬프트 변경인지, 케이스 자체의 문제인지)을 먼저 분류하고, 케이스 문제라면 케이스를 개선하되 임계값을 건드리지 않는 것이 원칙입니다.


8. 토큰 비용·지연 시간 추적: 평가 자체의 Cost Budget

평가 파이프라인은 코드 품질을 지키는 도구지만, 평가 자체가 비용을 씁니다. 우리 팀은 고객 응대 도메인 케이스 70개를 매 PR마다 gpt-4o로 평가할 때 회당 약 $0.85의 비용이 발생함을 확인했습니다. 월간 PR 수를 80개로 가정하면 월 $68, 연간 약 $816입니다. 도메인이 3개로 늘어나면 연간 $2,400 이상이 되고, 이는 무시할 수 없는 운영 비용입니다.

아래는 평가 실행의 토큰 사용량과 지연 시간을 함께 계측하는 Python 코드입니다. G-Eval 스타일 LLM-as-Judge 호출 시 매 케이스의 비용을 누적하고 PR 종료 시 요약을 출력합니다.

# scripts/eval_cost_tracker.py
import time
import json
from dataclasses import dataclass, field
from typing import Optional
import openai

PRICING_PER_1M = {
    "gpt-4o": {"input": 2.50, "output": 10.00},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60},
    "claude-3-5-sonnet-20241022": {"input": 3.00, "output": 15.00},
}


@dataclass
class EvalRunMetrics:
    model: str
    total_input_tokens: int = 0
    total_output_tokens: int = 0
    total_latency_ms: float = 0.0
    num_calls: int = 0
    errors: int = 0
    scores: list = field(default_factory=list)

    @property
    def estimated_cost_usd(self) -> float:
        pricing = PRICING_PER_1M.get(self.model, {"input": 0, "output": 0})
        return (
            self.total_input_tokens / 1_000_000 * pricing["input"]
            + self.total_output_tokens / 1_000_000 * pricing["output"]
        )

    @property
    def avg_latency_ms(self) -> float:
        return self.total_latency_ms / self.num_calls if self.num_calls else 0

    @property
    def mean_score(self) -> float:
        return sum(self.scores) / len(self.scores) if self.scores else 0.0


def llm_judge_score(
    client: openai.OpenAI,
    response_text: str,
    rubric: str,
    model: str,
    metrics: EvalRunMetrics,
) -> Optional[float]:
    """
    G-Eval 스타일 LLM-as-Judge.
    Liu et al. (EMNLP 2023): https://arxiv.org/abs/2303.16634
    CoT로 평가 단계 생성 후 0-10 점수를 JSON으로 반환.
    """
    judge_prompt = f"""당신은 LLM 응답 품질을 평가하는 전문 심사자입니다.
아래 루브릭을 기준으로 단계적 이유를 설명한 뒤 최종 점수(0-10)를 JSON으로 반환하세요.

루브릭:
{rubric}

평가할 응답:
{response_text}

출력 형식: {{"reasoning": "...", "score": 7}}"""

    start = time.perf_counter()
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": judge_prompt}],
            temperature=0,
            response_format={"type": "json_object"},
            max_tokens=512,
        )
        elapsed_ms = (time.perf_counter() - start) * 1000

        usage = response.usage
        metrics.total_input_tokens += usage.prompt_tokens
        metrics.total_output_tokens += usage.completion_tokens
        metrics.total_latency_ms += elapsed_ms
        metrics.num_calls += 1

        result = json.loads(response.choices[0].message.content)
        score = float(result["score"]) / 10.0  # 0-1 정규화
        metrics.scores.append(score)
        return score

    except Exception as e:
        metrics.errors += 1
        print(f"Judge error: {e}")
        return None

비용 최적화에서 가장 효과적인 레버는 Judge 모델 선택입니다. 우리 팀은 일반 케이스는 gpt-4o-mini(입력 $0.15/1M 토큰), safety 관련 크리티컬 케이스만 gpt-4o(입력 $2.50/1M 토큰)로 분리하여 전체 평가 비용을 약 60% 절감했습니다. 두 모델의 판단 일치율이 95% 이상임을 사전에 검증한 뒤 이 분리 전략을 적용했습니다.


9. LLM-as-Judge 신뢰도 검증: 인간 라벨러 샘플과 일치도

LLM-as-Judge를 CI 게이트로 사용하려면 그 판단을 신뢰할 수 있어야 합니다. "GPT-4가 좋다고 했으니 좋은 것이다"는 논리는 프로덕션에서 통하지 않습니다. 특히 LLM 생성 텍스트를 LLM이 더 선호하는 자기 편향(self-preference bias)을 고려하면, Judge 신뢰도 보정은 필수 절차입니다.

우리 팀의 검증 프로세스는 이렇습니다. 매 분기 평가 케이스에서 무작위 50개를 샘플링하여 내부 도메인 전문가 2명에게 독립 평가를 요청합니다. 인간 평균 점수와 LLM Judge 점수 간의 Spearman 상관계수가 0.65 미만이면 루브릭을 재작성하고, 0.80 이상이면 해당 도메인에서 Judge를 단독 게이트로 신뢰합니다. 처음 도입 시 우리 팀의 상관계수는 0.61이었고, 루브릭에 구체적인 예시(앵커 응답)를 추가한 뒤 0.79로 올랐습니다.

LangSmith Evaluations 문서는 "evaluator 자체를 평가(meta-evaluation)"하는 워크플로를 공식적으로 권장합니다. Judge 응답의 reasoning 필드를 사람이 샘플링하여 판단 논리가 일관되는지 주기적으로 확인하는 것이 핵심입니다.

Judge 신뢰도를 높이는 실전 팁 세 가지입니다. 첫째, 루브릭에 특정 점수를 받아야 하는 응답 예시(앵커)를 포함하면 판단 일관성이 크게 향상됩니다. 둘째, 점수를 연속값이 아닌 3단계(1/5/10)나 5단계로 제한하면 Judge 분산이 줄어듭니다. 셋째, Judge 모델과 평가받는 모델을 다른 제공사로 구성하면 자기 편향을 구조적으로 줄일 수 있습니다. 프롬프트를 체계적으로 설계하는 방법은 프롬프트 엔지니어링 체계적 설계에서 이어서 다룹니다.

또한 Judge 결과는 단일 케이스가 아닌 케이스 배치 단위로 해석해야 합니다. 개별 케이스 점수의 분산은 크더라도 배치 평균은 안정적으로 수렴하는 경향이 있습니다. 우리 팀은 케이스당 3회 실행 평균을 Judge 점수로 채택하여 단일 실행 분산을 완화합니다.


10. 결론: 사고 실험에서 운영 가능한 평가 파이프라인으로 가는 체크리스트

LLM 평가 파이프라인은 한 번의 스프린트로 완성되는 것이 아닙니다. 우리 팀도 처음 세 달은 골든 데이터셋 설계와 Judge 루브릭 재작성의 반복이었습니다. 아래 다섯 가지는 그 과정을 단축하는 실무 체크리스트입니다.

  1. 골든 데이터셋을 먼저, 프레임워크는 나중에: 케이스 50개가 없는 상태에서 도구를 고르면 도구가 케이스 설계를 왜곡합니다. 스프레드시트에 케이스를 먼저 채운 뒤, 그 케이스를 가장 자연스럽게 표현하는 프레임워크(promptfoo YAML 또는 Python dict)를 고릅니다.

  2. Rule-based → Similarity → LLM-as-Judge 순서로 단계적으로 도입: 세 번째 계층이 없어도 첫 번째와 두 번째만으로 많은 회귀를 잡을 수 있습니다. LLM-as-Judge는 비용이 발생하므로 앞선 계층을 통과한 케이스에만 적용하면 비용을 절반 이하로 낮출 수 있습니다.

  3. Judge 신뢰도를 분기마다 인간 샘플로 보정: Spearman 상관계수 0.70 이상을 유지하지 못하면 CI 게이트가 아닌 참고 지표로만 활용합니다. Judge가 틀리면 CI가 거짓 신호를 발생시키므로, Judge 자체의 품질 관리가 파이프라인 전체의 신뢰도를 좌우합니다.

  4. 평가 비용을 토큰 예산으로 관리하고 PR에 리포트: 월간 평가 비용에 상한을 설정하고, PR마다 비용을 코멘트로 노출합니다. 비용이 보이면 팀원이 Judge 모델 선택에 경제적 관점을 자연스럽게 포함하게 됩니다.

  5. Critical 태그 케이스는 절대 완화하지 않는다: 비즈니스 임팩트가 큰 도메인(환불, 안전, 법적 고지)의 케이스는 회귀 허용 폭을 일반의 절반 이하로 유지합니다. 이 기준을 완화하는 순간 파이프라인의 존재 의의가 무너집니다. 회귀가 감지되면 케이스를 보강하는 것이 원칙이며, 임계값 완화는 마지막 수단입니다.

LLM 시스템은 코드만 관리해서는 품질을 보장할 수 없습니다. 프롬프트, 모델, 데이터가 함께 움직이는 복잡계이기 때문에, 평가 파이프라인을 코드 리포의 일급 시민으로 대우하는 문화 변화가 기술 설계만큼 중요합니다. 우리 팀이 경험으로 배운 것을 이 글로 공유합니다.


참고 자료