LLM 기반 시스템의 관측 가능성 설계 — 트레이싱, 메트릭, 로깅, 프롬프트 버전 관리, 품질 모니터링, 그리고 AI 특화 대시보드 구축을 다룹니다.
웹 서비스의 관측 가능성은 성숙한 분야입니다. Datadog, New Relic, Grafana 같은 도구로 HTTP 응답 시간, 에러율, CPU 사용률을 모니터링하는 것은 이미 표준 관행입니다. 그러나 AI 시스템에서는 이러한 전통적 메트릭만으로는 시스템의 실제 상태를 파악할 수 없습니다.
API가 200 OK를 반환하고, 응답 시간이 정상 범위 내이며, 서버 리소스에 여유가 있어도, 모델이 생성하는 응답의 품질이 하락하고 있을 수 있습니다. 이를 감지하려면 AI 시스템에 특화된 관측 가능성 체계가 필요합니다.
비결정적 출력은 동일한 입력에 대해 매번 다른 출력이 생성된다는 점입니다. 전통적인 "기대값과 실제값 비교" 방식의 모니터링이 어렵습니다.
품질의 주관성은 응답이 "좋은지"를 판단하는 기준이 작업마다 다르다는 것입니다. 정형화된 메트릭으로 포착하기 어려운 뉘앙스가 존재합니다.
긴 피드백 루프는 응답의 품질 문제가 즉시 드러나지 않을 수 있다는 점입니다. 환각이 포함된 응답을 사용자가 나중에야 발견하는 경우가 흔합니다.
블랙박스 모델은 외부 API를 사용하는 경우 모델 내부를 관찰할 수 없다는 제약입니다. 입출력만으로 상태를 추론해야 합니다.
관측 가능성의 세 기둥은 AI 시스템에서도 유효하지만, 각 기둥의 내용을 AI에 맞게 확장해야 합니다.
AI 시스템의 단일 요청은 여러 LLM 호출과 도구 실행을 거칩니다. RAG 파이프라인의 경우 검색, 리랭킹, 프롬프트 구성, LLM 추론, 후처리가 순차적으로 발생합니다. 각 단계의 입출력, 소요 시간, 토큰 수를 추적하는 분산 트레이싱이 핵심입니다.
interface LLMSpan {
traceId: string;
spanId: string;
parentSpanId?: string;
operation: string;
startTime: number;
endTime?: number;
attributes: {
model?: string;
inputTokens?: number;
outputTokens?: number;
promptVersion?: string;
temperature?: number;
toolCalls?: string[];
cacheHit?: boolean;
costUsd?: number;
};
status: "ok" | "error";
errorMessage?: string;
}
class LLMTracer {
private spans: Map<string, LLMSpan> = new Map();
startSpan(
traceId: string,
operation: string,
parentSpanId?: string
): LLMSpan {
const span: LLMSpan = {
traceId,
spanId: crypto.randomUUID(),
parentSpanId,
operation,
startTime: Date.now(),
attributes: {},
status: "ok",
};
this.spans.set(span.spanId, span);
return span;
}
endSpan(spanId: string, attributes: Partial<LLMSpan["attributes"]>): void {
const span = this.spans.get(spanId);
if (!span) return;
span.endTime = Date.now();
span.attributes = { ...span.attributes, ...attributes };
this.export(span);
}
markError(spanId: string, error: string): void {
const span = this.spans.get(spanId);
if (!span) return;
span.status = "error";
span.errorMessage = error;
}
private export(span: LLMSpan): void {
// OpenTelemetry Collector 또는 Langfuse로 전송
const duration = span.endTime
? span.endTime - span.startTime
: 0;
console.info(JSON.stringify({
trace_id: span.traceId,
span_id: span.spanId,
parent_span_id: span.parentSpanId,
operation: span.operation,
duration_ms: duration,
...span.attributes,
status: span.status,
}));
}
}트레이싱의 핵심은 하나의 traceId로 요청의 전체 여정을 연결하는 것입니다. API Gateway에서 생성한 traceId가 AI Gateway, 모델 라우터, 캐시 조회, LLM 호출, 후처리까지 전파되어야 합니다. OpenTelemetry 표준을 따르면 기존 APM 도구와의 통합이 용이합니다.
전통적 메트릭(응답 시간, 에러율)에 더해, AI 시스템은 다음의 특화 메트릭을 추적해야 합니다.
성능 메트릭:
품질 메트릭:
비용 메트릭:
interface AIMetrics {
// 성능
ttfbMs: number;
tokensPerSecond: number;
endToEndLatencyMs: number;
// 토큰
inputTokens: number;
outputTokens: number;
totalTokens: number;
// 품질
guardrailTriggered: boolean;
hallucinationDetected: boolean;
userFeedbackScore?: number;
// 비용
estimatedCostUsd: number;
modelUsed: string;
cacheHit: boolean;
}
class AIMetricsCollector {
private metrics: AIMetrics[] = [];
record(metric: AIMetrics): void {
this.metrics.push(metric);
this.emitToPrometheus(metric);
}
getAggregated(windowMinutes: number): {
avgLatency: number;
p95Latency: number;
guardrailRate: number;
hallucinationRate: number;
avgCost: number;
cacheHitRate: number;
} {
const recent = this.metrics.filter(
(_, i) => i > this.metrics.length - 1000
);
const latencies = recent.map((m) => m.endToEndLatencyMs).sort((a, b) => a - b);
const guardrailCount = recent.filter((m) => m.guardrailTriggered).length;
const hallucinationCount = recent.filter((m) => m.hallucinationDetected).length;
const cacheHitCount = recent.filter((m) => m.cacheHit).length;
return {
avgLatency: latencies.reduce((a, b) => a + b, 0) / latencies.length,
p95Latency: latencies[Math.floor(latencies.length * 0.95)] ?? 0,
guardrailRate: guardrailCount / recent.length,
hallucinationRate: hallucinationCount / recent.length,
avgCost:
recent.reduce((sum, m) => sum + m.estimatedCostUsd, 0) / recent.length,
cacheHitRate: cacheHitCount / recent.length,
};
}
private emitToPrometheus(metric: AIMetrics): void {
// Prometheus 메트릭 내보내기
}
}AI 시스템의 로그는 일반적인 애플리케이션 로그에 더해 프롬프트-응답 쌍을 기록해야 합니다. 이 데이터는 디버깅, 품질 분석, 프롬프트 개선에 필수적입니다.
프롬프트와 응답 로그에는 사용자의 개인정보가 포함될 수 있습니다. 이름, 이메일, 주소 등의 PII(Personally Identifiable Information)는 로깅 전에 반드시 마스킹 처리해야 합니다. GDPR이나 개인정보보호법 위반은 기술적 실수로 면책되지 않습니다.
로그 레벨에 따라 기록하는 정보의 범위를 조절합니다. 프로덕션에서는 기본적으로 요약된 메타데이터만 기록하고, 디버깅이 필요한 경우 샘플링 비율을 높여 전체 프롬프트와 응답을 기록합니다.
프롬프트는 AI 시스템의 핵심 구성 요소이면서 가장 자주 변경되는 요소입니다. 코드에 대한 버전 관리(Git)와 동일한 수준의 관리가 프롬프트에도 필요합니다.
변경 추적: 언제, 누가, 왜 프롬프트를 변경했는지 기록합니다. 프롬프트 변경은 코드 변경과 동일한 수준의 리뷰를 거쳐야 합니다.
성능 연관 분석: 프롬프트 버전과 품질 메트릭을 연결하여, 특정 프롬프트 변경이 성능에 미친 영향을 추적합니다. 버전 A에서 B로 전환한 후 환각률이 증가했다면, 즉시 롤백할 수 있어야 합니다.
A/B 테스트: 새로운 프롬프트를 전체 트래픽에 적용하기 전에, 일부 트래픽에만 적용하여 성능을 비교합니다.
interface PromptVersion {
id: string;
name: string;
version: string;
template: string;
variables: string[];
metadata: {
author: string;
createdAt: string;
description: string;
parentVersion?: string;
};
abTest?: {
enabled: boolean;
trafficPercent: number;
controlVersion: string;
};
}
class PromptRegistry {
private versions: Map<string, PromptVersion[]> = new Map();
register(prompt: PromptVersion): void {
const existing = this.versions.get(prompt.name) ?? [];
existing.push(prompt);
this.versions.set(prompt.name, existing);
}
resolve(name: string, userId?: string): PromptVersion {
const versions = this.versions.get(name);
if (!versions || versions.length === 0) {
throw new Error(`프롬프트를 찾을 수 없습니다: ${name}`);
}
// A/B 테스트가 활성화된 버전 확인
const abVersion = versions.find((v) => v.abTest?.enabled);
if (abVersion && userId) {
const hash = this.hashUserId(userId);
if (hash % 100 < (abVersion.abTest?.trafficPercent ?? 0)) {
return abVersion;
}
}
// 최신 안정 버전 반환
return versions[versions.length - 1]!;
}
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = (hash << 5) - hash + userId.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
}AI 시스템의 품질을 프로덕션 환경에서 자동으로 모니터링하는 것은 관측 가능성의 정점입니다. 오프라인 평가(벤치마크)만으로는 프로덕션의 다양한 입력 패턴을 포괄할 수 없습니다.
프로덕션 트래픽의 일정 비율(통상 1-5%)을 샘플링하여 자동 품질 평가를 수행합니다. 평가 항목은 작업 유형에 따라 달라지지만, 공통적으로 다음을 포함합니다.
"LLM-as-Judge" 패턴을 활용하면 사람의 개입 없이 대규모로 품질을 평가할 수 있습니다. 단, 평가 모델은 응답 생성 모델과 다른 모델을 사용하여 편향을 줄이는 것이 좋습니다. 예를 들어, Sonnet이 생성한 응답을 Opus가 평가하는 구성입니다.
AI 관측 가능성 전용 도구들이 빠르게 성장하고 있습니다.
| 도구 | 주요 기능 | 특징 |
|---|---|---|
| Langfuse | 트레이싱, 프롬프트 관리, 평가 | 오픈소스, 셀프호스팅 가능 |
| LangSmith | 트레이싱, 데이터셋, 평가 | LangChain 생태계 통합 |
| Arize Phoenix | 트레이싱, 평가, 임베딩 분석 | 오픈소스, 벡터 시각화 |
| Helicone | 로깅, 비용 추적, 캐싱 | 프록시 기반, 설정 간편 |
이들 도구는 기존 APM 도구를 대체하는 것이 아니라 보완합니다. 인프라 수준의 모니터링은 Datadog이나 Grafana로, AI 특화 메트릭은 Langfuse나 Arize로 추적하는 이중 구조가 현실적입니다.
이번 장에서는 AI 시스템에 특화된 관측 가능성 체계를 설계했습니다. 트레이싱으로 요청의 전체 여정을 추적하고, AI 특화 메트릭으로 품질과 비용을 측정하며, 프롬프트 버전 관리와 자동 평가로 지속적인 품질 개선을 가능하게 합니다. 다음 장에서는 이러한 관측 데이터를 기반으로 시스템을 확장하고 멀티테넌시를 지원하는 아키텍처를 다루겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
AI 시스템의 수평 확장, 멀티테넌시 아키텍처, 속도 제한, 공정 스케줄링, 그리고 대규모 AI 서비스 운영을 위한 인프라 설계를 다룹니다.
AI 시스템의 장애 시나리오와 회복 탄력성 패턴 — 서킷 브레이커, 폴백, 재시도, 타임아웃, 모델 장애 조치, 그리고 그레이스풀 디그레이데이션을 다룹니다.
시리즈 전체의 아키텍처 패턴을 종합하여 프로덕션 AI-Native 시스템을 설계합니다. 전체 아키텍처 다이어그램, 기술 선택, 배포 전략을 다룹니다.