RAGAS란?
RAGAS는 RAG Assessment의 줄임말로, RAG 파이프라인을 자동으로 평가하는 프레임워크입니다. 2023년 논문 RAGAS: Automated Evaluation of Retrieval Augmented Generation에서 제안되었고, 이후 오픈소스로 공개되어 현재 RAG 평가의 사실상 표준으로 자리잡았습니다.
기존 LLM 평가 지표와 달리 RAGAS는 RAG의 파일 서칭 능력, 생성 일관성 등 생성 자체가 아닌 RAG의 여러 기능들에 대한 평가를 반영하는 지표입니다. 평가를 위해 강력한 LLM을 심판으로 두고 활용하는 LLM-as-a-Judge 방식을 채택하여, 사람이 직접 평가하지 않고도 RAG 파이프라인의 품질을 정량적 점수로 산출할 수 있습니다. 다양한 RAG를 구축하고 비교해야 하는 현재 상황에서 가장 적합한 평가 지표로 판단됩니다.
- GitHub: explodinggradients/ragas
등장 배경
RAG는 색인, 검색, 생성 세 단계가 모두 최종 답변 품질에 영향을 줍니다. 답변이 나쁘게 나왔을 때, 문제가 검색 단계에 있는 건지 생성 단계에 있는 건지 파악하기가 쉽지 않습니다.
기존에 LLM 평가에 사용하던 BLEU, ROUGE 같은 지표는 정답 문장과 생성 문장을 단순히 비교하는 방식이라, 의미가 같더라도 표현이 다르면 낮은 점수가 나오는 한계가 있었습니다. 그렇다고 사람이 직접 평가하자니 비용과 시간이 많이 들고, 평가자마다 기준이 달라 일관성 유지도 어렵습니다.
RAGAS는 이런 문제를 해결하기 위해 검색 품질과 생성 품질을 분리해서 측정하는 새로운 평가 체계를 제안했습니다.
평가 방식
RAGAS는 다양한 평가 지표를 결합하여 RAG 파이프라인을 평가합니다. 각 지표는 0에서 1 사이의 점수로 산출되며, 1에 가까울수록 품질이 높다는 의미입니다.
평가에는 다음 네 가지 입력값이 사용됩니다.
| 입력값 | 설명 |
|---|---|
question |
사용자의 질문 |
answer |
LLM이 생성한 답변 |
contexts |
검색 단계에서 가져온 청크 목록 |
ground_truth |
정답 (사람이 작성하거나 자동 생성) |
RAGAS 패키지를 보면 정말 다양한 평가지표가 있지만, 그 중 가장 대표적인 4개만 알아보겠습니다.
Faithfulness (충실도)
생성된 답변이 얼마나 검색된 문서에 기반하고 있는지 측정하는 지표입니다.
평가 방법
1단계: LLM을 사용하여 최종 답변을 독립적인 statement로 분할합니다. 답변을 더 작은 정보 단위로 쪼개기 위해 진행합니다.
1
2
3
4
Given a question and answer, create one or more statements from each sentence
in the given answer.
question: [question]
answer: [answer]
2단계: LLM을 활용해 각 statement가 검색된 문서(context)에 의해 논리적으로 뒷받침되는지 검증합니다. 생성 시 얼마나 context에 충실한지 측정하기 위해 진행합니다.
1
2
3
4
5
6
7
8
Consider the given context and following statements, then determine whether they
are supported by the information present in the context. Provide a brief
explanation for each statement before arriving at the verdict (Yes/No). Provide
a final verdict for each statement in order at the end in the given format.
Do not deviate from the specified format.
statement: [statement 1]
...
statement: [statement n]
3단계: Faithfulness Score를 계산합니다.
\[\text{Faithfulness} = \frac{|V|}{|S|}\]- $V$ = Context에 의해 Yes로 판별된 statement 개수
- $S$ = 전체 statement 개수
점수가 낮다면 LLM이 검색된 컨텍스트를 무시하고 자체적으로 내용을 지어내는, 즉 할루시네이션이 발생하고 있다는 신호입니다.
Answer Relevance (답변 관련성)
생성한 답변이 사용자의 원래 질문에 얼마나 직접적이고 핵심적으로 답변했는지 측정하는 지표입니다. 답변이 지나치게 장황하거나, 질문에 없는 내용을 임의로 추가하는 등 질문의 의도를 얼마나 반영했는지를 평가합니다.
평가 방법
1단계: 주어진 답변을 바탕으로 이 답변이 나올 만한 질문을 여러 개 역으로 생성합니다.
1
2
Generate a question for the given answer.
answer: [answer]
2단계: 원래 질문과 역생성된 질문들을 임베딩 모델을 통해 벡터로 변환합니다. 논문에서는 text-embedding-ada-002를 사용합니다.
3단계: 원래 질문 벡터와 역생성된 질문 벡터들 간의 코사인 유사도(Cosine Similarity)를 계산하여 평균을 냅니다.
\[\text{Answer Relevance} = \frac{1}{N}\sum_{i=1}^{N} \text{sim}(E(q),\ E(\hat{q_i}))\]- $N$ = 생성된 질문의 개수
- $E(q)$ = 원본 질문의 임베딩 벡터
- $E(\hat{q_i})$ = 역생성된 $i$번째 질문의 임베딩 벡터
점수가 낮다면 답변이 질문과 동떨어진 내용을 포함하거나, 질문에 충분히 답하지 못하고 있다는 의미입니다.
Context Relevance (문맥 연관도)
검색된 문서 중 정답과 관련 있는 핵심 문서들이 추출되었는지 확인하는 검색 품질 지표입니다. 중복적이거나 쓸모없는 정보가 포함될수록 점수가 낮아집니다.
평가 방법
1단계: LLM을 사용하여 컨텍스트로부터 질문에 답하는 데 중요한 문장 집합을 추출합니다.
1
2
3
4
5
6
7
Please extract relevant sentences from the provided context that can potentially
help answer the following question. If no relevant sentences are found, or if
you believe the question cannot be answered from the given context, return the
phrase "Insufficient Information". While extracting candidate sentences you're
not allowed to make any changes to sentences from given context.
question: [question]
context: [context]
2단계: Context Relevance Score를 계산합니다.
\[\text{Context Relevance} = \frac{|S_{ext}|}{|c(q)|}\]- $S_{ext}$ = 추출된 문장 개수
- $c(q)$ = context의 전체 문장 개수
점수가 낮다면 검색기가 관련 없는 청크를 많이 가져오거나 필요 없는 정보가 섞여 있다는 의미입니다. 청킹 전략이나 검색 방식을 개선해야 합니다.
Context Recall (문맥 재현율)
Ground Truth를 참고하여 답변에 필요한 컨텍스트가 충분히 검색되었는지 판단하는 지표입니다.
평가 방법
1단계: context와 Ground Truth를 제공하여 추출된 Context가 Ground Truth를 뒷받침하는지 T/F로 판단합니다.
1
2
3
4
5
6
7
8
Given a context and an answer, analyze each statement in the answer and classify
if the statement can be attributed to the given context or not.
Use only binary classification: 1 if the statement can be attributed to the
context, 0 if it cannot.
Provide detailed reasoning for each classification.
question: [question]
context: [context]
answer: [reference]
2단계: 결과를 모두 더한 뒤 평균을 내어 계산합니다.
\[\text{Context Recall} = \frac{\sum S_{attr}}{|S_{ref}|}\]- $S_{attr}$ = Ground Truth 뒷받침 여부 T/F (1/0)
- $S_{ref}$ = 총 컨텍스트 수
점수가 낮다면 검색 단계에서 정답 생성에 필요한 정보를 제대로 가져오지 못하고 있다는 의미입니다. 임베딩 모델 교체, 청킹 크기 조정, 또는 검색 방식 변경을 검토해야 합니다.
구현 방법
직접 구현할 필요 없이 공식 라이브러리를 활용할 수 있습니다.
RAGAS 패키지에서는 TestsetGenerator를 통해 평가용 Ground Truth 데이터셋을 합성 생성하는 기능도 제공합니다.
다만 버전 업데이트가 잦아 데이터셋 생성 API가 Deprecated되는 경우가 있으니 주의가 필요합니다.
아래 내용은 RAGAS 0.4.3 기준으로 정리한 내용입니다.
Synthesizer
이전 버전에서는 simple, reasoning, multi_context, conditional 총 4가지 유형의 질문 비율을 설정해 한 번에 생성하는 방식이었습니다.
최신 버전에서는 Synthesizer를 이용해 유형별로 직접 조합하는 방식으로 변경되었습니다.
conditional은 최신 버전에서 지원되지 않습니다.
SingleHopSpecificQuerySynthesizer
적은 수의 청크에서 데이터를 추출하는 유형입니다. VectorDB가 강점을 보이는 데이터셋으로, 이전 버전의 simple에 해당합니다.
예시: “상품 주문 API를 호출할 때 필요한 인증 방식은 무엇인가요?”
→ API 인증 방식이 적힌 단일 청크 하나에서 답 가능
MultiHopSpecificQuerySynthesizer
정확히 두 개의 청크에서 정보가 필요한 유형입니다. 엔티티가 연결된 두 청크를 결합하는 능력이 필요하며, 이전 버전의 multi_context에 해당합니다.
예시: “회원 가입 시 이메일 인증은 어떤 흐름으로 처리되며, 인증 토큰의 유효 기간은 얼마인가요?”
→ “회원 가입 프로세스” 청크 + “이메일 인증 토큰 정책” 청크 둘 다 필요
MultiHopAbstractQuerySynthesizer
두 개 이상의 청크가 필요한 추상적인 질문 유형입니다. 난이도가 가장 높으며, GraphRAG가 강점을 보이는 질문입니다. 이전 버전의 reasoning에 해당합니다.
예시: “서비스의 전반적인 배포 전략과 장애 대응 프로세스는 어떻게 연결되어 있나요?”
→ 배포 전략 문서 + 장애 대응 가이드 문서 둘 다 읽어야 답 가능
데이터셋 생성
청크 생성 및 VectorDB 저장
평가에 사용할 문서를 사전에 청킹하여 VectorDB에 저장해두어야 합니다. 아래는 권장 설정 예시입니다.
| 설정 | 값 |
|---|---|
| CHUNK_SIZE | 1500 |
| CHUNK_OVERLAP | 200 |
| EMBED_MODEL | text-embedding-3-large |
페르소나 설정
RAGAS는 페르소나(Persona)를 설정하면 해당 역할에 맞는 질문 스타일로 데이터를 생성할 수 있습니다. 실제 서비스의 사용자 유형을 페르소나로 정의해두면 더 현실적인 데이터셋을 얻을 수 있습니다.
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
28
PERSONAS = [
Persona(
name="신입 개발자",
role_description=(
"최근 입사한 신입 개발자로, 사내 온보딩 가이드, 내부 API 사용법, "
"개발 환경 설정, 업무 프로세스를 처음 접하고 있습니다. 구체적이고 실용적인 답변을 선호합니다. "
"한국인으로, 질문과 답변은 모두 한국어로만 진행합니다."
),
),
Persona(
name="백엔드 개발자",
role_description=(
"백엔드 시스템과 API를 담당하는 시니어 개발자입니다. "
"REST API 설계, 데이터베이스, 인증/보안, 서버 아키텍처에 깊은 관심을 가지고 있습니다. "
"오래전에 작성한 문서 내용을 확인하는 질문을 주로 합니다. "
"한국인으로, 질문과 답변은 모두 한국어로만 진행합니다."
),
),
Persona(
name="운영 담당자",
role_description=(
"서비스 운영과 관련된 데이터를 관리하는 담당자입니다. "
"주요 기능의 사용 방법, 관련 정책 문서, 외부 연동 서비스 관련 문서를 주로 참조합니다. "
"업무에 필요한 API나 기능 정보를 검색하는 질문을 주로 합니다. "
"한국인으로, 질문과 답변은 모두 한국어로만 진행합니다."
),
),
]
데이터 생성
멀티홉 데이터셋 생성을 위해 RAGAS는 내부적으로 Knowledge Graph(KG)를 먼저 구성합니다. KG는 생성 비용이 크기 때문에 한 번 생성한 뒤 파일로 저장해두고 재사용하는 방식을 권장합니다.
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def generate(docs: list[Document], testset_size: int = 75):
llm = ChatOpenAI(model='gpt-4o', api_key=OPENAI_API_KEY, max_tokens=64000)
embeddings = OpenAIEmbeddings(model='text-embedding-3-large', api_key=OPENAI_API_KEY)
generator = TestsetGenerator.from_langchain(
llm=llm,
embedding_model=embeddings,
)
generator.persona_list = PERSONAS
kg_path = Path('ragas_kg')
kg_path.mkdir(parents=True, exist_ok=True)
if (kg_path / 'kg.json').exists():
kg = KnowledgeGraph.load(kg_path / 'kg.json')
else:
transforms = default_transforms_for_prechunked(
llm=generator.llm,
embedding_model=generator.embedding_model,
)
nodes = []
for chunk in docs:
page_content = chunk.page_content if hasattr(chunk, 'page_content') else chunk
metadata = chunk.metadata if hasattr(chunk, 'metadata') else {}
if page_content and page_content.strip():
node = Node(
type=NodeType.CHUNK,
properties={"page_content": page_content, "document_metadata": metadata},
)
nodes.append(node)
kg = KnowledgeGraph(nodes=nodes)
apply_transforms(kg, transforms, run_config=RunConfig(max_retries=20))
kg.save(kg_path / 'kg.json')
generator.knowledge_graph = kg
distribution = [
(SingleHopSpecificQuerySynthesizer(llm=generator.llm), 0.5),
(MultiHopAbstractQuerySynthesizer(llm=generator.llm), 0.25),
(MultiHopSpecificQuerySynthesizer(llm=generator.llm), 0.25),
]
for query, _ in distribution:
prompts = asyncio.run(query.adapt_prompts("korean", llm=generator.llm))
query.set_prompts(**prompts)
return generator.generate(
testset_size=testset_size,
num_personas=len(PERSONAS),
query_distribution=distribution,
raise_exceptions=False,
with_debugging_logs=True
)
각 Synthesizer별 비율은 필요에 따라 조정할 수 있으며, 아래는 권장 비율 예시입니다.
| Synthesizer | 비율 | 예시 개수 (60개 기준) |
|---|---|---|
| SingleHopSpecificQuerySynthesizer | 50% | 30개 |
| MultiHopAbstractQuerySynthesizer | 25% | 15개 |
| MultiHopSpecificQuerySynthesizer | 25% | 15개 |
Ground Truth 데이터 생성은 품질이 좋아야 정확한 평가가 가능하기 때문에 비교적 성능이 좋은 모델을 사용하는 것을 권장합니다.
RAG 응답 수집
데이터셋이 준비되었다면 평가할 RAG 시스템에 질문을 던져 응답과 검색된 컨텍스트를 수집해야 합니다. 질문 수가 많을 경우 OpenAI Batch API를 활용하면 비용을 절반으로 줄일 수 있습니다.
Batch API는 요청을 JSONL 파일로 묶어 한 번에 제출하고, 완료되면 결과를 파일로 받아오는 방식입니다.
1
2
3
4
5
6
7
8
9
# 배치 결과 수신
client = OpenAI(api_key=OPENAI_API_KEY)
batch = client.batches.retrieve(batch_id)
if batch.status == "completed":
raw_content = client.files.content(batch.output_file_id).text
for line in raw_content.splitlines():
obj = json.loads(line)
answer = obj["response"]["body"]["choices"][0]["message"]["content"]
배치 완료까지는 수 분에서 수 시간이 걸릴 수 있으며, batch.status로 진행 상태를 확인할 수 있습니다.
이 단계에서서는 이전에 생성한 Ground Truth 질문별 검색한 컨텍스트와 답변 데이터를 저장해야 합니다.
RAGAS 평가
응답과 컨텍스트가 수집되었다면 RAGAS로 평가를 진행할 수 있습니다.
데이터셋 구성
RAGAS는 EvaluationDataset과 SingleTurnSample을 이용해 평가 데이터셋을 구성합니다.
SingleTurnSample 하나가 질문-답변-컨텍스트-정답의 한 쌍에 해당합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from ragas import SingleTurnSample
from ragas.evaluation import EvaluationDataset
dataset = EvaluationDataset(
samples=[
SingleTurnSample(
user_input=row["user_input"], # 질문
response=row["response"], # RAG가 생성한 답변
retrieved_contexts=row["contexts"], # 검색된 청크 목록
reference=row["reference"], # Ground Truth 정답
)
for row in rows
]
)
평가자 구성
평가에 사용할 LLM과 임베딩 모델을 설정합니다. Faithfulness, ContextRecall은 LLM만 필요하지만, AnswerRelevancy는 역질문 생성 후 유사도를 계산하기 때문에 임베딩 모델도 함께 필요합니다.
1
2
3
4
5
6
7
8
9
10
11
12
from openai import AsyncOpenAI
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from ragas.metrics.collections import Faithfulness, AnswerRelevancy, ContextRecall
client = AsyncOpenAI(api_key=OPENAI_API_KEY)
evaluator_llm = llm_factory(CHAT_MODEL, client=client, max_tokens=8192)
evaluator_embedding = embedding_factory(model=EMBED_MODEL, client=client)
faithfulness = Faithfulness(llm=evaluator_llm)
answer_rel = AnswerRelevancy(llm=evaluator_llm, embeddings=evaluator_embedding)
context_recall = ContextRecall(llm=evaluator_llm)
평가 실행
각 지표는 비동기(ascore)로 개별 호출할 수 있으며, asyncio.Semaphore로 동시 요청 수를 제어하면 API 호출 한도를 초과하지 않고 안정적으로 평가할 수 있습니다.
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
28
29
async def evaluate_sample(sample):
faith = await faithfulness.ascore(
user_input=sample.user_input,
response=sample.response,
retrieved_contexts=sample.retrieved_contexts,
)
ans_rel = await answer_rel.ascore(
user_input=sample.user_input,
response=sample.response,
)
ctx_rec = await context_recall.ascore(
user_input=sample.user_input,
retrieved_contexts=sample.retrieved_contexts,
reference=sample.reference,
)
return {
"faithfulness": faith.value,
"answer_relevancy": ans_rel.value,
"context_recall": ctx_rec.value,
}
async def evaluate_all(samples, concurrency=5):
sem = asyncio.Semaphore(concurrency)
async def bounded(s):
async with sem:
return await evaluate_sample(s)
return await asyncio.gather(*[bounded(s) for s in samples])
results = asyncio.run(evaluate_all(list(dataset)))
평가가 완료되면 샘플별 점수와 전체 평균을 CSV/JSON으로 저장해두고 RAG 방식 간 비교에 활용할 수 있습니다.