Skip to content

필사 모드: RAGAS 완전 가이드: RAG 시스템을 어떻게 정량적으로 평가하는가

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

RAG 평가가 어려운 이유

"잘 작동하는 것 같은데, 실제로 얼마나 좋은지 모르겠어요."

RAG를 운영하는 팀에서 가장 많이 듣는 말이다. 개발하다 보면 직관적으로 "좋아진 것 같다"는 느낌은 있는데, 정확히 얼마나 좋아졌는지를 수치로 보여주기가 어렵다.

전통적인 소프트웨어처럼 테스트 케이스를 짜기도 어렵다. 정답이 하나가 아니고, 같은 질문에도 맥락에 따라 좋은 답변이 달라진다.

**RAGAS(RAG Assessment)**는 이 문제를 해결하기 위해 설계된 프레임워크다. LLM을 judge로 활용해서 RAG 시스템의 여러 측면을 0-1 사이 점수로 정량화한다.

이 글에서는 RAGAS의 4가지 핵심 지표를 이해하고, 실제 코드로 구현하고, 자동화된 평가 파이프라인을 구축하는 방법을 다룬다.

RAGAS가 측정하는 4가지 지표

RAGAS는 RAG 시스템을 두 가지 차원으로 평가한다: **검색(Retrieval)**의 품질과 **생성(Generation)**의 품질.

RAG 파이프라인 전체:

[사용자 질문]

[검색 단계] ← Context Precision, Context Recall로 평가

[생성 단계] ← Faithfulness, Answer Relevancy로 평가

[최종 답변]

지표 1: Faithfulness (충실성)

**질문**: 생성된 답변이 검색된 컨텍스트에 기반하는가?

할루시네이션 탐지 지표다. 모델이 컨텍스트에 없는 내용을 "만들어내면" Faithfulness가 낮아진다.

**계산 방법**:

1. 생성된 답변에서 개별 claim(주장)을 추출

2. 각 claim이 검색된 컨텍스트에서 지지되는지 LLM이 판단

3. Faithfulness = 지지되는 claim 수 / 전체 claim 수

예시:

질문: "제품의 배터리 수명은 얼마인가요?"

컨텍스트: "배터리는 최대 12시간 지속됩니다. 충전 시간은 2시간입니다."

답변: "배터리는 12시간이며, 방수 기능도 있습니다."

Claim 분석:

- "배터리는 12시간" → 컨텍스트에 지지됨 ✓

- "방수 기능도 있습니다" → 컨텍스트에 없음 ✗

Faithfulness = 1/2 = 0.5 (낮음! 할루시네이션 발생)

지표 2: Answer Relevancy (답변 관련성)

**질문**: 생성된 답변이 사용자의 질문에 실제로 대답하는가?

컨텍스트에 충실하지만 질문과 관련 없는 답변을 탐지한다.

**계산 방법**:

1. 생성된 답변으로부터 역으로 synthetic 질문을 여러 개 생성

2. 이 synthetic 질문들의 임베딩과 원래 질문의 임베딩 간 평균 코사인 유사도 계산

3. Answer Relevancy = 이 유사도 점수

예시:

원래 질문: "배터리 수명이 얼마인가요?"

생성된 답변: "이 제품은 고급 재질로 만들어졌으며 다양한 색상이 있습니다."

Synthetic 질문 생성:

- "이 제품은 어떤 재질로 만들어졌나요?"

- "어떤 색상이 있나요?"

원래 질문과의 유사도: 매우 낮음

Answer Relevancy ≈ 0.1 (낮음! 답변이 질문을 무시함)

지표 3: Context Precision (컨텍스트 정밀도)

**질문**: 검색된 컨텍스트 중 실제로 도움이 된 것이 얼마나 되는가?

검색의 "노이즈"를 측정한다. 5개 청크를 검색했는데 1개만 실제로 유용했다면 낮은 점수를 받는다.

**계산 방법**:

- 상위 k개 컨텍스트 중 관련있는 것의 비율 (Precision@k의 가중 합)

- Ground truth 답변과 비교해서 각 컨텍스트의 관련성 판단

높은 Context Precision = 검색이 정확해서 LLM이 관련 없는 정보에 혼란받지 않음

지표 4: Context Recall (컨텍스트 재현율)

**질문**: 답변에 필요한 정보를 검색으로 모두 가져왔는가?

빠진 정보가 있는지 측정한다. Ground truth 답변이 필요로 하는 정보를 검색이 모두 찾아왔는지 확인한다.

**계산 방법**:

- Ground truth 답변의 각 문장이 검색된 컨텍스트에 의해 지지되는지 판단

- Context Recall = 지지되는 문장 수 / 전체 Ground truth 문장 수

높은 Context Recall = 검색이 포괄적이어서 LLM이 답변에 필요한 정보를 모두 갖고 있음

실제 RAGAS 구현

from ragas import evaluate

from ragas.metrics import (

faithfulness,

answer_relevancy,

context_precision,

context_recall,

)

from datasets import Dataset

평가 데이터셋 준비

각 샘플: question + 시스템이 생성한 answer + retrieved contexts + ground_truth

eval_data = {

"question": [

"제품의 배터리 수명은 얼마인가요?",

"환불 정책은 어떻게 되나요?",

"배송 기간은 얼마나 걸리나요?"

],

"answer": [

"배터리는 최대 12시간 지속됩니다.",

"구매 후 30일 이내에 환불 신청이 가능합니다.",

"일반 배송은 3-5 영업일이 소요됩니다."

],

"contexts": [

각 질문에 대해 검색된 컨텍스트 청크 목록

[

"배터리는 최대 12시간 지속됩니다. 충전 시간은 2시간입니다.",

"제품 크기는 15cm x 10cm x 2cm입니다."

],

[

"고객은 구매 후 30일 이내에 환불을 신청할 수 있습니다.",

"환불은 영업일 기준 5-7일 내에 처리됩니다."

],

[

"일반 배송: 3-5 영업일, 빠른 배송: 1-2 영업일",

"배송비는 3만원 미만 주문 시 3,000원이 부과됩니다."

]

],

"ground_truth": [

"배터리 수명은 최대 12시간이며, 충전 시간은 2시간입니다.",

"환불은 구매 후 30일 이내 신청 가능하고 5-7 영업일 내 처리됩니다.",

"일반 배송은 3-5 영업일, 빠른 배송은 1-2 영업일이 소요됩니다."

]

}

dataset = Dataset.from_dict(eval_data)

평가 실행

results = evaluate(

dataset,

metrics=[

faithfulness,

answer_relevancy,

context_precision,

context_recall,

]

)

print(results)

출력 예시:

{

'faithfulness': 0.95,

'answer_relevancy': 0.88,

'context_precision': 0.82,

'context_recall': 0.91

}

결과를 DataFrame으로 변환해서 상세 분석

df = results.to_pandas()

print(df.to_string())

LLM Judge 설정

RAGAS는 기본적으로 OpenAI를 judge LLM으로 사용하지만, 로컬 모델이나 다른 프로바이더로 교체 가능하다:

from ragas.llms import LangchainLLMWrapper

from langchain_openai import ChatOpenAI

from langchain_openai import OpenAIEmbeddings

from ragas.embeddings import LangchainEmbeddingsWrapper

Judge LLM 설정 (비용 절감을 위해 GPT-4o-mini 사용 가능)

judge_llm = LangchainLLMWrapper(

ChatOpenAI(model="gpt-4o-mini", temperature=0)

)

임베딩 설정 (Answer Relevancy 계산에 사용)

embeddings = LangchainEmbeddingsWrapper(OpenAIEmbeddings())

results = evaluate(

dataset,

metrics=[faithfulness, answer_relevancy, context_precision, context_recall],

llm=judge_llm,

embeddings=embeddings,

)

자동화 평가 파이프라인 구축 (CI/CD for RAG)

RAG 시스템의 변경이 있을 때마다 자동으로 평가가 실행되도록 하는 파이프라인이다.

evaluate_rag.py - CI/CD에서 실행되는 평가 스크립트

from datetime import datetime

from pathlib import Path

from ragas import evaluate

from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall

from datasets import Dataset

def load_test_dataset(path: str) -> Dataset:

"""저장된 테스트 데이터셋 로드"""

with open(path, 'r', encoding='utf-8') as f:

data = json.load(f)

return Dataset.from_dict(data)

def run_rag_on_testset(testset: Dataset, rag_chain) -> Dataset:

"""테스트셋의 각 질문을 RAG로 실행해서 answer와 contexts 수집"""

answers = []

contexts_list = []

for question in testset["question"]:

RAG 실행

result = rag_chain.invoke({"query": question})

answers.append(result["result"])

contexts_list.append([

doc.page_content

for doc in result["source_documents"]

])

return testset.add_column("answer", answers).add_column("contexts", contexts_list)

def evaluate_and_gate(

dataset: Dataset,

thresholds: dict,

save_path: str = None

) -> bool:

"""

평가 실행 후 임계값 기반 통과/실패 판정.

thresholds 예시:

{

"faithfulness": 0.8,

"answer_relevancy": 0.75,

"context_precision": 0.7,

"context_recall": 0.75

}

"""

results = evaluate(

dataset,

metrics=[faithfulness, answer_relevancy, context_precision, context_recall],

)

print("\n" + "="*50)

print(f"RAG 평가 결과 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

print("="*50)

all_passed = True

for metric_name, threshold in thresholds.items():

score = results[metric_name]

passed = score >= threshold

status = "PASS" if passed else "FAIL"

print(f"{metric_name:25s}: {score:.3f} (임계값: {threshold}) [{status}]")

if not passed:

all_passed = False

결과 저장 (히스토리 추적용)

if save_path:

result_data = {

"timestamp": datetime.now().isoformat(),

"scores": {k: float(v) for k, v in results.items()},

"passed": all_passed

}

Path(save_path).parent.mkdir(parents=True, exist_ok=True)

with open(save_path, 'a') as f:

f.write(json.dumps(result_data) + "\n")

return all_passed

실행

if __name__ == "__main__":

테스트셋과 RAG 체인 로드

testset = load_test_dataset("tests/rag_testset.json")

rag_chain = load_your_rag_chain() # 실제 RAG 체인 로드

testset = run_rag_on_testset(testset, rag_chain)

thresholds = {

"faithfulness": 0.80,

"answer_relevancy": 0.75,

"context_precision": 0.70,

"context_recall": 0.75,

}

passed = evaluate_and_gate(

testset,

thresholds=thresholds,

save_path="results/rag_eval_history.jsonl"

)

sys.exit(0 if passed else 1) # CI/CD에서 실패 시 빌드 중단

GitHub Actions에서 이렇게 사용한다:

.github/workflows/rag-evaluation.yml

name: RAG Quality Gate

on:

push:

branches: [main]

pull_request:

branches: [main]

jobs:

evaluate-rag:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v3

- name: Setup Python

uses: actions/setup-python@v4

with:

python-version: '3.11'

- name: Install dependencies

run: pip install ragas langchain openai datasets

- name: Run RAG Evaluation

env:

OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

run: python evaluate_rag.py

평가 데이터셋 자동 생성

테스트셋을 수동으로 만드는 건 시간이 많이 걸린다. RAGAS의 `TestsetGenerator`로 자동화할 수 있다.

from ragas.testset.generator import TestsetGenerator

from ragas.testset.evolutions import simple, reasoning, multi_context

from langchain_community.document_loaders import DirectoryLoader

from langchain_openai import ChatOpenAI, OpenAIEmbeddings

문서 로드

loader = DirectoryLoader("./docs", glob="**/*.md")

documents = loader.load()

테스트셋 생성기 초기화

generator = TestsetGenerator.with_openai()

테스트셋 생성 (질문 유형 지정 가능)

testset = generator.generate_with_langchain_docs(

documents,

test_size=50, # 총 50개 질문

distributions={

simple: 0.5, # 단순 사실 질문 50%

reasoning: 0.25, # 추론 필요 질문 25%

multi_context: 0.25 # 여러 문서 참조 필요 25%

}

)

데이터셋을 파일로 저장

testset_df = testset.to_pandas()

testset_df.to_json("tests/rag_testset.json", orient='records', force_ascii=False)

print(f"생성된 질문 수: {len(testset_df)}")

print("\n샘플 질문:")

print(testset_df[['question', 'ground_truth']].head())

생성되는 질문 유형 예시:

- **Simple**: "반품 기간은 며칠인가요?" (단순 사실 검색)

- **Reasoning**: "A 제품과 B 제품 중 어떤 것이 더 오래 쓸 수 있나요?" (비교 추론)

- **Multi-context**: "최신 제품 라인업에서 배터리 용량이 가장 큰 제품은?" (여러 문서 필요)

RAGAS 점수 해석 가이드

| 지표 | 위험 수준 | 수용 가능 | 좋음 | 우수 |

|------|---------|---------|------|------|

| Faithfulness | 0.5 미만 | 0.5-0.7 | 0.7-0.9 | 0.9 이상 |

| Answer Relevancy | 0.6 미만 | 0.6-0.75 | 0.75-0.9 | 0.9 이상 |

| Context Precision | 0.5 미만 | 0.5-0.7 | 0.7-0.85 | 0.85 이상 |

| Context Recall | 0.6 미만 | 0.6-0.75 | 0.75-0.9 | 0.9 이상 |

점수가 낮을 때 어떻게 개선하는가

**Faithfulness 낮음** → 할루시네이션 발생

- System prompt에 "컨텍스트에 없는 내용은 답변하지 말 것" 강화

- 더 구체적인 citation 지시 추가

- LLM을 더 conservative한 모델로 교체

**Answer Relevancy 낮음** → 답변이 질문을 무시

- Prompt에서 질문을 더 명확하게 강조

- Retrieved context가 너무 많아서 질문을 잊는 경우 → 컨텍스트 수 줄이기

- Re-ranking 도입

**Context Precision 낮음** → 관련 없는 청크가 너무 많이 검색됨

- Retrieval top-k 수를 줄임 (5 → 3)

- Re-ranking 추가 (Cohere Rerank, BGE Reranker 등)

- 청킹 크기 조정

**Context Recall 낮음** → 필요한 정보를 검색에서 놓침

- Retrieval top-k 수를 늘림

- Hybrid Search 도입 (BM25 + 벡터)

- 청킹 전략 재검토 (너무 큰 청크는 관련 정보를 희석시킴)

실전 평가 루프

이 모든 것을 합쳐서 지속적인 개선 사이클을 만들자:

1. 기준선 측정 (RAGAS 실행)

2. 가장 낮은 지표 확인

3. 해당 지표 개선 (청킹/검색/프롬프트 수정)

4. 다시 측정 → 개선 확인

5. CI/CD에 Quality Gate 추가

6. 코드 변경마다 자동 평가

마무리

RAGAS는 RAG 시스템 개발을 "느낌" 기반에서 "데이터" 기반으로 전환시켜주는 도구다. 처음 도입할 때는 점수가 생각보다 낮게 나와서 당황할 수 있다. 하지만 그게 정상이다 — 현실을 직시하는 것이 첫걸음이다.

작은 테스트셋(20-50개)으로 시작하고, CI/CD에 Quality Gate를 추가하고, 매 변경마다 점수를 추적하자. 몇 주 지나면 체계적으로 개선되는 수치를 보게 된다.

이 포스트 시리즈의 4개 글이 RAG 구축의 핵심 요소를 커버했다: 접근법 선택 → 청킹 → 검색 → 평가. 이 네 가지를 제대로 하면 프로덕션 수준의 RAG 시스템을 만들 수 있다.

현재 단락 (1/283)

"잘 작동하는 것 같은데, 실제로 얼마나 좋은지 모르겠어요."

작성 글자: 0원문 글자: 7,908작성 단락: 0/283