[RAG] RAG 성능 평가 - RAGAS

Hunie_07·2025년 4월 9일
0

Langchain

목록 보기
23/35

📌 RAGAS - RAG Evaluation

RAG 시스템 성능 평가

  • RAG 기술의 핵심: 외부 지식 검색과 LLM 결합으로 응답 품질 향상

  • 평가 기준: LLM-as-judge 방식으로 사실성, 관련성, 충실도, 유용성 평가

  • 체계적인 A/B 테스트: 각 컴포넌트별 성능 비교 및 영향도 분석으로 최적 구성 도출

  • 평가 방법론: 오프라인(참조답변 기반), 온라인(실시간), 페어와이즈(비교) 평가 구분


1️⃣ 평가 대상 (Evaluation Target)

  • 검색(Retrieval) 단계:

    1. 관련 문서와 쿼리 간의 연관성(Relevance)을 통해 검색된 문서가 쿼리의 정보 요구를 얼마나 잘 충족하는지 평가
    2. 관련 문서와 후보 문서 간의 정확성(Accuracy)을 통해 시스템이 적절한 문서를 식별하는 능력을 측정
  • 생성(Generation) 단계:

    1. 응답과 쿼리의 연관성(Relavance)
    2. 응답과 관련 문서 간의 충실도(Faithfulness)
    3. 응답과 샘플 응답 간의 정확성(Correctness)
  • 추가 고려사항:

    • 핵심 성능 지표: Latency(응답 속도), Diversity(검색 다양성), Noise Robustness(잡음 내구성)
    • 안전성 평가: Negative Rejection(불충분 정보 거부), Counterfactual Robustness(오정보 식별)
    • 사용자 경험: Readability(가독성), Toxicity(유해성), Perplexity(복잡성) 등 추가 고려

[출처] https://arxiv.org/abs/2405.07437


2️⃣ 평가 데이터셋 구축 (Evaluation Dataset)

  • 데이터셋 구성 방식: LLM 기반 새로운 데이터셋 생성 (Synthetic Data)

  • 맞춤형 데이터셋 구축으로 RAG 시스템의 실용성 평가 강화

  • [실습] : Ragas (https://docs.ragas.io/en/stable/) 활용

    • RAG 시스템 평가를 위한 오픈소스 프레임워크

    • 실용성: 자동화된 평가 파이프라인 구축 가능

    • 주요 지표:

      1. 충실도(Faithfulness):
        • 생성된 답변이 주어진 컨텍스트와 얼마나 일치하는지 평가
      2. 답변 관련성(Answer Relevancy):
        • 생성된 답변이 주어진 질문과 얼마나 관련이 있는지 평가
      3. 컨텍스트 정확도(Context Precision):
        • 검색된 컨텍스트들이 얼마나 적절하게 순위가 매겨졌는지 평가
        • 0~1 사이의 값으로, 높을수록 좋음

1. Langchain 문서 준비

from langchain_community.document_loaders import TextLoader

# 데이터 로드
def load_text_files(txt_files):
    data = []

    for text_file in txt_files:
        loader = TextLoader(text_file, encoding='utf-8')
        data += loader.load()

    return data

korean_txt_files = glob(os.path.join('data', '*_KR.md')) 
korean_data = load_text_files(korean_txt_files)

print('Korean data:')
pprint(korean_data)

- 출력

Korean data:
[Document(metadata={'source': 'data\\리비안_KR.md'}, page_content='Rivian Automotive, Inc.는 ...'),
 Document(metadata={'source': 'data\\테슬라_KR.md'}, page_content='Tesla, Inc.는 ...' )]

문서 분할

from langchain_text_splitters import RecursiveCharacterTextSplitter

# 문장을 구분하여 분할 - 정규표현식 사용 (문장 구분자: 마침표, 느낌표, 물음표 다음에 공백이 오는 경우)
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",    # TikToken 인코더 이름
    separators=['\n\n', '\n', r'(?<=[.!?])\s+'],   # 구분자
    chunk_size=400,
    chunk_overlap=100,
    is_separator_regex=True,      # 구분자가 정규식인지 여부
    keep_separator=True,          # 구분자 유지 여부
)

korean_docs = text_splitter.split_documents(korean_data)

print("한국어 문서 수:", len(korean_docs))
print("-"*100)
print(korean_docs[0].metadata)
pprint(korean_docs[0].page_content)
print("-"*100)
print(korean_docs[1].metadata)
pprint(korean_docs[1].page_content)

- 출력

한국어 문서 수: 31
----------------------------------------------------------------------------------------------------
{'source': 'data\\리비안_KR.md'}
('Rivian Automotive, Inc.는 2009년에 설립된 미국의 전기 자동차 제조업체, 자동차 기술 및 야외 레크리에이션 '
 '회사입니다.\n'
 '\n'
 '**주요 정보:**')
----------------------------------------------------------------------------------------------------
{'source': 'data\\리비안_KR.md'}
('**주요 정보:**\n'
 '\n'
 '- **회사 유형:** 상장\n'
 '- **거래소:** NASDAQ: RIVN\n'
 ...
 '\n'
 '**개요**')

벡터 저장소 생성

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# OpenAI Embeddings 모델을 로드
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# Chroma 벡터 저장소 생성하기
vector_store = Chroma.from_documents(
    documents=korean_docs,
    embedding=embedding_model,    
    collection_name="db_korean_cosine", 
    persist_directory="./chroma_db",
    collection_metadata = {'hnsw:space': 'cosine'}, # l2, ip, cosine 중에서 선택 
)

# 결과 확인
print(f"저장된 Document 개수: {len(vector_store.get()['ids'])}")

- 출력

저장된 Document 개수: 31

벡터 저장소 로드

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# OpenAI Embeddings 모델을 로드
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# Chroma 벡터 저장소 로드
vector_store = Chroma(
    embedding_function=embedding_model,    
    collection_name="db_korean_cosine", 
    persist_directory="./chroma_db",
)

# 결과 확인
print(f"저장된 Document 개수: {len(vector_store.get()['ids'])}")

- 출력

저장된 Document 개수: 31

2. LLM 모델 설정

from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings

# LLM과 임베딩 모델 초기화
generator_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini"))
generator_embeddings = LangchainEmbeddingsWrapper(OpenAIEmbeddings(model="text-embedding-3-small"))

3. Test Data 생성

페르소나 정의

from ragas.testset.persona import Persona

# 페르소나 정의 (다양한 관점에서 질문 생성)
personas = [
    Persona(
        name="graduate_researcher",  # 박사과정 연구원: 심도 있는 분석적 질문
        role_description="미국 전기차 시장을 연구하는 한국인 박사과정 연구원으로, 전기차 정책과 시장 동향에 대해 깊이 있는 분석을 하고 있습니다. 영어를 못하고 한국어만을 사용합니다.",
    ),
    Persona(
        name="masters_student",    # 석사과정 학생: 개념 이해를 위한 질문
        role_description="전기차 산업을 공부하는 한국인 석사과정 학생으로, 미국 전기차 시장의 기초적인 개념과 트렌드를 이해하려 노력하고 있습니다. 영어를 못하고 한국어만을 사용합니다.",
    ),
    Persona(
        name="industry_analyst",   # 산업 분석가: 실무 중심적 질문
        role_description="한국 자동차 회사에서 미국 전기차 시장을 분석하는 주니어 연구원으로, 실무적인 시장 데이터와 경쟁사 동향에 관심이 많습니다. 영어를 못하고 한국어만을 사용합니다.",
    ),
    Persona(
        name="undergraduate_student",  # 학부생: 기초적인 학습 질문
        role_description="자동차 공학을 전공하는 한국인 학부생으로, 미국 전기차 기술과 시장에 대해 기본적인 지식을 습득하고자 합니다. 영어를 못하고 한국어만을 사용합니다.",
    )
]

합성 데이터 생성

testset_size : 생성할 데이터셋 샘플 개수

from ragas.testset import TestsetGenerator

# TestsetGenerator 생성
generator = TestsetGenerator(llm=generator_llm, embedding_model=generator_embeddings, persona_list=personas)

# 합성 데이터 생성
dataset = generator.generate_with_langchain_docs(korean_docs, testset_size=20)

- 출력

Applying CustomNodeFilter:   0%|          | 0/31 [00:00<?, ?it/s]         Node c0cb0e20-53a6-42ad-b499-7e201cbb3296 does not have a summary. Skipping filtering.
Generating Scenarios: 100%|██████████| 2/2 [00:25<00:00, 12.81s/it]                                          
Generating Samples: 100%|██████████| 20/20 [00:04<00:00,  4.01it/s]

테스트 데이터 확인

test_data = dataset.to_pandas()
test_data

CSV 파일 저장

# CSV 저장
test_data.to_csv('./data/ragas_testset.csv', index=False)

3️⃣ 평가 지표 (Evaluation Metric)

1) 검색(Retrieval) 평가

  • Non-Rank Based Metrics: Accuracy, Precision, Recall@k 등을 통해 관련성의 이진적 평가를 수행

  • Rank-Based Metrics: MRR(Mean Reciprocal Rank), MAP(Mean Average Precision)를 통해 검색 결과의 순위를 고려한 평가를 수행

  • RAG 특화 지표: 기존 검색 평가 방식의 한계를 보완하는 LLM-as-judge 방식 도입

  • 포괄적 평가: 정확도, 관련성, 다양성, 강건성을 통합적으로 측정

2) 생성(Generation) 평가

  • 전통적 평가: ROUGE(요약), BLEU(번역), BertScore(의미 유사도) 지표 활용

  • LLM 기반 평가: 응집성, 관련성, 유창성을 종합적으로 판단하는 새로운 접근법 도입 (전통적인 참조 비교가 어려운 상황에서 유용)

  • 다차원 평가: 품질, 일관성, 사실성, 가독성, 사용자 만족도를 포괄적 측정

  • 상세 프롬프트사용자 선호도 기준으로 생성 텍스트 품질 평가


1. RAG 체인 - 평가 대상

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 벡터 저장소 검색기 생성
retriever = vector_store.as_retriever(search_kwargs={"k": 5})

# RAG 체인 
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 템플릿 생성
template = """Answer the question based only on the following context:

[Context]
{context}

[Question]
{query}

[Answer]
"""
prompt = ChatPromptTemplate.from_template(template)

qa_chain = prompt | llm | StrOutputParser()

def format_docs(relevant_docs):
    return "\n".join(doc.page_content for doc in relevant_docs)


query = "Tesla는 언제 누가 만들었나?"

relevant_docs = retriever.invoke(query)
qa_chain.invoke({"context": format_docs(relevant_docs), "query": query})

- 출력

'Tesla는 2003년 7월 1일에 Martin Eberhard와 Marc Tarpenning에 의해 설립되었습니다.'

2. 평가 수행을 위한 데이터셋 전처리

# 데이터 로드
import pandas as pd
testset = pd.read_excel('data/testset.xlsx')

# 데이터 확인
testset.head()

평가 데이터셋 생성

from ragas import EvaluationDataset

# 데이터셋 생성
dataset = []

# 각 행에 대해 RAG 체인을 호출하여 결과를 저장
for row in testset.itertuples():
    query = row.user_input   # 사용자 입력
    reference = row.reference  # 참조 답변
    relevant_docs = retriever.invoke(query)  # 검색된 문서
    response = qa_chain.invoke(      # RAG 체인 생성 답변 생성
        {
            "context": format_docs(relevant_docs),
            "query": query,
        })  
    
    dataset.append(
        {
            "user_input": query,
            "retrieved_contexts": [rdoc.page_content for rdoc in relevant_docs],
            "response": response,
            "reference": reference,
        }
    )

evaluation_dataset = EvaluationDataset.from_list(dataset)

평가 데이터셋 저장

# 데이터 저장
evaluation_dataset.to_pandas().to_csv('data/evaluation_dataset.csv', index=False)

3. 평가 수행

  • LLMContextRecall

    • 검색 기반 응답 생성 시스템의 컨텍스트 검색 성능을 평가
  • Faithfulness

    • 검색된 컨텍스트에 대한 응답의 충실도를 측정하는 메트릭
  • FactualCorrectness

    • 생성된 응답의 사실 정확성을 평가

평가 실행

from ragas import evaluate
from ragas.llms import LangchainLLMWrapper
from ragas.metrics import LLMContextRecall, Faithfulness, FactualCorrectness

# LLM 래퍼 생성
evaluator_llm = LangchainLLMWrapper(llm)

# 평가
result = evaluate(
    dataset=evaluation_dataset,   # 평가 데이터셋
    metrics=[LLMContextRecall(), Faithfulness(), FactualCorrectness()],   # 평가 메트릭
    llm=evaluator_llm,   # LLM 래퍼
    callbacks=[langfuse_handler],   # 콜백
)

result

- 출력

Evaluating: 100%|██████████| 147/147 [01:43<00:00,  1.42it/s]
{'context_recall': 0.8371, 'faithfulness': 0.8920, 'factual_correctness': 0.6106}

평가 결과 확인

# 결과를 데이터프레임으로 변환
result.to_pandas()

평가 결과 저장

# 데이터프레임 저장
result.to_pandas().to_csv('data/evaluation_result.csv', index=False)

0개의 댓글