[RAG 시리즈] RAGAS 를 이용한 실전 RAG 평가

Judy·2025년 1월 21일
0

[글또] RAG 시리즈

목록 보기
6/8

RAGAS 는 RAG 의 평가지표 중 특히 Rule Base 지표들을 자동으로 계산하기 위해 만들어진 프레임워크입니다.

쉽게 말해 RAG 파이프라인을 구축한 후,
파이프라인의 input / output 을 이용해 평가 지표 계산에 필요한 데이터셋 (question, answer, context, ground truth) 을 만들고
RAGAS 는 이 데이터셋을 이용하여 함수 단 한줄로 각 평가 지표를 빠르게 자동으로 계산해 줍니다.

RAGAS 에 대한 자세한 설명은 [우아한 스터디] RAGAS : RAG 파이프라인 평가 프레임워크 을,
이 포스팅의 전체 코드 및 샘플은 GitHub 을 참고해 주세요.

1. RAGAS 준비물

1.1 Dataset

데이터는 총 4가지를 준비합니다.

  • question : 사용자의 질문
  • ground truth : 질문에 맞는 정답 (= 모범 답안)
  • answer : LLM이 생성한 답변
  • context : LLM이 답변을 생성하기 위해 참고한 정보 (=context)

1.2 평가 지표

보편적으로 다음 4개의 평가 지표를 이용합니다.

  • Faithfulness : 주어진 context에 대한 생성된 답변의 일관성에 대한 값​
    (context - answer)

  • Context Recall : ground_truth의 문장 중 contex로부터 추론할 수 있는 문장의 비율을 측정한 값
    (ground truth - context)

  • Context Precision : contexts에 존재하는 ground_truth와 관련된 항목을 높은 순위로 잘 검색해 왔는지의 여부를 평가하는 지표
    (grund truth - context 및 참조 순위와 지표)

  • Answer Relevance : 생성된 답변이 주어진 질문과 얼마나 관련성이 있는지에 대한 값​
    (answer - question)

https://smilegate.ai/2024/11/18/ragas-rag-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%B1%EB%8A%A5-%ED%8F%89%EA%B0%80-%EC%9D%B4%EC%A0%9C%EB%8A%94-%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%95%B4%EB%B3%B4%EC%9E%90/

2. Langchain 없이 RAGAS 적용하기

Langchain 을 사용하지 않고, 데이터셋이 모두 구축된 경우 스코어만 계산하는 간단한 튜토리얼 코드입니다.
RAGAS 는 주어진 데이터에 각 평가지표별 수식을 계산하기 때문에
질문과 실제 답안뿐만이 아니라 LLM 으로 생성한 답변과 답변을 생성하는 데 참고한 context 만 준비한다면 RAGAS 를 이용해 품질 평가를 할 수가 있습니다.

2.1 Dataset 생성

from datasets import Dataset

questions = ["보험가격지수란 무엇인가요?"]
ground_truths = ["해당상품의 보험료총액(보험금 지급을 위한 보험료 및 보험회사의 사업경비 등을 위한 보험료)을 참조순보험료 총액*과 평균사업비 총액**을 합한 금액으로 나눈 비율을 “보험가격지수”라고 합니다.보험가격지수란 고객이 납입하는 보험료 중 사업비로 사용되는 금액을 수준에 대하여 상품군별 생명보험상품 전체의 평균 사업비율을 반영하여 계산한 값입니다."]
answers = ["보험가격지수는 해당 상품의 보험료 총액을 참조순보험료 총액과 평균사업비 총액을 합한 금액으로 나눈 비율을 말합니다. 보험료 총액은 보험금 지급을 위한 보험료와 보험회사의 사업경비 등을 위한 보험료를 포함합니다. 참조순보험료 총액은 감독원장이 정하는 바에 따라 산정한 전체 보험회사 공시이율의 평균(평균공시이율) 및 참조순보험요율을 적용하여 산출한 보험금 지급을 위한 보험료입니다. 평균사업비 총액은 상품군별 생명보험상품 전체의 평균 사업비율을 반영하여 계산(역산)한 값입니다."]
contexts = [['A : 해당상품의 보험료총액(보험금 지급을 위한 보험료 및 보험회사의 사업경비 등을 위한 보험료)을 참조순보험료 총액*과 평균사업비 총액**을 합한 금액으로 나눈 비율을 “보험가격지수”라고 합니다.* 감독원장이 정하는 바에 따라 산정한 전체 보험회사 공시이율의 평균(평균공시이율) 및 참조순보험요율을 적용하여 산출한 보험금 지급을 위한 보험료 ** 상품군별 생명보험상품 전체의 평균 사업비율을 반영하여 계산(역산)한 값(기준 : 40세) | 상품명 | 상품명 | 상품명 | 보험기간 | 납입기간 | 보험가격지수 | 보험가격지수 | 가입금액 (만원) | | --- | --- | --- | --- | --- | --- | --- | --- | | 상품명 | 상품명 | 상품명 | 보험기간 | 납입기간 | 남자 | 여자 | 가입금액 (만원) | | 보험상품명 | 1종 | 1종 | 종신 | 20년 | 110.9% | 113.6% | 10,000 | | 보험상품명 | 2종(1% 지급형) | 은퇴나이 55세 | 종신 | 20년 | 110.7% | 113.6% | 10,000 || 보험상품명 | 2종(1% 지급형) | 은퇴나이 60세 | 종신 | 20년 | 110.6% | 113.4% | 10,000 | | 보험상품명 | 2종(1% 지급형) | 은퇴나이 65세 | 종신 | 20년 | 110.4% | 113.3% | 10,000 | | 보험상품명 | 2종(2% 지급형) | 은퇴나이 55세 | 종신 | 20년 | 110.3% | 113.2% | 10,000 | | 보험상품명 | 2종(2% 지급형) | 은퇴나이 60세 | 종신 | 20년 | 110.0% | 113.0% | 10,000 | | 보험상품명 | 2종(2% 지급형) | 은퇴나이 65세 | 종신 | 20년 | 109.6% | 112.8% | 10,000 || 보험상품명 | 3종 | 3종 | 종신 | 20년 | 108.7% | 111.1% | 10,000 |']]


# To dict
data = {
  "user_input": questions,
  "response": answers,
  "retrieved_contexts": contexts,
  "reference": ground_truths
}

# Convert dict to dataset
dataset = Dataset.from_dict(data)

참고
기존(2024년 여름 기준) 의 data dict 포맷은 다음과 같았으나, 현재는 해당 포맷을 그대로 사용하면 에러가 납니다.

data_samples = {
    'question': ...
    'answer': ...
    'contexts' : ... 
    'ground_truth': ...
}

다음과 같이 최신 dict 포맷으로 고쳐 주세요.

data = {
  "user_input": questions,
  "response": answers,
  "retrieved_contexts": contexts,
  "reference": ground_truths
}

2.2 Score 계산 (평가)

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

result = evaluate(
    dataset = dataset, 
    metrics=[
        context_precision,
        context_recall,
        faithfulness,
        answer_relevancy,
    ],
)

# score 출력
print(result)
# DataFrame 생성
df = result.to_pandas()
df

2.3 결과

3. RAG Langchain 을 생성하고 자동으로 RAGAS 적용하기

실전에서는 사용자의 질문과(question), 질문에 맞는 정답(ground truth) 만 엑셀 파일 등에 미리 만들어 두고
RAG Langchain 에 입력하고, 자동으로 RAG 실행 후 RAGAS 를 적용할 수 있습니다.

RAG Langchain 을 구축한 후 RAGAS 까지 한번에 적용하는 코드는 다음과 같습니다.

3.1 RAG Langchain 구축

LLM 정의

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4-0125-preview", temperature=0)

프롬프트 템플릿 작성

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.messages import SystemMessage

system_prompt = SystemMessage(content=
        """당신은 전문 상담원입니다. 아래 지침에 따라 사용자의 질문에 답변을 제공하세요.
        ---------------------
        1. 주어진 정보만 활용하여 답변을 제공하세요. 주어진 정보로 답변을 할 수 없는 경우, 정중하게 답변을 제공할 수 없다고 설명합니다.
        2. 답변은 정제된 형식과 문어체로 작성하며, 친절하고 자세한 내용을 제공합니다.
        ---------------------
        """
)

template = (
        "Below is the context information.\n"
        "---------------------\n"
        "{context}"
        "\n---------------------\n"
        "Given the context information, provide a most relevant chunk to {query}."
        "If there is no title that matches, output '해당정보 존재하지 않음'."
        "Do not include the title on your final output."
)

prompt = ChatPromptTemplate.from_messages([system_prompt, template])

검색기(Retriever) 설정

retriever = vector_store.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 10, "fetch_k": 3, "lambda_mult": 0.5},
)

Langchain 생성

chain = (
        {"context": retriever, "query": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )

3.2 1개 데이터셋에 RAGAS 적용

Langchain 구축 및 RAGAS 적용에 집중하기 위해 우선 1개 데이터셋만 만들어서 실행해 봅니다.

데이터셋 준비 (question, ground truth)

from datasets import Dataset

questions = ["보험금의 지급사유"]
ground_truths = ["회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보험금을 지급합니다. 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다): 사망보험금 2. 보험기간 중 상해로 장해분류표(<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해상태가 되었을 때: 후유장해보험금(장해분류표에서 정한 지급률을 보험가입금액에 곱하여 산출한 금액)"]

RAG Langchain 연결 및 데이터셋 dict 형으로 변환

answer 에는 질문에 따른 RAG 의 답변을,
context 에는 RAG가 질문을 생성하는 데 참고한 context (=Retreiver 가 검색해 온 Document) 를 넣어 줍니다.

answers = []
contexts = []

# Inference
for query in questions:
  answers.append(chain.invoke(query))
  contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])

# To dict
data = {
  "user_input": questions,
  "response": answers,
  "retrieved_contexts": contexts,
  "reference": ground_truths
}

# Convert dict to dataset
dataset = Dataset.from_dict(data)

Score 계산 (평가)

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

result = evaluate(
    dataset = dataset, 
    metrics=[
        context_precision,
        context_recall,
        faithfulness,
        answer_relevancy,
    ],
)

# score 출력
print(result)
# DataFrame 생성
df = result.to_pandas()
df

결과

3.3 실전 : N개 데이터셋에 RAGAS 적용

실전에서는 미리 질문 - 답안 세트를 여러 개 만들어 엑셀파일에 정리한 뒤
자동으로 엑셀 파일을 읽어서 RAG 결과를 얻고, RAGAS 를 적용하여 효율성을 높일 수 있습니다.

데이터셋 준비 : .xlsx 파일 로드

import pandas as pd

# .xlsx 파일 경로 지정
file_path = 'input.xlsx'

# .xlsx 파일을 읽어서 DataFrame으로 저장
data = pd.read_excel(file_path)

# 첫 번째 행의 'question'과 'ground_truths' 열 데이터를 리스트로 저장
questions = data['questions'].tolist()
ground_truths = data['ground_truths'].tolist()

# question과 ground_truths를 새로운 DataFrame으로 결합
result = pd.DataFrame({
    'questions': questions,
    'ground_truths': ground_truths
})

result

# # 만약 왼쪽 정렬로 데이터를 보고 싶다면 아래 코드 이용
# # DataFrame의 각 열을 왼쪽 정렬로 스타일링
# result_styled = result.style.set_properties(**{'text-align': 'left'})

# # 결과 출력 (Jupyter 노트북 환경에서 스타일이 반영됨)
# result_styled

RAG Langchain 연결 및 데이터셋 dict 형으로 변환

from tqdm import tqdm

answers = []
contexts = []

# Inference
for query in tqdm(questions):
  answers.append(chain.invoke(query))
  contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])

# To dict
data = {
  "user_input": questions,
  "response": answers,
  "retrieved_contexts": contexts,
  "reference": ground_truths
}

# Convert dict to dataset
dataset = Dataset.from_dict(data)

Score 계산 (평가) & 결과 저장

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

result = evaluate(
    dataset = dataset, 
    metrics=[
        context_precision,
        context_recall,
        faithfulness,
        answer_relevancy,
    ],
)

# score 출력
print(result)
# DataFrame 생성
df = result.to_pandas()
df.to_excel('output.xlsx', index=False)
df

결과

4. 결과 분석 및 문제 해결 방안

3.3 실전 : N개 데이터셋에 RAGAS 적용 의 결과만 분석합니다.

4.1 평가지표별 결과 분석 및 문제 해결 방안

4.1.1 Faithfullness

  • 주어진 context에 대한 생성된 답변의 일관성에 대한 값 (context - answer)

  • Faithfullness == 1 인 경우

    • LLM이 context 를 잘 참고하여 정확한 답변 생성
      • question : 보험금의 지급사유
      • context : ...제3조(보험금의 지급사유 )\n회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보\n험금을 지급합니다 .\n 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다 ): 사\n망보험금\n 2. 보험기간 중 상해로 장해분류표 (<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해\n상태가 되었을 때: 후유장해보험금 (장해분류표에서 정한 지급률을 보험가입금액에 곱하\n여 산출한 금액...
      • answer : 회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보험금을 지급합니다. 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다 ): 사망보험금 2. 보험기간 중 상해로 장해분류표 (<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해상태가 되었을 때: 후유장해보험금 (장해분류표에서 정한 지급률을 보험가입금액에 곱하여 산출한 금액)
      • ground truth : 회사는 피보험자에게 다음 중 어느 하나의 사유가 발생한 경우에는 보험수익자에게 약정한 보험금을 지급합니다. 1. 보험기간 중에 상해의 직접결과로써 사망한 경우(질병으로 인한 사망은 제외합니다): 사망보험금 2. 보험기간 중 상해로 장해분류표(<별표1> 참조)에서 정한 각 장해지급률에 해당하는 장해상태가 되었을 때: 후유장해보험금(장해분류표에서 정한 지급률을 보험가입금액에 곱하여 산출한 금액)
  • Faithfullness == 0 인 경우

    • LLM이 context 로부터 답변을 생성하지 못함
      • question : 보험료 할증이란 무엇인가요?
      • context : ...(context 내에 ground truth 와 연관된 키워드조차 없음)...
      • answer : 해당정보 존재하지 않음
      • ground truth : 일반적인 경우보다 위험이 높은 피보험자가 가입하기 위한 방법의 하나로, 보험가입 후 기간이 경과함에 따라 위험의 크기 및 정도가 점차 증가하는 위험 또는 기간의 경과에 상관없이 일정한 상태를 유지하는 위험에 적용하는 방법으로 위험 정도에 따라 해당 보험료 이외에 특별보험료를 부가하는 방법입니다.

4.1.2 Context Recall

  • ground_truth의 문장 중 contex로부터 추론할 수 있는 문장의 비율을 측정한 값 (ground truth - context)

    • 검색된 context가 ground truth와 얼마나 잘 일치하는지 평가.
    • 점수 범위: 0~1 (1에 가까울수록 좋음)
    • ground truth의 각 claim이 검색된 context와 얼마나 일치하는지 계산함.
  • Context Recall == 1 인 경우

    • 검색된 context 가 ground truth 와 일치
    • Faithfullness == 1 인 경우 와 동일.
  • Context Recall == 0 인 경우

    • 검색된 context 가 ground truth 와 맞지 않음
      • question : 보험가액이란 무엇인가요?
      • context : ...(보험가액의 설명과 상관없는 내용)...
      • answer : 해당정보 존재하지 않음
      • ground truth : 재산보험에 있어 피보험 이익을 금전으로 평가한 금액으로 보험의 목적에 발생할 수 있는 최대 손해액을 말합니다.(회사가 실제 지급하는 보험금은 보험가액을 초과할 수 없습니다)

4.1.3 Context Precision

  • Context Precision : contexts에 존재하는 ground_truth와 관련된 항목을 높은 순위로 잘 검색해 왔는지의 여부를 평가하는 지표 (context - question - ground truth)

    • 검색된 각 k개의 context가 question, ground truth와 연관이 있는지 평가.
    • 점수 범위: 0~1 (1에 가까울수록 좋음)
    • 검색 결과와 유효성을 기반으로 precision을 계산함.
  • Context Precision == 1 인 경우

    • LLM이 question, ground truth 와 연관 있는 context 를 잘 검색해 옴
    • Faithfullness == 1 인 경우 와 동일.
  • Context Precision == 0 에 가까운 경우

    • LLM이 question, ground truth 와 연관 있는 context 를 잘 검색해 오지 못함
    • Context Recall == 0 인 경우 와 동일.

4.1.4 Answer Relevance

  • 생성된 답변이 주어진 질문과 얼마나 관련성이 있는지에 대한 값 (answer - question)

    • 생성된 답변이 질문에 얼마나 잘 부합하는지 평가
    • 점수 범위: -1~1 (1에 가까울수록 좋음)
    • 생성된 답변을 기반으로 생성한 질문과 원래 질문의 cosine 유사도를 활용함.
    • answer과 context만으로 question을 재구성할 수 있다는 개념을 활용한 방식.
  • Answer Relevance == 0.81

    • 생성된 답변이 질문과 유사함
    • Faithfullness == 1 인 경우 와 동일.
  • Answer Relevance == 0 인 경우

    • 생성된 답변이 질문과 유사하지 않음 (혹은 답변을 아예 생성하지 못함)
    • Faithfullness == 1 인 경우 와 동일.

4.1.5 평가지표를 이용한 문제 분석 및 해결방안

  • 문제 : Context Precision, Context Recall 점수가 낮은 경우
  • 분석 : 적합한 Context 를 검색하지 못함. 또는 불필요한 context 를 너무 많이 가져옴
  • 해결 : Retreiver 를 개선하기 위해 옵션 조절 (top k 개 축소, threshold 상향), 프롬프트 개선

4.2 기타 문제

4.2.1 VectorDB에 데이터가 깨져서 저장된 경우

  • 문제 : 표 형식의 텍스트가 VectorDB에 저장될 때 검색에 적합한 형태로 저장되지 않아 context 검색 불가

    • 원문
    • Vector DB 에 저장된 값
  • 개선방안 : PDF Loader 를 Fitz 등 테이블을 인식할 수 있는 라이브러리로 교체하여 가급적 Markdown 형태로 변환하여 VectorDB 에 문서를 적재한다.

4.2.2 Retriever 가 적합한 context 를 검색하지 못하는 경우

  • 문제 : Retriever 가 질문에 맞는 context 를 검색하지 못하거나 불필요한 context 를 너무 많이 가져옴 (ex : Faithfullness == 0 인 경우)

  • 개선방안 : Retreiver 를 개선하기 위해 옵션 조절 (top k 개 축소, threshold 상향), 프롬프트 개선

4.2.3 질문이 모호한 경우

  • 문제 : LLM이 질문의 정확한 의도를 파악하지 못하여 적합하지 않은 context 를 가져옴

    • question : KB개인상해보험에 가입한 후에 재해로 인해 골절이 되었는데 보험금 청구가 가능한가요?
    • context : ['KB개인상해보험', '제31조(회사의 파산선고와 해지) ························································································· ··················································································································· 65\n골절발생위로금 (I) 특별약관 ··················································································································· 67\n골절발생위로금 (II) 특별약관 ················································································································· 68\n골절수술위로금 특별약관 ...]
    • '골절' 키워드가 포함된 목차를 context 로 가져옴.
  • 개선방안 :

    • Retreiver 검색옵션을 바꿔 본다
    • 만약 Ensemble Retriever 를 사용하는데 너무 키워드에 국한되어 검색될 경우, weights 변수를 조정해서 키워드 검색 비율을 줄여 본다. (ex: 키워드 : 유사도 검색 비율 [0.5, 0.5] -> [0.3, 0.7]
    • 질문을 수정한다 (명확한 질문으로 변경하거나 대체 용어 추가)

5. 결론

  • RAGAS 를 이용한 평가 자동화 코드 소개 및 지표 설명
  • 4개 지표별 결과 분석 팁
    • Faithfullness, Answer Relevancy 값이 낮다 : 답변이 생성되지 않았거나 이상함
    • Context Precision, Context Recall 값이 낮다 : Context 를 제대로 못 가져온다 -> Retriever 를 조지거나 Prompt 를 변경해 본다
    • Answer Relevancy 만 높고 나머지 지표 값은 낮다 : context 에 정확한 답(ground truth) 가 없는데 answer 는 생성되는 경우임.
  • 기타 문제 사례 분석 및 개선방안 제안

6. Future Work

문서 안에 검색할 내용이 포함되어 있고, VectorDB에 깨지지 않고 잘 적재되었는데
이상하게 RAG 답변이 부정확하게 나오거나 아예 생성되지 않는 경우가 많다. 🤔

튜토리얼에 사용한 embedding model 의 성능이 강력하게 의심되는데..
현재 회사에서는 보험 용어로 따로 학습한 embedding model 을 쓰고 있어서인지 검색 성능이 준수하다.

기회가 되면 embedding 모델별 품질 평가도 해보는 것으로...!

profile
NLP Researcher

0개의 댓글

관련 채용 정보