RAG 연습 (3) — Ragas는 base_url 한 줄로 로컬 LLM에 붙는다
RAG 연습 (2)에서 retriever를 만들었으니 다음은 평가입니다. “이 시스템이 얼마나 잘 답하나”를 수치로 보고 싶었습니다. Ragas(Retrieval Augmented Generation Assessment)가 그걸 해주는 프레임워크입니다. RAG 파이프라인 전용으로 만들어진 평가 도구입니다.
문제는 Ragas 공식 예제가 전부 OpenAI API 전제라는 점이었습니다. 로컬 LLM으로 돌리는 방법은 공식 문서에 흩어져 있어서 찾는 데 시간이 걸렸습니다.
Ragas 4메트릭
Ragas는 RAG 파이프라인을 4가지 관점에서 점수 매깁니다.
faithfulness는 “답변이 검색 문서에만 근거하는가”를 봅니다. 답변을 문장 단위로 쪼갠 뒤 컨텍스트가 그 문장을 지지하는지 확인합니다. answer_relevancy는 답변에서 거꾸로 질문을 생성한 뒤 원래 질문과의 임베딩 유사도를 잽니다. context_precision은 검색된 청크 각각이 정답과 관련 있는지를 평균 낸 값이고, context_recall은 반대 방향으로 정답을 분해해 컨텍스트가 그걸 얼마나 커버하는지를 측정합니다.
이 중 faithfulness와 context_recall은 평가용 LLM이 직접 채점합니다. “이 문장이 저 컨텍스트로 지지되나?”는 단순 문자열 비교로 판단할 수 없어서, Ragas가 또 다른 LLM에게 물어보고 답을 점수로 환산합니다. RAG 답변을 만든 LLM과는 별개로, 채점관 역할을 하는 LLM이 따로 필요합니다.
ChatOpenAI에 base_url만 바꿔 끼우기
vLLM 같은 로컬 LLM 서버는 OpenAI와 똑같은 형식의 API를 제공합니다. 요청 경로(/v1/chat/completions)도, 요청·응답 JSON 구조도 동일합니다. 그래서 OpenAI용 클라이언트인 ChatOpenAI에 접속 주소(base_url)만 로컬 주소로 바꿔 주면 라이브러리는 자기가 OpenAI와 통신하는 줄 알고 그대로 작동합니다. Ragas는 LangChain 래퍼를 받는 구조라 이 ChatOpenAI를 그대로 넘기면 평가용 LLM도 로컬로 돌아갑니다.
pip install "ragas>=0.2" langchain-openai datasets로 깔고, 핵심 설정만 보면 이렇습니다.
1
2
3
4
5
6
7
8
9
10
11
from langchain_openai import ChatOpenAI
from ragas.llms import LangchainLLMWrapper
llm = ChatOpenAI(
base_url="http://localhost:8000/v1", # vLLM 주소
api_key="local", # 임의값. 빈 문자열은 거부됨
model="solar-10.7b-instruct",
temperature=0,
max_tokens=512,
)
ragas_llm = LangchainLLMWrapper(llm)
임베딩도 같은 방식입니다. OpenAIEmbeddings의 base_url을 infinity-emb 주소(http://localhost:8001/embeddings)로 바꾸고 LangchainEmbeddingsWrapper로 감싸면 됩니다. 작업 시 버전은 ragas 0.4.3, langchain-openai 1.2.1이었습니다.
준비한 LLM·임베딩은 메트릭 객체에 꽂아 줍니다. m.llm = ragas_llm 식으로 한 줄씩 넣고, 4개 메트릭 중 answer_relevancy만 임베딩을 추가로 받습니다.
평가 데이터셋
Ragas는 Hugging Face datasets.Dataset을 입력으로 받습니다. 컬럼 4개가 필수입니다. question은 사용자 질문, answer는 RAG 파이프라인이 만든 답변, contexts는 검색된 청크 목록, ground_truth는 정답 참조 텍스트입니다.
1
2
3
4
5
6
{
"question": "미국 법인에 지급하는 SW 사용료의 원천세율은?",
"answer": "한-미 조세조약 제12조에 따라 10%입니다. [출처: 법인세법 제98조]",
"contexts": ["법인세법 제98조 조문 내용...", "조세조약 제12조 내용..."],
"ground_truth": "한-미 조세조약 거주자증명서 취득 시 원천세율 10%이다.",
}
평가 실행
1
2
3
4
from ragas import evaluate
result = evaluate(dataset, metrics=metrics)
# {'faithfulness': 0.847, 'answer_relevancy': 0.912,
# 'context_precision': 0.783, 'context_recall': 0.801}
Solar-10.7B AWQ 기준 질문당 3~5초 정도 걸립니다 (RTX 3090, 컨텍스트 8000 토큰). 30개 골든셋(미리 만들어 둔 평가용 질문·정답 묶음)이면 2~3분 안에 끝납니다.
골든셋이 점수의 절반
Ragas 점수는 골든셋 품질에 강하게 의존합니다. 30개 QA를 만들 때 지킨 원칙은 두 가지였습니다.
첫째, 도메인을 균등하게 섞었습니다. 수익인식 8개, 세무 리스크 8개, 부정 탐지 7개, 계약판례 7개. 한 도메인에 쏠리면 전체 점수가 그 도메인 검색 품질만 반영합니다.
둘째, ground_truth에 구체적인 수치와 조문 번호를 박았습니다. “납부지연 가산세는 미납세액의 몇 퍼센트”가 아니라, “국세기본법 제47조의3에 따라 미납세액 × 경과일수 × 0.022% / 1일, 한도는 미납세액의 75%”처럼 적습니다. “0.022%” 같은 정확한 수치가 들어가야 context_recall이 검색 품질을 제대로 반영합니다. 두루뭉술한 정답은 두루뭉술한 점수를 만듭니다.
골든셋이 일관된지 확인하는 방법이 하나 있습니다. ground_truth를 그대로 answer 칸에 넣고 핵심 키워드 매칭률을 봅니다. 100%가 안 나오는 항목은 정답과 키워드가 어긋나 있다는 뜻이라 골든셋 자체를 손봐야 합니다.
키워드 폴백을 같이 둔 이유
Ragas는 평가 LLM이 살아있어야 돌아갑니다. 개발 중에 vLLM이 꺼져 있거나 GPU를 다른 작업에 쓰는 동안에도 빠르게 회귀를 보고 싶었습니다. 그래서 답변에 정답 키워드가 몇 개나 들어 있나(hit / total)만 세는 단순 점수를 폴백으로 같이 씁니다.
이 폴백이 Ragas의 context_recall과 0.85 정도의 상관을 보입니다. 정확한 측정은 아니지만 “회귀가 일어났는지” 정도는 잡힙니다. 평가 LLM이 살아있을 때의 본격 측정과, LLM 없이도 도는 가벼운 회귀 측정을 나눠 두면 개발 사이클이 빨라집니다.
Solar로 Ragas 돌릴 때 발 묶이는 지점들
max_tokens=512가 최소값입니다. Ragas는 내부적으로 LLM에 긴 프롬프트를 여러 번 보내는데, 짧으면 메트릭 계산 도중 응답이 잘려서 NaN이 나옵니다. 처음에 이걸 몰라서 점수가 비어 있는 게 모델 문제인 줄 알았습니다.
temperature=0은 필수입니다. 평가는 결정론적이어야 같은 데이터를 두 번 돌렸을 때 점수가 일치합니다.
한국어 입력에서 faithfulness가 10~15% 낮게 나오는 경향도 있습니다. Ragas 내부 평가 프롬프트가 영어 기반이라 그렇습니다. 0.4.x에서는 metric._faithfulness_prompt로 한국어 프롬프트를 오버라이드할 수 있습니다.
context 길이도 신경 써야 합니다. 청크 5개에 청크당 300자 정도, 합쳐서 1,500자 내외로 유지합니다. 너무 길면 Solar의 컨텍스트를 초과해 환각이 늘어납니다.
결과 해석 기준
대략 이런 기준으로 봤습니다.
- 0.85 이상: 운영 가능 수준
- 0.70 ~ 0.85: 프롬프트 개선 또는 청크 전략 재검토
- 0.70 미만: 검색 파이프라인 또는 데이터 품질 문제
35개 체크리스트 테스트를 통과시키는 동안 점수가 이렇게 움직였습니다.
1
2
3
4
초기 60% → 한국어 강제 시스템 프롬프트 추가
86% → 출처 형식 구체적 예시로 교체
97% → 유저 템플릿에 출처 요구 추가
100% → _ensure_citation() 후처리기 추가
Ragas 점수보다 먼저 결정론적인 체크리스트를 통과시키고, 그다음 Ragas로 수치화하는 순서가 효율적이었습니다. Ragas는 평가 LLM이 직접 채점하는 방식이라 점수 자체에 노이즈가 있습니다. 노이즈가 있는 지표로 디버깅하면 어디를 고쳐야 할지 헷갈립니다. 결정론적 테스트로 굵직한 문제를 먼저 잡고, Ragas는 마지막에 품질을 확인하는 용도로 썼습니다.
faithfulness가 낮으면 LLM 환각, context_recall이 낮으면 검색 품질. 메트릭이 어느 단계가 약한지 가리켜 줍니다. 그래서 Ragas를 씁니다.
(클로드 코드의 도움을 받았습니다.)