Skip to main content

프롬프트 평가

프롬프트를 설계했다면 어떤 프롬프트가 더 나은지 객관적으로 판단할 방법이 필요합니다. “느낌”이 아닌 데이터 기반으로 프롬프트 품질을 측정하고, 반복적으로 개선하는 체계적 평가 방법론을 학습합니다.

학습 목표

이 문서를 완료하면 다음을 수행할 수 있습니다.
  • 프롬프트 A/B 테스트를 설계하고 실행할 수 있습니다
  • 출력의 일관성(Consistency)을 정량적으로 측정할 수 있습니다
  • 주요 LLM 벤치마크(MMLU, HellaSwag 등)의 의미를 해석할 수 있습니다
  • LLM-as-Judge 패턴으로 자동 평가 파이프라인을 구축할 수 있습니다
  • 태스크별 평가 데이터셋을 설계할 수 있습니다

왜 중요한가

프롬프트 엔지니어링에서 가장 흔한 실수는 “몇 번 테스트해보니 괜찮아 보여서” 배포하는 것입니다.
단계비체계적 접근체계적 접근
설계”이 정도면 될 것 같다”평가 기준 먼저 정의
테스트3~5개 예시로 “느낌” 확인50~100개 테스트셋으로 정량 평가
비교”이전보다 나아 보인다”A/B 테스트로 통계적 비교
배포한 번 배포 후 방치모니터링 + 지속적 평가

핵심 개념

1. 평가 데이터셋 구축

모든 평가의 기초는 좋은 테스트 데이터셋입니다.
# 평가 데이터셋 구조
evaluation_dataset = [
    {
        "id": "eval_001",
        "input": "이 영화는 지루했지만 배우 연기는 좋았어요",
        "expected_output": {"감성": "혼합"},
        "category": "edge_case",    # 분류: 일반, 경계 사례, 어려운 사례
        "difficulty": "medium",
    },
    {
        "id": "eval_002",
        "input": "최고의 영화! 강추합니다!",
        "expected_output": {"감성": "긍정"},
        "category": "normal",
        "difficulty": "easy",
    },
    # ... 50~100개 이상
]
좋은 평가 데이터셋의 조건:
조건설명비율 가이드
대표성실제 사용 패턴을 반영60% 일반 사례
다양성다양한 유형과 길이 포함20% 다양한 변형
경계 사례모호하거나 어려운 입력15% 경계 사례
악의적 입력Prompt Injection 시도5% 적대적 사례

2. A/B 테스트

두 프롬프트의 성능을 동일한 테스트셋에서 비교합니다.
from openai import OpenAI
import json
import numpy as np

client = OpenAI()

def run_ab_test(prompt_a, prompt_b, test_data, model="gpt-4o-mini"):
    """프롬프트 A/B 테스트 실행"""
    results_a = []
    results_b = []

    for item in test_data:
        # 프롬프트 A 실행
        resp_a = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": prompt_a},
                {"role": "user", "content": item["input"]},
            ],
            temperature=0,
        )

        # 프롬프트 B 실행
        resp_b = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": prompt_b},
                {"role": "user", "content": item["input"]},
            ],
            temperature=0,
        )

        # 정답 대비 평가
        result_a = evaluate_single(
            resp_a.choices[0].message.content,
            item["expected_output"],
        )
        result_b = evaluate_single(
            resp_b.choices[0].message.content,
            item["expected_output"],
        )

        results_a.append(result_a)
        results_b.append(result_b)

    return {
        "prompt_a": {
            "accuracy": np.mean(results_a),
            "results": results_a,
        },
        "prompt_b": {
            "accuracy": np.mean(results_b),
            "results": results_b,
        },
    }

def evaluate_single(prediction, expected):
    """단일 예측 정확도 평가"""
    try:
        pred = json.loads(prediction)
        return int(pred.get("감성") == expected.get("감성"))
    except (json.JSONDecodeError, AttributeError):
        return 0

# A/B 테스트 실행
prompt_a = "고객 리뷰의 감성을 분류하세요. JSON으로 출력."
prompt_b = """고객 리뷰의 감성을 분류하세요.

규칙:
- 긍정과 부정이 모두 포함되면 "혼합"
- 출력: {"감성": "긍정|부정|혼합"}

예시:
입력: "배송은 빠른데 품질이 나빠요"
출력: {"감성": "혼합"}"""

results = run_ab_test(prompt_a, prompt_b, evaluation_dataset)
print(f"프롬프트 A 정확도: {results['prompt_a']['accuracy']:.2%}")
print(f"프롬프트 B 정확도: {results['prompt_b']['accuracy']:.2%}")

3. 일관성 측정 (Consistency)

동일한 입력에 대해 여러 번 실행했을 때 동일한 결과가 나오는 정도를 측정합니다.
def measure_consistency(prompt, test_input, n_runs=10, temperature=0.3):
    """프롬프트 출력 일관성 측정"""
    outputs = []

    for _ in range(n_runs):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": prompt},
                {"role": "user", "content": test_input},
            ],
            temperature=temperature,
        )
        outputs.append(response.choices[0].message.content)

    # 고유한 출력 수
    unique_outputs = set(outputs)
    consistency_score = 1 - (len(unique_outputs) - 1) / n_runs

    # 가장 빈번한 출력
    from collections import Counter
    most_common = Counter(outputs).most_common(1)[0]

    return {
        "consistency": consistency_score,
        "unique_outputs": len(unique_outputs),
        "most_common": most_common[0],
        "most_common_count": most_common[1],
        "n_runs": n_runs,
    }

result = measure_consistency(
    prompt="리뷰 감성을 JSON으로 분류하세요: {\"감성\": \"긍정|부정|혼합\"}",
    test_input="그냥 그래요. 특별한 건 없어요.",
    n_runs=10,
)
print(f"일관성: {result['consistency']:.2%}")
print(f"고유 출력 수: {result['unique_outputs']}/{result['n_runs']}")
print(f"가장 빈번한 답: {result['most_common']} ({result['most_common_count']}회)")
일관성이 높을수록 좋은 프롬프트입니다. 동일한 입력에 대해 다른 결과가 나오면 프롬프트가 모호하다는 신호입니다. temperature=0으로도 결과가 달라진다면 프롬프트 구조를 재설계해야 합니다.

4. LLM 벤치마크 이해

프롬프트 성능을 비교할 때 참고할 수 있는 주요 벤치마크입니다.
벤치마크평가 대상문제 수설명
MMLU지식 + 추론15,90857개 분야 객관식 (초등~전문가)
HellaSwag상식 추론10,042문장 완성 (상식적 다음 문장 선택)
HumanEval코드 생성164Python 함수 구현 + 테스트
MATH수학 추론12,500고등~대학 수학 문제
TruthfulQA사실 정확성817환각 방지 능력 측정
GSM8K산술 추론8,500초등학교 수준 수학 문제
KMMLU한국어 지식35,030한국어 버전 MMLU
KoBEST한국어 이해다양한국어 NLU 벤치마크
벤치마크 해석 시 주의사항:
  • 벤치마크 점수가 높다고 모든 태스크에서 좋은 것은 아닙니다
  • 프롬프팅 기법(CoT, Few-shot)에 따라 벤치마크 점수가 크게 달라집니다
  • 한국어 태스크는 영어 벤치마크와 성능 차이가 있을 수 있습니다

5. LLM-as-Judge (LLM을 평가자로 활용)

사람 대신 다른 LLM이 출력 품질을 평가하는 방법입니다. 대규모 평가에서 비용과 시간을 크게 절약합니다.
def llm_as_judge(task, response_a, response_b, model="gpt-4o"):
    """LLM-as-Judge: GPT-4o가 두 답변을 비교 평가"""
    judge_prompt = f"""당신은 AI 출력 품질을 평가하는 전문 심사위원입니다.

## 태스크
{task}

## 답변 A
{response_a}

## 답변 B
{response_b}

## 평가 기준
1. **정확성** (1~5): 사실적으로 정확한가?
2. **완전성** (1~5): 태스크의 모든 요구사항을 충족했는가?
3. **명확성** (1~5): 이해하기 쉽고 구조적인가?
4. **유용성** (1~5): 실제로 도움이 되는가?

## 출력 형식 (JSON)
{{
  "answer_a": {{"정확성": N, "완전성": N, "명확성": N, "유용성": N, "총점": N}},
  "answer_b": {{"정확성": N, "완전성": N, "명확성": N, "유용성": N, "총점": N}},
  "winner": "A 또는 B 또는 무승부",
  "reasoning": "판단 근거 (2~3문장)"
}}"""

    response = client.chat.completions.create(
        model=model,
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": "반드시 JSON 형식으로 답변하세요."},
            {"role": "user", "content": judge_prompt},
        ],
        temperature=0,
    )

    return json.loads(response.choices[0].message.content)

# 사용 예시
judgment = llm_as_judge(
    task="Python에서 리스트 컴프리헨션을 초보자에게 설명하세요",
    response_a="리스트 컴프리헨션은 [expr for item in iterable] 형태의 구문입니다.",
    response_b="""리스트 컴프리헨션은 기존 리스트에서 새 리스트를 만드는 간결한 방법입니다.

예시:
```python
# 일반 for 루프
squares = []
for x in range(5):
    squares.append(x**2)

# 리스트 컴프리헨션 (같은 결과)
squares = [x**2 for x in range(5)]
핵심: [표현식 for 변수 in 반복가능한것]""", ) print(f”승자: 답변 ”) print(f”근거: “)

**LLM-as-Judge의 편향(Bias) 주의**:

| 편향 유형 | 설명 | 완화 방법 |
|----------|------|----------|
| **위치 편향** | 먼저 나온 답변을 선호 | A/B 순서를 무작위로 바꿔 2회 평가 |
| **장문 편향** | 긴 답변을 더 좋게 평가 | 평가 기준에 "간결성" 포함 |
| **자기 편향** | 자신이 생성한 스타일 선호 | 다른 모델로 평가 |
| **형식 편향** | 마크다운, 코드블록 선호 | 내용 중심 평가 기준 명시 |

```python
def unbiased_llm_judge(task, response_a, response_b):
    """순서 편향을 제거한 LLM-as-Judge"""
    # 1회차: A 먼저
    result_1 = llm_as_judge(task, response_a, response_b)

    # 2회차: B 먼저 (순서 뒤바꿈)
    result_2 = llm_as_judge(task, response_b, response_a)

    # 양쪽 모두 같은 답변을 승자로 선택했는지 확인
    winner_1 = result_1["winner"]
    winner_2 = "B" if result_2["winner"] == "A" else (
        "A" if result_2["winner"] == "B" else "무승부"
    )

    if winner_1 == winner_2:
        return {"winner": winner_1, "confidence": "높음"}
    else:
        return {"winner": "무승부", "confidence": "낮음 (순서 편향 감지)"}

AI/ML 활용: 종합 평가 파이프라인

실무에서 사용할 수 있는 종합 프롬프트 평가 파이프라인입니다.
class PromptEvaluator:
    """프롬프트 종합 평가 도구"""

    def __init__(self, model="gpt-4o-mini"):
        self.client = OpenAI()
        self.model = model

    def evaluate(self, prompt, test_data, metrics=None):
        """종합 평가 실행"""
        if metrics is None:
            metrics = ["accuracy", "consistency", "format_compliance"]

        results = {}

        if "accuracy" in metrics:
            results["accuracy"] = self._eval_accuracy(prompt, test_data)

        if "consistency" in metrics:
            # 테스트 데이터의 처음 5개로 일관성 측정
            results["consistency"] = self._eval_consistency(
                prompt, test_data[:5]
            )

        if "format_compliance" in metrics:
            results["format_compliance"] = self._eval_format(
                prompt, test_data
            )

        results["overall_score"] = np.mean(list(results.values()))
        return results

    def _eval_accuracy(self, prompt, test_data):
        """정확도 평가"""
        correct = 0
        for item in test_data:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": prompt},
                    {"role": "user", "content": item["input"]},
                ],
                temperature=0,
            )
            if evaluate_single(
                response.choices[0].message.content,
                item["expected_output"]
            ):
                correct += 1
        return correct / len(test_data)

    def _eval_consistency(self, prompt, test_data, n_runs=5):
        """일관성 평가"""
        scores = []
        for item in test_data:
            result = measure_consistency(
                prompt, item["input"], n_runs=n_runs
            )
            scores.append(result["consistency"])
        return np.mean(scores)

    def _eval_format(self, prompt, test_data):
        """형식 준수율 평가"""
        valid = 0
        for item in test_data:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": prompt},
                    {"role": "user", "content": item["input"]},
                ],
                temperature=0,
            )
            try:
                json.loads(response.choices[0].message.content)
                valid += 1
            except json.JSONDecodeError:
                pass
        return valid / len(test_data)

# 사용 예시
evaluator = PromptEvaluator()
results = evaluator.evaluate(
    prompt="리뷰 감성을 JSON으로 분류하세요.",
    test_data=evaluation_dataset,
)
print(f"정확도: {results['accuracy']:.2%}")
print(f"일관성: {results['consistency']:.2%}")
print(f"형식 준수: {results['format_compliance']:.2%}")
print(f"종합 점수: {results['overall_score']:.2%}")
최소 50개, 이상적으로는 100~200개입니다. 너무 적으면 통계적 유의성이 낮고, 너무 많으면 API 비용이 증가합니다. 카테고리(일반/경계/어려움)별로 균등하게 배분하세요. 시작은 30개로 하고, 실패 사례를 분석하여 점진적으로 확대하는 것이 효율적입니다.
GPT-4 수준의 모델을 평가자로 사용하면 인간 평가와 80~85% 일치율을 보입니다. 완벽하지는 않지만, 대규모 평가에서 비용 대비 충분히 유용합니다. 중요한 결정에는 LLM-as-Judge 결과를 1차 필터로 사용하고, 최종 판단은 사람이 하는 하이브리드 접근을 추천합니다.
McNemar’s test 또는 paired t-test를 사용합니다. scipy.stats.ttest_rel(results_a, results_b) 로 p-value를 구하고, p < 0.05이면 통계적으로 유의미한 차이가 있다고 판단합니다. 표본 수가 적을 때는 bootstrap 방법을 사용할 수도 있습니다.
  1. Git으로 관리: 프롬프트를 별도 파일(.txt, .yaml)로 저장하고 버전 관리합니다. 2. 평가 결과 기록: 각 버전의 평가 점수를 스프레드시트에 추적합니다. 3. 변경 이유 문서화: 왜 변경했는지, 어떤 문제를 해결했는지 기록합니다. LangSmith, Weights & Biases 같은 도구도 프롬프트 버전 관리에 활용할 수 있습니다.

체크리스트

학습을 마치기 전에 아래 항목을 확인하세요.
  • 평가 데이터셋 구축 시 고려해야 할 조건(대표성, 다양성, 경계 사례)을 이해하는가?
  • 프롬프트 A/B 테스트를 설계하고 실행할 수 있는가?
  • 일관성 점수의 의미를 해석하고, 낮은 일관성의 원인을 파악할 수 있는가?
  • LLM-as-Judge의 장점과 편향 유형을 설명할 수 있는가?
  • 주요 벤치마크(MMLU, HumanEval, KMMLU)의 평가 대상을 이해하는가?

다음 문서