LLM 평가 프레임워크 직접 구축 vs 기성 도구: 2026 팀별 선택 기준
RAG·챗봇·에이전트를 프로덕션에 올리는 팀이라면 지금 이 선택이 기다리고 있습니다
AI 기능을 제품에 붙이고 나서 "이게 동작하는 건 맞는데, 얼마나 잘 동작하는지는 어떻게 알지?"라는 질문을 해본 적 있다면, 딱 맞는 시점에 닿은 겁니다. 저도 처음엔 "프롬프트 몇 개 수동으로 확인하면 되지 않나?"라고 생각했는데, 모델 버전이 바뀐 뒤 엉뚱한 곳에서 회귀가 터지고 나서야 생각이 바뀌었습니다. 막상 평가 인프라를 갖추려고 하면 첫 번째로 맞닥뜨리는 질문이 이겁니다: RAGAS, DeepEval, Promptfoo 같은 기성 도구를 쓸 것인가, 아니면 직접 만들 것인가. 이 글을 읽고 나면 두 방향의 핵심 차이를 이해하고, 지금 여러분 팀 상황에 맞는 선택 기준을 정할 수 있게 됩니다.
2025~2026년 기준으로 LLM 평가는 더 이상 연구팀의 숙제가 아닙니다. 배포 파이프라인을 실질적으로 막는 품질 게이트가 됐고, 이 인프라를 어떻게 갖추느냐가 팀의 AI 역량을 가르는 경우도 많습니다. 한 줄로 먼저 정리하면: 기성 도구는 빠르게 시작할 수 있지만 커스터마이징에 한계가 있고, 직접 구축은 도메인 특화가 가능하지만 초기 비용이 높습니다.
핵심 개념
LLM 평가 하네스란 무엇인가
평가 하네스(Evaluation Harness): 언어 모델의 출력 품질을 체계적으로 측정하기 위한 테스트 인프라. 입력 데이터셋, 평가 지표, 채점 로직, 결과 리포팅을 하나의 파이프라인으로 묶은 시스템.
소프트웨어 테스트의 pytest나 Jest 같은 존재를 LLM 출력에 적용한 것이라고 생각하면 이해하기 쉽습니다. 차이가 있다면 LLM 출력이 비결정론적이라는 점입니다. "서울의 수도는 어디인가요?"에는 정답이 있지만, "이 답변이 충분히 공감적인가?"는 숫자 하나로 바꾸는 게 훨씬 까다롭습니다. 그래서 채점 방식이 일반 단위 테스트보다 복잡해지는 겁니다.
평가는 언제 실행하는가
평가가 작동하는 시점은 세 가지로 나뉩니다.
| 시점 | 설명 | 실무 활용 |
|---|---|---|
| 오프라인 | 정제된 데이터셋 기반 배치 평가 | 모델 교체·파인튜닝 전후 비교 |
| CI/CD | 프롬프트·모델 변경 전 회귀 테스트 | PR 머지 전 자동 실행 |
| 온라인(프로덕션) | 실시간 트래픽 샘플링 모니터링 | 드리프트 감지, 이상 탐지 |
회귀 테스트(Regression Test): 코드·프롬프트·모델을 변경했을 때 이전에 잘 작동하던 기능이 망가지지 않았는지 확인하는 테스트. LLM 시스템에서도 소프트웨어 단위 테스트와 동일한 역할을 합니다.
세 시점 모두를 커버하는 팀은 생각보다 드뭅니다. 에이전트를 프로덕션에 올린 팀들이 공통적으로 말하는 건 CI/CD 연동부터 시작하는 것이 가장 레버리지가 크다는 점입니다.
평가 지표의 세 가지 유형
# 유형 1: 결정론적(Deterministic) 지표 — 정규식·코드 일치
import json
def evaluate_json_output(response: str) -> bool:
"""모델이 유효한 JSON을 반환했는지 확인"""
try:
json.loads(response)
return True
except json.JSONDecodeError:
return False# 유형 2: 심사 기반(LLM-as-Judge) 지표
# 아래는 개념 설명용 의사코드입니다.
# 실제 구현 시 openai.chat.completions.create(...) 또는
# anthropic.messages.create(...)를 사용하세요.
def llm_judge_relevance(question: str, answer: str, judge_model) -> float:
prompt = f"""
질문: {question}
답변: {answer}
위 답변이 질문에 얼마나 관련성 있는지 0~1 사이 점수로만 반환하세요.
"""
score = judge_model.complete(prompt) # 실제 API 인터페이스로 교체 필요
return float(score.strip())# 유형 3: 통계적·참조 기반 지표 (ROUGE, BERTScore 등)
from rouge_score import rouge_scorer
def evaluate_with_rouge(reference: str, generated: str) -> dict:
scorer = rouge_scorer.RougeScorer(['rouge1', 'rougeL'], use_stemmer=True)
scores = scorer.score(reference, generated)
return {
'rouge1': scores['rouge1'].fmeasure,
'rougeL': scores['rougeL'].fmeasure
}LLM-as-Judge: GPT-4o, Claude 같은 강력한 모델을 채점자로 활용하는 패턴. 결정론적 지표만으로 잡기 어려운 뉘앙스나 일관성을 평가할 때 유용합니다. 단, 프레임워크마다 구현 철학이 다릅니다. DeepEval은 Python metric 객체 기반이고, Promptfoo는 YAML assertion 기반이라 설계 방식이 달라 혼동하지 않는 것이 좋습니다.
실무에서는 세 가지 유형을 섞어 쓰는 복합(Composite) 방식이 현실적입니다.
실전 적용
예시 1: 기성 도구로 RAG 파이프라인 품질 측정 — RAGAS
RAG(Retrieval-Augmented Generation): 모델이 답변을 생성할 때 외부 문서나 데이터베이스에서 관련 정보를 먼저 검색한 뒤 참고하는 방식. 사내 문서 기반 Q&A나 고객 지원 챗봇에서 많이 씁니다.
RAG 기반 서비스를 만들고 있다면 RAGAS가 가장 빠른 출발점입니다. 핵심을 먼저 짚으면, faithfulness와 answer_relevancy는 정답 레이블 없이도 계산할 수 있습니다. 하지만 context_recall은 ground_truth(정답 레이블)가 반드시 있어야 계산됩니다. 저도 처음에 이 차이를 놓쳤다가 코드 실행 중 오류로 알게 됐는데, 미리 알고 있으면 삽질을 줄일 수 있습니다.
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall
from ragas.llms import LangchainLLMWrapper
from langchain_openai import ChatOpenAI
from datasets import Dataset
# RAGAS는 채점에 쓸 LLM 설정이 필요합니다 (2024년 이후 필수)
judge_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o"))
eval_data = {
"question": [
"우리 제품의 환불 정책은 어떻게 되나요?",
"배송 기간은 얼마나 걸리나요?"
],
"answer": [
"구매 후 30일 이내에 환불 신청이 가능합니다.",
"평균 3~5 영업일이 소요됩니다."
],
"contexts": [
["환불은 구매일로부터 30일 이내 가능하며...", "교환은 7일 이내..."],
["일반 배송은 3~5 영업일, 특급 배송은 1~2 영업일..."]
],
# context_recall 계산에는 ground_truth가 필수입니다.
# faithfulness, answer_relevancy만 쓸 경우 이 필드는 생략 가능합니다.
"ground_truth": [
"30일 이내 환불 가능",
"3~5 영업일 소요"
]
}
dataset = Dataset.from_dict(eval_data)
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_recall],
llm=judge_llm
)
print(result)
# {'faithfulness': 0.92, 'answer_relevancy': 0.87, 'context_recall': 0.95}| 지표 | 의미 | 낮을 때 의심할 점 | ground_truth 필요 |
|---|---|---|---|
faithfulness |
답변이 컨텍스트에 근거하는가 | 모델이 환각(hallucination)을 생성 중 | 불필요 |
answer_relevancy |
답변이 질문에 얼마나 관련 있는가 | 프롬프트 또는 검색 설계 문제 | 불필요 |
context_recall |
필요한 정보가 검색됐는가 | 임베딩 모델 또는 청킹 전략 문제 | 필수 |
faithfulness가 내부적으로 어떻게 작동하는지 알면 더 신뢰하기 편합니다. RAGAS는 답변을 여러 개의 claim으로 분해한 뒤, 각 claim이 검색된 컨텍스트에 근거하는지를 판단하는 방식으로 채점합니다. 이 숫자가 0.8 아래로 내려갔을 때는 검색 설계 자체를 다시 들여다볼 필요가 있다는 신호입니다.
실제로 써보면 LangchainLLMWrapper 설정까지는 문서를 한 번 더 봐야 하지만, 그 이후는 생각보다 빠릅니다.
예시 2: CI/CD에 Promptfoo로 프롬프트 회귀 테스트 연동
프롬프트를 수정할 때마다 수동으로 확인하는 건 금방 한계에 부딪힙니다. Promptfoo는 YAML 설정 하나로 여러 모델과 프롬프트 변형을 동시에 비교할 수 있어서, 이 문제를 꽤 깔끔하게 해결해 줍니다. DeepEval이 Python 코드로 metric 객체를 정의하는 방식이라면, Promptfoo는 YAML assertion으로 동작합니다. 철학이 다르기 때문에 취향과 팀 상황에 맞는 걸 고르면 됩니다.
# promptfooconfig.yaml
description: "고객 지원 챗봇 회귀 테스트"
prompts:
- "당신은 친절한 고객 지원 담당자입니다. 다음 질문에 답하세요: {{question}}"
- "고객 질문: {{question}}\n\n간결하고 정확하게 답변해 주세요."
providers:
- openai:gpt-4o
- anthropic:claude-3-5-sonnet-20241022
tests:
- vars:
question: "환불하고 싶어요"
assert:
- type: contains
value: "환불"
- type: llm-rubric
value: "답변이 공감적이고 다음 단계를 안내하는가?"
- type: not-contains
value: "모르겠습니다"
- vars:
question: "배송이 너무 늦어요"
assert:
- type: llm-rubric
value: "사과 표현이 포함되어 있는가?"
- type: javascript
# output은 모델의 응답 텍스트를 가리키는 Promptfoo 내장 변수입니다
value: "output.length < 500"# GitHub Actions에서 실행 (eval 먼저, 그 결과 기반으로 dataset 생성)
npx promptfoo eval --config promptfooconfig.yaml --output results.json
npx promptfoo generate dataset # 결과 기반으로 추가 테스트 케이스 자동 생성실제로 써보면 YAML 관리가 케이스가 늘어날수록 조금 부담이 되기 시작합니다. 프롬프트 변형이 많고 멀티 모델 비교가 주 목적이라면 Promptfoo가 빛을 발하지만, 도메인 특화 지표가 필요해지는 순간부터는 커스텀 assertion 작성이 늘어납니다.
예시 3: 에이전트 시스템에 맞게 직접 구축하는 경우
다단계 도구 호출이 포함된 에이전트 시스템은 범용 도구가 커버하기 어려운 영역입니다. 예를 들어 "주문 취소 요청이 들어왔을 때 에이전트가 get_order → cancel_order → notify_user 순서로 도구를 호출했는가"를 체크하려면, 기성 도구로는 어색한 우회를 해야 합니다. 이런 경우엔 직접 구축이 현실적입니다.
에이전트를 프로덕션에 올린 팀에서 실제로 쓰는 패턴을 단순화하면 이런 모습입니다.
import asyncio
from dataclasses import dataclass
from typing import Callable, Any
from enum import Enum
class TestCategory(Enum):
HAPPY_PATH = "happy_path"
# 모델이 잘못된 시도 후 올바른 방향으로 스스로 수정하는 케이스
# 예: "주문번호를 잘못 입력했다가 재시도해서 성공"
RECOVERABLE = "recoverable"
# 에러를 내야 하는 케이스 — cancel_order를 호출하지 않고 거절해야 함
# 예: "이미 배송된 주문을 취소 시도 → 취소 불가 안내"
UNRECOVERABLE = "unrecoverable"
ADVERSARIAL = "adversarial" # 적대적 입력
@dataclass
class EvalCase:
name: str
category: TestCategory
input: dict
expected_tool_calls: list[str]
success_criteria: Callable[[Any], bool]
class AgentEvalHarness:
def __init__(self, agent, cases: list[EvalCase]):
self.agent = agent
self.cases = cases
self.results = []
async def run(self) -> dict:
for case in self.cases:
result = await self._evaluate_case(case)
self.results.append(result)
return self._summarize()
async def _evaluate_case(self, case: EvalCase) -> dict:
# 에이전트가 (output, tool_calls) 튜플을 반환한다고 가정합니다.
# 실제 에이전트 인터페이스에 따라 어댑터 레이어가 필요할 수 있습니다.
actual_output, actual_tool_calls = await self.agent.run(case.input)
return {
"name": case.name,
"category": case.category.value,
"passed": case.success_criteria(actual_output),
"tool_call_order_correct": actual_tool_calls == case.expected_tool_calls,
}
def _summarize(self) -> dict:
by_category = {}
for r in self.results:
cat = r["category"]
by_category.setdefault(cat, {"passed": 0, "total": 0})
by_category[cat]["total"] += 1
if r["passed"]:
by_category[cat]["passed"] += 1
return by_category
harness = AgentEvalHarness(agent=my_agent, cases=[
EvalCase(
name="주문 취소 정상 처리",
category=TestCategory.HAPPY_PATH,
input={"user_message": "주문 12345를 취소해주세요"},
expected_tool_calls=["get_order", "cancel_order", "notify_user"],
success_criteria=lambda out: "취소" in out and "확인" in out
),
# 실제 프로덕션 버그에서 추가된 케이스
EvalCase(
name="이미 배송된 주문 취소 시도",
category=TestCategory.UNRECOVERABLE,
input={"user_message": "어제 배송된 주문 취소해주세요"},
expected_tool_calls=["get_order"], # cancel_order는 호출되면 안 됨
success_criteria=lambda out: "취소 불가" in out or "배송 완료" in out
),
])이 패턴의 핵심은 프로덕션에서 실제 버그가 발생할 때마다 케이스를 추가하는 워크플로우에 있습니다. 처음부터 완벽한 케이스셋을 만들려고 하면 시작도 못 하고, 실제로 터진 버그에서 나온 케이스가 훨씬 가치가 있습니다.
장단점 분석
직접 비교해보면 이렇게 정리됩니다.
| 항목 | 직접 구축 | 기성 도구 |
|---|---|---|
| 시작 속도 | 느림 (설계부터 해야 함) | 빠름 (수 시간 내 첫 평가 실행) |
| 커스터마이징 | 완전 자유 | 제한적 (확장 포인트 확인 필요) |
| 데이터 보안 | 외부 전송 없음 | 플랫폼에 따라 외부 전송 가능 |
| 유지 비용 | 높음 (팀 직접 유지) | 낮음 (벤더 업데이트) |
| 벤더 종속 | 없음 | 상용 플랫폼은 가격 변경 리스크 |
| 표준 벤치마크 | 직접 구현 필요 | MMLU 등과 바로 비교 가능 |
| 에이전트 특화 | 강점 | 우회 구현 필요 |
장점
직접 구축
| 항목 | 내용 |
|---|---|
| 도메인 특화 | 비즈니스 로직과 지표를 완전히 맞춤화할 수 있습니다 |
| 인프라 통합 | 기존 CI/CD, 데이터 파이프라인과 자유롭게 연결됩니다 |
| 데이터 보안 | 독점 데이터가 외부 서비스로 나가지 않습니다 |
| 전략적 자산 | 축적된 평가 데이터셋 자체가 경쟁력이 됩니다 |
기성 도구 사용
| 항목 | 내용 |
|---|---|
| 빠른 시작 | 설치 후 수 시간 내 첫 평가 실행이 가능합니다 |
| 검증된 지표 | 커뮤니티와 논문에서 검증된 지표를 바로 활용할 수 있습니다 |
| 벤더 업데이트 | 새 LLM이 나와도 지원이 자동으로 추가됩니다 |
| 표준 비교 | MMLU, GSM8K 같은 벤치마크와 직접 비교할 수 있습니다 |
단점 및 주의사항
직접 구축
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 높은 초기 비용 | 설계·구현·유지 모두 팀 리소스를 씁니다 | MVP는 작게 시작, 케이스를 점진적으로 확장 |
| 기술 부채 | 노트북 수준 구현이 쌓이면 나중에 손대기 어려워집니다 | 초기부터 모듈화 구조 유지 |
| 전문 지식 필요 | 좋은 평가 지표 설계는 생각보다 어렵습니다 | 기성 도구의 지표 설계를 참고·차용 |
기성 도구 사용
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 커스터마이징 한계 | 도메인 특화 지표를 추가하기 어려울 수 있습니다 | 커스텀 평가자 확장 포인트 먼저 확인 |
| 벤더 종속 | 상용 플랫폼은 가격 정책 변경에 취약합니다 | 오픈소스 우선 검토, 데이터 export 가능 여부 확인 |
| 데이터 외부 전송 | 프롬프트·응답이 외부 서버로 나갈 수 있습니다 | 셀프호스팅 옵션 또는 온프레미스 지원 여부 확인 |
실무에서 가장 흔한 실수
저도 한 번씩은 다 당해봐서, 비슷한 경험 하시는 분들께 미리 전해드리는 내용입니다.
-
평가 케이스를 처음부터 완벽하게 만들려는 것 — 처음엔 20~30개로 시작해서 프로덕션 실패가 날 때마다 케이스를 추가하는 방식이 훨씬 효과적입니다. 완벽한 데이터셋을 기다리다가 아무것도 안 하게 되는 상황이 생각보다 많습니다.
-
LLM-as-Judge를 맹신하는 것 — "AI가 채점하니까 객관적이겠지"라고 믿으면 나중에 당합니다. 채점자 모델도 틀리고, 특히 긴 컨텍스트나 전문 도메인에서 오류율이 올라갑니다. 결정론적 지표와 반드시 병행하는 것을 권장합니다.
-
오프라인 평가만 하고 프로덕션 모니터링을 빠뜨리는 것 — 솔직히 이게 가장 자주 빠뜨리는 부분입니다. 실제 사용자 트래픽에서 나타나는 패턴은 테스트셋에서 예상하기 어렵고, 나중에 추가하려고 하면 공수가 많이 듭니다. 초기 설계에 포함시키는 걸 권장합니다.
마치며
평가 인프라를 갖추는 것은 어떤 모델을 선택하느냐 못지않게 중요합니다. 에이전트를 프로덕션에 올린 팀들과 이야기해보면, 하네스를 개선해서 얻은 성과가 더 강력한 모델로 교체했을 때보다 컸다는 경우를 적지 않게 접했습니다. 모델이 아니라 어떻게 평가하고 개선하느냐가 차이를 만들 때가 많습니다.
선택 기준이 막막하다면 이렇게 생각해볼 수 있습니다:
- 기성 도구가 맞는 경우 → AI가 핵심 기능보다는 부가 기능이고, RAG·챗봇 같은 표준적인 사용 사례이며, 빠른 출시가 우선인 팀
- 직접 구축이 맞는 경우 → AI가 핵심 비즈니스 가치이고, 도메인 특화 지표가 필요하거나, 규제상 데이터를 외부로 보낼 수 없는 팀
- 하이브리드가 현실적인 경우 → 기성 도구로 시작해서 커스텀이 필요한 부분만 직접 구현하는 방식. 두 방향이 배타적이지 않아서 실제로 이게 가장 많이 쓰이는 패턴입니다.
지금 바로 시작해볼 수 있는 3단계:
-
작은 테스트셋부터 만들어볼 수 있습니다 — RAG나 챗봇을 운영 중이라면
pip install ragas로 설치한 뒤, 현재 서비스에서 실제로 들어온 질문 20개와 모델 응답을 모아서faithfulness,answer_relevancy두 지표만 측정해볼 수 있습니다. 숫자가 나오는 순간 방향이 보이기 시작합니다. -
CI/CD 연동을 시도해볼 수 있습니다 — Promptfoo를 쓴다면
npx promptfoo@latest init으로 초기 설정 파일을 생성하고, GitHub Actions의 PR 트리거에promptfoo eval커맨드를 추가해볼 수 있습니다. 첫 회귀가 잡히는 순간 평가 인프라의 가치를 직관적으로 느낄 수 있습니다. -
하이브리드 접근도 충분히 실용적입니다 — 기성 도구로 표준 지표를 커버하고, 도메인 특화 로직은 예시 3처럼 직접 구현하는 구조로 시작하면 초기 비용과 커스터마이징 사이의 균형을 잡을 수 있습니다.
참고 자료
- Top 10 LLM Evaluation Harnesses: Features, Pros, Cons & Comparison | DevOpsSchool
- Top 7 LLM Evaluation Tools in 2026 | Confident AI
- EleutherAI lm-evaluation-harness | GitHub
- Promptfoo vs DeepEval vs RAGAS: 2026 LLM Evaluation Tools Comparison | genai.qa
- LLM Evaluation Frameworks: Head-to-Head Comparison | Comet ML
- DeepEval vs. RAGAS vs. LangSmith: Choosing the Right Evaluation Framework | Descope
- Best AI Eval Tools for CI/CD Pipelines 2026 | Braintrust
- How to Build an LLM Evaluation Framework in 2025 | Deepchecks
- The Model vs. the Harness: Which matters more? | Medium