파이프라인 출력 품질 평가하기
파이프라인 출력 품질 평가하기 (pipeline-output-quality)
이번 튜토리얼은 **파이프라인 출력 품질(pipeline output quality)**을 다뤄요. 쉽게 말해, 검색된 결과가 최종 소비자(대부분 RAG 시스템의 LLM 생성기)에게 도달했을 때 전체 검색 파이프라인이 올바른 출력을 만들어내는지를 평가하는 작업이에요. 파이프라인 출력 품질을 측정하려면 골든 세트(golden set)를 전체 파이프라인에 통과시키고, 각각의 (question, retrieved_context, answer) 트리플을 캡처한 다음, 이 트리플들을 faithfulness(충실성), answer relevancy(답변 관련성), context precision(문맥 정밀도) 같은 판단 지표로 채점하면 됩니다.
> 출처: [Qdrant 공식 문서 — pipeline-output-quality](https://qdrant.tech/documentation/improve-search/pipeline-output-quality)
이 주제를 이해하면 검색 파이프라인이 '제대로 답을 내놓는지'를 객관적으로 측정하고, 어느 부분이 문제인지 진단할 수 있게 돼요. 바로 시작해 볼게요.
RAG 파이프라인 연결하기 (Wiring the RAG Pipeline)
많은 프레임워크가 LLM 판정자(judge)로 RAG 출력을 채점할 수 있어요. 대표적으로 Ragas, DeepEval 등이 있죠. 여기서는 이 튜토리얼이 다루는 세 가지 지표를 가장 가볍게 구성할 수 있는 Ragas를 사용할게요. 팀이 다른 프레임워크에 이미 표준화했거나 판정 LLM을 직접 호출하는 걸 선호한다면, 동일한 워크플로우가 그대로 적용됩니다.
Ragas는 LLM을 판정자로 사용해 RAG 출력을 채점하는 Python 라이브러리예요(각 답변을 faithfulness, relevancy 같은 기준으로 평가하죠). (question, retrieved_context, answer) 트리플 형태의 샘플을 기대하므로, 라벨링된 데이터에서 새 평가 세트를 만들어야 해요. 크게 세 단계로 구성됩니다.
1. 평가 데이터 준비하기
각 항목에는 query_id, query_text(생성기 프롬프팅과 임베딩 기반 검색 양쪽에 사용), 그리고 labels가 필요해요. context_precision만을 위해 추가로 ground_truth 참조 답변이 필요합니다.
합성 쿼리(synthetic query)에는 보통 ground-truth 답변이 포함되지 않아요. faithfulness와 answer_relevancy만 채점한다면 두 지표 모두 참조 답변이 필요 없으니 이 단계를 건너뛰어도 됩니다. 그렇지 않다면 각 쿼리를 해당 소스 문서 범위로 제한한 LLM에 통과시켜 참조 답변을 생성하세요. 소스가 답할 수 없을 때는 모델이 NO_ANSWER를 반환하도록 하고, 채점 전에 그 행들을 제거해야 해요. 그렇지 않으면 context_precision이 소스 문서가 뒷받침하지 않는 참조를 기준으로 검색을 판단하게 됩니다.
# 평가에 바로 쓸 수 있는 항목의 예시예요.
{
"query_id": "q1",
"query_text": "how does X work",
"labels": {"doc_42": 1},
"ground_truth": "...", # 선택 사항; context_precision에만 필요해요.
}
골든 세트의 출처(사람이 주석을 단 것, 로그 샘플링, LLM 합성 등)에 따라 원본 형태가 제각각이에요. 루프를 돌리기 전에 위 구조로 정규화해 주세요.
2. 근거 프롬프트 정의하기 (Define the Grounding Prompt)
프롬프트는 검색과 생성 사이의 경계면(seam)이에요. 버저닝된 문자열로 유지하면 평가 코드를 건드리지 않고 모델을 바꿔 끼울 수 있습니다.
PROMPT_TEMPLATE = """You are answering questions using retrieved source material.
Answer the question below using only the provided context.
If the context does not contain the answer, say so explicitly.
Do not rely on outside knowledge.
Context:
{retrieved_context}
Question:
{query_text}
"""
위 프롬프트는 시작점일 뿐이에요. 도메인에 맞게 답변 스타일, 거절 동작, 외부 지식 허용 여부, 출력 형식 등을 조정해 보세요.
3. 검색과 생성 수행하기 (Run Retrieval and Generation)
각 항목마다 top-k 청크를 검색하고, 이를 생성기에 통과시킨 다음, SingleTurnSample(Ragas의 평가 레코드용 데이터 클래스 — 질문, 검색된 컨텍스트, 생성된 답변, 선택적 참조를 담아요)로 기록합니다.
import os
import anthropic
from qdrant_client import QdrantClient
from ragas import SingleTurnSample
from your_embedding_model import embed # Qdrant 컬렉션이 쓰는 모델과 일치해야 해요.
client = QdrantClient("http://localhost:6333") # Qdrant Cloud라면 QdrantClient(url="https://<id>.cloud.qdrant.io", api_key="...")
# 예시는 Anthropic을 쓰지만, 어떤 LLM 프로바이더든 동작해요.
anthropic_client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
def generate_answer(query_text: str, contexts: list) -> str:
"""프롬프트 템플릿에 컨텍스트와 질문을 채운 뒤 LLM을 호출해요."""
prompt = PROMPT_TEMPLATE.format(
retrieved_context="\n\n".join(contexts),
query_text=query_text,
)
response = anthropic_client.messages.create(
model=os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-6"),
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
def build_eval_set(golden_set: list, collection: str, k: int = 10) -> list:
"""라벨링된 각 쿼리에 대해: Qdrant에서 검색하고, 답변을 생성하고, Ragas 샘플로 묶어요."""
samples = []
for entry in golden_set:
# Qdrant에서 top-k 청크를 검색해요.
results = client.query_points(
collection_name=collection,
query=embed(entry["query_text"]),
limit=k,
).points
contexts = [p.payload["text"] for p in results] # 스키마에 맞게 payload 키를 조정하세요.
# 그 청크들에 근거해 답변을 생성해요.
answer = generate_answer(entry["query_text"], contexts)
# Ragas 샘플로 묶기: 질문, 컨텍스트, 답변, 선택적 참조.
samples.append(SingleTurnSample(
user_input=entry["query_text"],
retrieved_contexts=contexts,
response=answer,
reference=entry.get("ground_truth", ""),
))
return samples
항목의 ground_truth가 비어 있으면 Ragas는 참조가 필요한 지표(예: context_precision)에 대해 그 샘플을 조용히 건너뛰어요. 그 지표들을 실제로 채점할 때만 채워 주세요.
Ragas로 채점하기 (Scoring with Ragas)
파이프라인 출력 품질의 일반적인 실패 양상을 다루는 Ragas 지표는 세 가지예요:
- faithfulness: 답변이 검색된 컨텍스트가 뒷받침하는 주장만 내놓는지 확인해요. 생성기가 환각(hallucination)을 일으키거나 학습 지식을 대신 사용하면 점수가 떨어집니다.
- answer_relevancy: 답변이 질문에 부합하는지 확인해요. 생성기가 늘어놓기만 하거나, 회피하거나, 주제에서 벗어나면 점수가 떨어집니다.
- context_precision: 검색된 청크가 ground-truth 답변과 관련 있고 높은 순위에 있는지 확인해요. 검색이 유용한 청크를 밀어내는 노이즈를 표면화하면 점수가 떨어집니다.
context_precision은reference필드와 비교하므로 ground-truth 답변이 있는 쿼리만 채점합니다.
평가 샘플을 위 세 가지 지표와 함께 evaluate()에 넣어 주세요:
from anthropic import Anthropic
from openai import OpenAI
from ragas import EvaluationDataset, evaluate
from ragas.embeddings.base import embedding_factory
from ragas.llms import llm_factory
from ragas.metrics.collections import AnswerRelevancy, ContextPrecision, Faithfulness
# 판정 LLM. 자기 평가 편향을 피하려면 생성기와 다른 LLM 계열을 쓰세요.
judge_client = OpenAI() # 환경에서 OPENAI_API_KEY를 읽어요.
judge_llm = llm_factory("gpt-5.4", client=judge_client)
# 판정자의 질문 유사도 검사용입니다. 검색 임베더와 일치할 필요는 없어요.
judge_embeddings = embedding_factory("openai", model="text-embedding-3-large", client=judge_client)
metrics = [
Faithfulness(llm=judge_llm),
AnswerRelevancy(llm=judge_llm, embeddings=judge_embeddings),
ContextPrecision(llm=judge_llm),
]
dataset = EvaluationDataset(samples=samples)
scores = evaluate(dataset, metrics=metrics)
evaluate()는 EvaluationResult 객체를 반환해요. 집계 점수는 다음과 같이 출력됩니다.
{"faithfulness": 0.88, "answer_relevancy": 0.81, "context_precision": 0.74}
세 지표 모두 높을수록 좋아요. 그런데 집계값은 '무엇이 망가졌는지'를 알려주는 분포를 숨겨 버려요. 쿼리별 뷰로 내려가 가장 점수가 낮은 샘플을 찾아보세요.
per_query = scores.to_pandas() # 쿼리별 행 점수
worst = per_query.nsmallest(10, "faithfulness")
CI에서 실행하기 (Running in CI)
검색을 자주 변경해서 배포한다면 이 평가는 CI에서 충분히 자리 잡을 만해요. 변경할 때마다 고정된 골든 세트로 평가를 돌리면 프롬프트 수정, 모델 교체, 청킹 변경으로 인한 생성기 성능 회귀(regression)를 프로덕션에 닿기 전에 잡아낼 수 있어요. 흔한 패턴은 지표별 목표 임계값을 정하고, 어떤 점수가 그 아래로 떨어지면 잡(job)을 실패시키는 것입니다.
대안 (Alternatives)
골든 세트가 없는 경우. faithfulness와 answer_relevancy는 참조 없이 평가할 수 있어요. context_precision 대신 LLMContextPrecisionWithoutReference로 바꾸면 됩니다. 그러면 합성 쿼리를 오프라인으로, 혹은 샘플링된 프로덕션 트래픽을 실시간으로 채점할 수 있어요. 다만 회귀 게이팅을 위한 고정된 기준선은 없어집니다.
검색과 생성 분리하기 (Isolating Retrieval vs Generation)
같은 골든 세트에 대해 검색 평가도 함께 돌리고 있다면, 매 실행마다 두 점수를 짝지어 보면 점수 변화를 진단하는 2x2 표가 나와요. 변경(새 임베딩 모델, 새 프롬프트, 새 청킹 전략) 후 지표가 떨어지면, 그 짝이 파이프라인의 어느 절반을 조사해야 하는지 알려줍니다.
검색 평가의 recall@10을 파이프라인 출력 평가의 faithfulness와 짝지어 봐요. 표에서 High/Low는 지표별로 설정한 목표 임계값에 대한 상대적인 값이에요.
| Recall@10 | Faithfulness | 진단 |
|---|---|---|
| High | High | 배포 준비 완료. |
| High | Low | 생성기 또는 프롬프트 문제. 검색은 올바른 컨텍스트를 표면화하고 있으니, 하류(prompt, model, temperature)에서 뭔가 잘못 쓰고 있어요. |
| Low | Low | 먼저 검색을 고쳐야 해요. 생성기는 본 적 없는 컨텍스트에 충실할 수 없어요. |
| Low | High | 드문 경우예요. 보통 골든 세트 라벨이 불완전하거나(라벨이 다루지 않는 유용한 문서를 검색이 찾음) 생성기가 단정 없는 답변으로 얼버무려서 실패할 주장이 없는 경우예요. 행동에 옮기기 전에 쿼리별 출력 샘플을 읽어보세요. |
검색과 파이프라인 출력 평가를 따로 두는 이유가 바로 이 분리 덕분이에요. 둘을 하나의 end-to-end 점수로 합치면 파이프라인이 '움직였다'는 것만 알 수 있고, 어느 절반이 움직였는지는 모르니 다음 반복이 추측이 되어 버립니다.
비-RAG 사용 사례 (Non-RAG Use Cases)
Ragas의 지표는 소비자가 LLM 생성기라고 가정해요. 검색이 다른 것(랭커, 추천 서피스, 에이전트, 검색 UI)을 먹인다면 지표를 그에 맞게 바꾸세요. UI에는 CTR이나 체류 시간, 랭커에는 등급 루브릭, 에이전트에는 작업 완료율 같은 걸 쓰면 됩니다. 방법 자체는 동일해요. 소비자를 고정하고, 골든 세트를 전체 파이프라인에 통과시키고, end-to-end 출력을 채점하세요. 바뀌는 건 지표뿐이에요.
주의할 함정들 (Pitfalls to Watch For)
- 판정자 편향(Judge bias). LLM 판정자는 기본 주장이 약해도 장황하고 자신만만하고 형식이 잘 잡힌 답변에 점수를 후하게 줘요. 출력 샘플의 일부를 사람 평가지로 돌려 비교해 보정하세요. 판정자와 사람 점수가 자주 어긋나면 루브릭을 조정하거나 판정 모델을 바꾸세요.
- 자기 판정 오염(Self-judging contamination). 생성과 판정에 같은 모델을 쓰면 점수가 부풀어요. 판정자가 자신의 출력 스타일을 알아보고 상을 주기 때문이죠. 판정자에는 생성기와 다른 모델 계열을 쓰고, 매 실행마다 두 버전을 모두 기록해 점수 변동이 조용한 업그레이드 탓으로 돌려지지 않게 하세요.
- 비용 확장(Cost scaling). LLM-판정자 비용은 쿼리 수 × 지표 수 × 지표당 판정 호출 수로 늘어나고, Ragas는 샘플당 판정 호출을 여러 번 해요. 500개 쿼리의 골든 세트에 지표 세 개가 있으면 실행당 수천 번의 판정 모델 호출이 발생합니다. 반복 중에는 저렴한 판정자(claude-haiku-4-5 또는 gpt-4o-mini)로 50~100개 쿼리를 샘플링하고, 강력한 판정자로 전체 스윕(sweep)을 돌리는 건 릴리스 후보로 아껴 두세요.
마무리 (Wrapping Up)
이제 전체 RAG 파이프라인을 위한 Ragas 기반 채점 루프와, 회귀를 검색·생성 어느 쪽 문제인지 귀속시키는 2x2 표, 그리고 faithfulness·answer_relevancy·context_precision으로 릴리스를 게이팅하는 CI 패턴까지 갖췄어요.