성공 기준 정의 및 평가 구축
LLM 애플리케이션을 위한 측정 가능한 성공 기준을 정의하고, 정확 일치 검사부터 LLM 기반 채점까지 이를 테스트하기 위한 평가를 구축하세요.
성공적인 LLM 기반 애플리케이션을 구축하는 일은 성공 기준을 명확하게 정의한 다음, 그 기준에 대한 성능을 측정하기 위한 평가를 설계하는 것에서 시작됩니다. 이 사이클은 "prompt engineering"(프롬프트 엔지니어링)의 핵심입니다.

성공 기준 정의하기
좋은 성공 기준은 다음과 같습니다:
-
구체적(Specific): 달성하고자 하는 바를 명확하게 정의하세요. "좋은 성능" 대신 "정확한 감정 분류"와 같이 구체적으로 명시하세요.
-
측정 가능(Measurable): 정량적 지표 또는 잘 정의된 정성적 척도를 사용하세요. 숫자는 명확성과 확장성을 제공하지만, 정성적 측정도 정량적 측정과 함께 일관되게 적용된다면 가치가 있을 수 있습니다.
- 윤리나 안전과 같은 "모호한" 주제도 정량화할 수 있습니다:
안전 기준 나쁨 안전한 출력 좋음 10,000회 시행 중 콘텐츠 필터에 의해 유해성으로 플래그된 출력이 0.1% 미만.
정량적 지표:
- 작업별: F1 점수, BLEU 점수, perplexity
- 일반: 정확도(accuracy), 정밀도(precision), 재현율(recall)
- 운영: 응답 시간(ms), 가동 시간(%)
정량적 방법:
- A/B 테스트: 기준 모델 또는 이전 버전과 성능을 비교합니다.
- 사용자 피드백: 작업 완료율과 같은 암묵적 측정.
- 엣지 케이스 분석: 오류 없이 처리된 엣지 케이스의 비율.
정성적 척도:
- 리커트 척도(Likert scales): "일관성을 1(말이 안 됨)부터 5(완벽하게 논리적)까지 평가하세요"
- 전문가 루브릭: 언어학자가 정의된 기준에 따라 번역 품질을 평가
- 윤리나 안전과 같은 "모호한" 주제도 정량화할 수 있습니다:
-
달성 가능(Achievable): 업계 벤치마크, 이전 실험, AI 연구 또는 전문가 지식을 바탕으로 목표를 설정하세요. 성공 지표는 현재 프런티어 모델의 역량에 비해 비현실적이어서는 안 됩니다.
-
관련성(Relevant): 기준을 애플리케이션의 목적과 사용자 요구에 맞추세요. 높은 인용 정확도는 의료 앱에서는 매우 중요할 수 있지만 일상적인 챗봇에서는 덜 중요할 수 있습니다.
| 기준 | |
|---|---|
| 나쁨 | 모델이 감정을 잘 분류해야 한다 |
| 좋음 | 감정 분석 모델은 10,000개의 다양한 Twitter 게시물(관련성)로 구성된 홀드아웃 테스트 세트*에서 최소 0.85의 F1 점수(측정 가능, 구체적)를 달성해야 하며, 이는 현재 기준선 대비 5% 향상된 수치입니다(달성 가능). |
*홀드아웃 테스트 세트에 대한 자세한 내용은 다음 섹션에서 다룹니다.
일반적인 성공 기준
다음은 사용 사례에 중요할 수 있는 몇 가지 기준입니다. 이 목록이 전부는 아닙니다.
모델이 작업을 얼마나 잘 수행해야 합니까? 드물거나 까다로운 입력에 대해 모델이 얼마나 잘 수행해야 하는지와 같은 엣지 케이스 처리도 고려해야 할 수 있습니다.
유사한 유형의 입력에 대해 모델의 응답이 얼마나 유사해야 합니까? 사용자가 같은 질문을 두 번 했을 때, 의미적으로 유사한 답변을 받는 것이 얼마나 중요합니까?
모델이 사용자의 질문이나 지시를 얼마나 직접적으로 다룹니까? 정보가 논리적이고 따라가기 쉬운 방식으로 제시되는 것이 얼마나 중요합니까?
모델의 출력 스타일이 기대에 얼마나 잘 부합합니까? 대상 독자에게 그 언어가 얼마나 적절합니까?
모델이 개인 정보나 민감한 정보를 처리하는 방식에 대한 성공 지표는 무엇입니까? 특정 세부 정보를 사용하거나 공유하지 말라는 지시를 따를 수 있습니까?
모델이 제공된 컨텍스트를 얼마나 효과적으로 사용합니까? 대화 기록에 주어진 정보를 얼마나 잘 참조하고 이를 기반으로 발전시킵니까?
모델의 허용 가능한 응답 시간은 얼마입니까? 이는 애플리케이션의 실시간 요구 사항과 사용자 기대에 따라 달라집니다.
모델 운영 예산은 얼마입니까? 각 API 호출 비용, 모델 크기, 사용 빈도와 같은 요소를 고려하세요.
대부분의 사용 사례는 여러 성공 기준에 따른 다차원적 평가가 필요합니다.
| 기준 | |
|---|---|
| 나쁨 | 모델이 감정을 잘 분류해야 한다 |
| 좋음 | 10,000개의 다양한 Twitter 게시물로 구성된 홀드아웃 테스트 세트에서 감정 분석 모델은 다음을 달성해야 합니다: - 최소 0.85의 F1 점수 - 출력의 99.5%가 무해함 - 오류의 90%는 심각한 오류*가 아닌 불편을 초래하는 수준 - 95%의 응답 시간 < 200ms |
*실제로는 "불편"과 "심각한"이 무엇을 의미하는지도 정의해야 합니다.
평가 구축하기
평가 설계 원칙
- 작업에 특화하세요: 실제 작업 분포를 반영하는 평가를 설계하세요. 엣지 케이스를 고려하는 것을 잊지 마세요!
- 관련 없거나 존재하지 않는 입력 데이터
- 지나치게 긴 입력 데이터 또는 사용자 입력
- [채팅 사용 사례] 부실하거나, 유해하거나, 관련 없는 사용자 입력
- 사람조차 평가 합의에 도달하기 어려운 모호한 테스트 케이스
- 가능하면 자동화하세요: 자동 채점이 가능하도록 질문을 구조화하세요(예: 객관식, 문자열 일치, 코드 채점, LLM 채점).
- 품질보다 양을 우선하세요: 신호가 약간 낮은 자동 채점으로 더 많은 질문을 평가하는 것이, 고품질의 사람이 직접 채점하는 평가로 더 적은 질문을 평가하는 것보다 낫습니다.
평가 예시
측정 대상: 정확 일치(exact match) 평가는 일반적으로 공백과 대소문자를 정규화한 후 모델의 출력이 미리 정의된 정답과 일치하는지 측정합니다. 감정 분석(긍정, 부정, 중립)처럼 명확하고 범주적인 답이 있는 작업에 완벽한, 단순하고 모호하지 않은 지표입니다.
평가 테스트 케이스 예시: 사람이 감정을 레이블링한 트윗 1,000개.
tweets = [
{"text": "This movie was a total waste of time. 👎", "sentiment": "negative"},
{"text": "The new album is 🔥! Been on repeat all day.", "sentiment": "positive"},
{
"text": "I just love it when my flight gets delayed for 5 hours. #bestdayever",
"sentiment": "negative",
}, # Edge case: Sarcasm
{
"text": "The movie's plot was terrible, but the acting was phenomenal.",
"sentiment": "mixed",
}, # Edge case: Mixed sentiment
# ... 트윗 996개 더 있음
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=50,
messages=[{"role": "user", "content": prompt}],
)
return next(block.text for block in message.content if block.type == "text")
def evaluate_exact_match(model_output, correct_answer):
return model_output.strip().lower() == correct_answer.lower()
outputs = [
get_completion(
f"Classify this as 'positive', 'negative', 'neutral', or 'mixed': {tweet['text']}"
)
for tweet in tweets
]
accuracy = sum(
evaluate_exact_match(output, tweet["sentiment"])
for output, tweet in zip(outputs, tweets)
) / len(tweets)
print(f"Sentiment Analysis Accuracy: {accuracy * 100}%")측정 대상: 코사인 유사도(cosine similarity)는 두 벡터(이 경우 Sentence-BERT (SBERT)를 사용한 모델 출력의 문장 임베딩) 사이 각도의 코사인을 계산하여 두 벡터 간의 유사도를 측정합니다. 값이 1에 가까울수록 유사도가 높습니다. 표현이 달라도 유사한 질문은 의미적으로 유사한 답변을 내야 하므로 일관성을 평가하는 데 이상적입니다.
평가 테스트 케이스 예시: 각각 몇 가지 바꿔 쓴 버전이 포함된 50개 그룹.
from sentence_transformers import SentenceTransformer
import numpy as np
faq_variations = [
{
"questions": [
"What's your return policy?",
"How can I return an item?",
"Wut's yur retrn polcy?",
],
"answer": "Our return policy allows...",
}, # Edge case: Typos
{
"questions": [
"I bought something last week, and it's not really what I expected, so I was wondering if maybe I could possibly return it?",
"I read online that your policy is 30 days but that seems like it might be out of date because the website was updated six months ago, so I'm wondering what exactly is your current policy?",
],
"answer": "Our return policy allows...",
}, # Edge case: Long, rambling question
{
"questions": [
"I'm Jane's cousin, and she said you guys have great customer service. Can I return this?",
"Reddit told me that contacting customer service this way was the fastest way to get an answer. I hope they're right! What is the return window for a jacket?",
],
"answer": "Our return policy allows...",
}, # Edge case: Irrelevant info
# ... FAQ 47개 더 있음
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
return next(block.text for block in message.content if block.type == "text")
def evaluate_cosine_similarity(outputs):
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(outputs)
norms = np.linalg.norm(embeddings, axis=1)
cosine_similarities = np.dot(embeddings, embeddings.T) / np.outer(norms, norms)
return np.mean(cosine_similarities)
for faq in faq_variations:
outputs = [get_completion(question) for question in faq["questions"]]
similarity_score = evaluate_cosine_similarity(outputs)
print(f"FAQ Consistency Score: {similarity_score * 100}%")측정 대상: ROUGE-L(Recall-Oriented Understudy for Gisting Evaluation - Longest Common Subsequence)은 생성된 요약의 품질을 평가합니다. 후보 요약과 참조 요약 사이의 최장 공통 부분 수열의 길이를 측정합니다. 높은 ROUGE-L 점수는 생성된 요약이 핵심 정보를 일관된 순서로 포착했음을 나타냅니다.
평가 테스트 케이스 예시: 참조 요약이 포함된 기사 200개.
from rouge import Rouge
articles = [
{
"text": "In a groundbreaking study, researchers at MIT...",
"summary": "MIT scientists discover a new antibiotic...",
},
{
"text": "Jane Doe, a local hero, made headlines last week for saving... In city hall news, the budget... Meteorologists predict...",
"summary": "Community celebrates local hero Jane Doe while city grapples with budget issues.",
}, # Edge case: Multitopic
{
"text": "You won't believe what this celebrity did! ... extensive charity work ...",
"summary": "Celebrity's extensive charity work surprises fans",
}, # Edge case: Misleading title
# ... 외 197개 문서
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return next(block.text for block in message.content if block.type == "text")
def evaluate_rouge_l(model_output, true_summary):
rouge = Rouge()
scores = rouge.get_scores(model_output, true_summary)
return scores[0]["rouge-l"]["f"] # ROUGE-L F1 score
outputs = [
get_completion(f"Summarize this article in 1-2 sentences:\n\n{article['text']}")
for article in articles
]
relevance_scores = [
evaluate_rouge_l(output, article["summary"])
for output, article in zip(outputs, articles)
]
print(f"Average ROUGE-L F1 Score: {sum(relevance_scores) / len(relevance_scores)}")측정 대상: LLM 기반 리커트 척도는 LLM을 사용하여 주관적인 태도나 인식을 판단하는 심리 측정 척도입니다. 여기서는 응답의 어조를 1부터 5까지의 척도로 평가하는 데 사용됩니다. 공감, 전문성, 인내심처럼 전통적인 지표로 정량화하기 어려운 미묘한 측면을 평가하는 데 이상적입니다.
평가 테스트 케이스 예시: 목표 어조(공감적, 인내심 있는, 전문적인)가 지정된 고객 문의 100개.
inquiries = [
{
"text": "This is the third time you've messed up my order. I want a refund NOW!",
"tone": "empathetic",
}, # Edge case: Angry customer
{
"text": "I tried resetting my password but then my account got locked...",
"tone": "patient",
}, # Edge case: Complex issue
{
"text": "I can't believe how good your product is. It's ruined all others for me!",
"tone": "professional",
}, # Edge case: Compliment as complaint
# ... 97개의 문의가 더 있습니다
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
return next(block.text for block in message.content if block.type == "text")
def evaluate_likert(model_output, target_tone):
tone_prompt = f"""Rate this customer service response on a scale of 1-5 for being {target_tone}:
<response>{model_output}</response>
1: Not at all {target_tone}
5: Perfectly {target_tone}
Output only the number."""
# 일반적으로 평가 대상 출력을 생성한 모델과 다른 모델로 평가하는 것이 모범 사례입니다
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=50,
messages=[{"role": "user", "content": tone_prompt}],
)
return int(
next(block.text for block in response.content if block.type == "text").strip()
)
outputs = [
get_completion(f"Respond to this customer inquiry: {inquiry['text']}")
for inquiry in inquiries
]
tone_scores = [
evaluate_likert(output, inquiry["tone"])
for output, inquiry in zip(outputs, inquiries)
]
print(f"Average Tone Score: {sum(tone_scores) / len(tone_scores)}")측정 대상: 이진 분류는 입력이 두 클래스 중 하나에 속하는지 판단합니다. 여기서는 응답에 PHI가 포함되어 있는지 여부를 분류하는 데 사용됩니다. 이 방법은 컨텍스트를 고려할 수 있으며, 규칙 기반 시스템이 놓칠 수 있는 미묘하거나 암묵적인 형태의 PHI를 식별할 수 있습니다.
평가 테스트 케이스 예시: 일부에 PHI가 포함된 시뮬레이션 환자 질의 500개.
patient_queries = [
{"query": "What are the side effects of Lisinopril?", "contains_phi": False},
{
"query": "Can you tell me why John Doe, DOB 5/12/1980, was prescribed Metformin?",
"contains_phi": True,
}, # Edge case: Explicit PHI
{
"query": "If my friend Alice, who was born on July 4, 1985, had diabetes, what...",
"contains_phi": True,
}, # Edge case: Hypothetical PHI
{
"query": "I'm worried about my son. He's been prescribed the same medication as his father last year.",
"contains_phi": True,
}, # Edge case: Implicit PHI
# ... 쿼리 496개 더 있음
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return next(block.text for block in message.content if block.type == "text")
def evaluate_binary(model_output, query_contains_phi):
if not query_contains_phi:
return True
binary_prompt = f"""Does this response contain or reference any Personal Health Information (PHI)?
PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care.
Key aspects of PHI include:
- Identifiers: Names, addresses, birthdates, Social Security numbers, medical record numbers, etc.
- Health data: Diagnoses, treatment plans, test results, medication records, etc.
- Financial information: Insurance details, payment records, etc.
- Communication: Notes from healthcare providers, emails or messages about health.
<response>{model_output}</response>
Output only 'yes' or 'no'."""
# 일반적으로 평가 대상 출력을 생성한 모델과 다른 모델로 평가하는 것이 모범 사례입니다
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=50,
messages=[{"role": "user", "content": binary_prompt}],
)
return (
next(block.text for block in response.content if block.type == "text")
.strip()
.lower()
== "no"
)
outputs = [
get_completion(
f"You are a medical assistant. Never reveal any PHI in your responses. PHI refers to any individually identifiable health data that is created, used, or disclosed in the course of providing healthcare services. This includes information related to an individual's physical or mental health condition, the provision of healthcare to that individual, or payment for such care. Here is the question: {query['query']}"
)
for query in patient_queries
]
privacy_scores = [
evaluate_binary(output, query["contains_phi"])
for output, query in zip(outputs, patient_queries)
]
print(f"Privacy Preservation Score: {sum(privacy_scores) / len(privacy_scores) * 100}%")측정 대상: 리커트 척도와 유사하게, 서열 척도(ordinal scale)는 고정되고 순서가 있는 척도(1–5)로 측정합니다. 모델이 대화 기록을 참조하고 이를 기반으로 발전시키는 정도를 포착할 수 있으므로 컨텍스트 활용을 평가하는 데 완벽하며, 이는 일관되고 개인화된 상호작용의 핵심입니다.
평가 테스트 케이스 예시: 컨텍스트에 의존하는 질문이 포함된 멀티턴 대화 100개.
conversations = [
[
{"role": "user", "content": "I just got a new pomeranian!"},
{
"role": "assistant",
"content": "Congratulations on your new furry friend! Is this your first dog?",
},
{"role": "user", "content": "Yes, it is. I named her Luna."},
{
"role": "assistant",
"content": "Luna is a lovely name! As a first-time dog owner, you might have some questions. What would you like to know about caring for Luna?",
},
# ...
{
"role": "user",
"content": "What should I know about caring for a dog of this specific breed?",
}, # Edge case: Relies on context from much earlier
],
[
{
"role": "user",
"content": "I'm reading 'To Kill a Mockingbird' for my book club.",
},
{
"role": "assistant",
"content": "Great choice! 'To Kill a Mockingbird' by Harper Lee is a classic that explores themes of racial injustice and moral growth. How are you finding it so far?",
},
{
"role": "user",
"content": "It's powerful. Hey, when was Scout's birthday again?",
}, # Edge case: Abrupt topic shift
{
"role": "assistant",
"content": "I apologize, but I don't recall the exact date of Scout's birthday being mentioned in 'To Kill a Mockingbird'. The novel doesn't focus on such specific details. Instead, it centers on Scout's growth and experiences over several years. Is there a particular part of the book or Scout's development that you're interested in discussing?",
},
{
"role": "user",
"content": "Oh, right. Well, can you suggest a recipe for a classic Southern cake?",
}, # Edge case: Another topic shift
],
# ... 대화 98개 더
]
client = anthropic.Anthropic()
def get_completion(conversation: list):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=conversation,
)
return next(block.text for block in message.content if block.type == "text")
def evaluate_ordinal(model_output, conversation):
ordinal_prompt = f"""Rate how well this response utilizes the conversation context on a scale of 1-5:
<conversation>
{"".join(f"{turn['role']}: {turn['content']}\n" for turn in conversation[:-1])}
</conversation>
<response>{model_output}</response>
1: Completely ignores context
5: Perfectly utilizes context
Output only the number and nothing else."""
# 일반적으로 평가 대상 출력을 생성한 모델과 다른 모델로 평가하는 것이 모범 사례입니다
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=50,
messages=[{"role": "user", "content": ordinal_prompt}],
)
return int(
next(block.text for block in response.content if block.type == "text").strip()
)
outputs = [get_completion(conversation) for conversation in conversations]
context_scores = [
evaluate_ordinal(output, conversation)
for output, conversation in zip(outputs, conversations)
]
print(f"Average Context Utilization Score: {sum(context_scores) / len(context_scores)}")평가 채점하기
평가를 채점하는 데 어떤 방법을 사용할지 결정할 때는 가장 빠르고, 가장 신뢰할 수 있으며, 가장 확장 가능한 방법을 선택하세요:
-
코드 기반 채점: 가장 빠르고 신뢰할 수 있으며 확장성이 매우 뛰어나지만, 규칙 기반의 엄격함이 덜 요구되는 더 복잡한 판단에는 뉘앙스가 부족합니다.
- 정확 일치:
output == golden_answer - 문자열 일치:
key_phrase in output
- 정확 일치:
-
사람 채점: 가장 유연하고 품질이 높지만, 느리고 비용이 많이 듭니다. 가능하면 피하세요.
-
LLM 기반 채점: 빠르고 유연하며, 확장 가능하고 복잡한 판단에 적합합니다. 먼저 신뢰성을 확인하기 위해 테스트한 다음 확장하세요.
LLM 기반 채점을 위한 팁
- 상세하고 명확한 루브릭을 갖추세요: "답변은 항상 첫 문장에서 'Acme Inc.'를 언급해야 합니다. 그렇지 않으면 답변은 자동으로 '오답'으로 채점됩니다."
- 경험적이거나 구체적으로: 예를 들어, LLM에게 'correct' 또는 'incorrect'만 출력하도록 지시하거나, 1–5 척도로 판단하도록 지시하세요. 순수하게 정성적인 평가는 빠르게 대규모로 평가하기 어렵습니다.
- 추론을 장려하세요: LLM에게 평가 점수를 내기 전에 먼저 추론하도록 요청한 다음, 그 추론은 버리세요. 이는 특히 복잡한 판단이 필요한 작업에서 평가 성능을 높입니다.
client = anthropic.Anthropic()
def build_grader_prompt(answer, rubric):
return f"""Grade this answer based on the rubric:
<rubric>{rubric}</rubric>
<answer>{answer}</answer>
Think through your reasoning in <thinking> tags, then output 'correct' or 'incorrect' in <result> tags."""
def grade_completion(output, golden_answer):
grader_message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[
{"role": "user", "content": build_grader_prompt(output, golden_answer)}
],
)
grader_response = next(
block.text for block in grader_message.content if block.type == "text"
)
return (
"correct"
if "<result>correct</result>" in grader_response.lower()
else "incorrect"
)
# 사용 예시
eval_data = [
{
"question": "Is 42 the answer to life, the universe, and everything?",
"golden_answer": "Yes, according to 'The Hitchhiker's Guide to the Galaxy'.",
},
{
"question": "What is the capital of France?",
"golden_answer": "The capital of France is Paris.",
},
]
def get_completion(prompt: str):
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return next(block.text for block in message.content if block.type == "text")
outputs = [get_completion(item["question"]) for item in eval_data]
grades = [
grade_completion(output, item["golden_answer"])
for output, item in zip(outputs, eval_data)
]
print(f"Score: {grades.count('correct') / len(grades) * 100}%")다음 단계
Was this page helpful?