왜 LLM 서비스에는 평가 파이프라인이 필요한가

LLM을 업무 시스템에 붙이면 데모는 빠르게 만들 수 있지만 운영 품질은 별개의 문제입니다. 프롬프트를 바꿨을 때 기존 답변이 깨지지 않았는지, 모델을 교체해도 고객 응대 기준이 유지되는지, RAG 검색 결과가 바뀌어도 근거 없는 답변이 늘지 않았는지를 계속 확인해야 합니다. 사람의 감으로만 검수하면 배포 속도와 품질이 동시에 흔들립니다. LLM 평가 파이프라인은 테스트 데이터, 채점 기준, 실행 로그, 회귀 기준선을 코드로 묶어 AI 기능을 일반 소프트웨어처럼 관리하게 해 주는 장치입니다.

OpenAI Evals 문서는 평가를 LLM 애플리케이션이 기대한 방식으로 동작하는지 확인하는 절차로 설명하며, 평가 정의, 테스트 입력 실행, 결과 분석이라는 흐름을 제시합니다. OpenTelemetry GenAI 시맨틱 컨벤션은 개발 상태이지만 모델 호출, 토큰 사용량 같은 관측 항목을 표준화하려는 방향을 보여 줍니다.

평가 대상을 작게 나눈다

가장 흔한 실수는 “챗봇이 좋은 답을 하는가”처럼 평가 범위를 크게 잡는 것입니다. 문서 검색형 RAG라면 검색 품질, 충실도, 금지 표현, 출처, 응답 시간, 토큰 비용을 각각 봐야 합니다. 상담 챗봇이라면 의도 분류, 개인정보 노출, 이관 판단, 답변 톤을 분리합니다. 그래야 점수 하락 원인을 빠르게 찾을 수 있습니다.

  • 정확성: 정답 라벨이 있는 질문에서 기대 답변과 일치하는지 확인합니다.
  • 충실도: 검색된 문서에 없는 내용을 지어내지 않았는지 확인합니다.
  • 근거성: 답변의 출처나 문서 ID가 실제 근거와 연결되는지 확인합니다.
  • 안전성: 개인정보, 내부 토큰, 금지된 법률·의료 단정 표현이 포함되지 않는지 확인합니다.
  • 운영성: 지연 시간, 토큰 사용량, 실패율이 허용 범위 안에 있는지 확인합니다.

테스트 데이터셋은 실제 장애 후보로 만든다

좋은 평가 데이터는 예쁜 예제가 아니라 운영에서 문제를 만들 가능성이 높은 질문입니다. 자주 들어오는 질문, 과거 오답 사례, 비슷한 문서가 많아 검색이 헷갈리는 질문, 최신 정책이 반영되어야 하는 문장, 답하면 안 되는 요청을 섞어야 합니다. 핵심 기능별로 30~50개 대표 케이스를 만들고, 운영 실패 사례를 계속 추가하는 방식이 더 오래 갑니다.

각 샘플에는 입력, 기대 결과, 금지 조건, 필요한 근거 문서 ID를 넣습니다. RAG라면 질문과 정답만 저장하지 말고 “반드시 참조해야 하는 문서”와 “참조하면 안 되는 오래된 문서”도 함께 넣는 것이 좋습니다. 그래야 청크 크기나 임베딩 모델을 바꿨을 때 품질 저하를 잡을 수 있습니다.

CI에서 실행할 최소 평가 스크립트 예시

아래 예시는 Node.js로 만든 간단한 평가 러너입니다. 실제 운영에서는 모델 호출 부분을 사내 LLM 게이트웨이나 API 클라이언트로 바꾸고, 결과를 JSON 파일과 대시보드에 함께 저장하면 됩니다. 핵심은 점수가 낮으면 배포를 실패시키는 기준선을 코드로 남기는 것입니다.

import fs from 'node:fs/promises';

const cases = JSON.parse(await fs.readFile('./eval-cases.json', 'utf8'));

async function callApp(input) {
  const res = await fetch('https://internal.example.com/ai/answer', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ question: input })
  });
  if (!res.ok) throw new Error('AI gateway failed: ' + res.status);
  return res.json();
}

function score(sample, output) {
  const text = output.answer ?? '';
  const hasRequired = sample.requiredKeywords.every((word) => text.includes(word));
  const hasForbidden = sample.forbiddenKeywords.some((word) => text.includes(word));
  const hasSource = sample.requiredSources.every((id) => output.sources?.includes(id));
  return { pass: hasRequired && !hasForbidden && hasSource, hasRequired, hasForbidden, hasSource };
}

let passed = 0;
const results = [];

for (const sample of cases) {
  const output = await callApp(sample.question);
  const result = score(sample, output);
  results.push({ id: sample.id, ...result });
  if (result.pass) passed += 1;
}

const passRate = passed / cases.length;
await fs.writeFile('./eval-result.json', JSON.stringify({ passRate, results }, null, 2));

if (passRate < 0.92) {
  throw new Error('LLM eval failed: passRate=' + passRate);
}

채점 기준은 규칙 기반과 LLM 기반을 섞는다

모든 것을 LLM 평가자에게 맡기면 비용이 커지고 판정이 흔들립니다. 반대로 문자열 일치만 쓰면 좋은 답변도 실패 처리합니다. 실무에서는 규칙 기반 검사를 먼저 통과시키고, 의미 판단만 평가 모델에 맡기는 구성이 안정적입니다. 개인정보 패턴, 금지어, 출처 ID, JSON 스키마, 응답 시간은 규칙으로 확인하고, 답변이 문서에 근거했는지나 상담 톤이 적절한지는 LLM 평가자 또는 사람이 검수한 라벨을 활용합니다.

평가 모델을 쓸 때는 채점 프롬프트도 버전 관리해야 합니다. “좋음/나쁨”처럼 모호하게 묻지 말고 0점부터 3점까지의 기준을 명확히 적습니다. 결과에는 원본 질문, 검색된 문서, 모델 답변, 채점 사유, 채점 모델 버전을 모두 남겨야 장애 분석이 가능합니다.

배포 기준선과 운영 로그를 연결한다

평가 파이프라인의 목표는 배포를 느리게 만드는 것이 아니라 위험한 변경만 막는 것입니다. 프롬프트, 시스템 메시지, 검색 인덱스, 임베딩 모델, LLM 모델, 안전 필터가 바뀔 때마다 같은 데이터셋으로 이전 버전과 새 버전을 비교합니다. 전체 통과율만 보지 말고 카테고리별 하락을 봐야 합니다. 전체 점수는 높아도 개인정보 관련 케이스가 실패했다면 배포를 중단해야 합니다.

운영에서는 모델 이름, 프롬프트 버전, 토큰 사용량, 지연 시간, 오류, 사용자 피드백, 검색 문서 ID를 남겨야 합니다. 마지막 체크리스트는 단순합니다. 평가 케이스, 코드 기준선, 실패 사례 재반영, 모델·프롬프트·검색 버전 로그를 확인하면 됩니다.