Post

LLM 평가 파이프라인 설계 - 프롬프트를 어떻게 테스트하는가

LLM 평가 파이프라인 설계 - 프롬프트를 어떻게 테스트하는가

문제

프롬프트를 수정할 때마다 “이번 수정이 전체적으로 더 나아졌는가?”를 판단하기 어렵다. 특정 케이스에서 좋아지면 다른 케이스에서 나빠지기도 한다.

전통적인 소프트웨어는 유닛 테스트로 회귀를 잡을 수 있지만, LLM 출력은 비결정적이다. 같은 프롬프트에 같은 입력을 넣어도 다른 출력이 나올 수 있다.

프롬프트 변경의 품질을 체계적으로 측정하는 파이프라인이 필요하다.


평가 파이프라인 아키텍처

flowchart TD
    A["프롬프트 변경"] --> B["평가 데이터셋<br>(입력 + 기대 출력)"]
    B --> C["LLM 실행<br>(변경 전/후 각각)"]
    C --> D["자동 평가<br>(메트릭 계산)"]
    D --> E["비교 리포트<br>(변경 전 vs 후)"]
    E --> F{"품질 향상?"}
    F -->|Yes| G["프롬프트 배포"]
    F -->|No| H["프롬프트 재수정"]

1단계: 평가 데이터셋 구축

평가의 품질은 데이터셋의 품질에 달려있다. “좋은 답변이란 무엇인가?”를 먼저 정의해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 평가 데이터셋 구조
eval_dataset = [
    {
        "input": "DB 마이그레이션 시 락 발생하면 어떻게 대응하나요?",
        "expected_keywords": ["lock_timeout", "저트래픽 시간대", "레플리카"],
        "expected_behavior": "구체적인 SQL 명령어를 포함한 대응 방법 제시",
        "category": "troubleshooting",
        "difficulty": "medium",
    },
    {
        "input": "안녕하세요",
        "expected_behavior": "업무 관련 질문을 안내",
        "category": "off-topic",
        "difficulty": "easy",
    },
    # ... 50-100개
]

데이터셋 구성 원칙

flowchart LR
    subgraph dataset["좋은 평가 데이터셋"]
        A["정상 케이스 60%<br>핵심 기능이 동작하는가"]
        B["엣지 케이스 25%<br>어려운 질문, 모호한 입력"]
        C["부정 케이스 15%<br>답이 없는 질문, 범위 밖"]
    end

부정 케이스가 중요하다. “이 질문에는 답하지 말아야 한다”를 테스트하지 않으면, 프롬프트 수정 후 hallucination이 증가해도 알 수 없다.


2단계: 자동 평가 메트릭

LLM 출력을 자동으로 평가하는 여러 방법이 있다.

규칙 기반 메트릭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def evaluate_response(response: str, expected: dict) -> dict:
    scores = {}
    
    # 1. 키워드 포함 여부
    if "expected_keywords" in expected:
        found = sum(1 for kw in expected["expected_keywords"] 
                    if kw.lower() in response.lower())
        scores["keyword_recall"] = found / len(expected["expected_keywords"])
    
    # 2. 응답 길이 적절성
    word_count = len(response.split())
    scores["length_appropriate"] = 1.0 if 50 < word_count < 500 else 0.5
    
    # 3. 출처 포함 여부 (RAG의 경우)
    scores["has_source"] = 1.0 if "[출처:" in response else 0.0
    
    return scores

LLM-as-Judge (LLM으로 LLM 평가)

규칙으로 잡기 어려운 “답변의 질”은 다른 LLM에게 평가를 맡긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def llm_judge(question: str, response: str, criteria: str) -> float:
    judge_prompt = f"""
다음 응답을 1-5점으로 평가하세요.

질문: {question}
응답: {response}

평가 기준: {criteria}

점수만 숫자로 답하세요.
"""
    result = claude.messages.create(
        model="claude-sonnet-4-20250514",
        messages=[{"role": "user", "content": judge_prompt}]
    )
    return float(result.content[0].text.strip())

메트릭 조합

flowchart TD
    subgraph metrics["평가 메트릭"]
        A["키워드 적중률<br>(규칙 기반)"]
        B["답변 적절성<br>(LLM Judge, 1-5)"]
        C["응답 거부 정확도<br>(부정 케이스)"]
        D["응답 시간<br>(latency)"]
    end
    metrics --> E["가중 평균 → 종합 점수"]
메트릭측정 방법가중치
키워드 적중률기대 키워드 포함 비율0.2
답변 적절성LLM Judge 1-5점0.4
거부 정확도부정 케이스에서 정확히 거부0.2
응답 시간p95 latency0.2

3단계: A/B 비교 리포트

프롬프트 변경 전/후를 동일 데이터셋으로 실행하고 비교한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def compare_prompts(prompt_a: str, prompt_b: str, dataset: list) -> dict:
    results_a, results_b = [], []
    
    for case in dataset:
        # 동일 입력에 대해 각 프롬프트로 실행
        resp_a = run_with_prompt(prompt_a, case["input"])
        resp_b = run_with_prompt(prompt_b, case["input"])
        
        # 평가
        score_a = evaluate_response(resp_a, case)
        score_b = evaluate_response(resp_b, case)
        
        results_a.append(score_a)
        results_b.append(score_b)
    
    return {
        "prompt_a_avg": mean([r["total"] for r in results_a]),
        "prompt_b_avg": mean([r["total"] for r in results_b]),
        "improved_cases": count_improved(results_a, results_b),
        "regressed_cases": count_regressed(results_a, results_b),
        "details": zip(dataset, results_a, results_b),
    }

리포트 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 프롬프트 A/B 비교 리포트 ===

종합 점수: A=3.42 → B=3.78 (+10.5%)

카테고리별:
  troubleshooting: 3.5 → 4.1 (+17.1%) ✓
  how-to:          3.8 → 3.9 (+2.6%)  ✓
  off-topic:       2.9 → 2.7 (-6.9%)  ✗ ← 회귀 발생

회귀 케이스 (2건):
  [off-topic] "오늘 날씨 어때?" → 거부해야 하는데 답변함
  [off-topic] "점심 뭐 먹지?" → 거부해야 하는데 답변함

결론: off-topic 거부 로직 보강 후 배포 권장

4단계: CI에 통합

프롬프트 파일이 변경되면 자동으로 평가 파이프라인이 실행되도록 한다.

flowchart LR
    A["프롬프트 파일 수정<br>(PR 생성)"] --> B["CI 트리거"]
    B --> C["평가 데이터셋으로<br>자동 테스트 실행"]
    C --> D{"종합 점수<br>하락 없음?"}
    D -->|Yes| E["PR 승인 가능"]
    D -->|No| F["회귀 리포트<br>PR 코멘트로 첨부"]

프롬프트도 코드다. 코드에 테스트가 있듯이, 프롬프트에도 테스트가 있어야 한다.


느낀 점

프롬프트 엔지니어링은 실험 과학이다

“이 프롬프트가 더 좋을 것 같다”는 직감은 대부분 틀린다. 평가 데이터셋으로 측정해보면 예상과 다른 결과가 나오는 경우가 많다. 측정 없는 프롬프트 수정은 도박이다.

LLM-as-Judge는 생각보다 잘 작동한다

LLM에게 “이 답변이 좋은가?”를 판단하게 하는 것이 비합리적으로 느껴질 수 있지만, 실제로 사람의 판단과 높은 상관관계를 보인다. 특히 평가 기준을 명확히 제시할 때 더 정확하다.

부정 케이스가 프롬프트의 품질에 영향을 미친다

“잘 답변하는가”보다 “답변하지 말아야 할 때 거부하는가”가 프로덕션에서 더 중요하다. Hallucination은 잘못된 답변보다 해로울 수 있다.

평가 파이프라인은 처음에 투자할 가치가 있다

프롬프트 1-2개일 때는 사람이 눈으로 확인해도 된다. 하지만 프롬프트가 10개를 넘어가고, 각각 여러 케이스를 처리해야 하면 자동화 없이는 불가능하다. 일찍 투자할수록 이후의 반복 비용이 줄어든다.

This post is licensed under CC BY 4.0 by the author.