RAG 기술의 핵심: 외부 지식 검색과 LLM 결합으로 응답 품질 향상
평가 기준: LLM-as-judge 방식으로 사실성, 관련성, 충실도, 유용성 평가
체계적인 A/B 테스트: 각 컴포넌트별 성능 비교 및 영향도 분석으로 최적 구성 도출
평가 방법론: 오프라인(참조답변 기반), 온라인(실시간), 페어와이즈(비교) 평가 구분
검색(Retrieval) 단계:
생성(Generation) 단계:
추가 고려사항:
[출처] https://arxiv.org/abs/2405.07437
데이터셋 구성 방식: LLM 기반 새로운 데이터셋 생성 (Synthetic Data)
맞춤형 데이터셋 구축으로 RAG 시스템의 실용성 평가 강화
[실습] : Ragas (https://docs.ragas.io/en/stable/) 활용
RAG 시스템 평가를 위한 오픈소스 프레임워크
실용성: 자동화된 평가 파이프라인 구축 가능
주요 지표:
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
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"))
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 저장
test_data.to_csv('./data/ragas_testset.csv', index=False)
Non-Rank Based Metrics: Accuracy, Precision, Recall@k 등을 통해 관련성의 이진적 평가를 수행
Rank-Based Metrics: MRR(Mean Reciprocal Rank), MAP(Mean Average Precision)를 통해 검색 결과의 순위를 고려한 평가를 수행
RAG 특화 지표: 기존 검색 평가 방식의 한계를 보완하는 LLM-as-judge 방식 도입
포괄적 평가: 정확도, 관련성, 다양성, 강건성을 통합적으로 측정
전통적 평가: ROUGE(요약), BLEU(번역), BertScore(의미 유사도) 지표 활용
LLM 기반 평가: 응집성, 관련성, 유창성을 종합적으로 판단하는 새로운 접근법 도입 (전통적인 참조 비교가 어려운 상황에서 유용)
다차원 평가: 품질, 일관성, 사실성, 가독성, 사용자 만족도를 포괄적 측정
상세 프롬프트와 사용자 선호도 기준으로 생성 텍스트 품질 평가
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에 의해 설립되었습니다.'
# 데이터 로드
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)
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)