Skip to content

Split View: 챗봇 평가 체계 구축 가이드: LLM-as-Judge·RAGAS·자동화 테스트 파이프라인

✨ Learn with Quiz
|

챗봇 평가 체계 구축 가이드: LLM-as-Judge·RAGAS·자동화 테스트 파이프라인

Chatbot Evaluation Guide

들어가며

"프롬프트를 바꿨더니 답변 품질이 좋아진 것 같은데, 정말 그런 건가요?" LLM 기반 챗봇을 운영하다 보면 이런 질문에 객관적으로 답하기가 어렵다. 사람이 일일이 답변을 확인하는 것은 확장 가능하지 않고, 단순한 키워드 매칭은 LLM 출력의 다양성을 반영하지 못한다.

2025년 이후 LLM 평가 생태계는 급격히 성숙했다. RAGAS가 RAG 파이프라인 전용 메트릭을 표준화했고, DeepEval이 pytest 스타일의 LLM 테스트를 대중화했으며, LangSmith가 트레이싱과 평가를 하나의 플랫폼에서 통합했다. 가장 핵심적인 혁신은 LLM-as-Judge 패턴으로, 강력한 LLM을 활용하여 다른 LLM의 출력을 자동으로 평가하는 방식이 인간 평가자 간 일치율(81%)을 능가하는 85%의 일치율을 달성했다는 연구 결과가 나왔다.

이 글에서는 챗봇 평가 체계를 처음부터 구축하는 전체 과정을 다룬다. 평가 메트릭 설계부터 RAGAS 프레임워크 활용, LLM-as-Judge 구현, 골든 데이터셋 구축, CI/CD 파이프라인 통합, A/B 테스트, 그리고 실전에서 마주치는 평가 편향 문제까지 포괄적으로 살펴본다.

챗봇 평가의 도전 과제

LLM 기반 챗봇 평가가 전통적인 소프트웨어 테스트와 근본적으로 다른 이유는 비결정론적 출력 때문이다. 같은 입력에 대해 매번 다른 답변이 생성될 수 있으며, "정답"이라는 개념 자체가 모호하다.

왜 전통적 테스트만으로는 부족한가

  • 출력 다양성: 동일한 질문에 대해 의미적으로 동일하지만 표현이 다른 수십 가지 정답이 존재한다
  • 맥락 의존성: 멀티턴 대화에서 이전 맥락에 따라 적절한 답변이 달라진다
  • 주관적 품질: "좋은 답변"의 기준이 정확성, 유용성, 톤, 간결성 등 다차원적이다
  • 할루시네이션 탐지: 자연스럽게 읽히지만 사실과 다른 내용을 자동으로 식별해야 한다

평가 피라미드: 3계층 전략

효과적인 챗봇 평가는 다음 세 계층을 조합해야 한다.

  1. 오프라인 자동 평가 (매 배포 시): 골든 데이터셋 기반 회귀 테스트, RAGAS 메트릭
  2. LLM-as-Judge 심층 평가 (주간/스프린트별): 복잡한 시나리오에 대한 세밀한 품질 판정
  3. 온라인 평가 (상시): 사용자 피드백, A/B 테스트, 프로덕션 모니터링

평가 메트릭 체계

챗봇 평가를 위한 메트릭은 크게 네 가지 차원으로 분류된다.

정확성 (Correctness)

생성된 답변이 사실적으로 올바른지 평가한다. 골든 데이터셋의 참조 답변과 비교하여 사실적 정확도를 측정하며, RAGAS의 Factual Correctness 메트릭이나 Semantic Similarity를 활용할 수 있다.

관련성 (Relevancy)

답변이 사용자 질문에 적절하게 대응하는지 측정한다. 질문과 무관한 정보를 포함하거나, 핵심 요점을 놓치는 경우를 탐지한다. RAGAS의 Answer Relevancy 메트릭이 이 차원을 다룬다.

충실도 (Faithfulness)

RAG 시스템에서 특히 중요한 메트릭이다. 생성된 답변이 검색된 컨텍스트에 근거하는지 확인하며, 컨텍스트에 없는 내용을 지어내는 할루시네이션을 탐지한다. RAGAS의 핵심 메트릭 중 하나다.

유해성 (Harmfulness)

답변에 유해하거나 편향된 내용, 개인정보, 부적절한 표현이 포함되지 않았는지 확인한다. 안전성 평가는 가드레일과 연동하여 운영한다.

RAGAS 프레임워크 심층 분석

RAGAS(Retrieval Augmented Generation Assessment)는 RAG 파이프라인을 참조 답변 없이도 평가할 수 있는 프레임워크다. LLM을 활용하여 검색과 생성 각 단계의 품질을 독립적으로 측정한다.

RAGAS 핵심 메트릭

  • Faithfulness: 답변의 각 문장이 컨텍스트에서 추론 가능한지 판정한다. 0에서 1 사이의 값으로, 1에 가까울수록 할루시네이션이 적다.
  • Answer Relevancy: 답변이 질문과 얼마나 관련 있는지 측정한다. 답변에서 역으로 질문을 생성하고 원래 질문과의 유사도를 계산한다.
  • Context Precision: 검색된 문서 중 실제로 관련 있는 문서의 비율을 측정한다. 불필요한 문서가 많이 검색되면 점수가 낮아진다.
  • Context Recall: 정답을 도출하는 데 필요한 정보가 검색 결과에 포함되어 있는지 측정한다.

RAGAS 실전 구현

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

# 평가용 데이터셋 준비
eval_data = {
    "question": [
        "회사의 연차 휴가 정책은 어떻게 되나요?",
        "재택근무 신청 절차를 알려주세요.",
        "경조사 휴가는 며칠인가요?",
    ],
    "answer": [
        "입사 1년 이상 직원은 연 15일의 연차 휴가를 사용할 수 있습니다. "
        "3년 이상 근무 시 2년마다 1일씩 가산됩니다.",
        "재택근무는 팀장 승인 후 HR 시스템에서 신청합니다. "
        "주 3일까지 가능하며, 월요일과 금요일은 사무실 출근이 필수입니다.",
        "결혼 5일, 배우자 출산 10일, 부모 사망 5일, "
        "형제자매 사망 3일의 경조사 휴가가 제공됩니다.",
    ],
    "contexts": [
        [
            "연차 휴가 정책: 입사 1년 이상 직원에게 연 15일의 유급 연차가 부여됩니다. "
            "근속 3년 이상부터는 2년마다 1일이 추가됩니다. 미사용 연차는 이월되지 않습니다."
        ],
        [
            "재택근무 가이드: 재택근무를 희망하는 직원은 팀장의 사전 승인을 받고 "
            "HR 포털에서 신청해야 합니다. 주 3일까지 재택근무가 가능하며, "
            "월요일과 금요일은 전 직원 사무실 출근일입니다."
        ],
        [
            "경조사 휴가 규정: 본인 결혼 5일, 배우자 출산 10일, "
            "부모 사망 5일, 조부모 사망 3일, 형제자매 사망 3일."
        ],
    ],
    "ground_truth": [
        "1년 이상 근무 시 연 15일, 3년 이상은 2년마다 1일 추가",
        "팀장 승인 후 HR 시스템 신청, 주 3일까지, 월/금 출근 필수",
        "결혼 5일, 배우자 출산 10일, 부모 사망 5일, 형제자매 사망 3일",
    ],
}

dataset = Dataset.from_dict(eval_data)

# RAGAS 평가 실행
result = evaluate(
    dataset=dataset,
    metrics=[
        faithfulness,
        answer_relevancy,
        context_precision,
        context_recall,
    ],
)

print(result)
# 결과 예시:
# faithfulness: 0.92
# answer_relevancy: 0.88
# context_precision: 0.95
# context_recall: 0.90

RAGAS 커스텀 메트릭 확장

기본 메트릭 외에도 도메인 특화 메트릭을 추가할 수 있다. 예를 들어 고객 상담 챗봇에서는 "공감 표현 포함 여부"나 "다음 단계 안내 제공 여부" 같은 메트릭이 필요할 수 있다.

from ragas.metrics.base import MetricWithLLM
from dataclasses import dataclass, field

@dataclass
class EmpathyScore(MetricWithLLM):
    """고객 상담 답변의 공감 표현 수준을 0~1로 평가하는 커스텀 메트릭"""
    name: str = "empathy_score"
    evaluation_mode: str = "qa"

    async def _ascore(self, row, callbacks=None):
        prompt = (
            "다음 고객 질문과 상담원 답변을 보고, "
            "답변에 공감 표현이 적절히 포함되어 있는지 0에서 1 사이로 평가하세요.\n\n"
            f"질문: {row['question']}\n"
            f"답변: {row['answer']}\n\n"
            "점수만 숫자로 응답하세요."
        )
        response = await self.llm.agenerate_text(prompt)
        try:
            return float(response.generations[0][0].text.strip())
        except (ValueError, IndexError):
            return 0.0

LLM-as-Judge 패턴 구현

LLM-as-Judge는 강력한 LLM(GPT-4o, Claude 등)을 심판으로 활용하여 다른 LLM의 출력을 평가하는 패턴이다. 연구에 따르면 정교한 Judge 모델은 인간 평가자 간 일치율(81%)보다 높은 85%의 일치율을 달성할 수 있다.

두 가지 평가 방식

  1. Direct Assessment (점수 매기기): Judge가 개별 응답을 평가하여 점수를 부여한다
  2. Pairwise Comparison (쌍대 비교): Judge가 두 응답을 비교하여 더 나은 것을 선택한다

Direct Assessment 구현

import openai
import json
from typing import TypedDict

class EvalResult(TypedDict):
    score: int
    reasoning: str

def llm_as_judge_evaluate(
    question: str,
    answer: str,
    criteria: str,
    model: str = "gpt-4o",
) -> EvalResult:
    """LLM-as-Judge로 답변 품질을 1~5점으로 평가"""

    system_prompt = """당신은 AI 챗봇 답변의 품질을 평가하는 전문 심판입니다.
주어진 평가 기준에 따라 답변을 1~5점으로 평가하고, 그 이유를 설명하세요.

평가 점수 기준:
- 1점: 완전히 부적절하거나 잘못된 답변
- 2점: 부분적으로 관련 있지만 중요한 오류 포함
- 3점: 기본적으로 올바르지만 개선 여지가 있음
- 4점: 좋은 품질이며 대부분의 기대를 충족
- 5점: 탁월한 답변으로 모든 기준을 완벽히 충족

반드시 JSON 형식으로 응답하세요:
{"score": 점수, "reasoning": "평가 이유"}"""

    user_prompt = f"""평가 기준: {criteria}

사용자 질문: {question}

챗봇 답변: {answer}

위 답변을 평가 기준에 따라 평가하세요."""

    client = openai.OpenAI()
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        temperature=0.0,
        response_format={"type": "json_object"},
    )

    return json.loads(response.choices[0].message.content)


# 사용 예시
result = llm_as_judge_evaluate(
    question="Python에서 리스트와 튜플의 차이점은 무엇인가요?",
    answer="리스트는 대괄호([])로 생성하고 변경 가능(mutable)합니다. "
           "튜플은 소괄호(())로 생성하고 변경 불가능(immutable)합니다. "
           "성능 면에서 튜플이 리스트보다 약간 빠릅니다.",
    criteria="정확성, 완전성, 명확성을 기준으로 평가",
)
print(f"점수: {result['score']}/5")
print(f"이유: {result['reasoning']}")

Pairwise Comparison 구현

A/B 테스트나 모델 비교 시 유용한 쌍대 비교 방식이다.

def pairwise_compare(
    question: str,
    answer_a: str,
    answer_b: str,
    criteria: str,
    model: str = "gpt-4o",
) -> dict:
    """두 답변을 비교하여 더 나은 답변을 선택"""

    system_prompt = """당신은 AI 챗봇 답변을 비교 평가하는 전문 심판입니다.
두 답변(A와 B)을 비교하여 어떤 것이 더 나은지 판단하세요.

반드시 JSON 형식으로 응답하세요:
{"winner": "A" 또는 "B" 또는 "tie", "reasoning": "비교 평가 이유"}

중요: 답변의 순서에 영향받지 말고, 오직 내용의 품질만으로 판단하세요."""

    user_prompt = f"""평가 기준: {criteria}

사용자 질문: {question}

답변 A: {answer_a}

답변 B: {answer_b}

두 답변을 비교 평가하세요."""

    client = openai.OpenAI()
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        temperature=0.0,
        response_format={"type": "json_object"},
    )

    return json.loads(response.choices[0].message.content)

Position Bias 완화

LLM-as-Judge의 가장 큰 한계는 **위치 편향(Position Bias)**이다. Judge가 첫 번째로 제시된 답변을 선호하는 경향이 있다. 이를 완화하기 위해 답변 순서를 바꿔 두 번 평가하고 결과를 종합하는 전략이 효과적이다.

def debiased_pairwise_compare(
    question: str,
    answer_a: str,
    answer_b: str,
    criteria: str,
) -> dict:
    """위치 편향을 완화한 쌍대 비교"""

    # 첫 번째 평가: A를 먼저 제시
    result_1 = pairwise_compare(question, answer_a, answer_b, criteria)

    # 두 번째 평가: B를 먼저 제시 (순서 반전)
    result_2 = pairwise_compare(question, answer_b, answer_a, criteria)
    # result_2의 winner를 반전
    if result_2["winner"] == "A":
        result_2["winner"] = "B"
    elif result_2["winner"] == "B":
        result_2["winner"] = "A"

    # 결과 종합
    if result_1["winner"] == result_2["winner"]:
        return {
            "winner": result_1["winner"],
            "confidence": "high",
            "reasoning": f"양쪽 평가 일치: {result_1['reasoning']}",
        }
    else:
        return {
            "winner": "tie",
            "confidence": "low",
            "reasoning": (
                f"평가 불일치 - "
                f"정순: {result_1['winner']}, "
                f"역순: {result_2['winner']}"
            ),
        }

Golden Dataset 구축 전략

골든 데이터셋은 평가의 기준이 되는 전문가 검증된 질문-답변 쌍이다. 데이터셋의 품질이 곧 평가의 신뢰도를 결정한다.

구축 원칙

  1. 대표성: 실제 사용자 질문 패턴을 반영해야 한다. 프로덕션 로그에서 빈도 높은 질문 유형을 추출한다
  2. 다양성: 쉬운 질문부터 에지 케이스까지 난이도를 골고루 포함한다
  3. 규모: 최소 100개 이상, 이상적으로 500개 이상의 테스트 케이스를 확보한다
  4. 버전 관리: 골든 데이터셋을 Git으로 관리하고 변경 이력을 추적한다

합성 데이터 활용

초기 구축 시 LLM을 활용하여 합성 테스트 데이터를 생성하고, 전문가가 검수하는 방식이 효율적이다.

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import DirectoryLoader

# 문서 로드
loader = DirectoryLoader("./knowledge_base/", glob="**/*.md")
documents = loader.load()

# 테스트셋 생성기 설정
generator_llm = ChatOpenAI(model="gpt-4o")
critic_llm = ChatOpenAI(model="gpt-4o")
embeddings = OpenAIEmbeddings()

generator = TestsetGenerator.from_langchain(
    generator_llm=generator_llm,
    critic_llm=critic_llm,
    embeddings=embeddings,
)

# 다양한 난이도의 테스트셋 생성
testset = generator.generate_with_langchain_docs(
    documents=documents,
    test_size=200,
    distributions={
        simple: 0.4,       # 단순 사실 확인 질문 40%
        reasoning: 0.3,    # 추론이 필요한 질문 30%
        multi_context: 0.3, # 여러 문서 참조 필요 질문 30%
    },
)

# 데이터프레임으로 변환하여 검수용으로 내보내기
df = testset.to_pandas()
df.to_csv("golden_dataset_draft.csv", index=False)
print(f"생성된 테스트 케이스: {len(df)}개")

자동화 테스트 파이프라인 (CI/CD)

프롬프트 변경, 모델 교체, RAG 설정 수정 시 기존 성능이 유지되는지 자동으로 확인하는 파이프라인은 프로덕션 운영의 핵심이다.

DeepEval을 활용한 pytest 스타일 테스트

DeepEval은 pytest와 통합되어 LLM 테스트를 기존 테스트 워크플로우에 자연스럽게 편입할 수 있다.

# tests/test_chatbot_quality.py
import pytest
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import (
    AnswerRelevancyMetric,
    FaithfulnessMetric,
    HallucinationMetric,
    GEval,
)

# 커스텀 G-Eval 메트릭: 응대 톤 평가
tone_metric = GEval(
    name="Professional Tone",
    criteria=(
        "답변이 전문적이고 정중한 톤을 유지하는지 평가합니다. "
        "구어체나 이모지, 부적절한 표현이 없어야 합니다."
    ),
    evaluation_params=["actual_output"],
    threshold=0.7,
)

faithfulness_metric = FaithfulnessMetric(threshold=0.8)
relevancy_metric = AnswerRelevancyMetric(threshold=0.7)
hallucination_metric = HallucinationMetric(threshold=0.5)


@pytest.fixture
def chatbot_response():
    """테스트용 챗봇 응답을 생성하는 픽스처"""
    from app.chatbot import get_response
    return get_response


class TestChatbotQuality:
    """챗봇 답변 품질 회귀 테스트"""

    def test_faq_faithfulness(self, chatbot_response):
        """FAQ 답변이 검색된 컨텍스트에 충실한지 확인"""
        question = "연차 휴가는 며칠인가요?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
            retrieval_context=response["contexts"],
        )
        assert_test(test_case, [faithfulness_metric])

    def test_answer_relevancy(self, chatbot_response):
        """답변이 질문과 관련 있는지 확인"""
        question = "재택근무 신청은 어떻게 하나요?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
        )
        assert_test(test_case, [relevancy_metric])

    def test_no_hallucination(self, chatbot_response):
        """할루시네이션이 없는지 확인"""
        question = "퇴직금 계산 방법을 알려주세요"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
            context=response["contexts"],
        )
        assert_test(test_case, [hallucination_metric])

    def test_professional_tone(self, chatbot_response):
        """전문적인 톤을 유지하는지 확인"""
        question = "급여일이 언제인가요?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
        )
        assert_test(test_case, [tone_metric])

GitHub Actions CI/CD 통합

# .github/workflows/chatbot-eval.yml
name: Chatbot Evaluation Pipeline

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'app/chatbot/**'
      - 'config/rag/**'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements-eval.txt

      - name: Run RAGAS evaluation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python scripts/run_ragas_eval.py \
            --dataset golden_dataset.json \
            --output eval_results.json

      - name: Run DeepEval tests
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          deepeval test run tests/test_chatbot_quality.py \
            --verbose

      - name: Check regression thresholds
        run: |
          python scripts/check_thresholds.py \
            --results eval_results.json \
            --thresholds config/eval_thresholds.json

      - name: Post evaluation summary to PR
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(
              fs.readFileSync('eval_results.json', 'utf8')
            );
            const body = `## Chatbot Evaluation Results
            | Metric | Score | Threshold | Status |
            |--------|-------|-----------|--------|
            | Faithfulness | ${results.faithfulness} | 0.85 | ${results.faithfulness >= 0.85 ? 'PASS' : 'FAIL'} |
            | Relevancy | ${results.relevancy} | 0.80 | ${results.relevancy >= 0.80 ? 'PASS' : 'FAIL'} |
            | Context Precision | ${results.context_precision} | 0.80 | ${results.context_precision >= 0.80 ? 'PASS' : 'FAIL'} |`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });

A/B 테스트와 온라인 평가

오프라인 평가만으로는 실제 사용자 경험을 완전히 예측할 수 없다. 프로덕션 환경에서의 A/B 테스트와 지속적인 모니터링이 필요하다.

온라인 평가 메트릭

  • 사용자 만족도: 썸업/썸다운 피드백 비율
  • 대화 완료율: 사용자가 원하는 정보를 얻고 대화를 종료한 비율
  • 에스컬레이션율: 챗봇에서 사람 상담원으로 전환된 비율
  • 재질문율: 같은 주제에 대해 다시 질문하는 비율 (낮을수록 좋음)

A/B 테스트 설계

프롬프트 변경이나 모델 교체 시 사용자 트래픽을 분할하여 두 버전의 성능을 비교한다. 통계적으로 유의미한 결과를 얻기 위해 최소 2주, 각 그룹 1,000건 이상의 대화를 확보하는 것이 권장된다.

프레임워크 비교표

항목RAGASDeepEvalLangSmithCustom (직접 구축)
주요 용도RAG 파이프라인 평가LLM 출력 테스트트레이싱 + 평가 통합도메인 특화 평가
핵심 메트릭Faithfulness, Relevancy, Context Precision/RecallG-Eval, Hallucination, Answer Relevancy, ToxicityLLM-as-Judge, Heuristic, Human자유 설계
pytest 통합가능 (별도 래핑)네이티브 지원SDK 활용직접 구현
트레이싱미제공Confident AI 연동네이티브 지원직접 구현
참조 답변 필요 여부선택적메트릭에 따라 다름선택적자유 설계
커스텀 메트릭LLM 기반 확장 가능G-Eval로 자유 정의커스텀 Evaluator완전 자유
학습 곡선낮음낮음중간높음
비용오픈소스 + LLM API 비용오픈소스 + 유료 플랫폼유료 (무료 티어 있음)LLM API 비용만
추천 상황RAG 성능 최적화CI/CD 품질 게이트전체 라이프사이클 관리특수 요구사항

장애 사례와 교훈

사례 1: 평가 편향으로 인한 잘못된 모델 선택

한 팀이 LLM-as-Judge로 두 모델을 비교할 때 항상 모델 A가 우세하다는 결과를 얻었다. 원인은 모델 A의 답변이 더 장황했고, Judge LLM이 **장문 편향(Verbosity Bias)**을 가지고 있었기 때문이다. 간결하지만 정확한 모델 B의 답변이 과소평가되었다.

교훈: 평가 프롬프트에 "간결성도 긍정적으로 평가할 것"을 명시하고, 답변 길이를 정규화한 별도 메트릭을 추가해야 한다.

사례 2: 골든 데이터셋 유효기간 만료

6개월 전에 만든 골든 데이터셋으로 평가했더니 모든 메트릭이 하락했다. 원인은 회사 정책이 변경되어 골든 데이터셋의 참조 답변이 더 이상 유효하지 않았기 때문이다.

교훈: 골든 데이터셋에 유효기간을 설정하고, 기반 문서가 변경될 때 관련 테스트 케이스를 자동으로 플래그 처리하는 시스템을 구축해야 한다.

사례 3: 메트릭 신뢰도 과신

RAGAS Faithfulness 점수가 0.95로 높았지만, 실제 사용자 불만이 지속되었다. 조사 결과 챗봇이 컨텍스트에 충실하게 답변하기는 했지만, 사용자가 실제로 원하는 정보와는 동떨어진 컨텍스트가 검색되고 있었다. Faithfulness는 높지만 Context Precision이 낮은 상태였다.

교훈: 단일 메트릭에 의존하지 말고 여러 메트릭을 종합적으로 모니터링해야 한다. 특히 검색 품질과 생성 품질을 분리하여 평가해야 한다.

프로덕션 체크리스트

챗봇 평가 체계를 구축할 때 다음 항목을 점검하자.

기반 구축

  • 최소 100개 이상의 골든 데이터셋을 확보했는가
  • 골든 데이터셋이 실제 사용자 질문 패턴을 반영하는가
  • 평가 메트릭이 비즈니스 목표와 연결되어 있는가

자동화 파이프라인

  • 프롬프트 변경 시 자동으로 회귀 테스트가 실행되는가
  • 평가 결과가 PR 코멘트로 자동 게시되는가
  • 메트릭 임계값 미달 시 배포가 차단되는가

LLM-as-Judge 운영

  • Judge 프롬프트에 위치 편향 완화 전략이 적용되어 있는가
  • 장문 편향에 대한 대응이 되어 있는가
  • Judge 모델의 평가 일관성을 주기적으로 검증하는가

온라인 모니터링

  • 사용자 피드백(썸업/썸다운)을 수집하고 있는가
  • 주요 메트릭의 시계열 대시보드가 운영되고 있는가
  • 메트릭 급격한 변화에 대한 알림이 설정되어 있는가

데이터 관리

  • 골든 데이터셋의 유효기간을 관리하고 있는가
  • 평가 결과 이력이 버전별로 저장되는가
  • 새로운 유형의 질문이 골든 데이터셋에 지속 추가되는가

마치며

챗봇 평가 체계 구축은 한 번에 완성되는 것이 아니라 지속적으로 발전시켜야 하는 시스템이다. 처음에는 소규모 골든 데이터셋과 기본 RAGAS 메트릭으로 시작하고, 점진적으로 LLM-as-Judge 평가, 자동화 파이프라인, 온라인 모니터링을 추가해 나가는 것이 현실적이다.

핵심은 "측정할 수 없으면 개선할 수 없다"는 원칙이다. 프롬프트 변경의 효과를 감으로 판단하지 말고, 객관적인 메트릭으로 검증하는 문화를 팀에 정착시키는 것이 장기적으로 가장 큰 가치를 만든다. RAGAS, DeepEval, LangSmith 같은 도구들은 이 문화를 기술적으로 뒷받침하는 인프라일 뿐이다.

Building a Chatbot Evaluation Framework: LLM-as-Judge, RAGAS, and Automated Testing Pipelines

Chatbot Evaluation Guide

Introduction

"We changed the prompt and the answers seem better, but are they really?" When operating an LLM-based chatbot, answering this question objectively is surprisingly difficult. Having humans review every response does not scale, and simple keyword matching fails to capture the diversity of LLM outputs.

Since 2025, the LLM evaluation ecosystem has matured rapidly. RAGAS standardized RAG pipeline-specific metrics, DeepEval popularized pytest-style LLM testing, and LangSmith unified tracing and evaluation on a single platform. The most impactful innovation is the LLM-as-Judge pattern, where a powerful LLM automatically evaluates outputs from other LLMs. Research has shown that sophisticated judge models can achieve 85% agreement with human judgment, which actually surpasses the 81% inter-annotator agreement among human evaluators themselves.

This guide covers the entire process of building a chatbot evaluation framework from scratch: designing evaluation metrics, leveraging the RAGAS framework, implementing LLM-as-Judge, constructing golden datasets, integrating with CI/CD pipelines, running A/B tests, and addressing evaluation bias issues encountered in production.

The Challenge of Chatbot Evaluation

LLM-based chatbot evaluation is fundamentally different from traditional software testing because of non-deterministic outputs. The same input can generate different answers every time, and the very concept of a "correct answer" is ambiguous.

Why Traditional Testing Is Not Enough

  • Output diversity: Dozens of semantically equivalent but differently worded correct answers exist for the same question
  • Context dependency: In multi-turn conversations, the appropriate answer changes based on prior context
  • Subjective quality: "Good answer" criteria are multi-dimensional, spanning accuracy, usefulness, tone, and conciseness
  • Hallucination detection: Content that reads naturally but is factually incorrect must be automatically identified

The Evaluation Pyramid: A Three-Layer Strategy

Effective chatbot evaluation requires combining three layers:

  1. Offline automated evaluation (every deployment): Golden dataset regression testing, RAGAS metrics
  2. LLM-as-Judge deep evaluation (weekly/per sprint): Fine-grained quality assessment on complex scenarios
  3. Online evaluation (continuous): User feedback, A/B testing, production monitoring

Evaluation Metrics Framework

Chatbot evaluation metrics are categorized into four dimensions.

Correctness

Evaluates whether the generated answer is factually accurate. Measures factual correctness by comparing against reference answers in the golden dataset, using RAGAS Factual Correctness or Semantic Similarity metrics.

Relevancy

Measures whether the answer appropriately addresses the user question. Detects cases where the answer includes irrelevant information or misses key points. RAGAS Answer Relevancy metric covers this dimension.

Faithfulness

A particularly important metric for RAG systems. Verifies whether the generated answer is grounded in retrieved contexts, detecting hallucinations where the model fabricates content not present in the context. This is one of the core RAGAS metrics.

Harmfulness

Checks whether the answer contains harmful, biased, or inappropriate content, personal information, or offensive language. Safety evaluation operates in conjunction with guardrails.

Deep Dive into the RAGAS Framework

RAGAS (Retrieval Augmented Generation Assessment) is a framework that can evaluate RAG pipelines even without reference answers. It uses LLMs to independently measure the quality of each stage in retrieval and generation.

Core RAGAS Metrics

  • Faithfulness: Determines whether each sentence in the answer can be inferred from the context. Values range from 0 to 1, with higher values indicating fewer hallucinations.
  • Answer Relevancy: Measures how relevant the answer is to the question. It generates questions from the answer in reverse and computes similarity with the original question.
  • Context Precision: Measures the proportion of retrieved documents that are actually relevant. The score decreases when many irrelevant documents are retrieved.
  • Context Recall: Measures whether the information needed to derive the correct answer is included in the retrieval results.

Practical RAGAS Implementation

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

# Prepare evaluation dataset
eval_data = {
    "question": [
        "What is the company's annual leave policy?",
        "How do I apply for remote work?",
        "How many days of family event leave are available?",
    ],
    "answer": [
        "Employees with over 1 year of service receive 15 days of annual leave. "
        "After 3 years of service, an additional day is added every 2 years.",
        "Remote work requires team lead approval followed by application "
        "through the HR system. Up to 3 days per week are allowed, "
        "with Monday and Friday being mandatory in-office days.",
        "Family event leave includes 5 days for marriage, "
        "10 days for spouse's childbirth, 5 days for parent's death, "
        "and 3 days for sibling's death.",
    ],
    "contexts": [
        [
            "Annual Leave Policy: Employees with over 1 year of service "
            "receive 15 paid annual leave days. After 3 years of service, "
            "1 additional day is added every 2 years. "
            "Unused leave does not carry over."
        ],
        [
            "Remote Work Guide: Employees wishing to work remotely must "
            "obtain prior approval from their team lead and apply through "
            "the HR portal. Up to 3 remote work days per week are allowed. "
            "Monday and Friday are mandatory in-office days for all employees."
        ],
        [
            "Family Event Leave: Marriage 5 days, spouse childbirth 10 days, "
            "parent death 5 days, grandparent death 3 days, "
            "sibling death 3 days."
        ],
    ],
    "ground_truth": [
        "15 days after 1 year, plus 1 additional day every 2 years after 3 years",
        "Team lead approval then HR system application, up to 3 days, Mon/Fri in-office required",
        "Marriage 5 days, spouse childbirth 10 days, parent death 5 days, sibling death 3 days",
    ],
}

dataset = Dataset.from_dict(eval_data)

# Run RAGAS evaluation
result = evaluate(
    dataset=dataset,
    metrics=[
        faithfulness,
        answer_relevancy,
        context_precision,
        context_recall,
    ],
)

print(result)
# Example results:
# faithfulness: 0.92
# answer_relevancy: 0.88
# context_precision: 0.95
# context_recall: 0.90

Extending RAGAS with Custom Metrics

Beyond the built-in metrics, you can add domain-specific metrics. For example, a customer support chatbot might need metrics like "includes empathetic expression" or "provides next-step guidance."

from ragas.metrics.base import MetricWithLLM
from dataclasses import dataclass, field

@dataclass
class EmpathyScore(MetricWithLLM):
    """Custom metric that evaluates empathy level in support responses (0-1)"""
    name: str = "empathy_score"
    evaluation_mode: str = "qa"

    async def _ascore(self, row, callbacks=None):
        prompt = (
            "Evaluate the following customer support response for "
            "appropriate empathetic expression on a scale of 0 to 1.\n\n"
            f"Question: {row['question']}\n"
            f"Answer: {row['answer']}\n\n"
            "Respond with only the numeric score."
        )
        response = await self.llm.agenerate_text(prompt)
        try:
            return float(response.generations[0][0].text.strip())
        except (ValueError, IndexError):
            return 0.0

Implementing the LLM-as-Judge Pattern

LLM-as-Judge uses a powerful LLM (such as GPT-4o or Claude) as a judge to evaluate the outputs of other LLMs. Research indicates that sophisticated judge models can achieve 85% agreement with human judgment, exceeding the 81% inter-annotator agreement among humans.

Two Evaluation Approaches

  1. Direct Assessment (Pointwise Scoring): The judge evaluates individual responses and assigns scores
  2. Pairwise Comparison: The judge compares two responses and selects the better one

Direct Assessment Implementation

import openai
import json
from typing import TypedDict

class EvalResult(TypedDict):
    score: int
    reasoning: str

def llm_as_judge_evaluate(
    question: str,
    answer: str,
    criteria: str,
    model: str = "gpt-4o",
) -> EvalResult:
    """Evaluate answer quality on a 1-5 scale using LLM-as-Judge"""

    system_prompt = """You are an expert judge evaluating AI chatbot response quality.
Rate the response on a 1-5 scale according to the given criteria and explain your reasoning.

Scoring rubric:
- 1: Completely inappropriate or incorrect response
- 2: Partially relevant but contains significant errors
- 3: Basically correct but has room for improvement
- 4: Good quality, meets most expectations
- 5: Excellent response, perfectly meets all criteria

You must respond in JSON format:
{"score": number, "reasoning": "evaluation reasoning"}"""

    user_prompt = f"""Evaluation criteria: {criteria}

User question: {question}

Chatbot response: {answer}

Evaluate the above response according to the criteria."""

    client = openai.OpenAI()
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        temperature=0.0,
        response_format={"type": "json_object"},
    )

    return json.loads(response.choices[0].message.content)


# Usage example
result = llm_as_judge_evaluate(
    question="What is the difference between a list and a tuple in Python?",
    answer="Lists are created with square brackets ([]) and are mutable. "
           "Tuples are created with parentheses (()) and are immutable. "
           "In terms of performance, tuples are slightly faster than lists.",
    criteria="Evaluate based on accuracy, completeness, and clarity",
)
print(f"Score: {result['score']}/5")
print(f"Reasoning: {result['reasoning']}")

Pairwise Comparison Implementation

Pairwise comparison is useful for A/B testing or model comparisons.

def pairwise_compare(
    question: str,
    answer_a: str,
    answer_b: str,
    criteria: str,
    model: str = "gpt-4o",
) -> dict:
    """Compare two answers and select the better one"""

    system_prompt = """You are an expert judge comparing AI chatbot responses.
Compare responses A and B and determine which is better.

You must respond in JSON format:
{"winner": "A" or "B" or "tie", "reasoning": "comparison reasoning"}

Important: Do not be influenced by the order of responses.
Judge solely on content quality."""

    user_prompt = f"""Evaluation criteria: {criteria}

User question: {question}

Response A: {answer_a}

Response B: {answer_b}

Compare and evaluate both responses."""

    client = openai.OpenAI()
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        temperature=0.0,
        response_format={"type": "json_object"},
    )

    return json.loads(response.choices[0].message.content)

Mitigating Position Bias

The biggest limitation of LLM-as-Judge is position bias: judges tend to favor whichever response is presented first. An effective mitigation strategy is to evaluate twice with swapped response order and aggregate the results.

def debiased_pairwise_compare(
    question: str,
    answer_a: str,
    answer_b: str,
    criteria: str,
) -> dict:
    """Pairwise comparison with position bias mitigation"""

    # First evaluation: A presented first
    result_1 = pairwise_compare(question, answer_a, answer_b, criteria)

    # Second evaluation: B presented first (order reversed)
    result_2 = pairwise_compare(question, answer_b, answer_a, criteria)
    # Invert result_2's winner
    if result_2["winner"] == "A":
        result_2["winner"] = "B"
    elif result_2["winner"] == "B":
        result_2["winner"] = "A"

    # Aggregate results
    if result_1["winner"] == result_2["winner"]:
        return {
            "winner": result_1["winner"],
            "confidence": "high",
            "reasoning": f"Both evaluations agree: {result_1['reasoning']}",
        }
    else:
        return {
            "winner": "tie",
            "confidence": "low",
            "reasoning": (
                f"Evaluations disagree - "
                f"Forward: {result_1['winner']}, "
                f"Reversed: {result_2['winner']}"
            ),
        }

Golden Dataset Construction Strategy

A golden dataset consists of expert-verified question-answer pairs that serve as the evaluation benchmark. The quality of the dataset directly determines the reliability of your evaluations.

Construction Principles

  1. Representativeness: Must reflect actual user question patterns. Extract high-frequency question types from production logs
  2. Diversity: Include a balanced range of difficulty levels, from easy questions to edge cases
  3. Scale: Secure a minimum of 100, ideally 500+ test cases
  4. Version control: Manage the golden dataset in Git and track change history

Leveraging Synthetic Data

For initial construction, generating synthetic test data with LLMs and having experts review it is an efficient approach.

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import DirectoryLoader

# Load documents
loader = DirectoryLoader("./knowledge_base/", glob="**/*.md")
documents = loader.load()

# Configure test set generator
generator_llm = ChatOpenAI(model="gpt-4o")
critic_llm = ChatOpenAI(model="gpt-4o")
embeddings = OpenAIEmbeddings()

generator = TestsetGenerator.from_langchain(
    generator_llm=generator_llm,
    critic_llm=critic_llm,
    embeddings=embeddings,
)

# Generate test set with varied difficulty levels
testset = generator.generate_with_langchain_docs(
    documents=documents,
    test_size=200,
    distributions={
        simple: 0.4,        # Simple factual questions 40%
        reasoning: 0.3,     # Questions requiring reasoning 30%
        multi_context: 0.3,  # Questions needing multiple documents 30%
    },
)

# Export as DataFrame for review
df = testset.to_pandas()
df.to_csv("golden_dataset_draft.csv", index=False)
print(f"Generated test cases: {len(df)}")

Automated Testing Pipeline (CI/CD)

A pipeline that automatically verifies performance is maintained when prompts change, models are swapped, or RAG configurations are modified is essential for production operations.

pytest-Style Testing with DeepEval

DeepEval integrates with pytest, allowing LLM tests to fit naturally into existing test workflows.

# tests/test_chatbot_quality.py
import pytest
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import (
    AnswerRelevancyMetric,
    FaithfulnessMetric,
    HallucinationMetric,
    GEval,
)

# Custom G-Eval metric: response tone evaluation
tone_metric = GEval(
    name="Professional Tone",
    criteria=(
        "Evaluate whether the response maintains a professional "
        "and courteous tone. It should not contain colloquialisms, "
        "emojis, or inappropriate expressions."
    ),
    evaluation_params=["actual_output"],
    threshold=0.7,
)

faithfulness_metric = FaithfulnessMetric(threshold=0.8)
relevancy_metric = AnswerRelevancyMetric(threshold=0.7)
hallucination_metric = HallucinationMetric(threshold=0.5)


@pytest.fixture
def chatbot_response():
    """Fixture to generate chatbot responses for testing"""
    from app.chatbot import get_response
    return get_response


class TestChatbotQuality:
    """Chatbot response quality regression tests"""

    def test_faq_faithfulness(self, chatbot_response):
        """Verify FAQ answers are faithful to retrieved context"""
        question = "How many annual leave days do I get?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
            retrieval_context=response["contexts"],
        )
        assert_test(test_case, [faithfulness_metric])

    def test_answer_relevancy(self, chatbot_response):
        """Verify answers are relevant to the question"""
        question = "How do I apply for remote work?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
        )
        assert_test(test_case, [relevancy_metric])

    def test_no_hallucination(self, chatbot_response):
        """Verify no hallucinations are present"""
        question = "How is severance pay calculated?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
            context=response["contexts"],
        )
        assert_test(test_case, [hallucination_metric])

    def test_professional_tone(self, chatbot_response):
        """Verify professional tone is maintained"""
        question = "When is payday?"
        response = chatbot_response(question)

        test_case = LLMTestCase(
            input=question,
            actual_output=response["answer"],
        )
        assert_test(test_case, [tone_metric])

GitHub Actions CI/CD Integration

# .github/workflows/chatbot-eval.yml
name: Chatbot Evaluation Pipeline

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'app/chatbot/**'
      - 'config/rag/**'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements-eval.txt

      - name: Run RAGAS evaluation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python scripts/run_ragas_eval.py \
            --dataset golden_dataset.json \
            --output eval_results.json

      - name: Run DeepEval tests
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          deepeval test run tests/test_chatbot_quality.py \
            --verbose

      - name: Check regression thresholds
        run: |
          python scripts/check_thresholds.py \
            --results eval_results.json \
            --thresholds config/eval_thresholds.json

      - name: Post evaluation summary to PR
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(
              fs.readFileSync('eval_results.json', 'utf8')
            );
            const body = `## Chatbot Evaluation Results
            | Metric | Score | Threshold | Status |
            |--------|-------|-----------|--------|
            | Faithfulness | ${results.faithfulness} | 0.85 | ${results.faithfulness >= 0.85 ? 'PASS' : 'FAIL'} |
            | Relevancy | ${results.relevancy} | 0.80 | ${results.relevancy >= 0.80 ? 'PASS' : 'FAIL'} |
            | Context Precision | ${results.context_precision} | 0.80 | ${results.context_precision >= 0.80 ? 'PASS' : 'FAIL'} |`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });

A/B Testing and Online Evaluation

Offline evaluation alone cannot fully predict real user experience. A/B testing and continuous monitoring in production environments are necessary.

Online Evaluation Metrics

  • User satisfaction: Thumbs up/down feedback ratio
  • Conversation completion rate: Percentage of conversations where users obtained desired information
  • Escalation rate: Percentage of conversations transferred from chatbot to human agents
  • Re-ask rate: Percentage of repeated questions on the same topic (lower is better)

A/B Test Design

When changing prompts or swapping models, split user traffic to compare performance of two versions. To achieve statistically significant results, a minimum of 2 weeks and 1,000+ conversations per group is recommended.

Framework Comparison

CategoryRAGASDeepEvalLangSmithCustom (Self-built)
Primary useRAG pipeline evaluationLLM output testingTracing + evaluationDomain-specific evaluation
Core metricsFaithfulness, Relevancy, Context Precision/RecallG-Eval, Hallucination, Answer Relevancy, ToxicityLLM-as-Judge, Heuristic, HumanFreely designed
pytest integrationPossible (wrapper needed)Native supportVia SDKManual implementation
TracingNot providedConfident AI integrationNative supportManual implementation
Reference answer requiredOptionalDepends on metricOptionalFreely designed
Custom metricsLLM-based extensionFree definition via G-EvalCustom EvaluatorFully flexible
Learning curveLowLowMediumHigh
CostOpen source + LLM API costsOpen source + paid platformPaid (free tier available)LLM API costs only
Recommended forRAG performance optimizationCI/CD quality gatesFull lifecycle managementSpecial requirements

Failure Cases and Lessons Learned

Case 1: Wrong Model Selection Due to Evaluation Bias

One team found that Model A consistently outperformed Model B in LLM-as-Judge comparisons. The cause was that Model A produced more verbose answers, and the judge LLM had a verbosity bias. Model B's concise but accurate answers were systematically undervalued.

Lesson: Explicitly state "conciseness should also be positively evaluated" in the evaluation prompt, and add a separate metric that normalizes for answer length.

Case 2: Expired Golden Dataset

When evaluating with a golden dataset created six months earlier, all metrics showed decline. The cause was that company policies had changed, making the reference answers in the golden dataset no longer valid.

Lesson: Set expiration dates on golden datasets and build a system that automatically flags related test cases when underlying documents change.

Case 3: Over-Trusting Metric Reliability

A RAGAS Faithfulness score of 0.95 was high, yet user complaints persisted. Investigation revealed that the chatbot was faithfully answering based on context, but the retrieved context itself was irrelevant to what users actually wanted. Faithfulness was high, but Context Precision was low.

Lesson: Do not rely on a single metric. Monitor multiple metrics comprehensively, and in particular, evaluate retrieval quality and generation quality separately.

Production Checklist

Review these items when building a chatbot evaluation framework.

Foundation

  • Have you secured at least 100 golden dataset entries
  • Does the golden dataset reflect actual user question patterns
  • Are evaluation metrics aligned with business objectives

Automation Pipeline

  • Do regression tests run automatically on prompt changes
  • Are evaluation results automatically posted as PR comments
  • Is deployment blocked when metric thresholds are not met

LLM-as-Judge Operations

  • Is position bias mitigation applied to judge prompts
  • Is verbosity bias addressed
  • Is judge model evaluation consistency periodically verified

Online Monitoring

  • Are you collecting user feedback (thumbs up/down)
  • Is a time-series dashboard for key metrics operational
  • Are alerts configured for sudden metric changes

Data Management

  • Are golden dataset expiration dates managed
  • Are evaluation results stored by version
  • Are new question types continuously added to the golden dataset

Conclusion

Building a chatbot evaluation framework is not a one-time project but a system that must be continuously evolved. Start with a small golden dataset and basic RAGAS metrics, then gradually add LLM-as-Judge evaluation, automated pipelines, and online monitoring.

The key principle is "you cannot improve what you cannot measure." Rather than judging prompt changes by intuition, establishing a culture of validating through objective metrics is what creates the most long-term value. Tools like RAGAS, DeepEval, and LangSmith are merely infrastructure that technically supports this culture.