프롬프트 평가하고 개선하기

프롬프트 평가하고 개선하기 (How to Evaluate Your Prompt and Improve It)

이 가이드에서는 Ragas를 사용해서 프롬프트를 평가하고 반복적으로 개선하는 방법을 배워요. 평가 오류 분석을 바탕으로 프롬프트를 반복 개선하고, 프롬프트 사이를 선택할 명확한 결정 기준을 세우며, 데이터셋에 대한 재사용 가능한 평가 파이프라인을 구축할 수 있어요.

출처: 문서

본문

이 가이드에서는 Ragas를 사용해서 프롬프트를 평가하고 반복적으로 개선하는 방법을 배워요.

달성할 것 (What you'll accomplish)

  • 평가 오류 분석을 바탕으로 프롬프트 반복 개선
  • 프롬프트 사이를 선택할 명확한 결정 기준 확립
  • 데이터셋에 대한 재사용 가능한 평가 파이프라인 구축
  • 평가 파이프라인 구축에 Ragas 활용하는 방법 배우기

전체 코드

  • 데이터셋과 스크립트는 repo의 examples/iterate_prompt/ 아래에 있어요
  • 전체 코드는 GitHub에서 확인할 수 있어요

작업 정의 (Task definition)

이 경우 고객 지원 티켓 분류 작업을 고려해요.

  • 라벨(멀티라벨): Billing, Account, ProductIssue, HowTo, Feature, RefundCancel
  • 우선순위(정확히 하나): P0, P1, 또는 P2

데이터셋

사용 사례를 위해 합성 데이터셋을 만들었어요. 각 행은 id, text, labels, priority를 가져요. 데이터셋의 예시 행:

id text labels priority
1 Upgraded to Plus… bank shows two charges the same day; want the duplicate reversed. Billing;RefundCancel P1
2 SSO via Okta succeeds then bounces back to /login; colleagues can sign in; state mismatch; blocked from boards. Account;ProductIssue P0
3 Need to export a board to PDF with comments and page numbers for audit; deadline next week. HowTo P2

사용 사례에 맞게 데이터셋을 커스터마이즈하려면 datasets/ 디렉토리를 만들고 자신의 CSV 파일을 추가해요. 다른 백엔드에 연결할 수도 있어요. 자세한 내용은 Core Concepts - Evaluation Dataset을 참고해요.

데이터셋을 만들려면 애플리케이션에서 실제 데이터를 샘플링하는 것이 더 좋아요. 사용할 수 없다면 LLM으로 합성 데이터를 생성할 수 있어요. 더 정확하고 복잡한 데이터를 생성할 수 있는 gpt-5 high-reasoning 같은 추론 모델을 권장해요. 사용하는 데이터는 항상 수동으로 검토하고 검증해요.

데이터셋에서 프롬프트 평가

프롬프트 실행기 (Prompt runner)

먼저 한 케이스에서 프롬프트를 실행해서 모든 것이 작동하는지 테스트해요.

전체 prompt v1 보기

You categorize a short customer support ticket into (a) one or more labels and (b) a single priority.

Allowed labels (multi-label):
- Billing: charges, taxes (GST/VAT), invoices, plans, credits.
- Account: login/SSO, password reset, identity/email/account merges.
- ProductIssue: malfunction (crash, error code, won't load, data loss, loops, outages).
- HowTo: usage questions ("where/how do I…", "where to find…").
- Feature: new capability or improvement request.
- RefundCancel: cancel/terminate and/or refund requests.
- AbuseSpam: insults/profanity/spam (not mild frustration).

Priority (exactly one):
- P0 (High): blocked from core action or money/data at risk.
- P1 (Normal): degraded/needs timely help, not fully blocked.
- P2 (Low): minor/info/how-to/feature.

Return exactly in JSON:
{"labels":[<labels>], "priority":"P0"|"P1"|"P2"}
cd examples/iterate_prompt
export OPENAI_API_KEY=your_openai_api_key
uv run run_prompt.py

이것은 샘플 케이스에서 프롬프트를 실행하고 결과를 출력해요.

샘플 출력

$ uv run run_prompt.py                      

Test ticket:
"SSO via Okta succeeds then bounces me back to /login with no session. Colleagues can sign in. I tried clearing cookies; same result. Error in devtools: state mismatch. I'm blocked from our boards."

Response:
{"labels":["Account","ProductIssue"], "priority":"P0"}

채점용 메트릭

복잡한 메트릭보다 간단한 메트릭을 사용하는 것이 일반적으로 더 좋아요. 사용 사례에 맞는 메트릭을 사용해야 해요. 메트릭에 대한 자세한 내용은 Core Concepts - Metrics에서 찾을 수 있어요. 여기서는 labels_exact_matchpriority_accuracy 두 개의 이산(discrete) 메트릭을 사용해요. 분리해 두면 서로 다른 실패 모드를 분석하고 고치는 데 도움이 돼요.

  • priority_accuracy: 예측된 우선순위가 기대 우선순위와 일치하는지 확인해요. 올바른 긴급도 트리아지에 중요해요.
  • labels_exact_match: 예측된 라벨 집합이 기대 라벨과 정확히 일치하는지 확인해요. 과대/과소 태깅을 피하고 케이스 라벨링에서 시스템 정확도를 측정하는 데 도움이 돼요.
# examples/iterate_prompt/evals.py
import json
from ragas.metrics.discrete import discrete_metric
from ragas.metrics.result import MetricResult

@discrete_metric(name="labels_exact_match", allowed_values=["correct", "incorrect"])
def labels_exact_match(prediction: str, expected_labels: str):
    try:
        predicted = set(json.loads(prediction).get("labels", []))
        expected = set(expected_labels.split(";")) if expected_labels else set()
        return MetricResult(
            value="correct" if predicted == expected else "incorrect",
            reason=f"Expected={sorted(expected)}; Got={sorted(predicted)}",
        )
    except Exception as e:
        return MetricResult(value="incorrect", reason=f"Parse error: {e}")

@discrete_metric(name="priority_accuracy", allowed_values=["correct", "incorrect"])
def priority_accuracy(prediction: str, expected_priority: str):
    try:
        predicted = json.loads(prediction).get("priority")
        return MetricResult(
            value="correct" if predicted == expected_priority else "incorrect",
            reason=f"Expected={expected_priority}; Got={predicted}",
        )
    except Exception as e:
        return MetricResult(value="incorrect", reason=f"Parse error: {e}")

실험 함수

실험 함수는 데이터셋에서 프롬프트를 실행하는 데 사용돼요. 실험에 대한 자세한 내용은 Core Concepts - Experimentation에서 찾을 수 있어요.

우리가 prompt_file을 파라미터로 전달하는 것을 주목해요. 이렇게 하면 서로 다른 프롬프트로 실험을 실행할 수 있어요. experiment 함수에 model, temperature 같은 다른 파라미터도 전달해서 다른 구성으로 실험할 수 있어요. 실험할 때는 한 번에 1개 파라미터만 바꾸는 것을 권장해요.

# examples/iterate_prompt/evals.py
import asyncio, json
from ragas import experiment
from run_prompt import run_prompt

@experiment()
async def support_triage_experiment(row, prompt_file: str, experiment_name: str):
    response = await asyncio.to_thread(run_prompt, row["text"], prompt_file=prompt_file)
    try:
        parsed = json.loads(response)
        predicted_labels = ";".join(parsed.get("labels", [])) or ""
        predicted_priority = parsed.get("priority")
    except Exception:
        predicted_labels, predicted_priority = "", None

    return {
        "id": row["id"],
        "text": row["text"],
        "response": response,
        "experiment_name": experiment_name,
        "expected_labels": row["labels"],
        "predicted_labels": predicted_labels,
        "expected_priority": row["priority"],
        "predicted_priority": predicted_priority,
        "labels_score": labels_exact_match.score(prediction=response, expected_labels=row["labels"]).value,
        "priority_score": priority_accuracy.score(prediction=response, expected_priority=row["priority"]).value,
    }

데이터셋 로더 (CSV)

데이터셋 로더는 데이터셋을 Ragas 데이터셋 객체로 로드하는 데 사용돼요. 데이터셋에 대한 자세한 내용은 Core Concepts - Evaluation Dataset에서 찾을 수 있어요.

# examples/iterate_prompt/evals.py
import os, pandas as pd
from ragas import Dataset

def load_dataset():
    current_dir = os.path.dirname(os.path.abspath(__file__))
    df = pd.read_csv(os.path.join(current_dir, "datasets", "support_triage.csv"))
    dataset = Dataset(name="support_triage", backend="local/csv", root_dir=".")
    for _, row in df.iterrows():
        dataset.append({
            "id": str(row["id"]),
            "text": row["text"],
            "labels": row["labels"],
            "priority": row["priority"],
        })
    return dataset

현재 프롬프트로 실험 실행

uv run evals.py run --prompt_file promptv1.txt

이것은 데이터셋에서 주어진 프롬프트를 실행하고 결과를 experiments/ 디렉토리에 저장해요.

샘플 출력

$ uv run evals.py run --prompt_file promptv1.txt        

Loading dataset...
Dataset loaded with 20 samples
Running evaluation with prompt file: promptv1.txt
Running experiment: 100%|██████████████████████████████████████████████████████████████████| 20/20 [00:11<00:00,  1.79it/s]
✅ promptv1: 20 cases evaluated
Results saved to: experiments/20250826-041332-promptv1.csv
promptv1 Labels Accuracy: 80.00%
promptv1 Priority Accuracy: 75.00%

프롬프트 개선

결과에서 오류 분석

좋아하는 스프레드시트 편집기에서 experiments/{timestamp}-promptv1.csv를 열고 결과를 분석해요. labels_score 또는 priority_score가 incorrect인 케이스를 찾아봐요.

promptv1 실험에서 몇 가지 오류 패턴을 식별할 수 있어요.

우선순위 오류: 과도한 우선순위 부여 (P1 → P0)

모델이 P1이어야 하는 청구 관련 문제에 일관되게 P0(최고 우선순위)를 부여해요.

Case Issue Expected Got Pattern
ID 19 Auto-charge after pausing workspace P1 P0 Billing dispute treated as urgent
ID 1 Duplicate charge on same day P1 P0 Billing dispute treated as urgent
ID 5 Cancellation with refund request P1 P0 Routine cancellation treated as urgent
ID 13 Follow-up on cancellation P1 P0 Follow-up treated as urgent

패턴: 모델은 대부분 일상적인 비즈니스 운영(P1)인데도 모든 청구/환불/취소를 긴급(P0)으로 취급해요.

라벨 오류: 과대 라벨링과 혼동
Case Issue Expected Got Pattern
ID 9 GST tax question from US user Billing;HowTo Billing;Account Confuses informational questions with account actions
ID 10 Account ownership transfer Account Account;Billing Adds Billing when money/plans mentioned
ID 20 API rate limit question ProductIssue;HowTo ProductIssue;Billing;HowTo Adds Billing when plans mentioned
ID 16 Feature request for offline mode Feature Feature;HowTo Adds HowTo for feature requests

식별된 패턴:

  1. Billing 과대 라벨링: 주로 청구 관련이 아닌데도 "Billing"을 추가
  2. HowTo vs Account 혼동: 정보성 질문을 계정 관리 작업으로 오분류
  3. HowTo 과대 라벨링: 사용자가 "how"라고 묻지만 "만들어줄 수 있나"를 의미할 때 feature 요청에 "HowTo" 추가

프롬프트 개선

오류 분석을 바탕으로 표적 개선이 포함된 promptv2_fewshot.txt를 만들 거예요. LLM을 사용해서 프롬프트를 생성하거나 수동으로 편집할 수 있어요. 이 경우 오류 패턴과 원본 프롬프트를 LLM에 전달해서 few-shot 예제가 포함된 수정된 프롬프트를 생성했어요.

promptv2_fewshot의 핵심 추가 사항:

1. 비즈니스 영향 중심의 강화된 우선순위 지침:

- P0: Blocked from core functionality OR money/data at risk OR business operations halted
- P1: Degraded experience OR needs timely help BUT has workarounds OR not fully blocked  
- P2: Minor issues OR information requests OR feature requests OR non-urgent how-to

2. 과대 태깅 방지를 위한 보수적 멀티라벨링 규칙:

## Multi-label Guidelines
Use single label for PRIMARY issue unless both aspects are equally important:
- Billing + RefundCancel: Always co-label. Cancellation/refund requests must include Billing.  
- Account + ProductIssue: For auth/login malfunctions (loops, "invalid_token", state mismatch, bounce-backs)
- Avoid adding Billing to account-only administration unless there is an explicit billing operation

Avoid over-tagging: Focus on which department should handle this ticket first.

3. 구체적인 시나리오가 있는 상세 우선순위 지침:

## Priority Guidelines  
- Ignore emotional tone - focus on business impact and available workarounds
- Billing disputes/adjustments (refunds, duplicate charges, incorrect taxes/pricing) = P1 unless causing an operational block
- Login workarounds: If Incognito/another account works, prefer P1; if cannot access at all, P0
- Core business functions failing (webhooks, API, sync) = P0

4. 추론이 포함된 포괄적인 예제:

올바른 분류를 보여주기 위해 명시적 추론과 함께 다양한 시나리오를 다루는 7개의 예제를 추가했어요.

## Examples with Reasoning

Input: "My colleague left and I need to change the team lead role to my email address."
Output: {"labels":["Account"], "priority":"P1"}
Reasoning: Administrative role change; avoid adding Billing unless a concrete billing action is requested.

Input: "Dashboard crashes when I click reports tab, but works fine in mobile app."
Output: {"labels":["ProductIssue"], "priority":"P1"}
Reasoning: Malfunction exists but workaround available (mobile app works); single label since primary issue is product malfunction.

데이터셋의 예제를 직접 추가하지 않도록 주의해요. 데이터셋에 과적합되어 다른 케이스에서 프롬프트가 실패할 수 있어요.

새 프롬프트 평가

개선 사항이 포함된 promptv2_fewshot.txt를 만든 후 새 프롬프트로 실험을 실행해요.

uv run evals.py run --prompt_file promptv2_fewshot.txt

이것은 동일한 데이터셋에서 개선된 프롬프트를 평가하고 새 타임스탬프 파일에 결과를 저장해요.

샘플 출력

$ uv run evals.py run --prompt_file promptv2_fewshot.txt

Loading dataset...
Dataset loaded with 20 samples
Running evaluation with prompt file: promptv2_fewshot.txt
Running experiment: 100%|██████████████████████████████████████████████████████████████| 20/20 [00:11<00:00,  1.75it/s]
✅ promptv2_fewshot: 20 cases evaluated
Results saved to: experiments/20250826-231414-promptv2_fewshot.csv
promptv2_fewshot Labels Accuracy: 90.00%
promptv2_fewshot Priority Accuracy: 95.00%

실험은 첫 번째 실행과 동일한 구조로 experiments/ 디렉토리에 새 CSV 파일을 만들어 바로 비교할 수 있게 해요.

결과 분석 및 비교

여러 CSV를 받아 결합해서 쉽게 비교할 수 있는 간단한 유틸리티 함수를 만들었어요.

uv run evals.py compare --inputs experiments/20250826-041332-promptv1.csv experiments/20250826-231414-promptv2_fewshot.csv

이것은 각 실험의 정확도를 출력하고 experiments/ 디렉토리에 결합된 CSV 파일을 저장해요.

샘플

$ uv run evals.py compare --inputs experiments/20250826-041332-promptv1.csv experiments/20250826-231414-promptv2_fewshot.csv 

promptv1 Labels Accuracy: 80.00%
promptv1 Priority Accuracy: 75.00%
promptv2_fewshot Labels Accuracy: 90.00%
promptv2_fewshot Priority Accuracy: 95.00%
Combined comparison saved to: experiments/20250826-231545-comparison.csv

여기서 promptv2_fewshot이 labels와 priority 둘 다의 정확도를 개선한 것을 볼 수 있어요. 하지만 일부 케이스는 여전히 실패하는 것도 볼 수 있어요. 오류를 분석해서 프롬프트를 더 개선할 수 있어요.

개선이 평평해지거나 정확도가 비즈니스 요구를 충족하면 반복을 중단해요.

프롬프트 개선만으로 정확도 향상에 한계에 부딪히면 더 나은 모델로 실험을 시도해볼 수 있어요.

이 루프를 사용 사례에 적용하기

  • 사용 사례에 대한 데이터셋, 메트릭, 실험 만들기
  • 평가 실행 및 오류 분석
  • 오류 분석을 바탕으로 프롬프트 개선
  • 평가 재실행 및 결과 비교
  • 개선이 평평해지거나 정확도가 비즈니스 요구를 충족하면 중단

데이터셋과 평가 루프를 구축하고 나면 model 같은 더 많은 파라미터를 테스트하도록 확장할 수 있어요.

Ragas 프레임워크는 오케스트레이션, 병렬 실행, 결과 집계를 자동으로 처리해서 평가에 집중할 수 있게 도와줘요!

고급: LLM 심판 정렬

평가에 LLM 기반 메트릭을 사용한다면 먼저 심판을 인간 전문가의 판단과 정렬해서 신뢰할 수 있는 평가를 보장하는 것을 고려해요. How to Align an LLM as a Judge를 참고해요.

더 알아보기 (Learn more)