LLM-as-Judge와 OpenTelemetry로 에이전트 품질을 CI에서 자동 검증하는 방법
프롬프트를 조금 바꿨는데 갑자기 응답 품질이 떨어졌다는 걸 사용자 신고로 처음 알게 된 경험, 한 번쯤 있으실 겁니다. 저도 그랬습니다. 에이전트를 프로덕션에 올려두고 "잘 돌아가겠지" 하며 방치했다가, 며칠 뒤 CS 팀에서 "응답이 이상해요"라는 슬랙 메시지를 받고 부랴부랴 로그를 뒤지던 기억이 납니다. LLM 기반 에이전트는 일반 소프트웨어와 달리 출력이 확률적이고, 외부 모델 API에 의존하며, 내부 추론 경로가 불투명합니다. 그래서 기존 테스트 방식만으로는 반드시 한계가 옵니다.
이 글은 LLM API를 호출하는 에이전트를 한 번이라도 구축해본 경험이 있는 분을 대상으로 합니다. LLM-as-Judge로 응답 품질을 자동 채점하고, OpenTelemetry로 레이턴시 병목을 스팬 단위로 추적하며, 이 둘을 CI/CD에 연결해 프롬프트 변경이 있을 때마다 자동으로 품질 게이팅을 거는 파이프라인을 구축하는 방법을 실제 코드와 도구 선택 기준까지 포함해 이야기합니다. RAGAS, DeepEval, Langfuse, Arize Phoenix가 각각 어떤 역할을 하는지도 함께 살펴봅니다.
업계는 이미 빠르게 움직이고 있습니다. LangChain 2025 State of AI Agents 조사에 따르면, 에이전트를 프로덕션에 배포한 팀의 절반 이상(53.3%)이 LLM-as-Judge를 이미 운영 중입니다. 이 글을 읽고 나면 RAG(Retrieval-Augmented Generation, 검색 증강 생성) 파이프라인에 자동 품질 게이팅을 달고, 멀티스텝 에이전트의 도구 호출 순서까지 검증할 수 있습니다.
핵심 개념
에이전트 평가(Eval) 파이프라인이란
에이전트가 "제대로" 동작하는지 검증하는 일은 단순한 단위 테스트와는 결이 다릅니다. 출력 정확도 하나만 보는 게 아니라, 멀티턴 추론 경로, 도구 호출 순서, 응답 레이턴시 분포, 심지어 토큰 비용까지 측정해야 합니다. 이 모든 걸 자동화해 체계적으로 수집·분석하는 구조가 에이전트 평가(Eval) 파이프라인입니다.
파이프라인의 큰 그림을 그려보면 이렇습니다.
사용자 요청
↓
에이전트 실행 (도구 호출, 추론, 응답 생성)
↓
트레이싱 수집 (OpenTelemetry → Langfuse / Arize Phoenix)
↓
Judge 채점 (LLM-as-Judge → 품질 지표)
↓
CI 게이팅 (임계값 미달 시 머지 차단)
↓
프로덕션 대시보드 (Grafana / Datadog)실전에서는 오프라인 eval(사전에 구축한 골든 데이터셋 대상)과 온라인 eval(프로덕션 트래픽 샘플링)을 분리해 운영하는 것이 현실적인 출발점입니다. 아래 예시 1~4는 이 파이프라인의 각 단계에 해당합니다. 예시 1은 Judge 채점, 예시 2는 트레이싱 수집, 예시 3은 멀티스텝 에이전트 검증, 예시 4는 CI 게이팅입니다.
LLM-as-Judge: 모델이 모델을 채점하는 구조
LLM-as-Judge는 다른 LLM의 출력을 평가자(Judge) 역할의 LLM이 채점하는 방식입니다. 인간 레이블링과 비교하면 500~5,000배 저렴하고, 태스크 유형과 Judge 모델에 따라 조건이 달라지지만 잘 설계된 Judge는 인간 선호도와 약 80% 일치율을 보이는 것으로 알려져 있습니다. "비싼 인간 평가자를 쓸 여유는 없는데, 그렇다고 품질 측정을 포기하고 싶지 않다"는 현실적인 고민에서 나온 접근입니다.
평가 대상에 따라 Judge는 네 가지 유형으로 구분됩니다.
| 유형 | 역할 | 대표 활용처 |
|---|---|---|
| Judge for Models | 모델 간 응답 품질 비교 | 모델 스왑 의사결정 |
| Judge for Data | 학습·평가 데이터셋 품질 검증 | 파인튜닝 전 데이터 정제 |
| Judge for Agents | 추론 경로·도구 호출 적절성 평가 | 에이전트 회귀 방지 |
| Judge for Reasoning | 사고 연쇄(CoT) 정합성 검증 | 복잡한 추론 태스크 |
LLM-as-Judge: "AI가 AI를 평가한다"는 개념이 처음엔 낯설지만, 핵심은 Judge 프롬프트를 얼마나 명확하게 설계하느냐에 있습니다. Judge도 학습 데이터와 편향을 갖고 있으므로, 편향 보정 없이 맹목적으로 신뢰하는 건 위험합니다.
그런데 Judge에도 함정이 있습니다. LLM-as-Judge 편향 연구(ScienceDirect 2025)에서 계량화된 주요 편향 세 가지는 실무에서 반드시 알아두실 만합니다.
- Position Bias: 먼저 제시된 답변을 선호하는 경향. 최대 40%까지 불일치가 발생할 수 있습니다. 솔직히 저도 처음엔 이 편향을 모르고 점수를 그대로 신뢰했다가, 순서만 바꿨더니 점수가 크게 달라지는 걸 보고 당황했습니다.
- Verbosity Bias: 길고 상세한 답변에 약 15% 과대평가가 발생합니다. Judge 프롬프트(루브릭)에 "길이와 무관하게 정확성만 평가"를 명시하면 완화됩니다.
- Self-Enhancement Bias: 동일 계열 모델의 응답을 5~7% 더 선호합니다. GPT-4o로 채점할 때 GPT 계열 응답이 유리해지는 경우가 이에 해당합니다.
루브릭(Rubric): Judge 프롬프트에 포함하는 채점 기준표입니다. "1~5점 중 하나를 고르세요"보다 "정확성, 간결성, 맥락 적합성 각각 0/1로 평가하세요"처럼 구체적일수록 편향이 줄어듭니다.
레이턴시 트레이싱: 스팬 단위로 병목 해부하기
LLM API를 호출하는 에이전트는 레이턴시 병목이 여러 곳에 숨어 있습니다. 리트리버가 느린 건지, LLM 자체가 느린 건지, 응답 포맷팅이 문제인지 — 전체 소요 시간만 보면 원인을 특정할 수 없습니다. 레이턴시 트레이싱은 요청 전체 경로를 스팬(Span) 단위로 쪼개 각 단계의 지연 시간을 구조화해 기록합니다.
핵심 지표 세 가지는 꼭 알아두실 만합니다.
| 지표 | 의미 | 실무 목표값 |
|---|---|---|
| TTFT (Time to First Token) | 첫 토큰 도달까지 걸린 시간 | 채팅 UI 기준 p95 ≤ 0.6 s |
| TPOT (Time Per Output Token) | 토큰당 생성 속도 | 스트리밍 경험 품질 결정 |
| E2E Latency | 전체 요청·응답 사이클 | 서비스 유형별 SLO 설정 |
2025년부터 주목받는 개념이 Goodput입니다. 단순히 초당 토큰 수(throughput)를 최적화하는 대신, "TTFT와 TPOT SLO를 동시에 만족하는 요청 비율"을 KPI로 삼는 관점입니다. 공식으로 표현하면 Goodput = SLO를 만족한 요청 수 / 전체 요청 수이며, NVIDIA, Anyscale, BentoML이 이를 핵심 지표로 채택하면서 업계 전반으로 퍼지고 있습니다.
Goodput: 서버가 토큰을 빠르게 생성해도 사용자의 체감 SLO를 만족하지 못하면 의미가 없습니다. 엔지니어링 지표와 사용자 경험 지표를 하나로 묶은 개념입니다.
업계 표준으로는 OpenTelemetry의 GenAI 시맨틱 컨벤션이 자리를 잡았습니다. 2024년 말 제안된 이 컨벤션은 Langfuse(오픈소스 LLM 관측성 플랫폼), Arize Phoenix(에이전트 궤적 시각화 도구), Traceloop 등 주요 도구에 통합됐고, 계측 오버헤드는 호출당 1 ms 미만으로 LLM API 레이턴시(100 ms~30 s)에 비하면 무시할 수 있는 수준입니다.
실전 적용
예시 1: RAG 품질 게이팅 — PR마다 자동으로 품질 검사하기
RAG 시스템에서 흔히 겪는 문제가 있습니다. 프롬프트를 살짝 바꿨더니 검색 내용과 다른 답변이 나오거나, 관련 없는 문서를 컨텍스트로 넣어주는 상황입니다. 이걸 매 PR마다 자동으로 잡아낼 수 있습니다.
RAGAS(RAG 평가 특화 프레임워크)로 네 가지 지표를 측정하고, 임계값 미달 시 배포를 차단하는 구조입니다.
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from datasets import Dataset
# 예시 데이터 — 실제로는 프로덕션 로그에서 "잘 된" 케이스를 골라 구축합니다
questions = ["반품 정책이 어떻게 되나요?", "배송 기간은 얼마나 걸리나요?"]
generated_answers = [
"구매 후 30일 이내에 반품 가능합니다.",
"영업일 기준 3~5일 소요됩니다.",
]
retrieved_contexts = [
["저희 반품 정책은 구매일로부터 30일 이내..."],
["표준 배송은 영업일 기준 3~5일..."],
]
ground_truths = ["구매 후 30일 이내 반품 가능", "영업일 기준 3~5일 소요"]
eval_dataset = Dataset.from_dict({
"question": questions,
"answer": generated_answers,
"contexts": retrieved_contexts,
"ground_truth": ground_truths,
})
result = evaluate(
eval_dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
# 임계값 검사 — 미달 시 CI 실패로 배포 차단
thresholds = {
"faithfulness": 0.90,
"answer_relevancy": 0.85,
"context_precision": 0.80,
"context_recall": 0.80,
}
for metric, threshold in thresholds.items():
score = result[metric]
if score < threshold:
raise SystemExit(f"❌ {metric} = {score:.3f} < {threshold} — 배포 차단")
print("✅ 모든 품질 지표 통과")각 지표가 의미하는 바를 정리하면:
| 지표 | 측정 내용 | 낮으면 의심해볼 것 |
|---|---|---|
| Faithfulness | 생성 답변이 검색 컨텍스트에 충실한가 | 환각(hallucination) 발생 가능성 |
| Answer Relevancy | 질문과 답변의 관련성 | 엉뚱한 방향으로 답변 |
| Context Precision | 검색된 문서 중 실제 필요한 비율 | 리트리버 과잉 검색 |
| Context Recall | 필요한 정보가 검색됐는가 | 리트리버 누락 |
Langfuse에 평가 결과를 트레이스 주석으로 달아두면, 어떤 질문 유형에서 점수가 낮은지 나중에 분석하기 훨씬 편합니다.
예시 2: LLM 스팬 계측 설정 — OpenTelemetry로 모든 호출 추적하기
솔직히 처음 OpenTelemetry를 LLM에 연결하려고 할 때 "어디서부터 시작하지?" 싶었습니다. 생각보다 간단합니다. 핵심은 GenAI 시맨틱 컨벤션에 정의된 속성 이름을 정확히 써주는 것입니다.
from openai import OpenAI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# 트레이서 초기화
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("agent-pipeline")
llm_client = OpenAI()
def call_llm_with_tracing(prompt: str, model: str = "gpt-4o") -> str:
with tracer.start_as_current_span("llm-call") as span:
# GenAI 시맨틱 컨벤션 속성
span.set_attribute("gen_ai.system", "openai")
span.set_attribute("gen_ai.request.model", model)
span.set_attribute("gen_ai.request.max_tokens", 1024)
response = llm_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
# 토큰 사용량 기록
span.set_attribute(
"gen_ai.usage.input_tokens",
response.usage.prompt_tokens
)
span.set_attribute(
"gen_ai.usage.output_tokens",
response.usage.completion_tokens
)
return response.choices[0].message.content매번 이걸 직접 작성하기 번거롭다면 openllmetry(Traceloop이 관리하는 OpenTelemetry 기반 LLM 자동 계측 라이브러리)를 써보시면 좋습니다. OpenAI, LangChain, LlamaIndex 등 주요 라이브러리를 자동으로 계측해줍니다.
from traceloop.sdk import Traceloop
Traceloop.init(app_name="my-agent", disable_batch=False)
# 이후 LLM 호출은 자동으로 스팬이 생성됩니다처음 Langfuse 대시보드에서 각 LLM 호출의 토큰 수와 레이턴시가 스팬 단위로 시각화되는 걸 보면 꽤 인상적입니다. 어느 단계에서 시간이 많이 걸리는지 한눈에 보이거든요.
예시 3: 멀티스텝 에이전트 도구 호출 검증 — 순서와 정확성을 자동으로 확인하기
에이전트가 CRM 조회 → 티켓 생성 → 이메일 발송 순서로 동작해야 하는데, 이 순서가 맞는지 자동으로 검증하고 싶을 때 DeepEval(Confident AI에서 개발한 LLM 평가 프레임워크)의 에이전트 메트릭이 도움이 됩니다.
실제로 팀에서 써보니 도구 호출 순서 오류가 생각보다 자주 발생했습니다. 특히 프롬프트를 조금 수정한 후에 호출 순서가 뒤바뀌는 경우가 있었는데, 자동 검증이 없었다면 훨씬 나중에야 알았을 것 같습니다.
from deepeval import evaluate
from deepeval.metrics import AgentTaskCompletionMetric, ToolCorrectnessMetric
from deepeval.test_case import LLMTestCase
# 에이전트 실행 후 궤적(trajectory) 수집
test_case = LLMTestCase(
input="고객 ID 1234의 환불 요청을 처리해줘",
actual_output=agent_response,
# 실제 도구 호출 순서 (DeepEval API 버전에 따라 파라미터명 확인 필요)
actual_tool_calls_made=["get_customer_crm", "create_ticket", "send_email"],
# 기대 도구 호출 순서
expected_tools=["get_customer_crm", "create_ticket", "send_email"],
)
task_metric = AgentTaskCompletionMetric(threshold=0.8, model="gpt-4o")
tool_metric = ToolCorrectnessMetric(threshold=0.9)
evaluate([test_case], metrics=[task_metric, tool_metric])실행 궤적(Trajectory): 에이전트가 태스크를 수행하는 동안 선택한 도구 호출 순서, 인자, 중간 결과의 전체 기록입니다. 최종 응답만 보는 것보다 "왜 그 결과가 나왔는가"를 훨씬 잘 설명해줍니다.
실패 케이스는 Langfuse에 needs_human_review 태그를 달아 인간 리뷰 큐로 라우팅하는 구조를 추가하면, 완전 자동화와 인간 검수 사이의 균형을 잡을 수 있습니다.
예시 4: CI 파이프라인에 Eval 스텝 삽입하기 — 프롬프트 변경마다 자동 채점
프롬프트 수정 PR이 올라올 때마다 골든 데이터셋 100개 케이스에 대해 Judge 채점을 자동 실행하는 워크플로입니다.
# .github/workflows/llm-eval.yml
name: LLM Eval CI
on:
pull_request:
paths:
- 'prompts/**'
- 'src/agent/**'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install deepeval ragas
- name: Run LLM Evals
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
deepeval test run tests/eval/ \
--min-success-rate 0.85 \
--model gpt-4o-mini
- name: Upload eval results
uses: actions/upload-artifact@v4
with:
name: eval-results
path: eval_results.json성공률이 85% 미만이면 CI가 실패하고 머지가 차단됩니다. gpt-4o-mini를 Judge로 쓰면 비용도 크게 줄일 수 있습니다. 100개 케이스 채점에 $0.01~$0.05 수준입니다(작성 시점 기준이며 모델 가격 정책에 따라 변동됩니다). paths: 트리거를 써서 프롬프트 파일이 변경된 PR에만 eval이 돌도록 제한하면 불필요한 비용도 아낄 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 비용 효율성 | 인간 레이블링 대비 500~5,000배 저렴하고 즉시 실행 가능 |
| 주관적 품질 평가 | 어조, 유창성, 맥락 적합성 등 규칙 기반으로 잡기 어려운 품질 측정 가능 |
| 평가 기준 유연성 | 새 기준 추가 시 Judge 프롬프트 수정만으로 바로 적용 |
| 자동화 통합 | CI/CD에 삽입해 프롬프트·모델 변경마다 회귀 자동 감지 |
| 트레이싱 오버헤드 | OpenTelemetry 계측 자체 오버헤드 1 ms 미만, 실 서비스 영향 거의 없음 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Position Bias | 먼저 제시된 답변을 선호 (최대 40% 불일치) | 응답 순서를 바꿔 두 번 평가한 뒤 평균 |
| Verbosity Bias | 길고 상세한 답변에 과대평가 (~15%) | 루브릭에 "길이와 무관하게 정확성만 평가" 명시 |
| Self-Enhancement Bias | 동일 계열 모델 응답 선호 (5~7%) | 이기종 Judge 교차 검증 (예: GPT-4o + Claude) |
| Preference Leakage | Judge 학습 데이터에 평가 대상 포함 가능 | 데이터 오염 감사 주기적 실시 |
| Judge 드리프트 | Judge 자체도 시간이 지나면 성능이 달라질 수 있음 | Judge-인간 일치율 메타 평가 정기 실시 |
| 샘플링 비용 | 고부하 환경에서 100% 샘플링은 스토리지·비용 부담 | 프로덕션은 5~10% 샘플링, 이상 감지 시 확대 |
실무에서 가장 흔한 실수
-
Judge 편향을 보정하지 않고 점수를 그대로 신뢰하는 것. 같은 내용인데 응답 순서만 바꿨더니 점수가 40%까지 달라지는 경우가 있습니다. 최소한 순서 스왑 평균은 적용해보시는 것을 권장합니다.
-
p50 레이턴시만 모니터링하는 것. 평균이 괜찮아 보여도 p99에서 30초씩 걸리는 요청이 섞여 있을 수 있습니다. SLO는 p95·p99 기준으로 설정하는 것이 실제 사용자 경험을 더 잘 반영합니다.
-
오프라인 eval만 구축하고 프로덕션 모니터링을 생략하는 것. 골든셋에서 통과해도 실제 트래픽에서는 다른 유형의 질문이 들어옵니다. 온라인 샘플링 eval을 병행하면 배포 후 드리프트를 빠르게 감지할 수 있습니다.
마치며
에이전트 평가 파이프라인의 핵심은 "언제 뭔가 잘못됐는지" 사람보다 먼저 알아채는 구조를 만드는 것입니다. 처음부터 완벽한 파이프라인을 구축하려 하면 오히려 아무것도 시작하지 못합니다. 작게 시작해서 점진적으로 확장하는 방식이 훨씬 현실적입니다.
지금 바로 시작해볼 수 있는 3단계:
-
Langfuse 무료 클라우드 계정을 만들고 기존 LLM 호출에 openllmetry를 연결해보시면 됩니다.
pip install traceloop-sdk후Traceloop.init()한 줄이면 트레이스가 자동으로 수집되기 시작합니다. -
RAG나 에이전트가 있다면 골든 데이터셋 20~50개부터 구축해보시면 좋습니다. 현재 프로덕션 로그에서 "잘 된" 케이스를 골라내는 것이 가장 빠른 방법입니다. RAGAS나 DeepEval로 베이스라인 점수를 측정해두면, 이후 변경이 개선인지 회귀인지 객관적으로 비교할 수 있습니다.
-
GitHub Actions에
deepeval test run스텝을 추가해 프롬프트 파일 변경 PR에만 eval이 실행되도록 설정해보시면 됩니다. 성공률 임계값은 처음엔 낮게 잡고(70~75%), 팀이 익숙해지면 점진적으로 올리는 방식을 권장합니다.
참고 자료
평가 방법론
- LLM-as-a-Judge: A Complete Guide | Evidently AI
- A Survey on LLM-as-a-Judge | ScienceDirect (2025)
- LLM-as-a-Judge | Langfuse 공식 문서
- LLM-as-a-Judge Done Right: Calibrating & Debiasing | Kinde
- When AIs Judge AIs: Agent-as-a-Judge Evaluation | arXiv
- Evaluating LLM Applications: A Comprehensive Roadmap | Langfuse Blog
도구
- 8 LLM Observability Tools to Monitor & Evaluate AI Agents | LangChain
- LLM Observability Best Practices for 2025 | Maxim AI
- Best AI Evals Tools for CI/CD in 2025 | Braintrust
CI 통합
- How to Add LLM Evaluations to CI/CD Pipelines | Arize AI
- Implement a CI/CD Pipeline using LangSmith | LangChain 공식 문서
성능 지표 및 관측성
- LLM Observability with OpenTelemetry: A Practical Guide | Medium
- An Introduction to Observability for LLM-based Applications using OpenTelemetry | OpenTelemetry 공식 블로그
- OpenTelemetry for LLMs: Complete SRE Guide | OpenObserve
- TTFT vs Throughput: Which Metric Impacts Users More? | Clarifai
- Key Metrics for LLM Inference | BentoML LLM Inference Handbook