AI 에이전트의 신뢰성은 관측성 계층(Observability Layer)에 달려 있습니다
요약
AI 에이전트 시스템을 프로토타입에서 신뢰할 수 있는 프로덕션 수준으로 끌어올리기 위해서는 관측성(Observability) 계층이 필수적입니다. 로깅, 비용 추적, 에러 모니터링을 통해 모델 업데이트로 인한 조용한 실패와 비용 낭비를 방지하는 방법을 다룹니다.
핵심 포인트
- 데모와 프로덕션의 차이는 프롬프트가 아닌 관측성에 있음
- 추적 ID, 구조화된 로그, 쿼리 가능성을 갖춘 최소 기능 추적(MVT) 구축 필요
- LLM 호출은 일반 앱 로그와 분리하여 전용 컬렉션으로 관리 권장
- 지연 시간, 토큰 사용량, 성공 플래그를 통한 실시간 모니터링 중요
저는 이를 고통스러운 경험을 통해 배웠습니다. 매일 10,000개의 채용 공고를 처리하며 GPT-4 함수 호출(function calling)로 각 공고의 점수를 매기는 파이프라인이 있었는데, 그것은 블랙박스(black box)였습니다. 무언가 고장 났을 때, 무엇이 고장 났는지, 왜 고장 났는지, 그리고 고장 나는 동안 비용이 얼마나 발생했는지 전혀 알 수 없었습니다.
그것은 프로덕션 시스템(production system)이 아닙니다. 프로덕션 인프라 위에서 돌아가는 프로토타입(prototype)일 뿐입니다.
데모(demo)와 신뢰할 수 있는 AI 에이전트의 차이는 더 나은 프롬프트(prompt)나 더 화려한 모델이 아닙니다. 바로 관측성(observability)입니다. 로깅(logging), 비용 추적(cost tracking), 에러 모니터링(error monitoring), 그리고 지연 시간 메트릭(latency metrics)이 그 취약한 파이프라인을 제가 신뢰할 수 있는 무언가로 바꾸어 놓았습니다. 제가 정확히 무엇을 했고 왜 그렇게 했는지 설명하겠습니다.
문제점: 대시보드가 없는 LLM 파이프라인
이 시스템은 GPT-4 함수 호출(function calling)을 사용하여 매일 10,000개 이상의 채용 공고에 점수를 매겼습니다. 각 공고는 소스에서 가져오기, 데이터 정규화(normalize), 점수 산출을 위한 LLM 호출, 구조화된 출력(structured output) 파싱, 그리고 결과 저장이라는 다단계 파이프라인을 거쳤습니다.
몇 주 동안은 잘 작동했습니다. 그러다 어느 날 문제가 발생했습니다.
모델 업데이트가 출력 형식을 조용히 변경했습니다. 함수 호출(function call)은 유효한 JSON을 반환했지만, 키(key) 구조가 달라져 있었습니다. 파이프라인은 계속 실행되었고, 계속해서 200 OK 응답을 반환했으며, 계속해서 null 점수를 저장했습니다. 저는 3일 동안 이를 알아차리지 못했습니다. 3일 동안 잘못된 데이터가 쌓였고, API 비용이 낭비되었으며, 점수가 매겨지지 않은 공고들이 밀려났습니다.
그때 저는 관측성(observability)을 선택 사항으로 취급하는 것을 그만두었습니다.
무엇을 로깅할 것인가: 최소 기능 추적(Minimum Viable Trace)
시작하기 위해 Datadog이나 전체 OpenTelemetry 설정이 필요한 것은 아닙니다. 세 가지가 필요합니다: 요청당 하나의 추적 ID(trace ID), 모든 LLM 호출에 대한 구조화된 로그(structured logs), 그리고 이를 쿼리(query)할 수 있는 방법입니다.
몇 가지 접근 방식을 시도한 끝에 제가 정착한 패턴은 다음과 같습니다:
interface LLMTrace {
traceId: string;
model: string;
...
모든 LLM 호출에는 이것 중 하나가 부여됩니다. 추적 ID(trace ID)는 이를 원래의 채용 공고와 연결해 줍니다. 토큰 수(token counts)를 통해 채용 공고당 비용을 추적할 수 있습니다. 성공 플래그(success flag)는 API가 200을 반환했지만 출력이 쓰레기 값인 경우와 같은 조용한 실패(silent failures)를 잡아냅니다.
저는 이를 전용 llm_traces 컬렉션에 기록합니다. LLM 호출은 고유한 실패 모드(failure modes)와 비용적 영향(cost implications)을 갖기 때문에 애플리케이션 로그와는 분리되어 있습니다. 이를 일반 앱 로그와 섞어버리면 두 종류 모두 쿼리하기가 더 어려워집니다.
로깅 첫 주를 통해 배운 점
구조화된 로깅(structured logging)을 시작한 첫 주에 저는 듣고 싶지 않았던 사실들을 알게 되었습니다.
첫째, 지연 시간(latency)이 제각각이었습니다. 어떤 호출은 400ms 만에 완료되었지만, 동일한 모델과 프롬프트 구조를 가진 다른 호출은 12초가 걸리기도 했습니다. 트레이스(trace) 데이터 없이는 어떤 패턴도 찾아낼 수 없었습니다. 데이터를 확보한 후 범인을 찾아냈는데, 바로 프롬프트 길이의 변동성(prompt length variance)이었습니다. 설명이 긴 리스팅(listings)들은 토큰 수가 특정 임계값을 넘어서면서 모델의 응답 시간이 두 배로 늘어났습니다. 저는 프롬프트 절단(truncation) 단계를 추가했고, 지연 시간은 평탄해졌습니다.
둘째, 비용입니다. 트레이스당 토큰 수를 기록하기 전까지는 단일 스코어링(scoring) 호출에 비용이 얼마나 드는지 전혀 몰랐습니다. 평균은 리스팅당 $0.003이었습니다. 하루 10,000개의 리스팅을 처리한다면 하루에 $30가 소요됩니다. 큰 문제는 아닙니다. 하지만 꼬리 부분(tail)이 심각했습니다. 설명이 매우 긴 일부 리스팅은 평균보다 10배의 비용이 들었습니다. 저는 최대 토큰 제한(max-token cap)을 추가하여 월간 API 지출의 약 18%를 절감했습니다.
셋째, 조용한 실패(silent failures)입니다. 가장 위험한 유형입니다. LLM이 배치(batch) 내의 모든 리스팅에 대해 점수 0을 가진 유효한 JSON을 반환했습니다. 에러도 없었고, 예외(exception)도 발생하지 않았습니다. 그저 쓸모없는 데이터일 뿐이었습니다. 제 트레이스의 성공 플래그(success flag)는 출력이 예상된 스키마(schema)와 일치하지 않았기 때문에 이를 잡아낼 수 있었습니다. 이 확인 절차가 없었다면 저는 며칠 동안 잘못된 점수를 배포했을 것입니다.
에이전트 트레이스(Agent Traces)를 구조화하는 방법
단일 LLM 호출은 트레이스하기 쉽습니다. 하지만 여러 번의 호출, 재시도(retries), 조건부 분기(conditional branches)를 수행하는 에이전트는 더 어렵습니다. 스팬(span) 모델이 필요합니다.
각 트레이스는 자식 스팬(child spans)을 가진 부모(parent)가 됩니다. 부모는 전체 작업(overall job)입니다. 각 자식 스팬은 하나의 LLM 호출, 하나의 도구 호출(tool invocation), 또는 하나의 결정 지점(decision point)입니다. 이들은 트레이스 ID(trace ID)를 공유하며 각자의 타이밍과 상태를 가집니다.
interface AgentSpan {
traceId: string;
spanId: string;
...
이러한 구조를 통해 저는 다음과 같은 질문에 답할 수 있습니다: 파이프라인의 어느 단계가 가장 느린가? 어떤 모델 호출이 가장 자주 실패하는가? 재시도 (Retries)가 실제로 성공하고 있는가, 아니면 단순히 비용만 낭비하고 있는가?
저는 타임아웃 (Timeout) 에러에 대한 재시도 성공률이 40%라는 것을 발견했습니다. 반면, 속도 제한 (Rate limit) 에러에 대한 재시도 성공률은 90%였습니다. 이 발견은 제가 각 케이스를 처리하는 방식을 바꾸어 놓았습니다. 타임아웃 재시도에는 더 긴 쿨다운 (Cooldown) 시간을 부여했고, 속도 제한 재시도에는 짧은 백오프 (Backoff)만 적용하면 되었습니다.
실패가 누적되기 전에 포착하기
관측성 (Observability)을 통해 포착한 가장 비용이 많이 들었던 버그는 크래시 (Crash)가 아니었습니다. 그것은 바로 소리 없는 스키마 드리프트 (Schema drift)였습니다.
LLM은 그동안 1에서 100 사이의 정수 (Integer)로 점수를 반환해 왔습니다. 그러다 API 업데이트로 인해 출력 형식이 변경되었습니다. 점수가 85 대신 "85.0"과 같은 문자열 (String)로 반환되기 시작했습니다. 파이프라인은 이를 수용하고 저장했으며, 프론트엔드 (Frontend)는 이를 그대로 표시했습니다. 정렬이 잘못되었다는 것을 누군가 알아차리기 전까지는 모든 것이 정상적으로 보였습니다. 문자열 비교는 "85"보다 "9"를 뒤에 배치합니다. 한 자릿수 점수를 가진 항목들이 높은 점수를 가진 항목들보다 상위에 랭킹되었습니다.
트레이스 핸들러 (Trace handler)에 있는 간단한 타입 체크 (Type check)가 이를 잡아냈습니다:
function validateScore(output: unknown): number | null {
if (typeof output === 'number' && output >= 1 && output <= 100) {
return output;
...
점수가 null일 때, 트레이스는 경고를 기록하고 파이프라인은 해당 배치 (Batch)를 일시 중지합니다. 잘못된 데이터가 전파되지 않으며, 소리 없는 데이터 오염 (Silent corruption)도 발생하지 않습니다.
AI 에이전트에게 중요한 지표들
모든 지표 (Metrics)가 동일한 가치를 지니는 것은 아닙니다. 제가 추적하는 지표들과 그 이유는 다음과 같습니다.
작업당 토큰 비용 (Token cost per job). 이것은 유닛 이코노믹스 (Unit economics)입니다. 하나의 AI 작업에 비용이 얼마나 드는지 모른다면, 제품의 가격을 책정하거나 성능 퇴보 (Regressions)를 포착할 수 없습니다. 200개의 토큰을 추가하는 프롬프트 (Prompt) 변경은 일일 작업량이 10,000개로 늘어나기 전까지는 별것 아닌 것처럼 느껴질 수 있습니다. 하지만 이는 하루에 200만 개의 추가 토큰을 의미합니다. GPT-4o 가격 기준으로 이는 하루에 약 6달러입니다. 한 달이면 180달러, 1년이면 2,000달러가 넘습니다. 단 한 번의 프롬프트 변경 때문에 말입니다.
에러 유형별 에러율 (Error rate by error type). 타임아웃 에러 (Timeout errors)와 콘텐츠 필터 에러 (Content filter errors)는 서로 다른 대응이 필요합니다. 타임아웃은 지수 백오프 (Backoff)를 적용한 재시도 (Retry)가 필요하며, 콘텐츠 필터 에러는 프롬프트 (Prompt) 조정이 필요합니다. 이들을 하나로 묶어버리면 그 어느 것도 최적화할 수 없습니다.
지연 시간 (Latency) p50, p95, p99. 평균값은 문제를 숨깁니다. p50이 800ms라면 괜찮아 보이지만, p99가 14초라는 것을 확인하기 전까지는 그렇습니다. 그 꼬리 부분 (Tail)이 사용자 경험 (User experience)을 망칩니다. 저는 p95가 3초를 초과할 때 알림 (Alert)이 울리도록 설정했습니다. 두 번 울렸는데, 두 번 모두 원인은 너무 많은 컨텍스트 (Context)를 포함하도록 드리프트 (Drift)된 프롬프트였습니다.
모델당 비용 (Cost per model). 모델 간 라우팅 (Routing)을 수행하는 경우 (예: 복잡한 스코어링에는 GPT-4o, 단순 분류에는 GPT-4o-mini 사용), 실제 비용 분할을 파악해야 합니다. 저는 호출의 30%가 저렴한 모델로도 충분했을 상황에서 비싼 모델을 호출하고 있다는 것을 발견했습니다. 라우팅 규칙 (Routing rule)을 통해 이를 해결했습니다.
신뢰 루프 (The Trust Loop)
관측성 (Observability)은 버그를 잡는 것보다 더 중요한 일을 합니다. 바로 신뢰를 구축하는 것입니다.
창업자가 "지금 AI가 제대로 작동하고 있나요?"라고 물을 때, 여러분은 느낌 (Vibes)이 아닌 데이터로 답해야 합니다. 최근 100개의 트레이스 (Traces), 상태, 지연 시간, 비용을 보여주는 대시보드 (Dashboard)는 그 어떤 테스트 스위트 (Testing suite)보다 가치 있습니다. 이는 시스템이 여러분이 의도한 대로 작동하고 있음을 증명합니다.
저는 파이프라인 (Pipeline)의 지난 1시간 활동을 보여주는 간단한 상태 페이지 (Status page)를 만들었습니다. 성공은 초록색 트레이스, 실패는 빨간색, 재시도는 노란색으로 표시됩니다. 창업자는 이를 하루에 한 번 확인합니다. 모두 초록색일 때, 그들은 AI에 대해 걱정하지 않습니다. 그 신뢰 덕분에 저는 더 빠르게 반복 (Iterate)할 수 있습니다. 프롬프트 변경을 배포하고 10분 동안 트레이스를 관찰함으로써, 무언가 망가졌는지 즉시 알 수 있습니다.
관측성이 없다면 모든 배포는 신념에 의존한 도약 (Leap of faith)입니다. 관측성이 있다면, 모든 배포는 롤백 트리거 (Rollback trigger)를 갖춘 측정된 리스크 (Measured risk)입니다.
관측성을 생략할 때 발생하는 실제 비용
저는 팀들이 관측성을 오버헤드 (Overhead)로 느껴 생략하는 것을 봅니다. 유지 관리해야 할 또 다른 시스템, 확인해야 할 또 다른 대시보드, 고장 날 수 있는 또 다른 요소라고 생각하기 때문입니다.
진정한 오버헤드는 블랙박스 (black box)를 디버깅하는 것입니다. 3일 동안 지속된 잘못된 데이터, 설명할 수 없는 500달러의 API 비용, 그리고 AI 기능에 대한 신뢰를 잃어버린 창업자 같은 상황 말입니다.
관측성 (Observability)은 AI 시스템을 위해 구매할 수 있는 가장 저렴한 보험입니다. 데이터베이스에 몇 개의 필드를 추가하는 것, 간단한 상태 엔드포인트 (status endpoint)를 만드는 것, 주간 비용 보고서를 작성하는 것. 그것뿐입니다. 분산 트레이싱 (distributed tracing)에 관한 박사 학위가 필요한 것이 아닙니다. 트레이스 ID (trace ID), 성공 플래그 (success flag), 그리고 타임스탬프 (timestamp)만 있으면 됩니다.
만약 당신의 팀이 AI 기능을 출시하고 있으면서 비용, 지연 시간 (latency), 또는 실패율 (failure rates)에 대한 기본적인 질문에 답하지 못하고 있다면, 그것이 바로 제가 도와드리는 영역입니다. 무엇을 추적할 가치가 있고 무엇이 노이즈 (noise)인지에 대해 함께 의견을 나눌 수 있다면 기쁘겠습니다.
작성자: Abdul Rehman, 프로덕션 SaaS, MVP 및 AI 자동화를 구축하는 풀스택 AI 엔지니어. 더 많은 내용은 PrimeStrides에서 확인하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기