Phase 11: 관찰성 | 예상 학습 시간: 35분


🎯 학습 목표

  • 자유 형식 텍스트 출력을 평가하는 것이 왜 근본적으로 어려운지 이해한다
  • LLM-as-judge, 규칙 기반 지표, 사람 평가를 상황에 맞게 선택할 수 있다
  • 작은 평가 데이터셋을 구축하고 프롬프트/모델 변경 전 회귀 테스트에 활용할 수 있다

1. “정답이 하나가 아닌” 출력을 어떻게 채점하는가

전통적인 소프트웨어 테스트는 assert result == expected로 충분합니다. 하지만 LLM 출력은 다음과 같은 이유로 단순 비교가 통하지 않습니다.

  • 표현의 다양성 — “환불은 7일 이내 가능합니다"와 “구매 후 일주일 안에 환불 신청하세요"는 문자열은 다르지만 의미상 동일하게 맞는 답
  • 부분 정답 — 5개 항목 중 4개는 맞고 1개가 틀린 답을 “틀림"으로 처리해야 하는가, “80% 맞음"으로 처리해야 하는가
  • 채점 기준 자체의 모호함 — “친절한 톤인가?”, “환각 없이 근거에 기반했는가?” 같은 기준은 사람마다 판단이 갈릴 수 있음
  • 회귀의 은밀함 — 프롬프트를 한 줄 바꿨더니 다른 케이스에서 품질이 떨어지는 경우, 전통적 유닛 테스트로는 절대 못 잡음

이 문제 때문에 LLM 애플리케이션 팀은 “정확히 일치하는가"가 아니라 **“허용 가능한 범위 안에 있는가”**를 판단하는 평가 체계를 별도로 구축해야 합니다.

💡 실무 팁: “평가 방법이 완벽하지 않으니 평가를 안 한다"는 흔한 함정입니다. 완벽한 평가는 불가능하다는 것을 인정하고, 방향성(품질이 좋아졌는지 나빠졌는지)을 잡을 수 있는 수준의 평가 체계라도 최대한 빨리 갖추는 것이 실무의 정답입니다.


2. 주요 평가 접근법 비교

방법원리장점단점적합한 상황
Exact Match / 정규식출력이 기대값과 문자 그대로(또는 패턴) 일치하는지 확인빠르고 결정적, 비용 0표현이 조금만 달라도 실패 처리분류(카테고리), 구조화 출력(JSON 필드), 숫자 답
ROUGE / BLEUn-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. 평가 데이터셋 구축하기

평가는 데이터셋 없이는 불가능합니다. 처음부터 완벽한 데이터셋을 만들 필요는 없고, 아래 순서로 점진적으로 키우는 것이 현실적입니다.

  1. 실제 실패 사례 수집 — 프로덕션 트레이스(Day 27의 Langfuse)에서 사용자가 재질문하거나 부정적 피드백을 남긴 케이스를 골라낸다
  2. 엣지 케이스 직접 작성 — 애매한 질문, 컨텍스트에 없는 질문(환각 유도), 여러 도구를 조합해야 하는 질문 등을 팀이 직접 설계
  3. 입력-기대출력 쌍으로 구조화
[
  {
    "id": "refund-001",
    "input": "3주 전에 산 물건인데 환불 되나요?",
    "context": "환불 정책: 구매 후 7일 이내에만 환불 가능",
    "expected_behavior": "환불 불가 안내 + 대안(교환/AS) 제시",
    "category": "policy-boundary"
  },
  {
    "id": "rag-002",
    "input": "이 회사 창립자가 누구인가요?",
    "context": "환불 정책 관련 문서만 검색됨 (관련 정보 없음)",
    "expected_behavior": "모른다고 답하거나 관련 부서 안내, 환각 금지",
    "category": "out-of-scope"
  }
]
  1. 카테고리별로 태깅 — 정책 경계, 범위 밖 질문, 멀티턴 문맥 유지, 도구 호출 정확도 등으로 분류해두면 회귀 발생 시 “어느 카테고리가 약해졌는지” 바로 보임
  2. 최소 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점으로 떨어졌다"는 명확한 경고 신호입니다.


📝 핵심 요약

  1. LLM 출력은 정답이 여러 형태로 존재할 수 있어 exact match만으로는 평가가 불가능
  2. Exact Match/ROUGE(구조화 출력), LLM-as-Judge(자유 형식), Human Eval(최종 검증)을 상황에 맞게 조합
  3. 판정용 LLM은 테스트 대상보다 강하거나 다른 모델을 써서 자기 확신 편향을 줄여야 함
  4. 평가 데이터셋은 실제 실패 사례 + 직접 설계한 엣지 케이스로 점진적으로 구축하고 git으로 버전 관리
  5. 프롬프트/모델 변경 시 평가셋 전체를 돌리는 회귀 테스트를 배포 프로세스에 편입시켜야 함

🔗 참고 자료


⬅️ 이전: Day 27 — Langfuse: Logging & Tracing | 다음: Day 29 — LoRA / QLoRA / PEFT ➡️