Skip to main content
Summary

출시 버튼을 누른 뒤에야 진짜 데이터가 온다

LLM 앱을 처음 만들 때 우리는 대개 프롬프트에 집중합니다. 같은 질문을 여러 번 던져 보고, 답변이 그럴듯하면 "이 정도면 됐다"고 생각합니다. 하지만 실제 사용자가 들어오는 순간 상황은 달라집니다. 사용자는 우리가 예상한 문장으로 질문하지 않습니다. 오타를 내고, 중간에 말을 바꾸고, 회사 내부 용어를 쓰고, 화가 난 상태로 짧게 묻고, 때로는 문서에 없는 정보를 요구합니다.

LLM 앱의 품질은 개발자의 노트북에서 결정되지 않습니다. 출시 이후 쌓이는 실제 질문, 검색 결과, 모델 답변, 오류, 사용자 반응 속에서 비로소 선명해집니다. 그래서 운영 단계에서 가장 중요한 재료는 "로그"입니다. 단, 아무 로그나 많이 남기는 것이 아니라 나중에 원인을 추적할 수 있는 형태로 남겨야 합니다.

전통적인 웹 서비스라면 HTTP 상태 코드, 응답 시간, 서버 CPU, 에러 로그만으로도 꽤 많은 문제를 찾을 수 있습니다. 그러나 LLM 앱은 조금 다릅니다. 서버는 200 OK를 반환했는데 답변은 완전히 틀렸을 수 있습니다. 응답 시간은 1.2초로 훌륭한데, 고객에게 존재하지 않는 환불 정책을 안내했을 수도 있습니다. LLM 모니터링이 일반 APM(Application Performance Monitoring)과 달리 입력, 출력, 검색, 도구 호출, 토큰, 비용, 품질 평가를 함께 봐야 하는 이유가 여기에 있습니다.1

이 장의 목표는 간단합니다. LLM 앱의 품질을 "느낌"이 아니라 "반복 측정 가능한 시스템"으로 바꾸는 것입니다. 한 번 잘 만든 프롬프트를 믿고 방치하는 것이 아니라, 실제 사용 로그를 바탕으로 계속 더 나은 제품을 만드는 운영 루프를 설계해 보겠습니다.

품질 평가와 로그 기반 개선

좋은 답변 하나보다 중요한 것은 재현 가능한 평가다

LLM 앱을 평가할 때 가장 흔한 실수는 마음에 드는 질문 몇 개로만 테스트하는 것입니다.

"우리 회사 환불 정책 알려 줘."

"이 PDF 요약해 줘."

"이 글을 블로그 스타일로 바꿔 줘."

이런 질문에 답이 괜찮으면 안심합니다. 하지만 다음 날 프롬프트를 조금 고쳤더니 원래 잘 답하던 질문이 망가질 수 있습니다. 검색 청크 크기를 바꿨더니 어려운 질문은 좋아졌지만 쉬운 질문에서 엉뚱한 문서를 가져올 수 있습니다. 모델을 더 저렴한 버전으로 바꿨더니 비용은 줄었지만 구조화 출력 오류가 늘어날 수 있습니다.

따라서 평가의 핵심은 "이번에 좋아 보인다"가 아니라 "같은 기준으로 반복해서 비교할 수 있다"입니다. Langfuse 문서에서는 평가를 LLM 앱의 동작을 반복적으로 확인하는 방법으로 설명합니다. 프롬프트를 바꾸거나 기능을 수정했을 때, 그 변화가 의도하지 않은 퇴행을 만들었는지 확인하는 장치라는 뜻입니다.2

LLM 앱 평가에서는 모델 자체의 능력만 보는 것도 부족합니다. 같은 모델을 쓰더라도 프롬프트, 검색 전략, 문서 품질, 후처리 코드, UI 흐름에 따라 사용자가 경험하는 품질은 크게 달라집니다. 그래서 제품 관점에서는 "모델 평가"보다 "시스템 평가"가 더 중요합니다. 사용자가 질문을 입력하고, 검색기가 문서를 찾고, 프롬프트가 조립되고, 모델이 답하고, 애플리케이션이 답변을 검증해 화면에 보여 주는 전체 흐름을 평가해야 합니다.3

좋은 평가 시스템은 세 가지 질문에 답할 수 있어야 합니다.

  • 지금 우리 AI 기능은 실제 사용자에게 충분히 도움이 되는가?

  • 방금 바꾼 프롬프트, 검색 설정, 모델 버전이 이전보다 나아졌는가?

  • 문제가 생겼을 때 원인이 검색 실패인지, 모델 답변 실패인지, 형식 오류인지, 안전성 문제인지 추적할 수 있는가?

이제 이 질문에 답하기 위한 첫 번째 재료인 trace부터 설계해 봅시다.

모든 요청을 하나의 trace로 묶기

LLM 앱의 로그는 단순히 "사용자 질문"과 "모델 답변"만 저장해서는 부족합니다. RAG 챗봇을 예로 들어 보겠습니다. 사용자가 질문합니다. 시스템은 질문을 임베딩으로 바꿉니다. 벡터 데이터베이스에서 문서를 검색합니다. 검색된 문서를 프롬프트에 넣습니다. 모델을 호출합니다. 답변을 구조화 출력으로 파싱합니다. 출처를 붙입니다. 화면에 보여 줍니다.

답변이 틀렸다면 어디서 틀어졌을까요? 검색된 문서가 잘못되었을 수도 있고, 검색은 잘됐지만 모델이 문서를 무시했을 수도 있습니다. 답변은 맞았지만 JSON 파싱에 실패했을 수도 있습니다. 모델 출력은 정상인데 UI 코드가 출처를 잘못 연결했을 수도 있습니다.

이런 문제를 추적하려면 하나의 사용자 요청을 여러 단계로 쪼개어 기록하되, 전체를 하나의 trace로 묶어야 합니다. trace는 "한 요청이 시스템 안에서 지나간 전체 경로"라고 생각하면 됩니다.

최소한 다음 정보는 남기는 것이 좋습니다.

구분남길 정보이유
요청사용자 질문, 세션 ID, 기능 이름, 시간어떤 사용 맥락에서 문제가 생겼는지 알기 위해
프롬프트시스템 프롬프트 버전, 템플릿 이름, 최종 조립된 메시지프롬프트 변경의 영향을 추적하기 위해
모델모델 이름, 버전, temperature, max tokens모델 설정 차이를 비교하기 위해
검색검색 쿼리, 검색 방식, top-k, 청크 ID, 점수검색 실패와 생성 실패를 구분하기 위해
답변모델 원문 출력, 후처리 결과, 출처실제 사용자에게 무엇이 전달됐는지 확인하기 위해
운영입력/출력 토큰, 지연 시간, 비용, 오류 코드비용과 성능 문제를 함께 보기 위해
피드백좋아요/싫어요, 재질문 여부, 상담원 전환 여부사용자가 답변을 어떻게 받아들였는지 보기 위해

LLM 모니터링 도구들이 입력, 출력, 토큰 사용량, 지연 시간, 비용, 다단계 trace를 함께 기록하는 이유도 여기에 있습니다.1 이 구조가 있어야 "오늘 비용이 왜 갑자기 늘었지?", "새 프롬프트 배포 뒤에 환각이 늘었나?", "특정 고객군에서만 검색 실패가 발생하나?" 같은 질문에 답할 수 있습니다.

아주 단순한 trace 구조를 코드로 표현하면 다음과 같습니다.

from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
import time
import uuid


@dataclass
class TraceStep:
    name: str
    input: dict[str, Any]
    output: dict[str, Any] | None = None
    started_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
    ended_at: str | None = None
    latency_ms: int | None = None
    error: str | None = None


@dataclass
class LLMTrace:
    trace_id: str
    user_id: str | None
    session_id: str | None
    feature: str
    prompt_version: str
    model: str
    steps: list[TraceStep] = field(default_factory=list)
    feedback: dict[str, Any] | None = None
    total_latency_ms: int | None = None
    total_cost_usd: float | None = None


def new_trace(
    feature: str,
    prompt_version: str,
    model: str,
    user_id: str | None = None,
    session_id: str | None = None,
) -> LLMTrace:
    return LLMTrace(
        trace_id=str(uuid.uuid4()),
        user_id=user_id,
        session_id=session_id,
        feature=feature,
        prompt_version=prompt_version,
        model=model,
    )


def run_step(trace: LLMTrace, name: str, input_data: dict, fn):
    step = TraceStep(name=name, input=input_data)
    start = time.perf_counter()
    try:
        output = fn()
        step.output = output
        return output
    except Exception as e:
        step.error = repr(e)
        raise
    finally:
        step.ended_at = datetime.utcnow().isoformat()
        step.latency_ms = int((time.perf_counter() - start) * 1000)
        trace.steps.append(step)

실제 서비스에서는 이 trace를 데이터베이스, 로그 수집 시스템, LLM 관측 도구, 데이터 웨어하우스 중 하나에 저장할 수 있습니다. 중요한 것은 저장 위치가 아닙니다. 나중에 분석할 수 있도록 같은 구조로 꾸준히 남기는 것입니다.

RAG 챗봇에 적용하면 다음처럼 단계별 기록을 남길 수 있습니다.

trace = new_trace(
    feature="book_qa",
    prompt_version="qa-v3",
    model="gpt-4.1-mini",
    user_id="user_123",
    session_id="session_456",
)

question = "이 책에서 RAG는 왜 필요한가요?"

search_result = run_step(
    trace,
    name="retrieve",
    input_data={"question": question, "top_k": 5},
    fn=lambda: {
        "chunks": [
            {"chunk_id": "ch_001", "score": 0.83, "text": "RAG는 외부 문서를 검색해..."},
            {"chunk_id": "ch_014", "score": 0.79, "text": "검색된 근거를 프롬프트에 넣으면..."},
        ]
    },
)

answer_result = run_step(
    trace,
    name="generate_answer",
    input_data={
        "question": question,
        "chunk_ids": [c["chunk_id"] for c in search_result["chunks"]],
        "prompt_version": trace.prompt_version,
    },
    fn=lambda: {
        "answer": "RAG는 모델이 내부 기억에만 의존하지 않고 외부 문서를 근거로 답하게 해 줍니다.",
        "input_tokens": 1200,
        "output_tokens": 80,
        "cost_usd": 0.003,
    },
)

trace.total_cost_usd = answer_result["cost_usd"]

이런 trace가 쌓이면 그때부터 품질 개선은 감이 아니라 수사에 가까워집니다. 사건 현장에는 질문, 검색 결과, 프롬프트, 모델 답변, 지연 시간, 비용, 사용자 반응이 모두 남아 있습니다.

품질 지표와 운영 지표를 함께 본다

LLM 앱의 대시보드를 만들 때 "평균 응답 시간"과 "에러율"만 보면 위험합니다. LLM 앱은 조용히 실패할 수 있습니다. 빠르게 틀릴 수 있고, 싸게 엉뚱한 답을 할 수 있으며, 문법적으로 완벽하지만 근거 없는 답을 낼 수 있습니다.

그래서 지표를 크게 두 덩어리로 나누어야 합니다. 하나는 품질 지표이고, 다른 하나는 운영 지표입니다. 품질 지표는 사용자가 받은 답변이 실제로 좋은지 측정합니다. 운영 지표는 시스템이 비용, 속도, 안정성 측면에서 지속 가능한지 측정합니다. Datadog의 LLM 평가 프레임워크 관련 글에서도 LLM 앱은 지연 시간, 에러율 같은 운영 지표뿐 아니라 정확성, 관련성, 일관성, 독성, 안전성처럼 사용 사례에 맞춘 기능적 품질 지표를 함께 정의해야 한다고 설명합니다.4

대표적인 품질 지표는 다음과 같습니다.

지표질문예시
정확성답이 사실적으로 맞는가?"환불 기간은 7일"인데 30일이라고 답하지 않았는가
근거성답변의 주장이 검색 문서로 뒷받침되는가?출처 문서에 없는 정책을 만들어 내지 않았는가
답변 관련성사용자 질문에 직접 답했는가?배송 질문에 회원가입 절차를 설명하지 않았는가
검색 관련성검색된 문서가 질문과 관련 있는가?RAG 질문에 가격표 문서를 가져오지 않았는가
완전성필요한 조건과 예외를 빠뜨리지 않았는가?"단, 개봉 상품 제외" 같은 핵심 조건을 누락하지 않았는가
형식 준수요구한 JSON, 표, 문장 수를 지켰는가?구조화 출력 스키마를 위반하지 않았는가
친절도사용자가 이해하기 쉽게 답했는가?딱딱한 오류 메시지 대신 다음 행동을 안내했는가
안전성개인정보, 유해 내용, 금지 조언을 피했는가?의료·법률 확정 조언을 단정하지 않았는가
문제 해결률사용자가 추가 도움 없이 목적을 달성했는가?재질문 없이 문제를 해결했는가

운영 지표도 함께 봐야 합니다.

지표질문예시
지연 시간사용자가 기다릴 만한 속도인가?p95 응답 시간이 8초를 넘지 않는가
비용요청당 비용이 감당 가능한가?PDF 요약 1회 비용이 목표 범위 안에 있는가
실패율API 오류, 파싱 오류, 검색 오류가 얼마나 나는가?JSON 파싱 실패율이 1% 미만인가
토큰 사용량프롬프트가 불필요하게 길지 않은가?검색 청크를 너무 많이 넣어 비용이 늘지 않는가
캐시 적중률반복 요청을 효율적으로 처리하는가?자주 묻는 질문이 캐시로 처리되는가
상담원 전환율AI가 해결하지 못해 사람이 개입하는 비율은?고객지원 챗봇의 전환율이 줄고 있는가

중요한 점은 모든 지표를 한 번에 완벽히 만들 필요가 없다는 것입니다. 처음에는 기능별로 가장 중요한 품질 지표 두세 개와 운영 지표 두세 개를 고르면 됩니다.

예를 들어 문서 기반 Q&A 챗봇이라면 정확성, 근거성, 검색 관련성, 응답 시간, 비용이 핵심입니다. 블로그 글 생성기라면 사실성보다 톤, 구조, 반복 여부, 사용자 수정률이 더 중요할 수 있습니다. 고객지원 챗봇이라면 문제 해결률, 정책 준수, 상담원 전환율, 불만 피드백이 중요합니다.

품질 지표는 앱의 목적에서 출발해야 합니다. 사용자가 이 AI 기능을 왜 쓰는지 묻고, 그 목적을 달성했는지를 측정해야 합니다.

RAG는 검색과 생성을 나누어 평가한다

RAG 앱에서 답변이 틀렸을 때 가장 먼저 해야 할 질문은 이것입니다.

"검색이 실패했나, 생성이 실패했나?"

이 둘을 구분하지 않으면 엉뚱한 곳을 고치게 됩니다. 검색기가 관련 문서를 전혀 가져오지 못했는데 프롬프트만 고치고 있을 수 있습니다. 반대로 검색은 정확했는데 모델이 문서를 무시하고 아는 척한 경우, 벡터 데이터베이스를 바꿔도 문제가 해결되지 않습니다.

RAG 평가는 검색(retrieval)과 생성(generation)을 나누어 보는 것이 좋습니다. RAG 시스템은 청킹, 임베딩, 검색, 재랭킹, 프롬프트 구성, 답변 생성처럼 여러 부품이 연결된 파이프라인이기 때문입니다. 검색과 생성을 분리해 평가하면 어떤 부품이 문제인지 훨씬 빨리 찾을 수 있습니다.5

검색 평가는 보통 다음 질문을 다룹니다.

검색 지표의미
Precision@k상위 k개 검색 결과 중 실제로 관련 있는 문서 비율
Recall@k찾아야 할 관련 문서 중 상위 k개 안에 들어온 비율
Hit Rate정답 근거 문서가 하나라도 검색되었는지
NDCG@k관련 문서가 얼마나 높은 순위에 배치되었는지
검색 관련성검색 결과가 질문과 의미적으로 관련 있는지

생성 평가는 다음 질문을 다룹니다.

생성 지표의미
Answer Correctness답변이 기준 정답과 비교해 맞는지
Faithfulness/Groundedness답변의 주장이 검색 문서에서 근거를 찾을 수 있는지
Answer Relevance답변이 질문에 직접적으로 도움이 되는지
Completeness필요한 정보를 충분히 포함했는지
Format Validity요구한 출력 형식을 지켰는지

예를 들어 사용자가 "프리미엄 요금제 해지 시 환불 기준은?"이라고 물었다고 해 봅시다. 검색 결과가 "계정 삭제 방법", "쿠폰 등록 방법", "비밀번호 변경"이라면 검색 실패입니다. 모델은 아무리 똑똑해도 올바른 답을 만들기 어렵습니다. 반대로 검색 결과에 정확한 환불 정책 문서가 들어 있는데 모델이 "언제든 전액 환불됩니다"라고 답했다면 생성 실패입니다. 이때는 프롬프트에 "근거 문서에 없는 내용은 답하지 말라"는 규칙을 강화하거나, 근거성 평가와 후처리 검증을 추가해야 합니다.

RAG 평가에서 자주 쓰이는 faithfulness 평가는 답변 안의 개별 주장이 검색된 문맥에서 추론 가능한지 확인하는 방식으로 구현할 수 있습니다. 여러 도구와 프레임워크가 이런 평가를 지원하며, 보통 보조 LLM을 평가자처럼 사용합니다.4

간단한 평가 프롬프트는 다음처럼 만들 수 있습니다.

당신은 RAG 답변 평가자입니다.
아래에는 사용자 질문, 검색된 근거 문서, 모델 답변이 있습니다.

평가 기준:
1. 답변의 모든 핵심 주장이 근거 문서로 뒷받침되면 grounded=true입니다.
2. 근거 문서에 없는 사실, 숫자, 정책, 날짜를 답변이 말하면 grounded=false입니다.
3. 답변이 질문과 무관하면 relevant=false입니다.

반드시 JSON으로만 답하세요.

{
  "grounded": true 또는 false,
  "relevant": true 또는 false,
  "reason": "짧은 평가 이유"
}

이런 평가는 완벽하지 않습니다. 보조 LLM도 실수할 수 있습니다. 하지만 사람이 모든 로그를 읽을 수 없는 상황에서는 매우 유용한 1차 필터가 됩니다. 특히 점수가 낮은 trace를 모아 사람이 검수하면 실패 사례를 빠르게 발견할 수 있습니다.

평가 데이터셋은 어디서 오는가

평가를 하려면 질문과 기대 결과가 필요합니다. 이 묶음을 평가 데이터셋이라고 부릅니다. LLM 앱의 평가 데이터셋은 시험 문제집과 비슷합니다. 제품이 바뀔 때마다 같은 문제집을 풀려 보고 점수가 올랐는지, 내려갔는지 확인합니다.

하지만 처음부터 완벽한 평가셋을 갖고 있는 팀은 거의 없습니다. 그래서 여러 출처를 섞어 만들어야 합니다. 평가 데이터셋을 만드는 대표적인 방법으로는 프로덕션 로그 활용, 사람 검수, 합성 데이터 생성, 도메인 지식베이스 추출, 공개 벤치마크 활용, 레드팀 사례 수집, 실패 분석 기반 반복 개선이 자주 언급됩니다.6

실무에서는 다음 흐름이 가장 현실적입니다.

첫째, 초기에는 도메인 문서에서 자주 나올 만한 질문을 직접 만듭니다. 고객지원 챗봇이라면 환불, 배송, 계정, 결제, 장애 상황을 중심으로 질문을 작성합니다. RAG 챗봇이라면 각 질문에 대해 정답 근거가 되는 문서 ID나 청크 ID를 함께 저장합니다.

둘째, 합성 데이터를 사용해 빈틈을 메웁니다. 예를 들어 지식베이스의 한 문단을 LLM에 주고 "이 문단으로 답할 수 있는 사용자 질문과 정답을 만들어 달라"고 요청할 수 있습니다. Hugging Face의 RAG 평가 예제도 지식베이스에서 문서를 뽑아 질문-답변 쌍을 생성하고, 다시 LLM 평가자를 사용해 생성된 질문의 groundedness, relevance, standalone 여부를 걸러 내는 흐름을 보여 줍니다.7

셋째, 출시 후에는 실제 사용자 로그에서 평가셋을 보강합니다. 이것이 가장 중요합니다. 실제 사용자는 개발자가 상상하지 못한 방식으로 질문합니다. "환불 정책 알려 줘" 대신 "어제 결제했는데 마음 바뀜"이라고 묻습니다. "PDF 요약해 줘" 대신 "이거 회의에서 3분 안에 말할 수 있게 해 줘"라고 말합니다. 이런 질문이 평가셋에 들어가야 제품이 현실을 따라갑니다.

넷째, 실패 사례를 반드시 평가셋으로 승격합니다. 한 번 사고가 난 질문은 다음 배포 때 다시 틀리면 안 됩니다. 사용자가 나쁜 피드백을 남긴 trace, 상담원이 수정한 답변, 환각으로 판정된 사례, JSON 파싱 실패 사례를 모아 회귀 테스트용 데이터셋에 넣습니다.

평가 데이터셋은 다음처럼 저장할 수 있습니다.

{
  "id": "qa_001",
  "feature": "customer_support_bot",
  "input": {
    "question": "프리미엄 요금제를 오늘 해지하면 환불되나요?"
  },
  "expected": {
    "answer_summary": "결제 후 7일 이내이고 사용량이 기준 이하이면 환불 가능하다.",
    "required_doc_ids": ["refund_policy_v4"],
    "must_include": ["7일 이내", "사용량 기준"],
    "must_not_include": ["언제든 전액 환불"]
  },
  "tags": ["refund", "billing", "high_risk"],
  "source": "human_curated",
  "created_at": "2026-05-12"
}

여기서 tags는 생각보다 중요합니다. 나중에 "환불 관련 질문에서만 점수가 떨어졌는가?", "고위험 질문에서 새 모델이 더 나쁜가?", "긴 질문에서 검색 실패가 늘었는가?"처럼 부분집합별 분석을 할 수 있기 때문입니다.

평가셋은 작게 시작해도 됩니다. 처음에는 30개만 있어도 없는 것보다 훨씬 낫습니다. 그다음 100개, 300개, 1000개로 늘려 갑니다. 중요한 것은 숫자가 아니라 대표성입니다. 실제 사용자 패턴, 중요한 비즈니스 흐름, 위험한 실패 사례가 들어 있어야 합니다.

자동 평가, 사람 평가, LLM-as-a-judge

LLM 앱 평가는 한 가지 방법만으로 충분하지 않습니다. 자동 평가는 빠르지만 놓치는 뉘앙스가 있습니다. 사람 평가는 정확하지만 느리고 비쌉니다. LLM-as-a-judge는 많은 답변을 빠르게 평가할 수 있지만 평가자 모델의 편향과 실수를 고려해야 합니다.

세 가지를 적절히 섞는 것이 좋습니다.

자동 규칙 평가는 명확한 기준이 있을 때 유용합니다. JSON 파싱이 되는지, 필수 필드가 있는지, 답변 길이가 제한을 넘지 않는지, 금지어가 포함되었는지, 출처 링크가 존재하는지 같은 검사는 코드로 처리할 수 있습니다. 이런 검사는 빠르고 저렴하며 CI/CD에 넣기 좋습니다.

사람 평가는 정답이 하나로 정해지기 어렵거나, 도메인 판단이 필요한 경우에 필요합니다. 법률, 의료, 금융, 고객지원 정책처럼 틀리면 위험한 영역에서는 도메인 전문가의 검수가 중요합니다. 사람 평가는 절대적인 점수를 매기는 방식보다 두 답변 중 어느 쪽이 더 나은지 고르는 pairwise comparison이 더 안정적일 때도 많습니다.

LLM-as-a-judge는 보조 LLM을 평가자로 사용하는 방법입니다. 정답, 근거 문서, 모델 답변을 함께 주고 정확성, 관련성, 근거성, 친절도 등을 평가하게 합니다. LangSmith의 RAG 평가 예제도 정답 기준 평가, 답변 관련성 평가, 근거성 평가, 검색 문서 관련성 평가를 별도 evaluator로 구성하는 흐름을 보여 줍니다.8

아주 단순한 평가 코드는 다음처럼 만들 수 있습니다.

import json


def build_judge_prompt(question: str, context: str, answer: str) -> str:
    return f"""
당신은 고객지원 AI 답변을 평가하는 심사위원입니다.

[사용자 질문]
{question}

[검색된 근거]
{context}

[AI 답변]
{answer}

다음 기준으로 평가하세요.
- correctness: 질문에 대한 답이 사실적으로 맞는가?
- groundedness: 답변의 핵심 주장이 근거에 의해 뒷받침되는가?
- helpfulness: 사용자가 다음 행동을 이해할 수 있을 만큼 도움이 되는가?

반드시 아래 JSON 형식으로만 답하세요.
{{
  "correctness": 0에서 1 사이 숫자,
  "groundedness": 0에서 1 사이 숫자,
  "helpfulness": 0에서 1 사이 숫자,
  "reason": "짧은 이유"
}}
"""


def parse_score(raw: str) -> dict:
    data = json.loads(raw)
    return {
        "correctness": float(data["correctness"]),
        "groundedness": float(data["groundedness"]),
        "helpfulness": float(data["helpfulness"]),
        "reason": data["reason"],
    }

실전에서는 평가자 프롬프트도 버전 관리해야 합니다. 평가 기준이 바뀌면 점수도 바뀌기 때문입니다. judge_prompt_version, judge_model, evaluation_time을 trace에 함께 남기면 나중에 점수 변화를 해석하기 쉽습니다.

또한 LLM-as-a-judge 결과를 맹신해서는 안 됩니다. 처음에는 사람이 평가한 작은 골드셋을 만들고, 보조 LLM의 판정이 사람 판단과 얼마나 맞는지 확인하는 것이 좋습니다. 평가자도 하나의 모델입니다. 평가자 역시 평가해야 합니다.

회귀 테스트: 좋아진 줄 알았는데 망가지는 것을 막기

LLM 앱 개선은 종종 두더지 잡기처럼 느껴집니다. 환불 질문을 잘 답하게 프롬프트를 고쳤더니 배송 질문 답변이 길어집니다. 구조화 출력 오류를 줄이려고 예시를 추가했더니 비용이 늘어납니다. 검색 top-k를 3에서 8로 늘렸더니 근거 누락은 줄었지만 응답 시간이 느려지고, 관련 없는 문서 때문에 환각이 늘어납니다.

그래서 변경할 때마다 고정된 평가셋으로 회귀 테스트를 실행해야 합니다. 회귀 테스트는 "새 버전이 예전보다 나쁜 일을 하지 않는지" 확인하는 안전망입니다.

테스트 대상은 생각보다 많습니다.

  • 시스템 프롬프트 또는 사용자 프롬프트 템플릿 변경

  • 모델 버전 변경 또는 temperature 같은 생성 설정 변경

  • 임베딩 모델, chunk 크기, overlap, top-k, reranking 변경

  • 문서 전처리, 메타데이터 필터, 검색 쿼리 재작성 로직 변경

  • 구조화 출력 스키마, 후처리 검증, 가드레일 변경

  • 도구 호출 함수 설명, 인자 스키마, 실행 정책 변경

회귀 테스트는 다음 흐름으로 만들 수 있습니다.

def run_eval_case(app, case):
    result = app.answer(case["input"]["question"])

    checks = {
        "has_required_docs": any(
            doc_id in result["retrieved_doc_ids"]
            for doc_id in case["expected"].get("required_doc_ids", [])
        ),
        "must_include": all(
            phrase in result["answer"]
            for phrase in case["expected"].get("must_include", [])
        ),
        "must_not_include": all(
            phrase not in result["answer"]
            for phrase in case["expected"].get("must_not_include", [])
        ),
    }

    passed = all(checks.values())
    return {
        "case_id": case["id"],
        "passed": passed,
        "checks": checks,
        "answer": result["answer"],
        "retrieved_doc_ids": result["retrieved_doc_ids"],
        "latency_ms": result["latency_ms"],
        "cost_usd": result["cost_usd"],
    }


def run_eval_suite(app, dataset):
    results = [run_eval_case(app, case) for case in dataset]
    pass_rate = sum(r["passed"] for r in results) / len(results)
    avg_latency = sum(r["latency_ms"] for r in results) / len(results)
    avg_cost = sum(r["cost_usd"] for r in results) / len(results)

    return {
        "pass_rate": pass_rate,
        "avg_latency_ms": avg_latency,
        "avg_cost_usd": avg_cost,
        "results": results,
    }

물론 실제 LLM 평가는 이렇게 단순한 문자열 포함 검사만으로 충분하지 않습니다. 하지만 이 정도의 규칙 검사도 많은 사고를 막아 줍니다. 여기에 LLM-as-a-judge, 검색 관련성 평가, 사람 검수를 조합하면 훨씬 강력해집니다.

회귀 테스트 결과는 전체 평균만 보면 안 됩니다. 태그별로 나누어 봐야 합니다.

전체 통과율: 86% → 89%  ✅
환불 질문: 92% → 96%  ✅
배송 질문: 88% → 71%  ❌
고위험 질문: 90% → 82%  ❌
평균 비용: $0.004 → $0.006  ⚠️
p95 지연 시간: 3.2초 → 5.8초  ⚠️

전체 점수는 올랐지만 특정 기능이 망가질 수 있습니다. 특히 고위험 태그, VIP 고객 흐름, 결제·환불·개인정보 관련 질문은 별도로 관리하는 것이 좋습니다.

CI/CD에 평가를 넣을 때는 모든 테스트를 매번 돌릴 필요는 없습니다. 빠른 소형 평가셋은 pull request마다 실행하고, 큰 평가셋은 배포 전이나 매일 밤 실행할 수 있습니다. 운영 중인 LLM 앱은 코드만 테스트하는 것이 아니라 프롬프트, 검색, 모델, 데이터까지 함께 테스트해야 합니다.

실패 로그를 분류하면 개선 방법이 보인다

로그가 쌓이면 처음에는 거대한 쓰레기장처럼 보입니다. 질문도 다양하고 답변도 다양합니다. 하지만 실패 사례를 일정한 기준으로 분류하기 시작하면 패턴이 보입니다.

실패 유형은 제품마다 다르지만, LLM 앱에서 자주 보이는 유형은 다음과 같습니다.

실패 유형증상주된 원인개선 방향
검색 실패관련 문서를 못 찾음청킹 문제, 임베딩 부적합, top-k 부족, 메타데이터 필터 오류청크 재설계, 하이브리드 검색, reranking, 쿼리 재작성
근거 없는 답변문서에 없는 내용을 단정프롬프트 약함, 모델 추론 과잉, 근거성 검증 부재"근거 없으면 모른다" 규칙, groundedness 평가, 출처 강제
지시 불이행사용자의 형식·조건을 무시프롬프트 충돌, 긴 컨텍스트, 모델 한계지시 우선순위 정리, 예시 추가, 출력 검증
형식 오류JSON 파싱 실패, 필드 누락구조화 출력 미사용, 스키마 불명확JSON Schema 기반 구조화 출력, 재시도, validator 추가
안전성 문제개인정보 노출, 금지 조언, 유해 답변가드레일 부족, 입력 필터 부재마스킹, 정책 프롬프트, 안전성 분류기, human review
도구 호출 오류잘못된 함수 선택, 인자 오류함수 설명 부족, 인자 스키마 모호함수 설명 개선, 인자 검증, 실행 전 확인
비용 과다토큰 사용량 급증너무 많은 문서 삽입, 긴 대화 누적컨텍스트 압축, top-k 조정, 요약 메모리, 캐싱
지연 시간 과다응답이 느림순차 호출 과다, 큰 모델 남용병렬화, 스트리밍, 모델 라우팅, 비동기 작업

이 분류가 중요한 이유는 실패 유형마다 처방이 다르기 때문입니다. 검색 실패에 프롬프트를 고치는 것은 감기약으로 치통을 치료하는 것과 비슷합니다. 형식 오류에 임베딩 모델을 바꿔도 소용이 없습니다. 비용 과다 문제를 해결하려면 품질 평가뿐 아니라 토큰 사용량과 지연 시간 trace를 함께 봐야 합니다.

실패 분류는 처음에는 사람이 해도 됩니다. 운영자가 나쁜 답변 trace를 열어 보고 failure_type을 붙입니다.

{
  "trace_id": "tr_9f12",
  "failure_type": "retrieval_failure",
  "severity": "high",
  "note": "환불 정책 질문인데 계정 삭제 문서만 검색됨",
  "action_item": "refund 문서에 billing/refund 메타데이터 추가, hybrid search 실험"
}

어느 정도 사례가 쌓이면 LLM을 사용해 1차 분류를 자동화할 수 있습니다. 단, 자동 분류 결과는 "의심 후보"로 보고 중요한 사례는 사람이 확인하는 것이 좋습니다.

다음 trace를 읽고 실패 유형을 하나 선택하세요.
가능한 유형:
- retrieval_failure
- ungrounded_answer
- instruction_following_failure
- format_error
- safety_issue
- tool_call_error
- latency_or_cost_issue
- no_failure

반드시 JSON으로 답하세요.
{
  "failure_type": "...",
  "severity": "low|medium|high",
  "reason": "..."
}

실패 분류가 쌓이면 개선 우선순위가 분명해집니다. 예를 들어 최근 7일간 나쁜 피드백 300건을 분석했더니 45%가 검색 실패, 25%가 근거 없는 답변, 15%가 형식 오류라면 먼저 검색 파이프라인을 봐야 합니다. 반대로 형식 오류가 많다면 구조화 출력과 검증 로직이 우선입니다.

로그에서 개선 루프 만들기

로그 기반 개선은 한 번 분석하고 끝나는 일이 아닙니다. 다음 루프를 계속 반복해야 합니다.

로그 수집 → 실패 탐지 → 실패 분류 → 원인 분석 → 개선안 실험 → 평가셋 검증 → 배포 → 온라인 모니터링

예를 들어 고객지원 챗봇에서 "계정 삭제 후 복구 가능 여부" 질문에 잘못 답하는 문제가 발견되었다고 해 봅시다. trace를 보니 검색 결과에 최신 정책 문서가 없었습니다. 원인은 문서 파이프라인이 업데이트된 정책 문서를 아직 인덱싱하지 않은 것이었습니다.

이 경우 개선 루프는 다음처럼 진행됩니다. 먼저 실패 trace를 평가셋에 추가합니다. 그다음 문서 파이프라인을 수정해 최신 정책 문서가 인덱싱되도록 합니다. 검색 평가를 돌려 해당 질문에서 올바른 문서가 top-k 안에 들어오는지 확인합니다. 생성 평가를 돌려 답변이 정책 문서에 근거하는지 확인합니다. 배포 후에는 같은 유형의 질문에서 나쁜 피드백이 줄었는지 모니터링합니다.

또 다른 예를 보겠습니다. 블로그 글 생성기에서 사용자가 "전문적이지만 너무 딱딱하지 않게"라고 요청했는데 결과가 광고 문구처럼 과장된다고 합시다. 이 경우 검색 문제가 아니라 톤과 스타일 문제입니다. 평가셋에 톤 관련 사례를 추가하고, 프롬프트에 금지 표현과 좋은 예시를 넣고, 사람 평가나 LLM-as-a-judge로 "과장도"와 "브랜드 톤 적합성"을 측정할 수 있습니다.

개선 루프에서 주의할 점은 로그를 무작정 학습 데이터처럼 쓰지 않는 것입니다. 사용자의 입력과 모델의 실패 답변을 그대로 파인튜닝 데이터로 넣으면 나쁜 패턴이 강화될 수 있습니다. 로그는 먼저 분류하고, 정제하고, 필요하면 사람이 정답 또는 선호 답변을 만들어야 합니다. 운영 로그는 원석이고, 평가 데이터셋은 가공된 보석입니다.

A/B 테스트로 실제 사용자 환경에서 검증하기

오프라인 평가셋에서 점수가 올랐다고 해서 실제 사용자에게도 항상 좋아지는 것은 아닙니다. 평가셋이 실제 트래픽을 완벽히 대표하지 못할 수 있고, 사용자의 만족도는 정답률만으로 설명되지 않을 수 있습니다. 답변이 더 정확해졌지만 너무 길어져 사용자가 불편해할 수도 있습니다. 비용을 줄이려고 모델을 바꿨는데 고급 사용자에게만 품질 저하가 느껴질 수도 있습니다.

그래서 중요한 변경은 A/B 테스트로 검증하는 것이 좋습니다. A/B 테스트는 일부 사용자에게 기존 버전 A를, 다른 일부 사용자에게 새 버전 B를 제공하고 실제 지표를 비교하는 방식입니다.

LLM 앱에서 A/B 테스트할 수 있는 대상은 다양합니다.

  • 프롬프트 버전 A와 B

  • 검색 top-k 5와 top-k 8

  • reranking 사용 여부

  • 빠른 모델과 고성능 모델의 라우팅 기준

  • 답변 길이 정책

  • 출처 표시 방식

  • 도구 호출 전 사용자 확인 여부

A/B 테스트에서는 품질 지표와 운영 지표를 함께 봐야 합니다.

B 버전 결과
- 좋아요 비율: +6%
- 재질문율: -8%
- 상담원 전환율: -3%
- 평균 비용: +18%
- p95 지연 시간: +1.4초
- 형식 오류율: 변화 없음

이 결과를 어떻게 해석할지는 제품의 목표에 달려 있습니다. 고객지원 챗봇에서 상담원 전환율이 크게 줄었다면 비용 증가를 감수할 수 있습니다. 반대로 무료 기능에서 비용이 18% 늘었다면 더 정교한 모델 라우팅이 필요할 수 있습니다.

A/B 테스트를 할 때는 trace에 실험 정보를 반드시 남겨야 합니다.

{
  "trace_id": "tr_ab_1024",
  "experiment": {
    "name": "refund_prompt_ab_test",
    "variant": "B",
    "prompt_version": "refund-v5"
  }
}

이 정보가 없으면 나중에 어떤 사용자가 어떤 버전을 경험했는지 알 수 없습니다. 실험은 기억으로 하는 것이 아니라 로그로 해야 합니다.

또한 위험한 변경은 전체 사용자에게 바로 노출하지 않는 것이 좋습니다. 내부 사용자, 베타 사용자, 5% 트래픽, 25% 트래픽, 전체 배포처럼 단계적으로 늘리면 문제가 생겼을 때 피해를 줄일 수 있습니다.

온라인 모니터링: 조용한 품질 저하를 잡아내기

LLM 앱은 시간이 지나면서 조용히 나빠질 수 있습니다. 모델 제공자가 같은 이름의 모델을 업데이트할 수 있고, 사용자 질문 패턴이 바뀔 수 있으며, 문서가 오래될 수 있습니다. 프롬프트는 그대로인데 검색 인덱스에 새 문서가 들어오면서 답변 품질이 달라질 수도 있습니다. 이런 현상을 넓게 품질 드리프트라고 부를 수 있습니다.

온라인 모니터링은 운영 중인 실제 트래픽에서 이런 변화를 감지하는 장치입니다. 예를 들어 다음 조건에서 알림을 보낼 수 있습니다.

- 최근 1시간 JSON 파싱 실패율이 3%를 초과
- 특정 기능의 평균 비용이 전날 대비 50% 증가
- RAG groundedness 점수가 0.75 아래로 하락
- p95 지연 시간이 10초 초과
- 환불 관련 질문의 싫어요 비율이 2배 증가
- 안전성 분류기에서 high risk 입력이 급증

최근 LLM 관측 도구들은 단순히 trace를 보여 주는 것을 넘어 품질 평가 점수에 기반한 알림, 프롬프트 버전별 비교, 데이터셋 기반 실험, 운영 트래픽의 live evaluator를 제공하는 방향으로 발전하고 있습니다.12

작은 팀이라면 처음부터 거대한 모니터링 시스템을 만들 필요는 없습니다. 간단한 대시보드로 시작하면 됩니다.

대시보드 영역확인할 것
품질정확성 점수, 근거성 점수, 나쁜 피드백 비율, 실패 유형 분포
비용총 비용, 요청당 비용, 사용자별·기능별 비용, 토큰 사용량
속도평균 지연 시간, p95 지연 시간, 단계별 병목
안정성API 오류율, 파싱 실패율, 검색 실패율, 재시도 횟수
안전개인정보 감지 건수, 정책 위반 후보, 프롬프트 인젝션 의심 사례

대시보드는 화려할 필요가 없습니다. 팀이 매일 보고 행동할 수 있어야 합니다. 좋은 대시보드는 "그래서 오늘 무엇을 고쳐야 하지?"라는 질문에 답을 줍니다.

개인정보를 지키지 못하는 로그는 부채가 된다

로그를 많이 남기자는 말은 개인정보를 마구 저장하자는 뜻이 아닙니다. 오히려 LLM 앱일수록 로그 보안이 더 중요합니다. 사용자는 챗봇에게 민감한 내용을 쉽게 입력합니다. 이름, 전화번호, 이메일, 주소, 주문번호, 건강 정보, 회사 내부 문서, 계약 내용이 질문 안에 들어올 수 있습니다.

품질 개선을 위해 로그가 필요하더라도 다음 원칙을 지켜야 합니다.

첫째, 필요한 정보만 저장합니다. 전체 원문이 꼭 필요하지 않다면 요약, 해시, 일부 필드만 남길 수 있습니다. 특히 인증 토큰, 비밀번호, 결제 정보는 절대 저장하지 않는 편이 안전합니다.

둘째, 개인정보와 민감 정보는 저장 전에 마스킹합니다. 이메일, 전화번호, 주민등록번호 형식, 카드번호 형식, 주소 패턴 등을 탐지해 [EMAIL], [PHONE], [CARD]처럼 바꿉니다.

import re


def redact(text: str) -> str:
    text = re.sub(r"[w.-]+@[w.-]+.w+", "[EMAIL]", text)
    text = re.sub(r"b01[016789]-?d{3,4}-?d{4}b", "[PHONE]", text)
    text = re.sub(r"bd{4}-d{4}-d{4}-d{4}b", "[CARD]", text)
    text = re.sub(r"bd{6}-d{7}b", "[KOREAN_RRN]", text)
    return text


raw_question = "제 이메일 [email protected]이고 전화번호는 010-1234-5678인데 주문 취소해주세요."
print(redact(raw_question))
# 제 이메일 [EMAIL]이고 전화번호는 [PHONE]인데 주문 취소해주세요.

셋째, 보관 기간을 정합니다. 모든 로그를 영원히 보관하면 위험과 비용이 함께 커집니다. 디버깅용 원문 로그는 7일 또는 30일만 보관하고, 장기 분석용 데이터는 마스킹·집계된 형태로 남기는 식의 정책이 필요합니다.

넷째, 접근 권한을 제한합니다. 모든 팀원이 모든 대화 로그를 볼 필요는 없습니다. 운영자, 개발자, 도메인 검수자에게 필요한 범위만 열어야 합니다. 누가 언제 어떤 로그를 봤는지도 감사 로그로 남기는 것이 좋습니다.

다섯째, 평가 데이터셋으로 승격할 때 한 번 더 정제합니다. 프로덕션 로그에서 뽑은 사례는 개인정보 제거, 민감 내용 제거, 사용 목적 확인을 거친 뒤 평가셋에 넣어야 합니다.

로그는 제품을 똑똑하게 만드는 연료입니다. 하지만 제대로 관리하지 않으면 보안 사고의 땔감이 됩니다.

작은 팀을 위한 현실적인 시작 순서

여기까지 읽으면 해야 할 일이 너무 많아 보일 수 있습니다. trace도 남겨야 하고, 평가셋도 만들어야 하고, LLM-as-a-judge도 붙여야 하고, 대시보드도 만들어야 합니다. 하지만 처음부터 모든 것을 완벽하게 만들 필요는 없습니다.

작은 팀이라면 다음 순서로 시작해 보세요.

1단계에서는 trace를 남깁니다. 사용자 질문, 검색 결과, 프롬프트 버전, 모델 이름, 답변, 지연 시간, 비용, 오류, 사용자 피드백을 하나의 trace ID로 묶습니다. 이 단계만 해도 나중에 문제를 추적할 수 있는 기반이 생깁니다.

2단계에서는 핵심 지표를 정합니다. 기능별로 품질 지표 두세 개와 운영 지표 두세 개를 고릅니다. 예를 들어 RAG 챗봇은 검색 관련성, 근거성, 나쁜 피드백 비율, p95 지연 시간, 요청당 비용을 볼 수 있습니다.

3단계에서는 작은 평가셋을 만듭니다. 사람이 직접 만든 30개 질문으로 시작해도 좋습니다. 중요한 질문, 자주 묻는 질문, 틀리면 위험한 질문을 먼저 넣습니다.

4단계에서는 회귀 테스트를 자동화합니다. 프롬프트나 검색 설정을 바꿀 때마다 같은 평가셋을 돌려 점수와 비용, 지연 시간을 비교합니다.

5단계에서는 실패 로그를 분류합니다. 나쁜 피드백, 상담원 전환, 평가 점수 낮은 trace를 모아 검색 실패, 근거 없는 답변, 형식 오류, 안전성 문제 등으로 태깅합니다.

6단계에서는 실제 사용자 환경에서 작은 A/B 테스트를 합니다. 오프라인 평가에서 좋아진 변경만 일부 트래픽에 적용하고, 품질과 비용을 함께 봅니다.

이 순서의 핵심은 "측정 가능한 작은 루프"를 만드는 것입니다. 완벽한 평가 플랫폼을 기다리지 말고, 오늘 남길 수 있는 trace 하나부터 시작하면 됩니다.

품질은 배포 후에 자란다

LLM 앱은 한 번 만들고 끝나는 소프트웨어가 아닙니다. 사용자 질문이 바뀌고, 문서가 바뀌고, 모델이 바뀌고, 제품 목표가 바뀝니다. 그래서 품질도 고정된 상태가 아니라 계속 관리해야 하는 흐름입니다.

이 장에서 본 것처럼 출시 이후의 데이터는 AI 기능을 실제로 쓸 만하게 만드는 가장 중요한 재료입니다. 사용자 질문, 검색 결과, 프롬프트와 모델 버전, 답변, 비용, 지연 시간, 오류, 피드백이 trace로 쌓이면 우리는 더 이상 감으로 말하지 않아도 됩니다. 어떤 변경이 좋아졌는지, 어떤 실패가 반복되는지, 어디에 비용이 새는지, 어떤 사용자 경험이 나빠지는지 볼 수 있습니다.

좋은 LLM 제품은 "처음부터 완벽한 답변을 내는 제품"이 아닙니다. 그런 제품은 거의 없습니다. 좋은 LLM 제품은 실패를 발견하고, 분류하고, 평가셋에 반영하고, 다시 테스트하고, 안전하게 배포하는 루프를 가진 제품입니다.

다음 장에서는 이 운영 루프에서 절대 빠뜨릴 수 없는 주제를 다룹니다. 바로 보안과 개인정보입니다. LLM 앱은 사용자의 말을 다루고, 외부 도구를 호출하고, 때로는 민감한 문서와 연결됩니다. 똑똑한 AI 기능을 만드는 것만큼 중요한 것은, 그 기능이 사용자를 위험하게 만들지 않도록 지키는 일입니다.

참고

15 best tools for monitoring LLM applications in 2026

2Evaluation of LLM Applications - Langfuse

3LLM Evaluation: The Ultimate Guide to Metrics, Methods & Best Practices

4Building an LLM evaluation framework: best practices

5A complete guide to RAG evaluation: metrics, testing and best practices

67 Ways to Create High-Quality Evaluation Datasets for LLMs

7RAG Evaluation · Hugging Face

8Evaluate a RAG application - Docs by LangChain

책 목록

처음 만나는 LLM: 원리부터 Mini GPT 구현, RAG 서비스 개발까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기