LLM 앱을 위한 관측성 (Observability): 트레이싱, 비용 추적 및 평가 루프
요약
LLM 애플리케이션의 프로덕션 환경에서 필수적인 관측성(Observability)의 세 가지 핵심 요소인 트레이싱, 비용 추적, 평가 루프를 다룹니다. 기존 APM의 한계를 지적하며 LLM 전용 속성을 포함한 모니터링 체계의 필요성을 설명합니다.
핵심 포인트
- LLM 앱은 토큰 사용량, 프롬프트/응답 쌍, 품질 드리프트 관측이 필수적임
- 트레이싱 시 재시도 횟수와 폴백 경로를 추적하여 시스템 안정성을 확보해야 함
- 단순 HTTP 상태 코드만으로는 모델의 품질 저하나 비용 문제를 파악하기 어려움
- OpenTelemetry를 활용하여 LLM 전용 스팬(Span)을 구성하고 시각화할 수 있음
만약 여러분이 전통적인 백엔드 서비스를 배포해 본 경험이 있다면, 관측성 (Observability) 체크리스트인 로그 (logs), 메트릭 (metrics), 트레이스 (traces), 알림 (alerts)에 대해 이미 알고 있을 것입니다. LLM 기반 앱도 이 모든 것이 필요합니다. 여기에 더해 일반적인 요청/응답 (request/response) 세계에는 존재하지 않는 몇 가지 요소들이 더 필요합니다: 토큰 사용량 (token spend), 프롬프트/응답 쌍 (prompt/response pairs), 그리고 그 어떤 HTTP 상태 코드도 알려주지 않을 품질 드리프트 (quality drift)입니다.
이 포스트에서는 LLM 앱이 데모 단계를 벗어난 후 실제로 중요해지는 세 가지 기둥을 다룹니다:
- 트레이싱 (Tracing) — 재시도 (retries), 도구 호출 (tool calls), 그리고 멀티 프로바이더 폴백 (multi-provider fallbacks)을 가로지르는 요청 추적
- 비용 추적 (Cost tracking) — 토큰 수를 경로 (route), 사용자 (user), 또는 기능 (feature)별 실시간 달러 가시성으로 전환
- 평가 루프 (Eval loops) — 사용자가 발견하기 전에 품질 저하 (quality regressions)를 포착
저는 실행 예시로 멀티 프로바이더 AI 게이트웨이 (Rust, Axum, Prometheus)의 패턴을 사용하겠지만, 이 아이디어들은 스택에 관계없이 적용됩니다.
일반적인 APM이 충분하지 않은 이유
표준 APM 트레이스는 _지연 시간 (latency)_과 _상태 코드 (status code)_를 알려줍니다. LLM 호출의 경우, 이는 겨우 절반의 그림에 불과합니다. 동일한 200 OK가 다음을 나타낼 수 있기 때문입니다:
- 빠른 모델로부터 얻은 저렴한 400토큰 완료 (completion)
- 두 번의 재시도 후 프런티어 모델 (frontier model)로부터 얻은 2달러짜리 완료
- 기술적으로는 성공했지만 환각 (hallucinated)이 발생한 응답
이 중 어느 것도 지속 시간 히스토그램 (duration histogram)에는 나타나지 않습니다. 단순히 HTTP 속성뿐만 아니라 LLM 전용 속성을 포함하는 트레이스가 필요합니다.
기둥 1: 트레이싱 (Tracing)
LLM 앱에서의 작업 단위는 대개 단일 호출이 아니라 하나의 체인입니다: 프롬프트 구성 (prompt construction) → 프로바이더 호출 (provider call) → 가능한 재시도/폴백 (retry/fallback) → 도구 호출 (tool invocation) → 최종 응답 (final response). 이 각각은 단순히 외부 요청이 아니라 하나의 스팬 (span)이 되어야 합니다.
A LLM 호출을 위한 최소한의 스팬 형태는 다음과 같습니다:
#[derive(Debug, Serialize)]
struct LlmSpan {
trace_id: String,
...
프로덕션 환경에서 디버깅할 때 가장 중요한 두 필드는 retry_count와 fallback_from입니다. 이 필드들이 없다면, "성공적인" 트레이스는 여러분의 기본 프로바이더가 성능 저하를 겪고 있고 폴백 로직이 조용히 그 고통을 흡수하고 있다는 사실을 숨기게 됩니다 — 더 이상 감당할 수 없게 될 때까지 말이죠.
이를 OpenTelemetry에 연결하면 이미 사용 중인 어떤 트레이스 백엔드 (trace backend)와도 원활하게 연동됩니다:
use opentelemetry::trace::{Tracer, Span};
let tracer = global::tracer("llm-gateway");
...
이를 통해 Jaeger/Tempo에서 폭포수 뷰 (waterfall view)를 확인할 수 있으며, 폴백 체인 (fallback chain)이 로그 속에 파묻히는 대신 시각적으로 명확하게 드러납니다.
Pillar 2: 비용 추적 (Cost tracking)
토큰 수 (Token counts)만으로는 엔지니어링 팀 외부의 사람들이 관심을 가질 만한 이야기를 들려줄 수 없습니다. 핵심은 토큰을 기록 시점에 비용으로 변환하고, 이를 나중에 토큰 수에 한 번 찾아보고 업데이트하는 것을 잊어버린 가격을 곱해 재구성하는 것이 아니라, 일급 시민 (first-class) Prometheus 메트릭 (metric)으로 노출하는 것입니다.
use prometheus::{register_counter_vec, CounterVec};
static LLM_COST_USD: Lazy<CounterVec> = Lazy::new(|| {
...
pricing_for를 코드에 하드코딩하지 말고 설정 파일에 유지하세요. 제공업체의 가격 정책은 생각보다 자주 변경되며, 오래된 요금표는 재무 대시보드에 조용히 거짓 정보를 전달합니다.
provider, model, route를 레이블 (labels)로 사용하면, 모든 팀이 결국 묻게 될 질문에 대해 단 하나의 PromQL 쿼리로 답할 수 있습니다:
sum by (route) (
rate(llm_cost_usd_total[1h])
)
이는 스프레드시트 없이도 시간당 "어떤 기능이 예산을 태우고 있는가"를 보여줍니다. 이를 예산 기반 알람 (budget-based alert)과 결합하세요:
- alert: LLMCostSpike
expr: sum(rate(llm_cost_usd_total[15m])) * 3600 > 5
for: 10m
...
이것은 추가할 수 있는 가장 영향력 있는 단일 대시보드 패널입니다. 실제 사용량이 시작되면 관리자가 가장 먼저 요청하는 것이 보통 이것입니다.
Pillar 3: 평가 루프 (Eval loops)
이 부분은 전통적인 백엔드 관측성 (observability)에는 상응하는 개념이 없는 영역입니다. 응답이 빠르고, 저렴하며, 200 상태 코드를 반환하더라도 여전히 틀릴 수 있습니다. 별도로 구축하지 않는 한, 메트릭 스택 (metrics stack)의 그 어떤 것도 이를 잡아내지 못합니다.
게이트웨이 규모에서 평가 루프 (eval loop)의 실질적인 버전은 거대한 오프라인 벤치마크 스위트 (benchmark suite)가 아닙니다. 그것은 프로덕션 트래픽 (production traffic)에 대한 가볍고 지속적인 점검입니다.
- Golden set regression (골든 세트 회귀) — 모델이나 제공자(provider)의 변경 사항이 배포되기 전, 매번 실행되는 버전 관리된 소규모 프롬프트/예상 출력(prompt/expected-output) 쌍 세트
- Sampled production scoring (샘플링된 프로덕션 점수 산정) — 실제 완료된 응답(completions) 중 무작위로 1~2%를 주기적으로 더 저렴한 판사 모델(judge model)을 사용하여 다시 점수를 매기고, 해당 점수를 시간에 따른 지표(metric)로 추적
- Structural checks (구조적 검사) — 판사 모델이 전혀 필요 없는 저렴하고 결정론적인(deterministic) 검사: JSON 파싱 성공 여부, 응답이 비어 있지 않은지 여부, 예상 길이 범위 내에 있는지 여부
struct EvalResult {
trace_id: String,
check_name: String, // "json_valid", "length_bounds", "judge_score"
...
통과율(pass rate)을 다른 모든 항목과 동일한 Prometheus 인스턴스에 입력합니다:
static EVAL_PASS_RATE: Lazy<GaugeVec> = Lazy::new(|| {
register_gauge_vec!(
"llm_eval_pass_rate",
...
이제 모델 교체, 프롬프트 템플릿 변경, 또는 제공자 측의 조용한 모델 업데이트가 3일 뒤에 지원 티켓(support ticket)으로 나타나는 대신, 이미 지연 시간(latency)과 비용을 모니터링하고 있는 동일한 대시보드 상의 하락 지표로 나타나게 됩니다.
종합하기 (Putting it together)
실제로 매일 사용되는 대시보드는 세 가지 기둥(pillars)에 맞춰 세 개의 행으로 구성됩니다:
- Tracing 행 (Tracing row): 제공자별 p50/p95 지연 시간(latency), 재시도율(retry rate), 폴백 비율(fallback rate)
- Cost 행 (Cost row): 경로/모델별 지출(spend), 시간당 소모율(hourly burn rate), 예산 경고 상태(budget alert status)
- Quality 행 (Quality row): 구조적 검사 통과율(structural check pass rate), 샘플링된 판사 점수 추세(sampled judge score trend)
LLM 기능에 실제 사용자가 있다면 이 기둥 중 어느 것도 선택 사항이 아닙니다. 트레이싱(Tracing)은 _무슨 일이 일어났는지_를 알려주고, 비용 추적(cost tracking)은 _비용이 얼마나 드는지_를 알려주며, 평가 루프(eval loops)는 _여전히 제대로 작동하고 있는지_를 알려줍니다. 그리고 세 번째 요소는 대부분의 팀이 이미 프로덕션 장애(production incident)가 발생할 때까지 건너뛰는 부분입니다.
다음 단계 (What's next)
이 시리즈의 다음 포스트에서는 추론 최적화(inference optimization)를 다룹니다. 하이퍼스케일러(hyperscaler) 규모의 인프라를 운영하지는 않지만, 모델 서빙 계층(model-serving layer)에서 지연 시간과 비용을 제어해야 하는 팀들을 위한 KV 캐시 전략(KV cache strategies) 및 양자화 트레이드오프(quantization tradeoffs)를 살펴봅니다.
이 글은 Rust를 사용하여 멀티 프로바이더 AI 게이트웨이(multi-provider AI gateways)를 구축한 실무 경험을 바탕으로 작성된 AI 인프라 패턴(AI infrastructure patterns) 시리즈의 일부입니다. 시리즈의 나머지 내용도 계속해서 확인해 보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기