지난 글에서 지식 그래프를 활용한 Graph RAG를 배웠다. 다양한 RAG 기법을 익혔다면 이제 자연스럽게 드는 질문은 「어떻게 내 RAG 시스템이 잘 작동하는지 알 수 있나」다. 답이 좋아 보인다는 인상만으로는 아무것도 못 고친다 — 프롬프트를 바꿔야 할지, 청크 크기를 바꿔야 할지, 임베딩 모델을 갈아야 할지가 인상에서는 나오지 않기 때문이다.
RAG 평가가 다른 평가와 다른 점은 고칠 자리가 둘이라는 데 있다. 검색이 정답 문서를 안 데려왔을 수도 있고, 데려왔는데 모델이 그것을 안 썼을 수도 있다. 겉으로 나타나는 증상은 같고 — 답이 틀렸다 — 고칠 자리는 정반대다. 그래서 평가의 첫 일은 점수를 매기는 것이 아니라 두 단계를 갈라 놓는 것이다.
검색 지표와 생성 지표
검색 단계의 세 지표
검색은 순위 문제이므로 정보 검색 분야의 지표를 그대로 쓴다. 셋이면 충분하다.
Recall@k는 정답 문서가 상위 k개 안에 들어왔는지를 본다. 들어왔으면 1, 아니면 0이고, 전체 질문에 대해 평균을 낸다. 생성 단계에 넘기는 조각 수가 k이므로, 이 값이 낮으면 그 아래 무엇을 해도 소용이 없다 — 모델에게 정답이 애초에 전달되지 않은 것이다.
MRR은 정답이 몇 번째에 있었는지를 본다. 첫 번째면 1, 세 번째면 3분의 1로 매기고 평균한다. Recall@k가 「들어왔나」라면 MRR은 「얼마나 위에 있나」다. 둘을 함께 보는 이유는, 정답이 열 번째로 겨우 들어오는 상태와 첫 번째로 오는 상태가 Recall@10에서는 구별되지 않기 때문이다.
nDCG는 정답이 하나가 아닐 때 쓴다. 문서마다 관련도를 여러 단계로 매기고, 관련도가 높은 것이 위에 올수록 점수를 높게 준다. 골든셋에 정답 문서를 하나만 붙였다면 nDCG는 MRR과 거의 같은 값을 주므로 굳이 둘 다 볼 필요가 없다.
숫자를 한 번 따라가 보면 왜 둘을 함께 보는지가 분명해진다. 질문 네 개를 돌렸고 정답이 각각 1위·2위·9위·순위 밖에 있었다고 하자. Recall@10은 4분의 3으로 0.75이고, MRR은 1과 0.5와 9분의 1과 0을 더해 넷으로 나눈 0.40이다. 여기서 청크 크기를 바꿨더니 세 번째 질문의 정답이 9위에서 2위로 올라왔다면 Recall@10은 0.75로 그대로지만 MRR은 0.50으로 오른다. Recall만 보고 있었다면 이 변경을 「효과 없음」으로 기록하고 되돌렸을 것이다.
반대 방향의 착시도 있다. k를 5에서 20으로 늘리면 Recall@k는 거의 반드시 오르지만, 그 정답이 20번째로 들어온 것이라면 생성 단계에서는 뒤쪽 조각에 묻혀 쓰이지 않는다. 그래서 k를 바꾼 실험에서는 Recall@k의 상승을 성과로 읽지 않는다 — 같은 k에서 MRR이 함께 올랐는지를 본다.
생성 단계의 물음
생성 지표는 「주어진 조각으로 답을 제대로 만들었나」만 묻는다. 검색이 실패한 질문은 생성 지표에서 빼야 한다 — 없는 근거로 답을 못 만든 것은 생성의 잘못이 아니고, 섞어서 평균 내면 두 단계의 점수가 서로를 가린다.
이 분리를 안 하면 흔히 이런 상태에 빠진다. 전체 정확도가 60퍼센트라는 값 하나만 들고 있고, 그 40퍼센트의 실패 중 몇이 검색 탓이고 몇이 생성 탓인지 모르는 상태다. 갈라 세어 보면 대개 한쪽이 압도적이다. 검색 실패가 30이고 생성 실패가 10이면 그날 할 일은 검색이고, 프롬프트 손질은 나중 일이다.
고치는 순서
순서는 늘 검색이 먼저다. 검색 지표를 올린 다음에 생성 지표를 보고, 검색 지표가 바닥인 채로 프롬프트를 손보는 일은 하지 않는다. 이 순서를 지키지 않으면 프롬프트를 열 번 고치고도 점수가 안 움직이는 시간을 보내게 된다.
순서에는 실용적인 이유가 하나 더 있다. 검색 지표는 모델 호출 없이 계산되므로 싸고 빠르다. 색인을 다시 만드는 시간만 있으면 설정 열 가지를 한 시간에 비교할 수 있다. 반면 생성 지표는 질문마다 답을 만들고 그 답을 다시 채점해야 해서 호출이 두 배 이상 든다. 싼 실험으로 좁힐 수 있는 것을 먼저 좁히고, 비싼 실험은 남은 자리에 쓴다.
골든셋
지표를 계산하려면 질문과 정답이 필요하다. 그 묶음이 골든셋이다. 여기 들인 품이 평가 전체의 상한을 정하므로, 도구를 고르는 것보다 이쪽이 먼저다.
질문 뽑기
질문은 지어내지 말고 실제 트래픽에서 뽑는다. 직접 만든 질문은 대개 문서의 문장을 그대로 베낀 형태가 되어 검색이 쉽게 맞히고, 그러면 골든셋이 통과 의례가 된다. 로그에서 뽑을 때는 빈도 상위만 고르지 말고 꼬리 쪽에서도 섞는다 — 자주 오는 질문은 이미 잘 맞고 있을 가능성이 높아 개선 여지를 못 보여 준다.
로그가 아직 없는 초기라면 문서를 읽고 질문을 만들되, 만든 사람과 검수하는 사람을 갈라 둔다. 문서를 쓴 사람이 만든 질문은 그 문서의 낱말을 그대로 쓰게 되어 있고, 그게 골든셋을 쉽게 만드는 가장 흔한 경로다. 실제 사용자는 사내 용어 대신 일상어로 묻는다 — 「연차 촉진 제도」 대신 「휴가 안 쓰면 어떻게 되나요」로 온다.
정답 문서 붙이기
질문마다 답이 실린 문서를 손으로 지정한다. 이 작업이 가장 지루하고 가장 많이 건너뛰는 자리인데, 생략하면 검색 지표를 아예 계산할 수 없다. 문서가 아니라 청크 단위로 붙여 두면 청크 크기를 바꿀 때마다 다시 붙여야 하므로, 문서 id로 붙이고 청크는 그 문서에 속하는지로 판정하는 편이 오래간다.
답이 두 문서에 걸쳐 있는 질문은 정답 문서를 둘 다 적고, 둘 다 들어와야 맞은 것으로 센다. 하나만 들어와도 맞은 것으로 세면 비교 질문이 전부 통과해 버려서, 정작 RAG가 가장 자주 무너지는 유형이 평가에서 빠진다.
이 작업은 한 번에 끝내려 하지 말고 쌓는다. 서비스에서 틀린 답이 나올 때마다 그 질문과 실제 정답 문서를 골든셋에 추가하는 습관이 가장 값싸게 좋은 셋을 만든다. 실패에서 자란 골든셋은 정의상 우리 시스템이 약한 자리에 몰려 있다.
답할 수 없는 질문
골든셋의 10~20퍼센트는 답할 수 없는 질문으로 채운다. 우리 문서에 답이 없는 질문이고, 정답은 「모른다고 답하는 것」이다. 이걸 안 섞으면 평가가 한쪽만 본다 — 모든 질문에 답이 있는 셋에서는 뭐라도 지어내는 모델이 가장 높은 점수를 받기 때문이다. 실제 서비스에서 가장 비싼 실패가 바로 그 지어내기다.
이 질문들의 점수는 다른 지표와 갈라 따로 센다. 정답이 「모른다」이므로 Faithfulness나 Answer Relevance를 적용하면 값이 이상하게 나온다. 대신 거부율 하나를 본다 — 답할 수 없는 질문 가운데 실제로 모른다고 답한 비율이다. 이 값과 나머지 질문의 정확도를 나란히 놓으면 모델이 지나치게 조심스러워졌는지도 함께 보인다.
규모
몇 건이면 되는가는 지표가 흔들리는 폭으로 정한다. 같은 시스템을 두 번 돌려 점수가 2퍼센트포인트씩 튀는데 개선폭이 1퍼센트포인트라면 그 골든셋으로는 아무것도 판단할 수 없다. 실무에서는 100건이 시작선이고 300건쯤이면 대부분의 변경을 가를 수 있다. 무작정 늘리기보다 갈래별로 고르게 담는 편이 값이 크다 — 단일 조회·비교·집계 질문이 섞여 있어야 어느 유형에서 무너지는지가 보인다.
합성 질문 생성기는 골든셋의 초안을 만드는 데까지만 쓴다. 사람이 훑어 걸러 내지 않은 합성 셋은 검증이 아니라 자기 확인에 가깝다. 생성기가 문서에서 질문을 만들 때 그 문서의 표현을 그대로 가져가므로, 걸러 내지 않으면 앞서 말한 쉬운 골든셋이 자동으로 만들어진다. 훑으면서 두 가지만 본다 — 질문이 문서 문장의 복사인지, 그리고 답이 정말 그 문서에만 있는지다.
from ragas.testset import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
generator = TestsetGenerator.from_langchain(
generator_llm=ChatAnthropic(model="claude-sonnet-4-6"),
critic_llm=ChatAnthropic(model="claude-sonnet-4-6"),
embeddings=OpenAIEmbeddings(),
)
testset = generator.generate_with_langchain_docs(
documents=docs,
test_size=50,
distributions={
simple: 0.5, # 단순 사실 질문
reasoning: 0.3, # 추론 필요 질문
multi_context: 0.2, # 여러 문서 필요 질문
},
)
RAGAS 네 지표
RAGAS는 위의 두 단계를 네 개의 값으로 나눠 재는 오픈소스 평가 도구다. 네 지표를 하나씩 보는 것보다, 어느 조합이 나왔을 때 무엇을 고치는지를 읽는 편이 실전에서 쓸모 있다.
Faithfulness
답변이 주어진 조각에 근거했는지를 본다. 답에서 주장을 하나씩 뽑고, 각 주장이 조각으로 지지되는지 확인해 지지된 주장의 비율을 낸다. 환각을 잡는 지표이고, 낮으면 모델이 조각 밖의 지식을 끌어다 쓰고 있다는 뜻이다.
주장 단위로 쪼개는 것이 이 지표의 핵심이다. 답 전체를 놓고 「근거가 있나」를 물으면 심판은 대체로 있다고 답한다 — 답의 대부분이 근거를 갖고 있으면 그렇게 보이기 때문이다. 문장 다섯 중 하나만 지어낸 답이 가장 위험한데, 쪼개서 세면 그 하나가 0.8이라는 값으로 나타난다.
Answer Relevance
답변이 질문에 실제로 응답했는지를 본다. 답변만 보고 역으로 질문을 여러 개 만들어, 그 질문들이 원래 질문과 얼마나 가까운지를 잰다. 조각에 충실하면서도 묻지 않은 것을 설명하는 답이 이 지표에서 떨어진다.
이 지표는 정답 답변을 요구하지 않는다는 점에서 쓸모가 크다. 골든셋에 정답 문장을 다 적어 두지 못한 초기에도 계산할 수 있고, 실트래픽에서도 그대로 잴 수 있다. 다만 「모른다」고 답한 경우에 낮게 나오므로, 답할 수 없는 질문은 이 지표의 평균에서 빼고 따로 센다.
Context Precision
꺼내 온 조각 가운데 답에 실제로 쓰인 비율이다. 낮다는 것은 쓸모없는 조각을 함께 밀어 넣고 있다는 뜻이고, 리랭킹을 넣거나 k를 줄이는 것이 첫 수다.
Context Recall
정답에 필요한 정보가 꺼내 온 조각에 얼마나 담겼는지다. 계산에 정답 답변이 필요하므로 골든셋 없이는 못 낸다. 이 값이 낮으면 청크 크기와 쿼리 재작성을 본다.
Context Precision과 Context Recall은 자주 반대로 움직인다. k를 키우면 재현율은 오르고 정밀도는 떨어진다. 그래서 둘을 각각 최대로 만들려는 시도는 방향이 없고, 정밀도를 일정 수준으로 묶어 둔 채 재현율을 올리는 식으로 한쪽을 고정하고 본다. 어느 쪽을 고정할지는 서비스가 정한다 — 답이 없으면 곤란한 상담용이면 재현율 쪽을, 컨텍스트 예산이 빠듯하면 정밀도 쪽을 우선한다.
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness, answer_relevancy, context_precision, context_recall,
)
dataset = Dataset.from_dict({
"question": [...],
"answer": [...],
"contexts": [...],
"ground_truths": [...],
})
results = evaluate(
dataset=dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(results)
네 값을 조합해 읽는 표가 실제로 매일 쓰는 것이다.
| 조합 | 읽는 법 | 다음 수 |
|---|---|---|
| Context Recall만 낮음 | 정답이 아예 안 들어왔다 | 청크 크기·쿼리 재작성 |
| Context Precision만 낮음 | 정답은 들어왔는데 잡동사니가 많다 | 리랭킹 추가, k 축소 |
| Faithfulness 높고 Answer Relevance 낮음 | 조각에 충실하지만 묻지 않은 걸 답했다 | 프롬프트의 질문 고정 |
| Faithfulness 낮고 Context Recall 높음 | 근거를 줬는데 안 썼다 | 지시 강화, 온도 낮추기 |
가장 자주 오해하는 자리가 마지막 줄이다. Faithfulness가 낮으면 검색을 의심하기 쉬운데, Context Recall이 함께 높으면 검색은 제 몫을 한 것이고 문제는 생성 쪽에 있다.
심판 모델의 편향
위 네 지표는 전부 모델이 모델을 채점해 나온 값이다. 이 방식을 LLM-as-Judge라 부르고, 사람 라벨보다 싸고 빠른 대신 사람에게 없는 편향을 갖는다. 심판을 검증하지 않은 평가는 그 편향을 점수로 착각한다.
세 가지 편향
위치 편향은 두 답을 나란히 놓고 고르게 할 때 앞에 놓인 쪽을 더 자주 고르는 경향이다. 순서를 뒤집어 두 번 물어 둘 다 같은 쪽을 골랐을 때만 채택하면 상당 부분 걷힌다.
길이 편향은 긴 답을 더 좋게 매기는 경향이다. 길이와 점수의 상관을 한 번 찍어 보면 우리 심판이 이 편향을 갖는지 바로 나온다. 상관이 뚜렷하면 채점 기준에 길이를 명시적으로 배제하는 문장을 넣는다.
자기 선호는 심판이 자기와 같은 모델이 쓴 답을 높게 매기는 경향이다. 생성 모델과 심판 모델을 갈라 두는 것으로 피한다.
사람 라벨과의 일치도
심판을 믿을지 말지는 재서 정한다. 사람이 채점한 50~100건을 놓고 심판의 판정과 얼마나 일치하는지를 계산한다. 일치도가 낮으면 점수를 쓰지 말고 채점 기준부터 고친다 — 기준이 「답변이 좋은가」처럼 열려 있으면 심판은 매번 다른 잣대를 쓴다.
일치도를 잴 때는 사람끼리의 일치도도 함께 본다. 같은 100건을 두 사람이 채점해 서로 70퍼센트만 일치한다면, 심판 모델에게 90퍼센트를 요구하는 것은 애초에 말이 안 되는 목표다. 사람끼리의 일치도가 심판이 도달할 수 있는 상한이고, 그 값이 낮다는 것 자체가 채점 기준이 모호하다는 신호다.
기준을 좁히기
편향을 줄이는 가장 효과적인 수는 모델을 바꾸는 것이 아니라 물음을 좁히는 것이다. 「이 답이 좋은가」 대신 「이 답의 모든 문장이 주어진 조각에 있는가」처럼 예·아니오로 답할 수 있는 형태로 쪼개면 일치도가 눈에 띄게 올라간다. RAGAS가 Faithfulness를 주장 단위로 쪼개 재는 것도 같은 이유다.
오프라인 점수와 온라인 신호
실사용 신호
골든셋 점수는 배포 전에 재는 값이고, 실제 품질은 배포 후에 사용자 행동으로 나타난다. 클릭 여부, 같은 질문을 말만 바꿔 다시 묻는 재질문 비율, 상담원으로 넘어가는 이관율, 인용된 출처를 펼쳐 보는 비율이 대표적인 신호다. 이 값들은 정답이 필요 없고 매일 쌓인다는 것이 장점이다.
대조하기
두 값을 따로 보면 의미가 없고 대조해야 쓸모가 생긴다. 오프라인 점수는 올랐는데 재질문이 늘었다면 골든셋이 실제 질문 분포와 어긋난 것이다. 오프라인이 그대로인데 이관율이 떨어졌다면 지표가 못 재는 개선이 있었다는 뜻이고, 그게 무엇인지 찾아 다음 골든셋에 넣는다.
온라인 신호를 읽을 때 주의할 것이 하나 있다. 이 값들은 품질 말고도 여러 가지에 반응한다. 응답이 빨라져도 재질문이 줄고, 화면에서 출처를 더 눈에 띄게 배치해도 펼쳐 보는 비율이 오른다. 그래서 온라인 신호는 단독으로 품질의 증거가 되지 못하고, 오프라인 점수와 같은 방향으로 움직였을 때만 근거가 된다.
실시간 모니터링 도구는 이 대조를 붙이는 자리다. TruLens처럼 체인을 감싸 호출마다 피드백 함수를 돌리는 방식이면 오프라인과 같은 지표를 실트래픽에서도 얻는다.
from trulens.core import Feedback, TruSession
from trulens.apps.langchain import TruChain
import numpy as np
context_relevance = (
Feedback(provider.context_relevance_with_cot_reasons)
.on_input()
.on(TruChain.select_context())
.aggregate(np.mean)
)
tru_rag = TruChain(rag_chain, app_name="ProductionRAG", feedbacks=[context_relevance])
TruSession().get_leaderboard()
회귀 평가
CI에 붙이기
평가는 한 번 하는 일이 아니라 매 변경마다 도는 일이다. 임베딩 모델, 청크 크기, 리랭커, 프롬프트 — 넷 중 무엇을 바꿔도 같은 골든셋을 돌려 점수를 남긴다. 사람이 기억해서 돌리는 방식은 반드시 빠지므로 배포 파이프라인에 건다.
전체 골든셋을 매번 돌리면 비용과 시간이 부담이라, 커밋마다 도는 작은 셋과 배포 전에 도는 전체 셋을 갈라 두는 구성이 흔하다. 작은 셋은 지표가 무너지는 것만 잡으면 되므로 30~50건이면 된다. 이때 작은 셋은 무작위로 뽑지 말고 갈래마다 몇 건씩 고르게 담는다 — 무작위로 30건을 뽑으면 흔한 유형이 대부분을 차지해 정작 약한 유형이 빠진다.
평가 로그
점수만 남기면 나중에 아무것도 못 한다. 질문별 점수, 그때 꺼내 온 문서 id, 프롬프트 버전, 모델 이름을 함께 남긴다. 그래야 점수가 떨어졌을 때 「어느 질문이」, 「무엇이 바뀌어서」에 답할 수 있다. 판정 근거 문장까지 남겨 두면 심판이 이상하게 매긴 경우도 되짚을 수 있다.
점수가 떨어진 질문의 목록을 배포마다 비교하는 것도 습관으로 둘 만하다. 평균이 같아도 맞던 질문이 틀리고 틀리던 질문이 맞은 상태라면, 그건 개선이 아니라 이동이다. 이 교체가 크게 일어나는 변경은 대개 검색 설정을 건드린 것이고, 평균만 보고 통과시키면 사용자 쪽에서는 갑자기 되던 게 안 된다는 보고로 돌아온다.
문턱은 절대값이 아니라 변화폭으로 건다. Faithfulness가 0.9를 넘어야 한다는 규칙은 골든셋이 바뀌는 순간 무의미해지지만, 직전 배포 대비 3퍼센트포인트 이상 떨어지면 막는다는 규칙은 계속 쓸 수 있다. 골든셋을 늘린 날에는 그 규칙을 한 번 쉬게 하고 새 기준선을 다시 잡는다 — 셋이 바뀌면 이전 점수와의 비교가 성립하지 않는다.
움직이지 않는 지표
점수가 개선에 반응하지 않는 상태가 평가에서 가장 답답한 자리다. 원인은 대개 셋 중 하나다.
쉬운 골든셋
질문이 문서의 문장을 거의 그대로 옮긴 형태면 어떤 검색 설정으로도 맞는다. 지금 셋의 Recall@5가 0.95를 넘는데 사용자 불만은 그대로라면 이쪽을 의심한다. 실제 로그에서 뽑은 질문으로 갈아 끼우면 점수가 뚝 떨어지는데, 그게 정상이다.
포화
지표가 상한에 붙어 더 올라갈 자리가 없는 경우다. 이때는 k를 줄여 더 빡빡한 조건에서 다시 재거나, 지표를 바꾼다. Recall@10이 포화면 Recall@3을 보고, 그것도 포화면 MRR로 옮긴다. 포화는 나쁜 소식이 아니라 그 지표로 할 일이 끝났다는 신호이므로, 지표를 바꾸는 것을 후퇴로 여길 이유가 없다.
몰린 실패
전체 평균은 그대로인데 특정 문서 유형에서만 무너지는 경우다. 표가 많은 문서, 스캔 PDF, 짧은 게시글처럼 유형별로 점수를 갈라 보면 드러난다. 평균 하나만 보고 있으면 이 실패는 영원히 안 보이고, 유형을 갈라 세는 한 줄이면 보인다.
갈라 보는 축은 문서 유형만이 아니다. 질문 갈래, 문서의 작성 시기, 부서, 언어가 전부 쓸 만한 축이다. 어느 축에서 점수 차가 크게 벌어지면 그 축이 지금 시스템의 약점을 설명하는 축이고, 다음 개선은 거기서 시작한다.
평가에서 얻는 가장 큰 것은 점수 자체가 아니라 이 갈라 보기다. 어디가 약한지 알면 다음에 무엇을 할지가 정해지고, 그때부터 RAG 개선은 감이 아니라 순서가 된다.
읽어주셔서 감사합니다. 😊

