본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 16. 02:49

회귀(Regression)를 실제로 잡아낼 수 있도록 CI/CD에 RAG 평가(Evals)를 설정하는 방법

요약

RAG 시스템의 성능 회귀를 방지하기 위해 CI/CD 파이프라인에 효과적인 평가(Evals) 게이트를 설정하는 전략을 다룹니다. 비용, 속도, 통계적 유의성을 모두 충족하기 위해 평가 단계를 세 가지 계층으로 분리하여 운영하는 방법을 제안합니다.

핵심 포인트

  • 단순 스모크 테스트를 넘어 통계적 유의성을 갖춘 RAG 평가 필요
  • 평가 단계를 Every push, Nightly main, Canary의 3단계로 분리
  • 운영 환경에서 샘플링한 데이터셋이 직접 만든 데이터셋보다 효과적
  • 경로당 100~200개의 예제를 유지하여 비용과 탐지 성능의 균형 확보

저는 이런 상황을 몇 번 겪었습니다. 업무 종료 직전에 PR(Pull Request)이 올라오고, RAG 평가(RAG eval)가 1분 안에 실행되어 초록색 체크 표시가 뜨면 그대로 머지(Merge)합니다. 그리고 12시간 뒤, 고객 지원 티켓이 들어오기 시작합니다.

트레이스(Trace)를 확인해 보니, 리트리버(Retriever)가 30개의 예제로 구성된 데이터셋이 전혀 다루지 않았던 특정 쿼리 유형에 대해 Top-1 청크(Chunk)를 변경한 것이 확인되었습니다. Suite Groundedness(근거성)는 0.91로 유지되었지만, 영향을 받은 트래픽의 실제 Production Groundedness(운영 근거성)는 0.62였습니다.

게이트(Gate)를 통과한 이유는 올바른 것을 체크하지 않았기 때문입니다. 제가 본 대부분의 RAG용 CI 평가 게이트는 스모크 테스트(Smoke tests) 수준이었습니다. 작은 데이터셋을 사용하고, 고정된 하한선(Fixed floor)과 평균을 비교하여, 심각하게 망가지지 않는 한 통과시키는 방식입니다.

데이터셋은 대표성이 없고, 하한선은 베이스라인 분산(Baseline variance)과 연결되어 있지 않으며, 임계값(Threshold)은 실제 회귀(Regression)와 판사 노이즈(Judge noise)를 구분하지 못합니다. 따라서 초록색 체크 표시가 많은 것을 알려주지는 않습니다.

현재 제가 생각하는 방식은 이렇습니다. 게이트는 세 가지 요소—저렴함(Cheap), 빠름(Fast), 통계적 유의성(Statistically significant)—를 동시에 원하지만, 보통 두 가지만 얻을 수 있습니다.
PR당 12센트가 드는 30개 예제 데이터셋은 유의성(Significance) 측면에서 실패합니다. PR당 9달러가 드는 2,000개 예제 스윕(Sweep)은 비용과 속도 측면에서 실패합니다.
과제는 이 세 가지를 모두 유지하는 것입니다.

게이트를 세 가지 계층으로 분리했습니다

모든 푸시(Push)마다 전체 LLM-판사(LLM-judge) 스윕을 실행하는 것이 저의 첫 번째 실수였습니다. 그것은 진행 중인 브랜치가 아니라, 매일 밤 메인(Nightly main) 브랜치에서 수행되어야 하는 작업입니다.

모든 푸시(Every push):

  • 저렴한 분류기 루브릭(Classifier rubrics) (NLI 충실도(Faithfulness), claim_support) 및 결정론적 하한선(Deterministic floors) (인용 유효성(Citation validity), 스키마(Schema), 지연 시간(Latency)).
  • 100~200개의 예제를 대상으로 3분 이내 실행.
  • 머지(Merge)를 차단.

매일 밤 메인(Nightly main):

  • 버전 관리된 데이터셋을 대상으로 한 전체 LLM-판사(LLM-judge) 스택 실행.
  • 15~30분 소요.
  • 다음 단계인 카나리(Canary) 배포를 차단.

카나리(Canary):

  • 실제 트래픽의 5~10%를 대상으로 동일한 루브릭(Rubrics) 점수 산출.
  • 이동 평균 드리프트(Rolling-mean drift) 발생 시 알람.

데이터셋은 게이트의 세계관입니다

  • 머릿속에서 직접 만든 2,000개의 예시 세트는 실제 운영 환경(production)에서 샘플링한 200개의 예시 세트에 패배합니다. 새벽 2시에 발생하는 실패 모드(failure modes)를 놓친다면, 게이트(gate) 또한 이를 놓치게 됩니다.
  • 경로(route)당 예시가 100개 미만이면 분산(variance)이 신호(signal)를 압도합니다.
  • 500개를 넘어가면 PR(Pull Request)당 판정 비용(judge bill)이 탐지 성능 개선 속도보다 빠르게 증가합니다. 저의 PR 범위는 경로당 100~200개이며, 해피 패스(happy paths), 엣지 케이스(edge cases), 거절(refusals), 그리고 과거 장애 사례 중 가장 어려웠던 상위 10%를 포함합니다.
  • 대부분의 사람들이 건너뛰는 분야는 정답(ground-truth) 문서 ID인 expected_chunks입니다.
  • 이것이 없으면 생성(generation)은 점수를 매길 수 있지만 검색(retrieval)은 할 수 없으며, 이분법적 분석(bisect)에 한 시간이 아닌 하루가 걸리게 됩니다.

대부분의 회귀(regression)를 커버하는 5가지 루브릭(rubrics)

CI에서 대부분의 RAG 회귀를 차단하는 5가지 루브릭은 다음과 같습니다:

  • 근거성 (Groundedness)
  • 문맥 관련성 (Context Relevance)
  • 답변 관련성 (Answer Relevance)
  • 인용 유효성 (Citation Validity)
  • 검색 재현율 (Retrieval Recall)

분석(bisect)을 용이하게 하기 위해 이를 레이어별로 나눕니다:

  • ContextRelevance는 떨어지는데 Groundedness가 유지된다면 → 검색기(retriever)에서 회귀가 발생한 것입니다.
  • Groundedness는 떨어지는데 ContextRelevance가 유지된다면 → 생성기(generator)에서 회귀가 발생한 것입니다.

인용 유효성(Citation validity)은 단순한 문자열 일치(string match)이므로 모든 응답의 100%에 대해 실행하며, 의미론적 점수 산출(semantic scoring)이 필요한 루브릭에 대해서만 판정 비용(judge bill)을 유지합니다.

평균이 아닌 변화량(delta)으로 게이팅하기

이 부분은 대부분의 설정이 건너뛰는 부분이자 가장 중요한 부분입니다.

초록색 체크 표시(green check)는 PR이 평균값을 특정 하한선 위에 유지했다는 의미가 아니라, 통계적으로 유의미한 회귀(statistically significant regression)를 유발하지 않았음을 의미해야 합니다.

저는 두 가지 임계값(thresholds)을 사용합니다.

명백한 고장을 잡아내기 위한 루브릭별 절대 하한선(absolute floor):

  • Groundedness ≥ 0.85
  • ContextRelevance ≥ 0.80
  • Citation validity ≥ 0.99

최근 7일간의 이동 평균 기준선(trailing 7-day rolling baseline) 대비 변화량(delta) 게이트는 예시별 점수에 Welch의 t-검정(Welch's t-test)을 사용하여 느린 드리프트(slow drift)를 잡아냅니다.

import statistics
from scipy import stats

...

30개의 예시 데이터셋은 1~5점 평균에서 약 ±0.07의 신뢰 구간(confidence interval)을 가지므로, 2점 하락은 노이즈 범위 내에 있게 됩니다.

그것을 기준으로 게이팅을 하면 절반의 확률로 틀리게 되며, 두 번의 오보(false alarms)가 발생하고 나면 사람들은 더 이상 게이트를 신뢰하지 않게 됩니다.

평균값 속에 숨어 있는 롱테일 실패(long-tail failures)의 경우에는 백분위수(percentiles)를 기준으로 게이트를 설정하세요.

이 과정을 정직하게 유지하는 단 하나의 규칙은 다음과 같습니다:

베이스라인(baseline)은 고정된 숫자가 아니라, 이동하는 운영 윈도우(rolling production window)여야 합니다.

GitHub Actions 연결 방식

워크플로우 자체는 경로 범위 트리거(path-scoped triggers), 캐시(cache), 병렬 pytest, 그리고 저비용 루브릭(cheap-rubric) 어설션(assertions)으로 구성됩니다.

다음은 제가 PR(Pull Request)에서 실행하는 대략적인 모습입니다.

name: RAG Evals

on:
...

별도의 야간 크론(nightly cron) 워크플로우가 모든 경로에 대해 전체 스윕(full sweep)을 실행하고, 일일 베이스라인을 옵저버(observer)에 다시 게시합니다.

이와 동일한 구조는 GitLab CI, Buildkite, Jenkins 또는 CircleCI에서도 작동합니다. 단순히 pytest, CLI, 그리고 캐시 액션(cache action)으로 이루어져 있기 때문입니다.

첫 주에 효과를 본 세 가지 요소는 다음과 같습니다:

  • 경로 범위 트리거 (Path-scoped triggers)
  • 동시성 진행 중 취소 (Concurrency cancel-in-progress)
  • 실패 사례가 포함된 평가 보고서 아티팩트 (Eval report artifacts with failing examples)

이 요소들은 경계선에 있는 회귀(regression) 현상을 논쟁이 아닌 짧은 대화로 바꾸어 놓았습니다.

운영 환경에서 동일한 루브릭 실행하기

오프라인 CI는 제가 생각할 수 있는 회귀를 잡아냅니다. 운영 환경은 그 외의 나머지를 잡아냅니다. 따라서 저는 두 곳 모두에서 동일한 루브릭 정의를 실행합니다. CI 버전은 코드 내에 존재합니다.

운영 버전은 라이브 OTel 스팬(OTel spans)을 대상으로 실행되며, 트레이스(trace)에 부착된 점수로 나타납니다.

from fi_instrumentation import register
from fi_instrumentation.fi_types import (
    EvalTag,
...

점수는 스팬 속성(span attribute)으로 기록되므로, 실패한 트레이스는 지연 시간(latency) 및 청크 ID(chunk IDs) 옆에 루브릭 점수와 함께 나타납니다.

저는 LLM-판사(LLM-judge) 루브릭의 경우 운영 트래픽의 5~10%를 샘플링하고, 저비용 루브릭은 100% 전체에 대해 실행합니다.

그 후, 1560분 윈도우 동안 루브릭의 이동 평균(rolling mean)이 지속적으로 25점 하락하면 알람을 발생시킵니다.

CI 베이스라인과 운영 이동 평균 사이의 격차는 그 자체로 하나의 신호가 됩니다.

이 격차가 벌어지면, 데이터셋이 더 이상 대표성을 갖지 못하고 있다는 뜻입니다.

데이터셋이 따라올 수 있도록 루프를 닫기

  • 데이터셋이 프로덕션의 드리프트(Drift)를 따라가지 못하는 순간, 그것은 더 이상 회귀 테스트 스위트 (Regression suite)로서의 기능을 상실합니다.
  • 루프 (Loop)를 구축하는 것이 게이트 (Gate)의 신뢰성을 유지하는 핵심입니다.
  • 실패한 프로덕션 트레이스 (Production traces)를 클러스터링하여 명명된 이슈 (Issues)로 분류합니다.
  • 분석 단계를 거쳐 각 이슈에 대한 근본 원인 (Root cause)과 해결책을 작성합니다.
  • 가장 심각한 대표 트레이스들을 루브릭 (Rubric) 레이블과 함께 평가셋 (Eval set)으로 승격시킵니다.
  • 해당 경로를 수정하는 다음 PR (Pull Request)은 새로운 항목들을 통과하거나, 혹은 해당 항목들로 인해 실패하게 됩니다.
  • 몇 주가 지나면 게이트는 약화되는 대신 더 강력해집니다. 설정 단계에서 추측했던 실패가 아니라, 실제로 발생한 실패를 학습하기 때문입니다.

주의해야 할 함정들 (Pitfalls)

근거성 (Groundedness)만 점수화하는 경우

  • 환각 (Hallucination)은 잡아낼 수 있습니다.
  • 검색 (Retrieval) 회귀는 놓치게 됩니다.
  • 다섯 가지 핵심 루브릭 (Rubrics)을 모두 실행하세요.

데이터셋에 expected_chunks가 없는 경우

  • 정답 (Ground truth) 없이는 검색 재현율 (Retrieval recall)을 측정할 수 없습니다.
  • 레이블링 (Labeling) 작업은 검색 기능이 깨지는 첫 순간에 그 가치를 증명합니다.

부유하는 판사 모델 (A floating judge model)

  • 동일한 평가를 반복 실행해도 점수가 드리프트 (Drift)됩니다.
  • 루브릭과 함께 판사 모델 (Judge model)을 고정하고 버전을 명시하세요.

델타 게이트 (Delta gate)가 없는 하한선 (Floors)

  • 느린 회귀 (Slow regressions)는 몇 달 동안 하한선 아래에 머물 수 있습니다.

  • 델타 게이트 (Delta gate)가 바로 이를 잡아내는 장치입니다.

    평균 게이트를 사용하는 30개 예시 데이터셋

  • 추적하려는 회귀 현상보다 분산 (Variance)이 더 큽니다.

  • 데이터셋 규모를 키우거나 백분위수 (Percentiles)를 기준으로 게이트를 설정하세요.

모든 PR에 대해 전체 LLM-판사 스윕 (Full LLM-judge sweep)을 수행하는 경우

  • 너무 느리고 비용이 많이 듭니다.
  • PR 단계에서는 저렴한 루브릭 (Cheap rubrics)을 사용하세요.
  • 무거운 스윕 (Heavy sweeps)은 매일 밤 실행하세요.

출시 시점에 고정된 데이터셋

2024년의 데이터셋으로 2026년의 제품을 평가하는 것은 벤치마크 (Benchmark)이지, 회귀 테스트 스위트 (Regression suite)가 아닙니다.

캐시 (Cache) 부재

  • 비용이 상승하고 불안정한 네트워크 호출이 PR을 차단합니다.
  • 판정 결과 (Verdicts)를 캐싱하세요.
  • 루브릭이나 판사 모델의 버전이 변경될 때 캐시를 무효화하세요.

CI와 다른 루브릭을 사용하는 프로덕션 옵저버 (Production observer)

  • 그러면 사람들은 버그를 고치는 대신 어떤 수치가 진짜인지에 대해 논쟁하게 됩니다.
  • 정의는 하나여야 합니다.
  • 양쪽 모두에서 동일하게 실행하세요.

의도적으로 선택한 트레이드오프 (Tradeoffs)

통계적 게이팅 (Statistical gating)으로 인한 첫 주간의 속도 저하

  • 베이스라인 윈도우(baseline window)를 구축하는 데에는 델타 게이트(delta gate)가 의미를 갖기 전까지 몇 번의 야간 실행(nightly runs)이 필요합니다.
  • 그 보상은 빨간색 PR(Pull Request)이 실제 회귀(regression)를 의미하게 된다는 것이며, 이것이 사람들이 게이트를 켜두는 유일한 이유입니다.

샤딩된(Sharded), 경로 범위 지정된(path-scoped) 실행의 오케스트레이션 비용

  • 경로 인식 테스트 선택(Route-aware test selection)과 분산 러너(distributed runner)는 추가적인 배선(wiring) 작업입니다.
  • 그 결과는 4분짜리 PR 게이트와 30초짜리 PR 게이트 사이의 차이로 나타납니다.

분류기 캐스케이드(Classifier cascade)에는 루브릭(rubric) 규율이 필요합니다

  • 캐스케이드(Cascades)는 분류기가 명확한 타겟을 가지고 있을 때 작동합니다.
  • 주관적인 축(subjective axes)에서는 무너집니다.

- 어떤 루브릭을 캐스케이드할지 결정하는 것은 실제로 내려야 하는 일회성 설계 결정입니다.

만약 여러분이 이와 같은 게이트를 구축했다면, 세 가지 측면 중 어느 부분이 가장 큰 어려움을 주었는지 궁금합니다.

저의 경우에는 매번 유의성(significance)이 문제였습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0