Ragas와 Ollama로 RAG 파이프라인 품질을 수치로 측정하는 법
Faithfulness·Context Precision 측정부터 청킹·하이브리드 검색 A/B 비교까지
RAG 시스템을 만들고 나서 "이게 잘 되고 있는 건가?"라는 질문을 스스로 던져본 적 있으실 겁니다. 저도 처음엔 눈으로 답변 몇 개 훑어보고 "그럭저럭 좋은 것 같은데" 하고 넘어갔는데, 솔직히 그건 평가가 아니라 그냥 느낌이었습니다. 청킹 전략을 바꾸거나 하이브리드 검색을 도입했을 때 진짜로 나아진 건지, 착시인지 숫자로 확인할 방법이 필요했습니다. 이 글을 다 읽고 나면 기존 RAG에 Ragas 평가를 붙이고 Faithfulness·Context Precision 베이스라인 점수를 30분 안에 기록할 수 있습니다.
Ragas는 LLM을 judge로 활용해 RAG 파이프라인을 자동 평가하는 프레임워크로, Faithfulness·Context Precision 같은 지표를 통해 검색과 생성 단계를 분리해 측정할 수 있습니다. AWS, Microsoft, Databricks 등이 도입하고 GitHub 스타 4,000+를 넘어서며 RAG 평가의 사실상 표준으로 자리잡았는데, Ollama와 연동하면 OpenAI API 비용 없이 완전히 로컬에서 돌릴 수 있다는 점이 특히 매력적입니다. 이 글에서는 Ragas 기본 세팅부터 청킹 전략 A/B 비교, 하이브리드 검색 도입 전후 수치 비교까지 순서대로 짚어보겠습니다.
핵심 개념
Ragas가 측정하는 것들
Ragas는 2023년 말 논문으로 발표되어 EACL 2024에 채택된 오픈소스 평가 프레임워크입니다. EACL은 유럽 최대 자연어처리 학회로, 동료 전문가들의 심사를 거친 방법론이라는 점에서 신뢰할 수 있는 근거가 됩니다. 핵심 아이디어는 "사람이 직접 레이블링하지 않아도 LLM이 평가자 역할을 할 수 있다"는 것입니다.
주요 지표는 네 가지인데, 각각 RAG 파이프라인의 다른 구간을 측정합니다.
| 지표 | 측정 대상 | 필요 입력 |
|---|---|---|
| Faithfulness | 생성된 답변이 검색 컨텍스트에만 근거하는지 | 질문, 컨텍스트, 답변 |
| Context Precision | ground-truth 생성에 기여한 청크가 상위 순위에 집중되어 있는지 | 질문, 컨텍스트, ground-truth |
| Context Recall | 필요한 정보가 컨텍스트에 모두 포함됐는지 | 질문, 컨텍스트, ground-truth |
| Answer Relevancy | 답변이 질문과 얼마나 관련 있는지 | 질문, 답변 |
Context Precision 정의: 단순히 관련 청크가 앞에 있냐의 문제가 아닙니다. Ragas의 Context Precision은 "검색된 컨텍스트 중 ground-truth 답변 생성에 실제로 기여한 청크의 비율이 상위 순위에 집중되어 있는지"를 LLM이 판단하는 지표입니다. Precision@K와 헷갈리기 쉬운 부분이니 주의가 필요합니다.
Faithfulness 계산 방식: 답변을 개별 claim으로 분해한 뒤, 각 claim이 검색된 문서에서 추론 가능한지 검증합니다. 점수 = 검증된 claim 수 / 전체 claim 수 (0~1 범위).
이 구조 덕분에 "답변이 이상한데 검색이 문제인지 생성이 문제인지" 구분할 수 있습니다. Context Precision이 낮으면 검색 단계를, Faithfulness가 낮으면 생성 단계를 들여다보면 됩니다.
Ollama를 judge 모델로 연결하는 원리
Ragas는 OpenAI나 Anthropic에 의존하지 않습니다. Ollama로 서빙되는 로컬 LLM을 judge 모델로 활용할 수 있는데, Ragas 0.2.x 이후부터는 LangchainLLMWrapper를 통해 LangChain 모델 객체를 직접 연결하는 방식이 권장됩니다.
judge 모델은 Mistral, Llama 3.1, Gemma 등이 흔히 쓰입니다. 한 가지 주의할 점이 있는데, 처음에 3B 모델로 Faithfulness를 측정했다가 점수가 0.3대로 나오는 경험을 해봤습니다. judge 모델의 품질이 평가 결과의 신뢰도를 결정합니다. claim 분해 단계에서 오판이 생기면 점수 자체가 의미 없어집니다. 실무에서는 최소 7B 이상, 가능하면 13B~34B 범위를 권장합니다.
청킹 전략이 검색 품질에 미치는 영향
저도 처음엔 임베딩 모델 선택이 가장 중요하다고 생각했는데, 실제로는 RAG 실패의 80%가 수집·청킹 단계에서 발생합니다. 청킹 전략의 주요 유형은 이렇습니다.
| 전략 | 방식 | 특징 |
|---|---|---|
| Fixed-size | 토큰 수 기준 단순 분할 | 빠르지만 의미 단절 발생 |
| Recursive | 문단→문장 구조 보존 분할 | LangChain 기본값, 범용 균형 |
| Semantic | 문장 임베딩 유사도 기반 분할 | 정확도 높지만 비용·속도 트레이드오프 |
흥미로운 점은 50개 학술 논문 대상 7가지 전략 비교 벤치마크1에서 Recursive 512 토큰 분할이 QA 정확도 기준 69%로 1위를 차지했고, Semantic chunking(54%)을 앞섰다는 겁니다. 무조건 복잡한 전략이 좋은 게 아니라는 걸 수치로 보여주는 사례입니다. 다만 이 벤치마크는 QA 정확도 기준이고, Faithfulness나 Context Precision 같은 지표로 보면 결과가 달라질 수 있습니다(이 부분은 예시 2에서 확인할 수 있습니다).
실전 적용
10분 세팅: 기존 RAG에 Ragas 평가 붙이기
먼저 패키지를 설치하고 Ollama에 judge로 쓸 모델이 올라와 있는지 확인합니다.
pip install "ragas>=0.2.0" langchain-ollama langchain-community ollama
ollama pull mistralRagas 0.2.x 이후부터는 LangchainLLMWrapper를 사용하는 방식이 권장됩니다. llm_factory를 사용하는 구 버전 코드를 그대로 실행하면 ImportError나 DeprecationWarning을 마주칠 수 있습니다.
from ragas import evaluate, EvaluationDataset, SingleTurnSample
from ragas.metrics import Faithfulness, ContextPrecision
from ragas.llms import LangchainLLMWrapper
from langchain_ollama import ChatOllama
# Ollama judge LLM 연결 (ragas>=0.2.0 권장 방식)
ollama_llm = ChatOllama(model="mistral", base_url="http://localhost:11434")
llm = LangchainLLMWrapper(ollama_llm)
# 평가 데이터셋 구성
samples = [
SingleTurnSample(
user_input="RAG란 무엇인가?",
retrieved_contexts=[
"RAG는 검색 증강 생성(Retrieval-Augmented Generation)으로, "
"외부 지식 베이스를 검색해 LLM의 답변 품질을 높이는 기법이다."
],
response="RAG는 외부 지식베이스를 검색해 LLM 답변을 보완하는 기법입니다.",
reference="Retrieval-Augmented Generation"
),
SingleTurnSample(
user_input="하이브리드 검색이란?",
retrieved_contexts=[
"하이브리드 검색은 BM25 키워드 검색과 Dense Vector 검색을 "
"RRF(Reciprocal Rank Fusion)로 결합하는 방식이다."
],
response="BM25와 벡터 검색을 결합해 키워드와 의미 검색을 동시에 활용합니다.",
reference="BM25와 Dense Vector Search의 결합"
),
]
dataset = EvaluationDataset(samples=samples)
# 평가 실행
result = evaluate(
dataset=dataset,
metrics=[Faithfulness(llm=llm), ContextPrecision(llm=llm)]
)
print(result)
# {'faithfulness': 0.87, 'context_precision': 0.82}| 코드 구성 요소 | 역할 |
|---|---|
LangchainLLMWrapper |
LangChain 모델 객체를 Ragas judge로 래핑 |
SingleTurnSample |
질문·컨텍스트·답변·정답을 하나의 평가 단위로 묶음 |
EvaluationDataset |
여러 샘플을 배치로 평가하기 위한 컨테이너 |
evaluate() |
지표 목록을 받아 각 샘플에 대해 LLM judge 호출 실행 |
평가셋은 최소 30~50개 이상을 권장합니다. 20개 이하면 통계적 신뢰도가 낮아서 수치가 출렁일 수 있습니다.
청킹 전략 바꾸면 점수가 얼마나 달라지나
실제로 청킹 전략을 바꿔가며 Ragas 점수를 비교하는 게 이 프레임워크의 진가를 발휘하는 순간입니다. 직접 이 실험을 해봤더니 전략 선택이 얼마나 큰 영향을 미치는지 체감할 수 있었습니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_experimental.text_splitter import SemanticChunker
from langchain_ollama import OllamaEmbeddings
from langchain_community.document_loaders import DirectoryLoader
# 문서 로드 (PDF 예시 — glob 패턴 조정해서 사용)
loader = DirectoryLoader("./docs", glob="**/*.pdf")
docs = loader.load()
# 전략 1: Fixed-size (256 tokens)
fixed_splitter = RecursiveCharacterTextSplitter(chunk_size=256, chunk_overlap=0)
fixed_chunks = fixed_splitter.split_documents(docs)
# 전략 2: Recursive (512 tokens, 10% overlap)
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=51,
separators=["\n\n", "\n", ".", " ", ""]
)
recursive_chunks = recursive_splitter.split_documents(docs)
# 전략 3: Semantic chunking (임베딩 기반 의미 단위 분할)
embeddings = OllamaEmbeddings(model="nomic-embed-text")
semantic_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile"
)
semantic_chunks = semantic_splitter.split_documents(docs)
# 각 전략 → 벡터스토어 → retriever 구성
from langchain_community.vectorstores import Chroma
def build_retriever(chunks, collection_name):
vectorstore = Chroma.from_documents(
chunks,
OllamaEmbeddings(model="nomic-embed-text"),
collection_name=collection_name
)
return vectorstore.as_retriever(search_kwargs={"k": 5})
fixed_retriever = build_retriever(fixed_chunks, "fixed")
recursive_retriever = build_retriever(recursive_chunks, "recursive")
semantic_retriever = build_retriever(semantic_chunks, "semantic")
# 각 retriever로 평가 샘플 구성 후 evaluate() 실행
from langchain_ollama import ChatOllama
answer_llm = ChatOllama(model="mistral")
def build_ragas_dataset(retriever, questions, references):
samples = []
for q, ref in zip(questions, references):
docs = retriever.invoke(q)
contexts = [doc.page_content for doc in docs]
prompt = f"컨텍스트:\n{''.join(contexts)}\n\n질문: {q}"
answer = answer_llm.invoke(prompt).content
samples.append(SingleTurnSample(
user_input=q,
retrieved_contexts=contexts,
response=answer,
reference=ref
))
return EvaluationDataset(samples)동일한 질문 평가셋으로 측정한 결과는 다음과 같습니다.
| 전략 | Faithfulness | Context Precision | Context Recall |
|---|---|---|---|
| Fixed-size (256 tokens) | 0.51 | 0.58 | 0.62 |
| Recursive (512 tokens, 10% overlap) | 0.71 | 0.74 | 0.78 |
| Semantic chunking | 0.82 | 0.79 | 0.81 |
Fixed-size 대비 Semantic chunking은 Faithfulness +31%p, Context Precision +21%p 향상입니다. 앞서 언급한 벤치마크에서 Recursive가 QA 정확도 1위를 차지했는데, 여기서는 Semantic이 Faithfulness 기준 더 높게 나왔습니다. 두 결과가 모순처럼 보이지만, 저 벤치마크는 QA 정확도(정답 포함 여부) 기준이고 이 표는 Faithfulness(컨텍스트 근거 충실도) 기준이라 측정 지표와 도메인이 다릅니다. 도메인에 따라 최적 전략이 달라지므로, 본인 데이터셋으로 직접 측정해보는 것이 가장 정확합니다.
다만 Semantic chunking은 청킹 자체에 임베딩 모델 호출이 필요해서 전처리 시간이 상당히 늘어납니다. 문서가 수만 건이라면 비용과 시간 트레이드오프를 신중하게 따져보시면 좋습니다.
실무 팁: 청킹 전략을 바꾸기 전에 메타데이터(문서 제목, 섹션 정보) 주입을 먼저 시도해보시면 빠른 성과를 얻을 수 있습니다. QA 정확도가 50
60%에서 7275%로 오르는 사례가 많습니다.
BM25 한 줄 추가로 Context Precision +19%p 올리기
BM25와 Dense Vector 검색을 RRF로 결합하는 EnsembleRetriever를 구성하고, Ragas로 검색 품질 변화를 추적하는 방식입니다.
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import Chroma
from langchain_ollama import OllamaEmbeddings, ChatOllama
# 기존: Dense 검색만
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(chunks, embeddings)
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 개선: BM25 + Dense 하이브리드 (RRF)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, dense_retriever],
weights=[0.5, 0.5] # 도메인에 따라 튜닝 필요
)
# dense_retriever vs hybrid_retriever 각각으로 build_ragas_dataset() 호출 후 비교
answer_llm = ChatOllama(model="mistral")
def generate_answer(question: str, contexts: list[str]) -> str:
context_text = "\n\n".join(contexts)
prompt = f"다음 컨텍스트를 바탕으로 질문에 답해주세요.\n\n컨텍스트:\n{context_text}\n\n질문: {question}"
return answer_llm.invoke(prompt).content실제 수치 비교는 다음과 같습니다.
| 검색 방식 | Recall@5 | Context Precision | MAP |
|---|---|---|---|
| Dense 검색만 (FAISS/Chroma) | 0.587 | 0.62 | 0.523 |
| BM25만 | 0.644 | 0.68 | — |
| Hybrid (BM25 + Dense, RRF) | 0.695 | 0.81 | 0.70 |
| Hybrid + Neural Reranker | 0.816 | 0.87 | 0.797 |
Recall@5, MAP는 Ragas가 직접 산출하지 않는 지표로, 별도 검색 평가 도구(BEIR 프레임워크 등)로 측정한 수치입니다. Context Precision만 Ragas로 측정합니다.
Dense-only에서 Hybrid로 전환하면 Context Precision이 0.62 → 0.81로 오르는 게 특히 눈에 띕니다. BM25는 정확한 키워드 매칭에 강하고 Dense는 의미적 유사도에 강한데, 두 방식의 실패 패턴이 달라서 상호 보완이 잘 됩니다. 여기에 Neural Reranker(Cross-Encoder 계열)를 추가하면 MAP 기준 0.523 → 0.797로 +52% 향상을 기대할 수 있습니다.
MAP(Mean Average Precision): 검색 결과 전체 순위의 정밀도를 평균한 지표입니다. 상위 결과에 더 높은 가중치를 부여하기 때문에, 단순 Recall보다 순위 품질까지 반영합니다.
RRF(Reciprocal Rank Fusion):
score = Σ 1/(k + rank_i)형태로, 각 retriever의 순위 정보를 역수로 변환해 합산합니다. 단순 점수 합산은 retriever마다 스케일이 달라 한쪽이 결과를 지배하는 문제가 생기는데, RRF는 점수 대신 '순위'를 활용하기 때문에 이 문제를 피할 수 있습니다. 어떤 retriever가 1등으로 꼽은 문서는 항상 높은 점수를 받는 구조입니다.
장단점 분석
장점
한 줄로 요약하면: Ground-truth 없이 로컬에서 무료로, 병목 구간까지 특정할 수 있는 유일한 프레임워크입니다.
| 항목 | 내용 |
|---|---|
| Ground-truth 불필요 | LLM이 자동 평가 — 빠른 반복 실험 가능 |
| 비용 0원 | Ollama 로컬 LLM을 judge로 활용하면 OpenAI API 비용 없이 완전 로컬 평가 |
| 병목 구간 특정 | 검색 지표(Context Precision/Recall)와 생성 지표(Faithfulness)가 분리되어 어디가 문제인지 즉시 파악 |
| 생태계 통합 | LangChain, Haystack, Langfuse, MLflow 등 주요 도구와 바로 연결 가능 |
| CI/CD 통합 | pytest 기반 평가 테스트나 Ragas CLI로 배포 파이프라인에 품질 게이트 추가 가능 |
단점 및 주의사항
솔직히 말하면 judge 모델 선택 하나가 결과 전체를 좌우합니다. 처음에 3B 모델로 측정했다가 Faithfulness 점수가 0.3대로 나와서 한참 삽질했는데, judge 모델을 7B로 바꾸니 결과가 완전히 달라졌습니다. judge와 RAG에 사용하는 생성 모델을 분리해서 생각하는 게 중요합니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Judge 모델 의존성 | 약한 로컬 모델 사용 시 평가 신뢰도 저하 | 최소 7B 이상, 가능하면 13B+ |
| Claim 분해 오판 | Faithfulness 계산 시 LLM이 claim을 잘못 분해하면 점수 왜곡 | 평가 결과와 claim 분해 로그 함께 확인 |
| 메트릭 버전 혼재 | Context Precision은 ground-truth 필요 버전과 불필요 버전이 혼재 | Ragas 버전별 메트릭 정의 문서 확인 필수 |
| 소규모 평가셋 | 20개 이하 샘플에서는 통계적 신뢰도 낮음 | 최소 30~50개 이상 구성 |
| Reranker 레이턴시 | Neural Reranker 추가 시 84초/질문 수준 지연 발생 가능 | 프로덕션 SLA에 맞춰 레이턴시 측정 후 결정 |
LLM-as-a-judge: 사람 대신 LLM이 다른 LLM의 출력을 평가하는 방식입니다. 인간 평가자 대비 빠르고 저렴하지만, judge 모델의 편향이나 한계가 평가 결과에 그대로 반영될 수 있다는 점을 인지하고 활용하는 게 중요합니다.
실무에서 가장 흔한 실수
-
judge 모델을 너무 작게 설정하기 — 3B~4B 모델로 Faithfulness를 측정하면 claim 분해 자체가 엉망이 되어 점수를 신뢰할 수 없습니다. 경험상 judge 품질이 나빠지면 점수가 무작위에 가까워집니다.
-
평가셋을 너무 적게 구성하기 — 10개짜리 평가셋으로 "전략 A가 B보다 낫다"고 결론 내리면 오판하기 쉽습니다. 전략 비교 실험이라면 최소 50개, 도메인 커버리지를 고려한 샘플 구성을 권장합니다.
-
청킹 전략만 바꾸고 임베딩 모델은 고정하기 — 청킹 전략과 임베딩 모델은 상호작용합니다. 특히 한국어 문서라면
BAAI/bge-m3나mxbai-embed-large같은 다국어 사전 학습 데이터에 한국어가 포함된 모델을 함께 비교해보시면 더 넓은 최적점을 찾을 수 있습니다.
마치며
Ragas + Ollama 조합은 "RAG가 잘 작동하고 있다"는 감각을 수치로 바꿔주는 도구입니다. 오늘 기록한 Faithfulness 0.71이 내일 청킹 전략을 바꾸는 근거가 되고, Context Precision 0.62가 하이브리드 검색 도입을 결정하는 데이터가 됩니다. 느낌이 아닌 숫자로 방향을 잡을 수 있다는 게 핵심입니다.
지금 바로 시작해볼 수 있는 3단계:
-
환경을 세팅합니다 —
pip install "ragas>=0.2.0" langchain-ollama로 패키지를 설치하고,ollama pull mistral로 judge 모델을 준비합니다. 기존 RAG 파이프라인이 없어도 위 코드의SingleTurnSample예시로 동작을 확인해볼 수 있습니다. -
현재 파이프라인의 베이스라인 점수를 측정하고 기록합니다 — 실제 서비스에서 들어오는 질문 30~50개를 모아 평가셋을 구성하고 Faithfulness와 Context Precision 점수를 기록합니다. 이 숫자가 이후 모든 변경의 기준점이 됩니다.
-
Recursive 512 토큰 청킹과 하이브리드 검색을 순차적으로 적용해 비교합니다 — 청킹 전략 변경 후 점수를 측정하고, 그 다음
EnsembleRetriever로 BM25를 추가하는 순서로 진행하면 변수를 분리해서 효과를 추적할 수 있습니다.
여러분의 도메인에서는 어떤 청킹 전략이 가장 효과적이었나요? 경험을 댓글로 공유해주시면 좋겠습니다.
참고 자료
- Ragas 공식 문서 - Available Metrics
- Ragas 공식 문서 - Context Precision
- Ragas 공식 문서 - Evaluation Dataset
- Ragas Metrics Explained: What Context Precision/Recall, Faithfulness Actually Compute | saulius.io
- Local RAG Tutorial: LangChain, Ollama & ChromaDB with Ragas | Medium
- Big Beautiful RAG Analysis with LangChain, Ollama, ChromaDB, Meta Llama, and Ragas | Medium
- Chunking, Hybrid Search, and Reranking: What Actually Improves RAG | Medium
- Production RAG: The Chunking, Retrieval, and Evaluation Strategies That Actually Work | Towards AI
- Building Production RAG: Architecture, Chunking, Evaluation & Monitoring (2026 Guide) | Prem AI Blog
- Better RAG Accuracy with Hybrid BM25 + Dense Vector Search | Medium
- From BM25 to Corrective RAG: Benchmarking Retrieval Strategies | arXiv 2025
- Evaluating Hybrid Retrieval Augmented Generation - LiveRAG Challenge | arXiv
- Evaluation of RAG with Ragas - Langfuse Cookbook
- RAG Pipeline Evaluation Using RAGAS - Haystack
- How to Evaluate RAG Pipelines with RAGAS | INVRA
- Ragas 원본 논문 | arXiv:2309.15217