RAGAS, 충실도, 컨텍스트 정밀도 등 RAG 시스템의 품질을 객관적으로 측정하는 평가 프레임워크와 핵심 메트릭을 다룹니다.
RAG 시스템은 검색과 생성이라는 두 단계가 결합된 복합 시스템입니다. 어느 단계에서 문제가 발생하는지 정확히 진단하지 못하면, 개선 방향을 잡을 수 없습니다. "답변이 부정확하다"는 피드백만으로는 청킹을 바꿔야 하는지, 임베딩 모델을 바꿔야 하는지, 프롬프트를 수정해야 하는지 알 수 없습니다.
체계적인 평가 프레임워크는 이 문제를 해결합니다. 검색 단계와 생성 단계를 독립적으로 측정하고, 각 단계의 성능 지표를 통해 병목 지점을 정확히 파악할 수 있습니다.
RAG 평가의 두 축:
검색 평가 (Retrieval):
"올바른 문서를 찾았는가?"
--> Context Precision, Context Recall, NDCG, MRR
생성 평가 (Generation):
"검색된 문서를 기반으로 정확한 답변을 생성했는가?"
--> Faithfulness, Answer Relevancy, Answer Correctness검색된 문서 중 실제로 관련 있는 문서의 비율입니다. 특히 상위 순위에 관련 문서가 배치되었는지를 중점적으로 평가합니다. 불필요한 문서가 많이 포함되면 LLM이 혼란스러워하고, 토큰 비용도 낭비됩니다.
검색 결과 5개:
1. 관련 문서 (O)
2. 비관련 문서 (X)
3. 관련 문서 (O)
4. 비관련 문서 (X)
5. 관련 문서 (O)
Context Precision@5 = 3/5 = 0.60답변에 필요한 모든 관련 문서가 검색 결과에 포함되었는지를 측정합니다. 정답을 구성하는 정보 조각들이 검색된 컨텍스트에 모두 존재하는지 확인합니다.
정답에 필요한 정보 3개:
A: "재택근무 주 2회 가능" --> 검색됨 (O)
B: "팀장 승인 필요" --> 검색됨 (O)
C: "전주 금요일까지 신청" --> 검색 안 됨 (X)
Context Recall = 2/3 = 0.67전통적인 정보 검색(IR) 메트릭으로, 상위 K개 결과에서의 정밀도와 재현율을 측정합니다. 2026년 기준 실무에서 권장되는 목표치는 다음과 같습니다.
상위 순위에 있는 관련 문서에 더 높은 가중치를 부여하는 메트릭입니다. 관련 문서가 존재하더라도 순위가 낮으면 점수가 낮아집니다. 검색 결과의 순위 품질을 평가하는 데 가장 적합합니다.
첫 번째 관련 문서가 몇 번째 순위에 나타나는지를 측정합니다. 사용자가 첫 번째 관련 결과를 빨리 만나는 것이 중요한 경우에 유용합니다.
생성된 답변이 검색된 컨텍스트에 얼마나 충실한지를 측정합니다. 컨텍스트에 없는 정보를 지어내는 환각 여부를 감지하는 핵심 메트릭입니다. 값의 범위는 0에서 1이며, 1에 가까울수록 컨텍스트에 충실합니다.
컨텍스트: "재택근무는 주 2회까지 가능합니다."
답변 A: "재택근무는 주 2회까지 가능합니다."
--> Faithfulness: 1.0 (컨텍스트에 있는 정보만 사용)
답변 B: "재택근무는 주 3회까지 가능하며, 금요일에는 필수입니다."
--> Faithfulness: 0.0 (컨텍스트에 없는 정보를 지어냄)생성된 답변이 사용자의 질문에 얼마나 관련 있는지를 측정합니다. 답변이 정확하더라도 질문과 동떨어진 내용이면 낮은 점수를 받습니다.
생성된 답변이 정답(Ground Truth)과 얼마나 일치하는지를 측정합니다. 이 메트릭은 사전에 준비된 정답 데이터셋이 필요합니다.
RAGAS(Retrieval Augmented Generation Assessment)는 RAG 파이프라인을 자동으로 평가하는 대표적인 오픈소스 프레임워크입니다. 사람의 주석(annotation) 없이도 LLM을 활용하여 위의 메트릭들을 자동으로 계산합니다.
# pip install ragas
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall
)
from ragas.dataset_schema import SingleTurnSample, EvaluationDataset
# 평가 데이터 준비
samples = [
SingleTurnSample(
user_input="재택근무는 주 몇 회까지 가능한가요?",
retrieved_contexts=[
"재택근무는 팀장 승인 후 주 2회까지 신청할 수 있습니다.",
"재택근무 신청은 전주 금요일까지 팀장에게 제출해야 합니다.",
],
response="재택근무는 주 2회까지 가능하며, 팀장 승인이 필요합니다. 신청은 전주 금요일까지 해야 합니다.",
reference="재택근무는 팀장 승인 후 주 2회까지 가능하며, 전주 금요일까지 신청해야 합니다."
),
SingleTurnSample(
user_input="연차 일수는 어떻게 되나요?",
retrieved_contexts=[
"연차는 입사일 기준으로 매년 15일이 부여됩니다.",
"3년 이상 근속 시 매 2년마다 1일씩 추가됩니다.",
],
response="연차는 매년 15일이 부여되며, 3년 이상 근속 시 추가 연차가 있습니다.",
reference="연차는 입사일 기준 매년 15일이며, 3년 이상 근속 시 매 2년마다 1일 추가됩니다."
),
]
dataset = EvaluationDataset(samples=samples)
# 평가 실행
results = evaluate(
dataset=dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall
]
)
print(results)
# {'faithfulness': 0.95, 'answer_relevancy': 0.92,
# 'context_precision': 0.85, 'context_recall': 0.90}수동으로 평가 데이터셋을 만드는 것은 시간이 많이 소요됩니다. RAGAS의 테스트셋 생성기를 활용하면, 기존 문서로부터 자동으로 질문-답변 쌍을 생성할 수 있습니다.
from ragas.testset import TestsetGenerator
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import DirectoryLoader
# 문서 로딩
loader = DirectoryLoader("documents/", glob="**/*.pdf")
documents = loader.load()
# 테스트셋 생성기
generator = TestsetGenerator(
llm=ChatOpenAI(model="gpt-4o"),
embedding_model=OpenAIEmbeddings(model="text-embedding-3-small")
)
# 다양한 유형의 질문 생성
testset = generator.generate_with_langchain_docs(
documents=documents,
testset_size=50
)
# 생성된 테스트셋 확인
df = testset.to_pandas()
print(df.head())
print(f"생성된 테스트 케이스: {len(df)}개")자동 생성된 테스트셋은 실제 사용자의 질문 패턴과 다를 수 있습니다. 자동 생성 데이터를 출발점으로 삼되, 실제 사용자 로그에서 추출한 질문으로 보완하는 것이 중요합니다. 프로덕션 환경에서는 사용자 질문과 피드백을 지속적으로 수집하여 평가 데이터셋을 확장해야 합니다.
생성 단계를 제외하고 검색 품질만 독립적으로 평가하는 것이 디버깅에 효과적입니다.
import numpy as np
def evaluate_retrieval(retriever, test_queries, ground_truth_docs, k=5):
"""검색 단계 성능 평가"""
precision_scores = []
recall_scores = []
mrr_scores = []
for query, relevant_ids in zip(test_queries, ground_truth_docs):
# 검색 수행
results = retriever.invoke(query)
retrieved_ids = [
doc.metadata.get("id") for doc in results[:k]
]
# Precision@K
hits = len(set(retrieved_ids) & set(relevant_ids))
precision = hits / k
precision_scores.append(precision)
# Recall@K
recall = hits / len(relevant_ids) if relevant_ids else 0
recall_scores.append(recall)
# MRR
mrr = 0
for rank, doc_id in enumerate(retrieved_ids, start=1):
if doc_id in relevant_ids:
mrr = 1.0 / rank
break
mrr_scores.append(mrr)
return {
"precision_at_k": np.mean(precision_scores),
"recall_at_k": np.mean(recall_scores),
"mrr": np.mean(mrr_scores),
"num_queries": len(test_queries)
}
# 사용
metrics = evaluate_retrieval(retriever, test_queries, ground_truth, k=5)
print(f"Precision@5: {metrics['precision_at_k']:.3f}")
print(f"Recall@5: {metrics['recall_at_k']:.3f}")
print(f"MRR: {metrics['mrr']:.3f}")사람의 평가를 LLM으로 대체하는 접근법입니다. 정답 데이터가 없어도 답변의 품질을 판단할 수 있으며, 대규모 평가에 적합합니다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
import json
def llm_judge_evaluate(question, context, answer):
"""LLM-as-Judge 평가"""
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """RAG 시스템의 답변 품질을 평가하세요.
다음 기준으로 각각 1~5 점수를 매기세요:
1. 충실도: 답변이 컨텍스트에 있는 정보만 사용했는가
2. 관련성: 답변이 질문에 적절히 대응하는가
3. 완전성: 컨텍스트에 있는 관련 정보를 빠짐없이 활용했는가
4. 간결성: 불필요한 반복이나 장황함 없이 핵심을 전달하는가
JSON 형식으로만 응답하세요."""),
("human", """질문: {question}
컨텍스트:
{context}
답변: {answer}""")
])
response = (prompt | llm).invoke({
"question": question,
"context": context,
"answer": answer
})
scores = json.loads(response.content)
return scores
# 사용 예시
scores = llm_judge_evaluate(
question="재택근무 신청 방법은?",
context="재택근무는 팀장 승인 후 주 2회까지 가능합니다...",
answer="재택근무는 주 2회까지 가능하며 팀장 승인이 필요합니다."
)
# {"충실도": 5, "관련성": 5, "완전성": 4, "간결성": 5}LLM-as-Judge는 편리하지만 한계가 있습니다. LLM은 긴 답변에 편향되거나, 자신이 생성한 답변에 더 높은 점수를 줄 수 있습니다. 중요한 평가에서는 LLM-as-Judge와 사람 평가를 병행하는 것이 바람직합니다.
지속적인 품질 관리를 위해 평가 파이프라인을 자동화합니다.
import json
import datetime
class RAGEvaluationPipeline:
"""자동화된 RAG 평가 파이프라인"""
def __init__(self, rag_pipeline, eval_dataset_path):
self.rag = rag_pipeline
self.eval_data = self._load_dataset(eval_dataset_path)
self.history = []
def _load_dataset(self, path):
with open(path, "r") as f:
return json.load(f)
def run_evaluation(self, run_name=None):
"""전체 평가 실행"""
results = []
for item in self.eval_data:
question = item["question"]
ground_truth = item.get("answer", "")
# RAG 파이프라인 실행
rag_result = self.rag.query_with_sources(question)
# 메트릭 계산
metrics = {
"question": question,
"generated_answer": rag_result["answer"],
"ground_truth": ground_truth,
"num_sources": len(rag_result["sources"]),
}
results.append(metrics)
# 집계
summary = {
"run_name": run_name or datetime.datetime.now().isoformat(),
"num_questions": len(results),
"avg_sources": sum(
r["num_sources"] for r in results
) / len(results),
"timestamp": datetime.datetime.now().isoformat(),
"details": results
}
self.history.append(summary)
return summary
def compare_runs(self, run_a, run_b):
"""두 평가 실행의 결과를 비교"""
print(f"비교: {run_a['run_name']} vs {run_b['run_name']}")
print(f"질문 수: {run_a['num_questions']} vs {run_b['num_questions']}")
print(f"평균 출처 수: {run_a['avg_sources']:.1f} vs {run_b['avg_sources']:.1f}")
def save_results(self, path):
"""평가 결과 저장"""
with open(path, "w") as f:
json.dump(self.history, f, indent=2, ensure_ascii=False)RAG 파이프라인의 변경 사항(청킹 전략 변경, 리랭커 추가 등)이 실제 성능 향상으로 이어지는지 확인하기 위한 A/B 테스트 방법입니다.
def ab_test_rag_configs(config_a, config_b, test_dataset, metrics_fn):
"""두 RAG 설정의 성능을 비교"""
# 설정 A로 평가
rag_a = build_rag_pipeline(config_a)
results_a = [
metrics_fn(rag_a.query(q["question"]), q["answer"])
for q in test_dataset
]
# 설정 B로 평가
rag_b = build_rag_pipeline(config_b)
results_b = [
metrics_fn(rag_b.query(q["question"]), q["answer"])
for q in test_dataset
]
# 결과 비교
avg_a = sum(results_a) / len(results_a)
avg_b = sum(results_b) / len(results_b)
print(f"설정 A 평균 점수: {avg_a:.4f}")
print(f"설정 B 평균 점수: {avg_b:.4f}")
print(f"차이: {avg_b - avg_a:+.4f}")
return {"config_a": avg_a, "config_b": avg_b}
# 예시: 리랭킹 도입 효과 측정
result = ab_test_rag_configs(
config_a={"reranking": False, "top_k": 5},
config_b={"reranking": True, "initial_k": 20, "final_k": 5},
test_dataset=eval_data,
metrics_fn=calculate_faithfulness
)평가 메트릭이 낮은 질문들을 분석하여 시스템의 약점을 파악합니다.
실패 유형 분류:
1. 검색 실패 (Context Recall 낮음):
--> 관련 문서가 검색되지 않음
--> 해결: 청킹 전략 변경, 임베딩 모델 교체, 쿼리 변환 추가
2. 순위 실패 (Context Precision 낮음):
--> 관련 문서는 있으나 순위가 낮음
--> 해결: 리랭킹 추가, 하이브리드 검색 가중치 조정
3. 생성 실패 (Faithfulness 낮음):
--> 검색은 정확하나 LLM이 환각을 생성
--> 해결: 프롬프트 개선, 컨텍스트 포맷 변경, 모델 변경
4. 불완전 답변 (Answer Correctness 낮음):
--> 답변이 부분적으로만 맞음
--> 해결: 검색 범위 확대, 멀티 쿼리 적용def analyze_failures(evaluation_results, threshold=0.5):
"""실패 케이스 분석"""
failures = {
"retrieval_failure": [],
"ranking_failure": [],
"generation_failure": [],
"incomplete_answer": [],
}
for result in evaluation_results:
if result.get("context_recall", 1.0) < threshold:
failures["retrieval_failure"].append(result)
elif result.get("context_precision", 1.0) < threshold:
failures["ranking_failure"].append(result)
elif result.get("faithfulness", 1.0) < threshold:
failures["generation_failure"].append(result)
elif result.get("answer_correctness", 1.0) < threshold:
failures["incomplete_answer"].append(result)
for category, items in failures.items():
print(f"{category}: {len(items)}건")
for item in items[:3]:
print(f" 질문: {item['question']}")
print(f" 점수: {item.get('score', 'N/A')}")
print()
return failuresRAG 평가는 검색 단계(Context Precision, Context Recall, MRR, NDCG)와 생성 단계(Faithfulness, Answer Relevancy, Answer Correctness)를 독립적으로 측정해야 합니다. RAGAS 프레임워크는 이 메트릭들을 LLM 기반으로 자동 계산하며, 사람의 주석 없이도 체계적인 평가가 가능합니다. 평가 파이프라인을 자동화하고 실패 분석을 체계화하면, RAG 시스템을 지속적으로 개선할 수 있습니다.
다음 장에서는 기본 RAG를 넘어선 고급 패턴인 Agentic RAG와 Self-Correcting RAG에 대해 다룹니다. 에이전트가 스스로 검색 전략을 판단하고, 실패를 감지하여 수정하는 자율적 RAG 시스템을 살펴보겠습니다.
이 글이 도움이 되셨나요?
에이전트가 검색 전략을 스스로 판단하고 실패를 자동 수정하는 Agentic RAG, CRAG, Self-RAG 등 고급 RAG 패턴을 심층 분석합니다.
Cross-Encoder 리랭킹의 원리, Cohere Rerank API, 오픈소스 리랭커 비교, 그리고 프로덕션 환경에서의 효과적인 리랭킹 전략을 다룹니다.
모니터링, 캐싱, 보안, 확장성, 배포 전략까지 프로덕션 수준의 RAG 시스템을 설계하고 운영하는 실전 가이드입니다.