Phase 11: 관찰성 | 예상 학습 시간: 35분
🎯 학습 목표
- 자유 형식 텍스트 출력을 평가하는 것이 왜 근본적으로 어려운지 이해한다
- LLM-as-judge, 규칙 기반 지표, 사람 평가를 상황에 맞게 선택할 수 있다
- 작은 평가 데이터셋을 구축하고 프롬프트/모델 변경 전 회귀 테스트에 활용할 수 있다
1. “정답이 하나가 아닌” 출력을 어떻게 채점하는가
전통적인 소프트웨어 테스트는 assert result == expected로 충분합니다. 하지만 LLM 출력은 다음과 같은 이유로 단순 비교가 통하지 않습니다.
- 표현의 다양성 — “환불은 7일 이내 가능합니다"와 “구매 후 일주일 안에 환불 신청하세요"는 문자열은 다르지만 의미상 동일하게 맞는 답
- 부분 정답 — 5개 항목 중 4개는 맞고 1개가 틀린 답을 “틀림"으로 처리해야 하는가, “80% 맞음"으로 처리해야 하는가
- 채점 기준 자체의 모호함 — “친절한 톤인가?”, “환각 없이 근거에 기반했는가?” 같은 기준은 사람마다 판단이 갈릴 수 있음
- 회귀의 은밀함 — 프롬프트를 한 줄 바꿨더니 다른 케이스에서 품질이 떨어지는 경우, 전통적 유닛 테스트로는 절대 못 잡음
이 문제 때문에 LLM 애플리케이션 팀은 “정확히 일치하는가"가 아니라 **“허용 가능한 범위 안에 있는가”**를 판단하는 평가 체계를 별도로 구축해야 합니다.
💡 실무 팁: “평가 방법이 완벽하지 않으니 평가를 안 한다"는 흔한 함정입니다. 완벽한 평가는 불가능하다는 것을 인정하고, 방향성(품질이 좋아졌는지 나빠졌는지)을 잡을 수 있는 수준의 평가 체계라도 최대한 빨리 갖추는 것이 실무의 정답입니다.
2. 주요 평가 접근법 비교
| 방법 | 원리 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|---|
| Exact Match / 정규식 | 출력이 기대값과 문자 그대로(또는 패턴) 일치하는지 확인 | 빠르고 결정적, 비용 0 | 표현이 조금만 달라도 실패 처리 | 분류(카테고리), 구조화 출력(JSON 필드), 숫자 답 |
| ROUGE / BLEU | n-gram 겹침 기반 유사도 점수 | 자동화 가능, 요약/번역 벤치마크 표준 | 의미는 같아도 단어가 다르면 점수 낮음 | 요약, 번역처럼 참조 텍스트가 명확한 태스크 |
| LLM-as-Judge | 별도의 강한 LLM에게 “이 답이 기준을 만족하는가?“를 채점시킴 | 자유 형식 텍스트에도 적용 가능, 확장성 좋음 | 판정 모델 자체의 편향/비일관성, 비용 발생 | 챗봇 응답 품질, RAG 근거 충실도, 톤/스타일 |
| 사람 평가 (Human Eval) | 실제 사람이 응답을 읽고 채점 | 가장 신뢰도 높음, 미묘한 뉘앙스 포착 | 느리고 비쌈, 스케일 안 됨 | 최종 검증, LLM-judge 자체의 신뢰도 검증 |
실무에서는 보통 이 네 가지를 조합합니다. 구조화된 출력(도구 호출 인자, 분류 라벨)은 exact match로, 자유 형식 응답은 LLM-as-judge로 자동화하고, 배포 직전 중요한 변경에는 소규모 사람 평가를 곁들이는 식입니다.
LLM-as-Judge 예시
JUDGE_PROMPT = """당신은 고객 지원 챗봇의 답변 품질을 평가하는 심사위원입니다.
아래 질문, 참고 문서, 챗봇의 답변을 보고 1~5점으로 채점하세요.
평가 기준:
- 답변이 참고 문서에 근거하는가 (환각 없음)
- 질문에 실제로 답했는가
- 톤이 친절하고 명확한가
질문: {question}
참고 문서: {context}
챗봇 답변: {answer}
점수(1~5)와 한 줄 이유를 JSON으로 출력하세요:
{{"score": <int>, "reason": "<string>"}}
"""
def judge_response(question, context, answer, judge_llm):
prompt = JUDGE_PROMPT.format(question=question, context=context, answer=answer)
result = judge_llm.invoke(prompt)
return json.loads(result.content)
💡 실무 팁: 판정용 LLM은 가급적 테스트 대상 모델보다 더 강한 모델(또는 최소한 다른 계열의 모델)을 쓰세요. 같은 모델이 자기 자신의 출력을 채점하면 스스로의 편향을 그대로 반영해 “자기 확신 편향(self-preference bias)“이 생길 수 있습니다.
3. 평가 데이터셋 구축하기
평가는 데이터셋 없이는 불가능합니다. 처음부터 완벽한 데이터셋을 만들 필요는 없고, 아래 순서로 점진적으로 키우는 것이 현실적입니다.
- 실제 실패 사례 수집 — 프로덕션 트레이스(Day 27의 Langfuse)에서 사용자가 재질문하거나 부정적 피드백을 남긴 케이스를 골라낸다
- 엣지 케이스 직접 작성 — 애매한 질문, 컨텍스트에 없는 질문(환각 유도), 여러 도구를 조합해야 하는 질문 등을 팀이 직접 설계
- 입력-기대출력 쌍으로 구조화
[
{
"id": "refund-001",
"input": "3주 전에 산 물건인데 환불 되나요?",
"context": "환불 정책: 구매 후 7일 이내에만 환불 가능",
"expected_behavior": "환불 불가 안내 + 대안(교환/AS) 제시",
"category": "policy-boundary"
},
{
"id": "rag-002",
"input": "이 회사 창립자가 누구인가요?",
"context": "환불 정책 관련 문서만 검색됨 (관련 정보 없음)",
"expected_behavior": "모른다고 답하거나 관련 부서 안내, 환각 금지",
"category": "out-of-scope"
}
]
- 카테고리별로 태깅 — 정책 경계, 범위 밖 질문, 멀티턴 문맥 유지, 도구 호출 정확도 등으로 분류해두면 회귀 발생 시 “어느 카테고리가 약해졌는지” 바로 보임
- 최소 30~50개로 시작, 지속적으로 추가 — 완벽한 커버리지보다 “새로운 실패를 발견할 때마다 데이터셋에 추가"하는 습관이 더 중요
💡 실무 팁: 평가 데이터셋은 코드와 마찬가지로 git으로 버전 관리하세요. 프롬프트 변경 이력과 평가셋 변경 이력을 나란히 볼 수 있어야 “이 프롬프트 변경이 회귀를 일으켰는지"를 정확히 추적할 수 있습니다.
4. 회귀 테스트 — 배포 전 자동 검증 파이프라인
프롬프트나 모델을 바꿀 때마다 평가 데이터셋 전체를 돌려서 점수 변화를 확인하는 것이 핵심입니다. CI 파이프라인에 넣을 수도 있고, 로컬 스크립트로 돌릴 수도 있습니다.
def run_regression_suite(agent, dataset, judge_llm):
results = []
for case in dataset:
answer = agent.invoke(case["input"], context=case["context"])
score = judge_response(case["input"], case["context"], answer, judge_llm)
results.append({**case, "answer": answer, "score": score["score"]})
avg_score = sum(r["score"] for r in results) / len(results)
failures = [r for r in results if r["score"] < 3]
print(f"평균 점수: {avg_score:.2f} / 5, 실패 케이스: {len(failures)}개")
return results, failures
이 스크립트를 프롬프트 변경 전/후로 각각 돌려서 비교하면:
- 평균 점수가 유의미하게 떨어졌다 → 배포 보류, 실패 케이스 상세 분석
- 특정 카테고리(예: out-of-scope)만 떨어졌다 → 그 카테고리에 해당하는 프롬프트 지시문을 보강
- 점수는 비슷한데 지연시간/비용이 늘었다 → Day 27의 Langfuse 트레이스와 교차 분석
💡 실무 팁: LLM-as-judge 점수는 절대값보다 **상대 비교(변경 전 vs 변경 후)**로 쓰는 것이 훨씬 신뢰도가 높습니다. “4.2점이 좋은 건가?“에 답하기는 어렵지만 “4.5점에서 3.8점으로 떨어졌다"는 명확한 경고 신호입니다.
📝 핵심 요약
- LLM 출력은 정답이 여러 형태로 존재할 수 있어 exact match만으로는 평가가 불가능
- Exact Match/ROUGE(구조화 출력), LLM-as-Judge(자유 형식), Human Eval(최종 검증)을 상황에 맞게 조합
- 판정용 LLM은 테스트 대상보다 강하거나 다른 모델을 써서 자기 확신 편향을 줄여야 함
- 평가 데이터셋은 실제 실패 사례 + 직접 설계한 엣지 케이스로 점진적으로 구축하고 git으로 버전 관리
- 프롬프트/모델 변경 시 평가셋 전체를 돌리는 회귀 테스트를 배포 프로세스에 편입시켜야 함
🔗 참고 자료
⬅️ 이전: Day 27 — Langfuse: Logging & Tracing | 다음: Day 29 — LoRA / QLoRA / PEFT ➡️