포스트

2026-04-09 강의 정리

LLM을 활용한 성능 평가 (LLM-as-Judge) - LangChain QA Evaluation

2026-04-09 강의 정리

목차

  1. A/B 테스트 개념 (t-test, Cohen’s d)
  2. A/B 테스트 구현 (ab_test)
  3. 리랭킹 효과 측정 실험
  4. BLEU / ROUGE 개요
  5. simple_rouge_f1 구현
  6. LLM-as-judge 개요
  7. basic_judge (비구조화 출력)
  8. basic_judge2 (JSON 구조화 출력)
  9. Pointwise Judge with RUBRIC
  10. Pairwise Judge + Position Bias 처리
  11. Reference-based Judge
  12. 자주 나오는 실수 / 주의사항
  13. [보충] 평가 기법 비교

1. A/B 테스트 개념 (t-test, Cohen’s d)

왜 필요한가?

리랭킹 기법을 바꿨을 때 “실제로 성능이 좋아진 건가?” 를 수치로 검증합니다.

1
2
3
4
5
6
7
8
A (기존 방식): 벡터 검색만 사용
B (변경 방식): 벡터 검색 + BM25 리랭킹

예시:
  A: AP = [0.87, 1.0, 0.58]  → mAP ≈ 0.82
  B: AP = [0.75, 1.0, 0.33]  → mAP ≈ 0.69

  → "A가 더 좋아 보이지만, 이 차이가 통계적으로 의미 있는가?"

t-test와 p-value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[검정 구조]
  귀무가설(H₀): A와 B의 성능 차이가 없다
  대립가설(H₁): A와 B의 성능 차이가 있다

t-test → p-value 계산
  p-value: H₀가 참일 때 지금의 결과(또는 더 극단적인 결과)가 나올 확률

  p-value = 0.01 → "차이가 없는데도 이런 결과가 나올 확률이 1%" → H₀ 기각 가능
  p-value < alpha(유의수준, 보통 0.05) → 통계적으로 유의미한 차이

[t-test 종류]
  stats.ttest_rel(a, b)  : 대응표본 (같은 쿼리셋에 A/B 적용)  ← 주로 사용
  welch's t-test          : 두 집단의 분산이 다를 때

  모집단이 같은 경우  : 같은 반의 수학점수 vs 영어점수
  모집단이 다른 경우  : 우리 반 수학점수 vs 옆 반 수학점수

Cohen’s d — 효과 크기

1
2
3
4
5
6
7
8
9
10
d = (mean_A - mean_B) / pooled_std

pooled_std: 두 집단을 합친 표준편차 (얼마나 퍼져있는지)

해석:
  d < 0.2  : 효과 작음
  d ≈ 0.5  : 효과 중간
  d > 0.8  : 효과 큼

p-value가 유의해도 Cohen's d가 작으면 → 통계적으로는 차이 있지만 실용적으로는 미미

2. A/B 테스트 구현 (ab_test)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scipy import stats

def ab_test(scores_a, scores_b, alpha=0.05):
    t_stat, p_val = stats.ttest_rel(scores_a, scores_b)  # 대응표본 t-test

    mean_a = np.mean(scores_a)
    mean_b = np.mean(scores_b)

    pooled_std = np.sqrt((np.std(scores_a)**2 + np.std(scores_b)**2) / 2)
    cohens_d = (mean_a - mean_b) / pooled_std if pooled_std > 0 else 0.0

    return {
        'mean_A':      mean_a,
        'mean_B':      mean_b,
        'p_val':       p_val,
        'cohens_d':    cohens_d,
        'significant': p_val < alpha   # True면 통계적으로 유의한 차이
    }

3. 리랭킹 효과 측정 실험

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test_queries = {
    '트랜스포머 아키텍처': {'d1', 'd2', 'd3'},
    '벡터 검색 방법':       {'d5'},
    'LLM 학습 방법':        {'d6', 'd7'}
}

aps_before = []   # 리랭킹 전 AP (벡터 검색만)
aps_after  = []   # 리랭킹 후 AP (BM25 리랭킹 적용)

bm25_reranker = BM25Reranker()

for q, relevant in test_queries.items():
    orig     = vector_search(q, vectorstore)
    reranked = bm25_reranker.rerank(q, orig)

    orig_ids     = [doc.metadata['id'] for doc, _ in orig]
    reranked_ids = [doc.metadata['id'] for doc, _ in reranked]

    aps_before.append(average_precision(orig_ids, relevant))
    aps_after.append(average_precision(reranked_ids, relevant))

result = ab_test(aps_before, aps_after, alpha=0.05)
1
2
3
4
5
6
결과 해석 예시:
  mean_A = 0.82, mean_B = 0.69
  p_val  = 0.32  → 유의수준 0.05보다 크다 → 통계적으로 유의하지 않음
  significant = False

  → 샘플이 3개뿐이라 통계 검정력 낮음 → 더 많은 쿼리로 실험 필요

4. BLEU / ROUGE 개요

텍스트 생성 품질을 n-gram 겹침으로 측정하는 전통적 지표입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[BLEU]
  - 기계 번역 평가에서 출발
  - n-gram precision 기반: 생성문이 정답에 얼마나 정확한지
  - 1-gram(unigram), 2-gram(bigram) 등 사용

[ROUGE]
  - 요약 평가에 주로 사용
  - n-gram recall 기반: 정답의 내용을 얼마나 커버하는지
  - ROUGE-1 (unigram), ROUGE-2 (bigram), ROUGE-L (최장 공통 부분수열)

[F1 = ROUGE-F1]
  Precision: 생성문 단어 중 정답에 있는 비율
  Recall:    정답 단어 중 생성문에 있는 비율
  F1 = 2 × P × R / (P + R)

한계

1
2
3
4
5
6
BLEU/ROUGE의 한계:
  - 단어 일치만 봄 → 의미적 유사성 측정 불가
  - "딥러닝은 신경망 기반 AI 기술" vs "딥러닝은 심층 신경망을 사용하는 머신러닝"
    → 의미는 같지만 단어가 다르면 낮은 점수
  - 언어, 전문용어, 패러프레이즈에 취약
  → 보완책으로 LLM-as-judge 사용

5. simple_rouge_f1 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import Counter

def simple_rouge_f1(reference, candidate):
    ref_words  = Counter(reference.split())
    cand_words = Counter(candidate.split())

    # 겹치는 단어 수 (최솟값 기준)
    overlap = sum(min(ref_words[w], cand_words.get(w, 0)) for w in ref_words)

    r = overlap / sum(ref_words.values())  if ref_words  else 0  # Recall
    p = overlap / sum(cand_words.values()) if cand_words else 0  # Precision

    return 2*p*r / (p+r) if (p+r) > 0 else 0  # F1
1
2
3
4
5
6
7
예시:
  reference : "딥러닝은 다층 신경망을 사용하여 데이터에서 복잡한 패턴을 학습하는 머신러닝 방법입니다"
  candidate1: "딥러닝은 다층 신경망을 사용하여..." (정답 복사)   → ROUGE ≈ 1.0
  candidate2: "인공 신경망의 층을 깊게 쌓아..."   (좋은 패러프레이즈) → ROUGE ↓ (단어 다름)
  candidate3: "딥러닝 딥러닝 신경망 데이터 학습..." (키워드 나열)    → ROUGE 중간 (단어 겹침)

  → ROUGE만으로는 candidate2 같은 좋은 패러프레이즈를 제대로 평가 못함

6. LLM-as-judge 개요

LLM을 평가자(Judge)로 활용하여 텍스트 품질을 평가합니다.

1
2
3
4
5
6
7
8
9
10
11
[LLM-as-judge 유형]

1. Pointwise  : 질문 + 답변 1개 → 점수(1~5)
   용도: 단일 답변의 절대적 품질 평가

2. Pairwise   : 질문 + 답변 A + 답변 B → A / B / Tie 중 선택
   용도: 두 시스템의 상대적 비교
   복잡도: N*(N-1)/2 쌍 (N=5 → 10쌍, N=500 → 125,000쌍)

3. Reference-based : 질문 + 정답 + 학생 답변 → 점수
   용도: 정답이 있는 경우의 채점

LLM-as-judge 장단점

1
2
3
4
5
6
7
8
9
10
[장점]
  - 의미적 이해: 단어가 달라도 의미가 같으면 높은 점수
  - 유연함: 다양한 평가 기준 적용 가능
  - 복잡한 품질 평가: 창의성, 논리성 등 ROUGE로 못 하는 평가 가능

[단점]
  - 비용: 평가마다 LLM API 호출
  - 기준 모호: 같은 답변도 실행마다 다른 점수가 나올 수 있음
  - 재현성: 동일 입력에 대해 결과가 달라질 수 있음
  - Position bias: pairwise에서 A를 먼저 보여주면 A에게 유리한 경향

7. basic_judge (비구조화 출력)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def basic_judge(question, answer, scale='1~5'):
    """기본 LLM 평가: 텍스트 형태로 점수 반환"""
    prompt = f"""다음 답변을 {scale}스케일로 평가하세요.

    질문 : {question}
    답변 : {answer}

    평가 기준
    - 정확성 : 사실적으로 올바른가?
    - 완전성 : 질문에 충분히 답했는가?
    - 명확성 : 이해하기 쉽게 설명했는가?

    반드시 아래 형식으로 답하세요:
    점수 : [숫자]
    이유 : [한 줄 설명]"""

    return llm.invoke(prompt).content
1
2
3
4
5
6
7
8
9
10
11
사용 예시:
  question = "파이썬의 장점은 무엇인가요?"

  답변A: "파이썬은 간결한 문법으로 초보자도 쉽게 배울 수 있으며..."
  → 점수: 5 / 이유: 핵심을 명확하게 잘 설명했습니다.

  답변B: "파이썬 좋아요."
  → 점수: 1 / 이유: 구체적인 내용이 전혀 없습니다.

  답변C: "자바스크립트는 웹 개발에 주로 사용됩니다."
  → 점수: 1 / 이유: 질문과 무관한 답변입니다.

8. basic_judge2 (JSON 구조화 출력)

파싱 가능한 JSON 형태로 점수를 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def basic_judge2(question, answer, scale='1~5'):
    prompt = f"""다음 답변을 {scale}스케일로 평가하세요. 답변은 반드시 JSON으로 반환하세요

    질문 : {question}
    답변 : {answer}

    평가 기준
    - 정확성 : 사실적으로 올바른가?
    - 완전성 : 질문에 충분히 답했는가?
    - 명확성 : 이해하기 쉽게 설명했는가?

    반드시 아래 형식으로 답하세요:
    {{"score": 1~5점수, "confidence": "high/mid/low"}}"""

    result = llm.invoke(prompt).content
    cleaned = result.strip()

    if cleaned.startswith("```"):
        cleaned = cleaned.split("```")[1]
        if cleaned.startswith("json"):
            cleaned = cleaned[4:]

    return json.loads(cleaned)
1
2
3
4
5
비구조화 vs 구조화 비교:
  basic_judge  → 텍스트 파싱 필요, 형식 불안정
  basic_judge2 → JSON 파싱으로 직접 .get('score') 접근 가능

  → 프로그램에서 점수를 활용할 때는 반드시 JSON 구조화 사용

9. Pointwise Judge with RUBRIC

루브릭(채점 기준표)을 제공하여 더 일관된 점수를 얻습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
RUBRIC = {
    5: "정확하고 완전하며, 예시와 설명이 풍부하다",
    4: "정확하고 핵심을 다루지만, 일부 세부사항이 부족하다",
    3: "대체로 정확하지만, 중요한 내용 일부가 누락되었다",
    2: "부분적으로만 정확하거나, 핵심을 빗나갔다",
    1: "부정확하거나 질문과 무관하다",
}

def pointwise_judge(question, answer, rubric=RUBRIC):
    rubric_text = '\n'.join(
        f' {k} 점: {v}' for k, v in sorted(rubric.items(), reverse=True)
    )

    prompt = f"""다음 답변을 아래 루브릭에 따라 평가해주세요.

    질문 : {question}
    답변 : {answer}

    루브릭 :
    {rubric_text}

    반드시 JSON으로 답하세요:
    {{"score": 1-5, "reasoning": "이유"}}"""

    result = llm.invoke(prompt).content
    result = result.replace('```json', '').replace('```', '')
    return json.loads(result)
1
2
3
4
5
6
7
8
9
10
루브릭 사용 효과:
  루브릭 없음: LLM이 기준을 자의적으로 설정 → 일관성 낮음
  루브릭 있음: 명확한 기준 제공 → 일관성 ↑, 재현성 ↑

사용 예시:
  question = "REST API와 GraphQL의 차이점을 설명해주세요"

  답변A (상세): REST는 리소스 단위 URL에 HTTP 메서드를 사용하고... → 점수: 5
  답변B (짧음): REST는 URL을 사용하고 GraphQL은 쿼리를 사용합니다. → 점수: 3
  답변C (부적절): 둘 다 API입니다. → 점수: 2

10. Pairwise Judge + Position Bias 처리

기본 Pairwise Judge

1
2
3
4
5
6
7
8
9
10
11
12
def pairwise_judge(question, answer_a, answer_b):
    prompt = f"""두 답변을 비교하여 더 나은 쪽을 선택하세요

    질문 : {question}
    답변 A : {answer_a}
    답변 B : {answer_b}

    반드시 JSON으로 답하세요:
    {{"winner": "A 또는 B 또는 Tie", "reasoning": "선택 이유"}}"""

    result = llm.invoke(prompt).content
    return json.loads(result.replace('```json', '').replace('```', ''))

Position Bias 문제와 해결

1
2
3
4
5
6
7
[Position Bias (위치 편향)]
  LLM은 "Lost-in-the-middle" 현상이 있어 먼저 보여준 답변에 유리한 경향이 있음

  (A, B) 순서: A 승리
  (B, A) 순서: B 승리  ← 실제로는 같은데 순서가 바뀌어서 결과가 달라짐

  → 순서를 바꿔서 두 번 비교하고 결과가 일치하는 경우만 신뢰
1
2
3
4
5
6
7
8
9
10
11
12
def pairwise_judge_with_swap(question, answer_a, answer_b):
    """A→B, B→A 순서로 두 번 비교하여 일관된 결과만 채택"""
    result1 = pairwise_judge(question, answer_a, answer_b)  # (A, B) 순서
    result2 = pairwise_judge(question, answer_b, answer_a)  # (B, A) 순서

    # result2에서 A가 이겼다면 실제 winner는 B (순서가 뒤집혔으므로)
    swapped_winner = {'A': 'B', 'B': 'A', 'Tie': 'Tie'}.get(result2['winner'])

    if result1['winner'] == swapped_winner:
        return {'final': result1['winner'], 'status': '일치'}
    else:
        return {'final': 'Tie', 'status': '불일치'}   # 결과가 다르면 Tie 처리

Position Bias 측정

1
2
3
4
5
6
7
8
9
10
11
12
13
def detect_position_bias(question, answer_a, answer_b, n_trials=3):
    """n번 반복 비교해서 불일치율(bias_rate) 계산"""
    consistent   = 0
    inconsistent = 0

    for _ in range(n_trials):
        result = pairwise_judge_with_swap(question, answer_a, answer_b)
        if result['status'] == '일치':
            consistent   += 1
        else:
            inconsistent += 1

    return inconsistent / (consistent + inconsistent)  # bias_rate
1
2
3
4
bias_rate 해석:
  0.0  → 항상 일관된 결과 (Position Bias 없음)
  0.5  → 절반은 일치, 절반은 불일치
  1.0  → 항상 순서에 따라 결과가 달라짐 (심각한 Bias)

11. Reference-based Judge

정답(reference)이 있을 때 학생 답변을 채점합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def reference_based_judge(question, reference, answer):
    prompt = f"""당신은 AI 교육 전문가입니다.
    정답을 기준으로 학생의 답변을 평가하세요.

    질문 : {question}
    정답 : {reference}
    학생 답변 : {answer}

    평가 기준:
    - 정답의 핵심 포인트를 얼마나 포함하는가
    - 사실적으로 틀린 내용이 있는가
    - 정답에 없는 올바른 추가 정보가 있는가

    JSON으로 답하세요:
    {{
        "score": 1-5,
        "covered_points": ["포함된 핵심 포인트들"],
        "missing_points":  ["누락된 포인트들"],
        "errors":          ["틀린 내용"]
    }}"""

    result = llm.invoke(prompt).content
    cleaned = result.replace('```json', '').replace('```', '')
    return json.loads(cleaned)
1
2
3
4
5
6
7
8
9
10
사용 예시:
  question  = "TCP와 UDP의 차이점은?"
  reference = "TCP는 연결지향적이고 신뢰성 있는 전송을 보장하며, UDP는 비연결지향적이고 빠르지만 신뢰성을 보장하지 않습니다."
  answer    = "TCP는 연결을 먼저 수립하고 데이터 전송의 순서와 무결성을 보장합니다. 반면 UDP는 연결 없이 바로 전송하여 빠르지만 패킷 손실이 가능합니다."

  결과:
    score: 5
    covered_points: ["연결지향 vs 비연결지향", "신뢰성", "속도"]
    missing_points: []
    errors: []

12. 자주 나오는 실수 / 주의사항

  • 대응표본 t-test 조건: ttest_rel은 두 배열의 길이가 같아야 함. 쿼리 수가 다르면 오류
  • p-value 오해: p-value < 0.05 → “통계적으로 유의한 차이” 이지 “실용적으로 중요한 차이”가 아님 → Cohen’s d로 효과 크기 함께 확인
  • 샘플 수 부족: 쿼리 3개로는 검정력(statistical power)이 너무 낮아 significant=False가 자주 나옴 → 최소 30개 이상 쿼리 권장
  • Pairwise O(N²) 비용: 답변 5개 → 10쌍, 답변 500개 → 125,000쌍 → 실용적으로 비용 폭발. 토너먼트 방식 또는 샘플링 필요
  • JSON 파싱 실패: LLM이 '''json 블록에 싱글쿼터를 써서 json.loads 실패 가능 → 파싱 전에 클리닝 로직 작성 필수
  • Rubric 일관성: 루브릭이 있어도 경계 케이스(3점 vs 4점)에서 LLM이 흔들릴 수 있음 → 여러 번 실행하고 평균 사용 권장

[보충] 평가 기법 비교

기법정답 필요속도의미 이해특징
ROUGE-F1필요빠름없음n-gram 겹침, 요약 평가에 적합
BLEU필요빠름없음n-gram precision, 번역 평가에 적합
LLM Pointwise불필요보통있음단일 답변 절대 평가
LLM Pointwise + Rubric불필요보통있음일관성 향상
LLM Pairwise불필요느림있음상대 비교, O(N²) 비용
Reference-based필요보통있음정답 있을 때 가장 정확

전체 평가 파이프라인

1
2
3
4
5
6
7
8
9
10
11
12
13
생성 시스템 평가
    ↓
[기초 평가]
  ROUGE / BLEU → 빠른 참고용 (의미 한계 인지)
    ↓
[LLM 평가]
  ├─ 정답 있음   → Reference-based Judge
  ├─ 비교 필요   → Pairwise Judge (+ Swap으로 Bias 제거)
  └─ 단일 평가   → Pointwise Judge with Rubric
    ↓
[통계 검증]
  A/B test (t-test) + Cohen's d
  → p-value < 0.05 AND d > 0.5 → 신뢰할 수 있는 성능 향상

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그