에이전트가 고장 난 것이 아니라 드리프트(Drift)된 것입니다: 자율 시스템에서의 느린 성능 저하 감지하기
요약
에이전트 시스템에서 오류 없이 성능이 서서히 저하되는 '드리프트(Drift)' 현상의 위험성을 경고합니다. 모델 업데이트, 환경 변화, 데이터 분포 변화 등으로 인해 발생하는 이 문제는 단일 실행이 아닌 시간 흐름에 따른 분포 변화를 모니터링해야 감지할 수 있습니다.
핵심 포인트
- 드리프트는 에러나 충돌 없이 발생하는 침묵의 성능 저하임
- 단일 실행 결과가 아닌 시간에 따른 점수 추세(Trending) 파악이 필수적임
- 모델 제공업체의 내부 업데이트가 성능 변화의 주요 원인이 됨
- 입력 데이터와 외부 환경의 변화가 에이전트 동작을 변화시킴
어떤 경보(Alert)도 울리지 않지만, 제가 가장 신뢰하지 않는 특정한 종류의 사고가 있습니다. 아무것도 충돌(Crash)하지 않았습니다. 예외(Exception)도, 500 에러도, 헬스 체크(Health check) 실패도 없었습니다. 에이전트(Agent)는 매일 실행되었고, 매번 답변을 반환했으며, 당신이 보유한 모든 대시보드에서 녹색(Green) 상태를 유지했습니다. 하지만 6주에 걸쳐 측정 가능한 수준으로 성능이 악화되었고, 당신은 모니터가 아닌 고객을 통해 이 사실을 알게 되었습니다.
이것이 바로 드리프트(Drift)이며, 업계가 가장 준비되지 않았다고 생각하는 실패 모드(Failure mode)입니다. 우리는 '절벽(Cliff)'을 포착하는 데는 능숙해졌습니다. 에이전트가 오류를 던지거나, 도구(Tool)가 500 에러를 내거나, JSON 파싱이 실패하거나, CI가 빨간색으로 변하는 상황 말입니다. 하지만 우리는 '경사면(Slope)'을 포착하는 데는 여전히 서툽니다. 모든 시스템이 완벽한 상태를 보고하는 동안, 답변 품질이 매주 2%씩 서서히 빠져나가는 상황 말입니다. 충돌(Crash)은 시끄럽고 스스로를 알립니다. 드리프트(Drift)는 구조적으로 침묵하며, 그 침묵이야말로 드리프트가 승리하는 정확한 이유입니다.
제가 고수할 의견은 다음과 같습니다: 드리프트(Drift)는 이상치(Outlier) 문제가 아니라 베이스라인(Baseline) 문제입니다. 단일 실행(Single run) 결과만 보고는 저하를 감지할 수 없습니다. 왜냐하면 단일 실행은 완전히 정상적으로 보이기 때문입니다. 드리프트(Drift)는 오직 *시간에 따른 분포의 변화(Change in a distribution over time)*로서만 존재합니다. 따라서 프로덕션(Production) 환경을 지속적으로 점수화(Scoring)하고 그 점수의 추세(Trending)를 파악하지 않는다면, 당신은 구조적으로 이를 볼 수 없습니다. 운이 없는 것이 아니라, 불가능한 것입니다.
코드는 변하지 않았는데 왜 동작은 변했을까
드리프트(Drift)를 매우 혼란스럽게 만드는 점은 우리의 가장 깊은 본능을 위반한다는 것입니다: 코드가 변하지 않았다면, 동작도 변하지 않았다. 에이전트(Agent)의 경우, 이는 틀린 말입니다. 당신의 git 히스토리가 완벽하게 정지해 있는 동안에도 당신의 에이전트(Agent)는 저하됩니다:
- 모델이 당신의 발밑에서 움직입니다. 당신은
gpt-4o를 고정(pin)했지만, 고정된 모델 이름은 고정된 모델이 아닙니다 — 제공업체(providers)는 안정적인 문자열 뒤에서 체크포인트(checkpoints)를 교체하고 조용히 재조정(re-tune)합니다. 당신의 프롬프트(prompt)는 바이트 단위로 동일하지만, 출력값은 어쨌든 변했습니다. - 세상이 당신의 프롬프트 발밑에서 움직입니다. 당신의 퓨샷(few-shot) 예시들은 3월의 현실을 기준으로 작성되었습니다. 지금은 9월입니다. 사용자들이 프롬프트를 고정했을 당시에는 존재하지 않았던 제품과 엣지 케이스(edge cases)에 대해 질문하며, 에이전트(agent)는 이를 즉흥적으로 처리합니다 — 더 나쁘게, 하지만 유창하게 말이죠.
- 의존성(dependencies)과 입력값(inputs)이 움직입니다. 검색 인덱스(retrieval index)가 다시 임베딩(re-embedded)되거나, 도구(tool)가 필드 이름을 변경하거나, 사용자 층이 새로운 지역으로 확장됩니다. 에이전트는 당신이 테스트한 입력값들에 대해서는 결코 고장 난 적이 없습니다 — 다만 실제로 서비스하는 데이터와 트래픽이 테스트 데이터로부터 드리프트(drift)되었을 뿐이며, 에이전트는 약간 틀린 결과를 자신 있게 인용하면서 계속 실행되고 있는 것입니다.
이 중 그 어느 것도 코드 디프(code diff)에 나타나지 않습니다. 그 어느 것도 오류를 발생시키지 않습니다. 하지만 그 모든 것이 사용자가 실제로 경험하는 것을 저하시킵니다. 이것이 바로 "고장 나면 알아차릴 것이다"라는 생각이 환상인 이유입니다 — 가장 비용이 많이 드는 에이전트 회귀(regressions)는 아무것도 고장 내지 않기 때문입니다.
베이스라인(Baseline)은 드리프트(drift)를 측정하는 유일한 기준입니다
드리프트(drift)를 감지하려면 두 가지가 필요합니다: 신뢰할 수 있는 기간 동안 "정상" 수치가 어떠했는지를 나타내는 베이스라인(baseline), 그리고 실제 트래픽에서 동일한 방식으로 계산된 동일한 점수인 **지속적인 신호(continuous signal)**입니다. 드리프트는 육안이 아닌 통계적으로 측정되는, 이 둘 사이의 간극입니다.
단순한 방식은 단일 임계값(threshold)을 사용하는 것입니다: "품질이 0.8 미만으로 떨어지면 경고를 보낸다." 이는 급격한 절벽(cliff)은 잡아내지만, 완만한 경사(slope)는 놓칩니다. 5주에 걸쳐 점수가 0.91에서 0.82로 내려가는 경우, 절대적인 하한선을 결코 건드리지 않지만 품질은 거의 10분의 1을 잃은 셈입니다. 당신이 찾는 것은 낮은 값이 아니라 움직이는 값입니다 — 이는 다른 통계적 질문이며, 반드시 베이스라인(baseline)이 필요합니다.
이 지점에서 평가(evaluation)와 관찰 가능성(observability)은 서로 별개의 관심사가 아니라 하나의 워크플로우가 됩니다. 왜냐하면 점수를 매기는 것(scores)과 경로를 기억하는 것(remembers the route)이 모두 필요하기 때문입니다. 저는 에이전트의 출력을 점수화하고 게이트를 통과시키기 위해 agent-eval을 실행합니다. 가능한 곳에서는 결정론적 체크(deterministic checks)를 수행하고, 반드시 필요한 곳에서는 모델 기반 판정 루브릭(model-as-judge rubric)을 사용하며, 결정적으로 각 판정 결과를 저장하여 점수의 *연속된 흐름(series)*이 존재하고 추세를 파악할 수 있게 합니다. 그리고 점수가 매겨진 모든 실행의 트레이스(trace)를 캡처하기 위해 AgentLens를 실행합니다. 즉, 모든 모델 및 도구 단계, 보간(interpolation) 후 모델이 실제로 본 해결된 입력값(resolved inputs), 그리고 돌아온 가공되지 않은 출력값(raw outputs)을 기록합니다. 이 둘을 결합하는 것이 핵심입니다. agent-eval은 점수가 드리프트(drift)되고 있다고 알려주고, AgentLens는 어떤 단계가 드리프트되기 시작했는지 알려줍니다. 트레이스가 동반되지 않은 드리프트 경고는 단순히 차트 위의 숫자가 떨어지는 것일 뿐이며, 왜 그런지 물을 방법이 없습니다. "이번 달 품질이 6% 하락했습니다, 원인 미상"이라는 말은 실행 가능한 신호(actionable signal)가 아니라 불안감을 조성하는 요소일 뿐입니다.
다음은 점수가 매겨진 프로덕션 실행의 이동 창(rolling window)에 대한 드리프트 탐지기입니다. 점수는 agent-eval에서 가져오며, 각 실행의 traceId는 AgentLens로 연결되어 있어 플래그가 지정된 창은 클릭 한 번으로 증거에 접근할 수 있습니다.
import { queryScoredRuns } from "agent-eval";
interface ScoredRun {
...
두 가지 설계 결정이 이 접근 방식 전체를 뒷받침합니다.
절대적인 하한선이 아니라 베이스라인 노이즈(baseline noise)와 비교합니다. zScore가 핵심 비결입니다. 모든 에이전트의 점수는 실행마다 흔들립니다. 이는 정상적인 비결정론(nondeterminism)이지 성능 저하(decay)가 아닙니다. 하락폭을 최근 윈도우의 *표준 오차(standard error)*로 나눔으로써, 에이전트 자체의 자연스러운 지터(jitter)보다 움직임이 더 클 때만 경고를 발생시킵니다. 노이즈가 많은 에이전트에서의 1% 하락은 아무것도 아니지만, 매우 안정적인 에이전트에서의 동일한 하락은 비상 신호입니다. 절대적인 임계값(absolute threshold)으로는 이 둘을 구분할 수 없습니다.
단순한 판결이 아니라 sampleTraceIds를 방출합니다. 대부분의 자체 제작 탐지기(homegrown detectors)는 불리언(boolean) 값인 drifting: true 단계에서 멈추며, 이것이 바로 탐지 결과가 무시되는 이유입니다. 아무도 그 결과에 따라 조치를 취할 수 없기 때문입니다. 최근 가장 성적이 좋지 않은 5개 실행(run)의 트레이스 ID(trace ID)를 첨부함으로써, 알림은 자체적인 증거를 지니게 됩니다. 즉, AgentLens 트레이스를 열어 낮은 점수를 생성한 해결된 입력값(resolved inputs)과 도구 출력값(tool outputs)을 직접 읽을 수 있습니다. 이것이 "품질이 떨어졌으니 누군가 조사해 보세요"와 "품질이 떨어졌으며, 오래된 문서(stale documents)를 반환하기 시작한 검색(retrieval) 단계는 여기입니다"의 차이입니다.
베이스라인(baseline)을 세분화하지 않으면 거짓말을 할 것입니다
가장 혼란스러운 드리프트(drift) 사고를 유발하기 때문에 반드시 지적해야 할 함정이 하나 있습니다. 바로 건강한 _합계(aggregate)_가 처참한 세그먼트별(per-segment) 붕괴를 숨길 수 있다는 점입니다. 전체 점수는 0.90을 유지하고 있지만, 스페인어 트래픽은 0.88에서 0.61로 조용히 폭락할 수 있습니다. 스페인어 트래픽이 전체 볼륨의 8%에 불과하고 나머지 92%가 정상이기 때문에 가려지는 것입니다. 이 합계 수치는 기술적으로는 정확하지만 완전히 쓸모가 없습니다.
따라서 언어, 도구 경로(tool path), 사용자 등급(user tier), 의도(intent)와 같이 실제로 변화가 발생하는 차원(dimension)을 따라 베이스라인을 슬라이스(slice)하고, 각 슬라이스별로 동일한 드리프트 체크를 실행하십시오.
async function driftBySegment(segmentBy: (r: ScoredRun) => string) {
const baseline = await queryScoredRuns({ from: "-30d", to: "-7d" });
const recent = await queryScoredRuns({ from: "-7d", to: "now" });
...
가장 위험한 드리프트는 평균값 안에 숨어 있습니다. 베이스라인을 세분화하면 이를 빛 아래로 끌어낼 수 있으며, 세그먼트별 트레이스 ID(trace ID)는 AgentLens를 통해 해당 입력값에서 정확히 어떤 단계가 제대로 작동하지 않는지 알려줍니다.
월요일에 해야 할 일
시작하기 위해 통계학 박사 학위나 플랫폼 팀이 필요한 것은 아닙니다. 당신에게 필요한 것은 베이스라인(baseline)과 추세(trend)입니다:
- 배포 시에만 점수를 매기지 말고, 지속적으로 점수를 산출하세요. 실제 트래픽을 샘플링하여 에이전트 평가 루브릭(agent-eval rubric)을 통해 실행하고, 모든 판정 결과(verdict)를 해당 AgentLens 추적 ID(trace ID)와 함께 저장하세요. 점수의 '시리즈(series)'가 없다면 추세(trend)가 존재할 수 없으며, 추세가 없다면 드리프트 감지(drift detection)는 불가능합니다. 그저 희망 사항만 남을 뿐입니다.
- 베이스라인 윈도우(baseline window)를 기준으로 추세를 파악하고, 하한선(floor)이 아닌 노이즈(noise)와 비교하세요. 통계적으로 유의미한 '변화(movement)'가 발생할 때 알림을 보내세요. 당신이 찾아야 하는 것은 절벽(cliff)이 아니라 기울기(slope)입니다.
- 베이스라인을 세분화(segment)하세요. 언어별, 도구별, 티어(tier)별로 세분화하여, 전체 합계(aggregate) 수치가 붕괴를 덮어버리기 전에 문제를 발견해야 합니다.
- 모든 드리프트 알림에 추적 ID(trace ID)를 포함하세요. 상세 분석(drill into)이 불가능한 신호는 팀이 무시하게 되는 신호가 됩니다. 점수는 증상을 나타내고, 추적(trace)은 원인을 나타냅니다.
에이전트들은 성능이 저하되는 과정에서 갑자기 충돌(crash)하지 않을 것입니다. 에이전트들은 계속해서 답변을 하고, 계속해서 200 OK 응답을 반환하며, 계속 건강해 보일 것입니다. 그러다 인간이 알아차릴 수 있을 정도로 성능 저하(decay)가 커질 때까지 조용히 나빠질 것입니다. 이는 가장 비용이 많이 드는 탐지 방식입니다. 에이전트 평가(agent-eval)로 출력을 점수화하고, AgentLens로 경로(route)를 유지하며, 이 두 가지를 베이스라인에 대해 추세화(trend)하십시오. 그러면 절벽을 먼저 발견한 고객에게 상황을 설명하는 대신, 기울기가 고작 2%일 때 문제를 포착할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기