Back

LLM 옵저버빌리티 완전 정복: 프로덕션 AI 에이전트를 모니터링하고, 추적하고, 디버깅하는 법

AI 에이전트가 고객한테 $2,400 손해를 끼쳤어요. 새벽 3시에 무한 툴콜 루프에 빠져서 토큰을 펑펑 태우면서 헛소리를 쏟아내고 있었거든요. 기존 APM 대시보드? 전부 초록불. 레이턴시 정상, 에러 없음, 크래시 없음. 근데 에이전트는 6시간 동안 자신만만하게 틀린 답을 주고 있었고, 우리 쪽에선 아무것도 안 보였죠.

이게 AI 프로덕트를 조용히 죽이는 옵저버빌리티 사각지대예요. 기존 모니터링 도구는 결정론적 소프트웨어 기준으로 만들어졌거든요. 요청 들어오고, 응답 나가고, 시간 재면 끝. 근데 AI 에이전트는 근본적으로 다른 물건이에요. 추론하고, 분기 치고, 툴 호출하고, 문서 뒤지고, 같은 입력에도 매번 다른 결정을 내리니까요. 뭔가 터졌을 때 HTTP 상태 코드만 보면 안 돼요. 추론 체인 자체를 까봐야 해요. 모든 결정 포인트, 모든 툴 호출, 소비된 모든 토큰을.

이 가이드에서는 LLM 기반 시스템의 프로덕션 급 옵저버빌리티를 구축하는 전 과정을 다뤄요. 분산 트레이싱부터 자동 평가, 비용 추적, 툴링 비교까지. 이론은 없어요. 하루 수백만 요청을 처리하는 에이전트를 실제로 운영하는 팀들의 실전 검증 패턴만 담았어요.

기존 모니터링이 LLM 앱에서 터지는 이유

Datadog, Grafana, New Relic만으로 AI 에이전트 운영하고 계신가요? 눈 감고 고속도로 달리는 거나 다름없어요. 왜 그런지 살펴볼게요.

결정론 문제

기존 소프트웨어는 결정론적이죠. 같은 입력이면 같은 출력. 모니터링도 간단해요. 레이턴시, 에러율, 처리량 추적하면 끝이니까요. P99 레이턴시 튀면 조사하면 되고.

근데 LLM은 비결정적이에요. 같은 프롬프트가 매번 다른 출력을 뱉어요. "성공"이라는 HTTP 200 응답 안에 완전히 지어낸 답변이 들어있을 수도 있거든요. 에러율 0%인데 정확도 40%? 기존 APM 도구로는 이런 실패를 감지하는 게 아예 불가능해요.

멀티스텝 문제

단순한 API 콜은 스팬 하나예요. 요청 → 응답. AI 에이전트는 복잡한 실행 그래프예요.

사용자 쿼리: "다음 달 뉴욕에서 도쿄 가는 가장 싼 비행기 찾아줘"
│
├─ Step 1: 의도 분류 (LLM 호출, 200ms, 150 토큰)
├─ Step 2: 파라미터 추출 (LLM 호출, 180ms, 120 토큰)
├─ Step 3: 툴 호출 - 항공편 API (외부 API, 2.1초)
├─ Step 4: 결과 파싱 (LLM 호출, 250ms, 800 토큰)
├─ Step 5: 가격 비교 (LLM 호출, 300ms, 1200 토큰)
├─ Step 6: 응답 생성 (LLM 호출, 400ms, 500 토큰)
│
합계: LLM 5회 호출, 3.4초, 2770 토큰, $0.008

이 에이전트가 틀린 결과를 줬을 때, 대체 어디서 깨진 건지 어떻게 알죠? 의도 분류를 잘못 잡은 건가? 툴이 엉뚱한 데이터를 뱉은 건가? LLM이 가격 비교하다가 환각한 건가? 스텝 단위로 추적 안 하면 디버깅 지옥이에요.

비용 문제

LLM 호출은 비싸요. 기존 컴퓨팅에서 CPU 사이클은 거의 공짜지만, 토큰 하나하나에 달러가 붙거든요. 에이전트 루프 하나가 폭주하면 몇 분 만에 수백 달러가 증발해요. 에이전트별, 사용자별, 조직별 실시간 비용 추적이 필수인데, 기존 APM 도구 중에 이걸 해주는 건 하나도 없어요.

LLM 옵저버빌리티 스택

프로덕션 급 LLM 옵저버빌리티를 제대로 하려면 네 개 레이어가 필요해요.

┌─────────────────────────────────────────────────────┐
│              Layer 4: 대시보드                        │
│       비용 분석, 품질 추이, SLA 트래킹               │
├─────────────────────────────────────────────────────┤
│              Layer 3: 평가                           │
│     자동 평가, 회귀 감지, A/B 테스트                 │
├─────────────────────────────────────────────────────┤
│              Layer 2: 트레이싱                       │
│     분산 트레이스, 스팬 계층 구조, 토큰 추적         │
├─────────────────────────────────────────────────────┤
│              Layer 1: 인스트루먼테이션               │
│      SDK 연동, 자동 캡처, 수동 어노테이션            │
└─────────────────────────────────────────────────────┘

아래부터 하나씩 쌓아올려 볼게요.

Layer 1: 인스트루먼테이션

인스트루먼테이션은 기반이에요. 앱 성능을 해치지 않으면서 모든 결정 포인트에서 데이터를 캡처해야 해요.

LLM을 위한 OpenTelemetry

업계가 표준 인스트루먼테이션 레이어로 OpenTelemetry(OTel)에 수렴하고 있어요. OpenLLMetry 프로젝트가 OTel을 LLM 전용 시맨틱 컨벤션으로 확장했고요.

import * as traceloop from '@traceloop/node-server-sdk'; // LLM 모듈 임포트 전에 초기화 traceloop.initialize({ baseUrl: 'https://your-collector.example.com', appName: 'my-ai-agent', }); // 또는 OpenTelemetry를 직접 사용하는 모듈 방식: import { NodeSDK } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { OpenAIInstrumentation } from '@traceloop/instrumentation-openai'; import { AnthropicInstrumentation } from '@traceloop/instrumentation-anthropic'; const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: 'https://your-collector.example.com/v1/traces', }), instrumentations: [ new OpenAIInstrumentation({ captureInputs: true, // 프롬프트 로깅 (프로덕션에서는 주의!) captureOutputs: true, // 응답 로깅 }), new AnthropicInstrumentation(), ], }); sdk.start();

이렇게 하면 OpenAI와 Anthropic API 호출이 전부 자동 계측돼요.

  • 모델명과 파라미터 (temperature, max_tokens)
  • 입력 프롬프트와 출력 응답
  • 토큰 사용량 (프롬프트 토큰, 응답 토큰)
  • 콜별 레이턴시
  • 툴/펑션 호출 디테일

수동 스팬 어노테이션

자동 계측은 LLM 호출을 잡아주지만, 비즈니스 로직에는 수동 스팬이 필요해요.

import { trace, SpanStatusCode } from '@opentelemetry/api'; const tracer = trace.getTracer('ai-agent'); async function processUserQuery(query: string, userId: string) { return tracer.startActiveSpan('agent.process_query', async (span) => { span.setAttributes({ 'user.id': userId, 'agent.query': query, 'agent.type': 'flight-search', }); try { // Step 1: 의도 분류 const intent = await tracer.startActiveSpan( 'agent.classify_intent', async (intentSpan) => { const result = await classifyIntent(query); intentSpan.setAttributes({ 'agent.intent': result.intent, 'agent.confidence': result.confidence, }); return result; } ); // Step 2: 툴 호출 실행 const toolResults = await tracer.startActiveSpan( 'agent.execute_tools', async (toolSpan) => { toolSpan.setAttribute('agent.tools_count', intent.tools.length); return Promise.all( intent.tools.map((tool) => executeTool(tool)) ); } ); // Step 3: 응답 생성 const response = await tracer.startActiveSpan( 'agent.generate_response', async (respSpan) => { const result = await generateResponse(toolResults); respSpan.setAttributes({ 'agent.response_length': result.length, 'agent.tokens_total': result.tokenUsage.total, }); return result; } ); span.setStatus({ code: SpanStatusCode.OK }); return response; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.recordException(error); throw error; } }); }

뭘 캡처하고 뭘 안 할지

중요한 결정이에요. 어떤 데이터를 로깅할지.

데이터개발 환경프로덕션이유
전체 프롬프트✅ 예⚠️ 샘플링PII 위험, 저장 비용
전체 응답✅ 예⚠️ 샘플링위와 동일
토큰 수✅ 예✅ 예비용 추적 필수
모델 파라미터✅ 예✅ 예회귀 디버깅
툴 호출 입출력✅ 예✅ 예디버깅 필수
사용자 ID✅ 예✅ 예사용자별 비용 추적
스텝별 레이턴시✅ 예✅ 예성능 모니터링
임베딩 벡터❌ 아니오❌ 아니오너무 큼, 유용하지 않음
원시 API 응답✅ 예❌ 아니오저장소 폭발

프로덕션에서는 전체 프롬프트/응답 로깅에 샘플링을 거세요. 메타데이터(토큰, 레이턴시, 모델)는 100% 잡되, 텍스트 전문은 10~20%만 남기면 돼요. 특정 이슈를 파고들 때는 해당 사용자나 쿼리의 샘플링 비율을 잠깐 올려서 까보면 되고요.

Layer 2: 분산 트레이싱

인스트루먼테이션이 깔리면, LLM 전용 데이터를 이해하는 트레이싱 백엔드가 필요해요. 여기서 전용 도구가 빛을 발하죠.

AI 에이전트의 트레이스 구조

잘 설계된 AI 에이전트 트레이스는 이렇게 생겼어요.

Trace: agent_run_abc123
│
├─ Span: agent.process_query (루트)
│  ├─ Attributes: user_id, query, session_id
│  │
│  ├─ Span: agent.classify_intent
│  │  ├─ Span: llm.openai.chat (model: gpt-4.1-mini)
│  │  │  └─ Attributes: tokens_in=150, tokens_out=30, cost=$0.0001
│  │  └─ Result: intent=flight_search, confidence=0.95
│  │
│  ├─ Span: agent.retrieve_context (RAG)
│  │  ├─ Span: vectordb.query (provider: pinecone)
│  │  │  └─ Attributes: top_k=5, similarity_threshold=0.8
│  │  └─ Span: agent.rerank
│  │     └─ Span: llm.anthropic.chat (model: claude-haiku-4.5)
│  │        └─ Attributes: tokens_in=2000, tokens_out=500
│  │
│  ├─ Span: agent.execute_tool
│  │  ├─ Span: tool.flight_api.search
│  │  │  └─ Attributes: duration=2100ms, results_count=15
│  │  └─ Span: tool.flight_api.get_prices
│  │     └─ Attributes: duration=800ms, results_count=15
│  │
│  └─ Span: agent.generate_response
│     └─ Span: llm.openai.chat (model: gpt-4.1)
│        └─ Attributes: tokens_in=3000, tokens_out=800, cost=$0.02
│
└─ 합계: LLM 4회 호출, 6800 토큰, $0.021, 4.2초

이 구조로 이런 질문에 답할 수 있어요.

  • "이 에이전트가 왜 10초 걸렸지?" → 항공편 API 호출이 8초 걸렸네요.
  • "비용이 왜 0.02가아니라0.02가 아니라 2야?" → 에이전트가 툴 호출을 100번 반복했어요.
  • "왜 환각했지?" → RAG 검색이 유사도가 낮은 엉뚱한 문서를 반환했어요.

트레이스 전파 구현

멀티서비스 아키텍처에서는 트레이스 컨텍스트가 서비스 경계를 넘어 전파돼야 해요.

// Service A: 에이전트 오케스트레이터 import { context, propagation } from '@opentelemetry/api'; async function callToolService(toolName: string, params: any) { const headers: Record<string, string> = {}; propagation.inject(context.active(), headers); const response = await fetch(`https://tools.internal/${toolName}`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); return response.json(); } // Service B: 툴 실행 서비스 import { context, propagation } from '@opentelemetry/api'; app.post('/flight-search', (req, res) => { const ctx = propagation.extract(context.active(), req.headers); context.with(ctx, async () => { const span = tracer.startSpan('tool.flight_search'); // ... 전체 트레이스 연계된 툴 실행 span.end(); }); });

Layer 3: 자동 평가

트레이싱은 뭐가 일어났는지 알려주죠. 평가는 얼마나 잘 했는지 알려주고요. 대부분 팀이 이 레이어를 건너뛰는데, 프로덕션 AI에서 성패를 가르는 건 바로 여기예요.

평가 파이프라인

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ 트레이스  │ →  │  샘플    │ →  │  평가    │ →  │  알림    │
│  저장소   │    │  선택    │    │  실행    │    │  리포트  │
└──────────┘    └──────────┘    └──────────┘    └──────────┘
                10-20%의        LLM-as-Judge    Slack/PD
                트레이스         + 결정론적       품질
                                 규칙             하락 시

LLM-as-Judge 평가

가장 강력한 평가 패턴은 별도의 LLM으로 에이전트 출력을 판단하는 거에요.

interface EvalResult { score: number; // 0-1 reasoning: string; // 이 점수를 준 이유 dimension: string; // 평가 차원 } async function evaluateResponse( query: string, response: string, groundTruth?: string ): Promise<EvalResult[]> { const evaluations: EvalResult[] = []; // 평가 1: 사실 정확성 const accuracyEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `당신은 전문 평가자입니다. AI 응답의 사실 정확성을 0에서 1까지 점수를 매기세요. 채점 기준: - 1.0: 모든 사실이 정확하고 검증 가능 - 0.7: 대부분 정확하되 사소한 부정확함 있음 - 0.4: 중대한 사실 오류 포함 - 0.0: 완전히 날조되었거나 틀림 JSON으로 응답: { "score": number, "reasoning": string }`, }, { role: 'user', content: `질문: ${query} AI 응답: ${response} ${groundTruth ? `정답: ${groundTruth}` : ''}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(accuracyEval.choices[0].message.content), dimension: 'factual_accuracy', }); // 평가 2: 관련성 const relevanceEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `사용자 질문에 대한 응답의 관련성을 점수 매기세요. 1.0 = 질문에 직접 답변 0.5 = 부분적으로 관련 0.0 = 완전히 주제 벗어남 JSON으로 응답: { "score": number, "reasoning": string }`, }, { role: 'user', content: `질문: ${query}\n응답: ${response}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(relevanceEval.choices[0].message.content), dimension: 'relevance', }); return evaluations; }

결정론적 가드

모든 걸 LLM 판사에게 맡길 필요는 없어요. 알려진 실패 패턴에는 결정론적 체크를 쓰세요.

interface GuardResult { passed: boolean; violation?: string; } function runDeterministicGuards( trace: AgentTrace ): GuardResult[] { const results: GuardResult[] = []; // 가드 1: 토큰 예산 초과 const totalTokens = trace.spans .filter((s) => s.name.startsWith('llm.')) .reduce((sum, s) => sum + (s.attributes.tokens_total || 0), 0); results.push({ passed: totalTokens < 50000, violation: totalTokens >= 50000 ? `토큰 예산 초과: ${totalTokens} 토큰` : undefined, }); // 가드 2: 툴 호출 루프 감지 const toolCalls = trace.spans .filter((s) => s.name.startsWith('tool.')); const uniqueTools = new Set(toolCalls.map((s) => s.name)); for (const tool of uniqueTools) { const count = toolCalls .filter((s) => s.name === tool).length; results.push({ passed: count <= 10, violation: count > 10 ? `루프 의심: ${tool}이(가) ${count}번 호출됨` : undefined, }); } // 가드 3: 레이턴시 예산 const totalLatency = trace.duration; results.push({ passed: totalLatency < 30000, violation: totalLatency >= 30000 ? `레이턴시 예산 초과: ${totalLatency}ms` : undefined, }); // 가드 4: 빈 응답 또는 의심스럽게 짧은 응답 const finalResponse = trace.output; results.push({ passed: finalResponse && finalResponse.length > 20, violation: !finalResponse || finalResponse.length <= 20 ? '응답이 비어있거나 의심스러울 만큼 짧음' : undefined, }); return results; }

자동 알림

평가를 알림 시스템에 연결하세요.

async function runEvalPipeline(trace: AgentTrace) { // 결정론적 가드 (빠름, 모든 트레이스에 실행) const guardResults = runDeterministicGuards(trace); const guardViolations = guardResults .filter((r) => !r.passed); if (guardViolations.length > 0) { await sendAlert({ severity: 'high', title: '에이전트 가드 위반', details: guardViolations .map((v) => v.violation) .join('\n'), traceId: trace.traceId, }); } // LLM-as-Judge (비쌈, 샘플링된 트레이스에만 실행) if (shouldSample(trace, 0.1)) { const evalResults = await evaluateResponse( trace.input, trace.output ); const lowScores = evalResults .filter((e) => e.score < 0.5); if (lowScores.length > 0) { await sendAlert({ severity: 'medium', title: '에이전트 품질 저하', details: lowScores .map((e) => `${e.dimension}: ${e.score} - ${e.reasoning}` ) .join('\n'), traceId: trace.traceId, }); } await storeEvalResults(trace.traceId, evalResults); } }

Layer 4: 비용 추적과 분석

토큰 비용은 AI 앱의 클라우드 청구서죠. 세밀하게 추적 안 하면? 그냥 감으로 운영하는 거예요.

실시간 비용 계산

const MODEL_PRICING: Record<string, { input: number; // 100만 토큰당 output: number; // 100만 토큰당 }> = { 'gpt-4.1': { input: 2.00, output: 8.00 }, 'gpt-4.1-mini': { input: 0.40, output: 1.60 }, 'gpt-4.1-nano': { input: 0.10, output: 0.40 }, 'claude-sonnet-4.6': { input: 3.00, output: 15.00 }, 'claude-haiku-4.5': { input: 1.00, output: 5.00 }, 'gemini-2.5-pro': { input: 1.25, output: 10.00 }, 'gemini-2.5-flash': { input: 0.30, output: 2.50 }, }; function calculateCost( model: string, inputTokens: number, outputTokens: number ): number { const pricing = MODEL_PRICING[model]; if (!pricing) return 0; return ( (inputTokens / 1_000_000) * pricing.input + (outputTokens / 1_000_000) * pricing.output ); } // 트레이스 단위 비용 추적 function aggregateTraceCost(trace: AgentTrace): CostBreakdown { const llmSpans = trace.spans .filter((s) => s.name.startsWith('llm.')); let totalCost = 0; const breakdown: Record<string, number> = {}; for (const span of llmSpans) { const model = span.attributes.model; const cost = calculateCost( model, span.attributes.tokens_in, span.attributes.tokens_out ); totalCost += cost; breakdown[model] = (breakdown[model] || 0) + cost; } return { totalCost, breakdown, tokenCount: llmSpans.reduce( (sum, s) => sum + s.attributes.tokens_in + s.attributes.tokens_out, 0 ), }; }

비용 알림 임계값

// 요청당 비용 가드 const MAX_COST_PER_REQUEST = 0.50; // $0.50 // 사용자별 시간당 예산 const MAX_COST_PER_USER_HOUR = 5.00; // $5.00 // 조직별 일일 예산 const MAX_COST_PER_ORG_DAY = 500.00; // $500.00 async function checkCostBudgets( cost: number, userId: string, orgId: string ) { if (cost > MAX_COST_PER_REQUEST) { await sendAlert({ severity: 'high', title: `요청 비용 초과: $${cost.toFixed(4)}`, }); } const userHourlyCost = await redis.incrbyfloat( `cost:user:${userId}:${getCurrentHour()}`, cost ); await redis.expire( `cost:user:${userId}:${getCurrentHour()}`, 7200 ); if (userHourlyCost > MAX_COST_PER_USER_HOUR) { await sendAlert({ severity: 'critical', title: `사용자 ${userId} 시간당 예산 초과`, }); } }

툴링 비교: LangSmith vs Langfuse vs Arize

옵저버빌리티 플랫폼 선택은 꽤 중요한 결정이죠. 각잡고 솔직하게 비교해볼게요.

LangSmith

추천 대상: LangChain/LangGraph 이미 쓰는 팀

import { Client } from 'langsmith'; import { traceable } from 'langsmith/traceable'; const client = new Client({ apiKey: process.env.LANGSMITH_API_KEY, }); const processQuery = traceable( async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); }, { name: 'process_query', tags: ['production'] } );

장점:

  • LangChain/LangGraph 깊은 연동 (퍼스트파티)
  • 빌트인 프롬프트 플레이그라운드 및 버전 관리
  • 프롬프트 공유·발견 허브
  • 사람 참여 평가가 가능한 강력한 eval 프레임워크
  • 에이전트 실행 그래프 시각화 우수

약점:

  • LangChain 생태계에 벤더 락인
  • 클로즈드 소스, 호스팅 전용 (셀프 호스팅 불가)
  • 트레이스 볼륨에 따라 과금 (비싸질 수 있음)
  • 비-LangChain 프레임워크 지원 제한적

Langfuse

추천 대상: 오픈소스, 프레임워크 무관한 트레이싱을 원하는 팀

// Langfuse v5+ 연동 (추천: @langfuse/tracing) import { observe } from '@langfuse/tracing'; // 데코레이터 기반 트레이싱 (가장 간단한 방법) const processQuery = observe( { name: 'flight-search-agent' }, async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); } ); // 또는 세밀한 제어가 필요하면 클래식 Langfuse 클라이언트 사용: import Langfuse from 'langfuse'; const langfuse = new Langfuse({ publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY, }); const trace = langfuse.trace({ name: 'flight-search-agent', userId: 'user-123', metadata: { environment: 'production' }, }); const generation = trace.generation({ name: 'classify-intent', model: 'gpt-4.1-mini', input: [{ role: 'user', content: query }], output: response, usage: { promptTokens: 150, completionTokens: 30, }, });

장점:

  • 오픈소스 (MIT 라이선스), 셀프 호스팅 가능
  • 프레임워크 무관 (어떤 LLM 프로바이더든 동작)
  • 빌트인 비용 추적과 토큰 분석
  • 프롬프트 관리와 버전 관리
  • 넉넉한 무료 티어

약점:

  • LangSmith보다 커뮤니티 작음
  • 셀프 호스팅에 인프라 관리 필요
  • 평가 기능이 LangSmith 대비 덜 성숙
  • UI 다소 덜 다듬어짐 (빠르게 개선 중)

Arize Phoenix

추천 대상: ML/데이터 사이언스 배경의 팀

import { trace as otelTrace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai'; registerInstrumentations({ instrumentations: [new OpenAIInstrumentation()], });

장점:

  • OpenTelemetry 기반 (프로프리에터리 락인 없음)
  • 임베딩 시각화와 드리프트 감지 탁월
  • RAG 분석 도구 우수
  • 로컬 우선 개발 경험 (Phoenix가 로컬에서 실행)
  • 검색 품질 디버깅 최고 수준

약점:

  • 학습 곡선 가파름
  • 에이전트 오케스트레이션 트레이싱 비중 적음
  • 직접 연동 생태계가 작음
  • 엔터프라이즈 기능은 Arize 클라우드 필요

비교 매트릭스

기능LangSmithLangfuseArize Phoenix
오픈소스✅ MIT✅ (Phoenix)
셀프 호스팅✅ (Phoenix)
LangChain 연동⭐⭐⭐⭐⭐
프레임워크 무관⭐⭐⭐⭐⭐⭐
비용 추적⭐⭐⭐⭐⭐⭐⭐
평가 프레임워크⭐⭐⭐⭐⭐⭐⭐
RAG 분석⭐⭐⭐⭐⭐
프롬프트 관리⭐⭐⭐⭐⭐
임베딩 분석⭐⭐⭐
가격 (스타트업 기준)$$$무료/$무료/$

프로덕션 패턴과 안티패턴

패턴 1: 서킷 브레이커

폭주하는 에이전트가 예산을 태우는 걸 막으세요.

class AgentCircuitBreaker { private tokenCount = 0; private llmCalls = 0; private toolCalls = 0; private startTime: number; constructor( private limits: { maxTokens: number; maxLLMCalls: number; maxToolCalls: number; maxDurationMs: number; } ) { this.startTime = Date.now(); } check(event: { type: 'llm' | 'tool'; tokens?: number }) { if (event.type === 'llm') { this.llmCalls++; this.tokenCount += event.tokens || 0; } else { this.toolCalls++; } const elapsed = Date.now() - this.startTime; if (this.tokenCount > this.limits.maxTokens) { throw new CircuitBreakerError( `토큰 한도 초과: ${this.tokenCount}` ); } if (this.llmCalls > this.limits.maxLLMCalls) { throw new CircuitBreakerError( `LLM 호출 한도 초과: ${this.llmCalls}` ); } if (this.toolCalls > this.limits.maxToolCalls) { throw new CircuitBreakerError( `툴 호출 한도 초과: ${this.toolCalls}` ); } if (elapsed > this.limits.maxDurationMs) { throw new CircuitBreakerError( `실행 시간 한도 초과: ${elapsed}ms` ); } } } // 사용법 const breaker = new AgentCircuitBreaker({ maxTokens: 50000, maxLLMCalls: 20, maxToolCalls: 30, maxDurationMs: 60000, // 1분 }); for (const step of agentSteps) { breaker.check({ type: step.type, tokens: step.tokenUsage, }); await executeStep(step); }

패턴 2: 트레이스 기반 디버깅 워크플로우

뭔가 깨졌을 때, 이 순서대로 따라가세요.

1. 감지: 자동 평가가 품질 저하 감지
   ↓
2. 식별: 낮은 평가 점수로 트레이스 필터링
   ↓
3. 비교: 성공한 트레이스와 나란히 비교
   ↓
4. 격리: 분기점 찾기
   ↓
5. 원인: 해당 스팬의 입출력 확인
   ↓
6. 수정: 프롬프트, 컨텍스트, 또는 툴 설정 수정
   ↓
7. 검증: 테스트 데이터셋에서 수정 사항 평가 실행

안티패턴 1: 전부 로깅하기

모든 요청의 모든 토큰을 로깅하지 마세요.

// ❌ 이렇게 하지 마세요 logger.info('LLM Response', { fullPrompt: systemPrompt + userMessage + context, // 50KB fullResponse: completion, // 10KB metadata: entireTraceObject, // 5KB }); // 결과: 요청당 65KB × 일 100만 요청 = 일 65GB // ✅ 이렇게 하세요 logger.info('LLM Response', { traceId: trace.id, model: 'gpt-4.1-mini', tokensIn: 150, tokensOut: 30, cost: 0.0001, latencyMs: 200, evalScore: 0.95, }); // 결과: 요청당 200바이트 × 일 100만 요청 = 일 200MB

안티패턴 2: LLM 에러를 HTTP 에러처럼 취급하기

// ❌ 잘못됨: HTTP 200인데 에이전트 응답은 엉망 if (response.status === 200) { metrics.increment('agent.success'); } // ✅ 올바름: 실제 품질을 측정 const evalScore = await quickEval(response.body); if (evalScore > 0.7) { metrics.increment('agent.quality.good'); } else { metrics.increment('agent.quality.poor'); // 이게 진짜 "에러"임. 조사 시작 }

안티패턴 3: 기준선 없이 운영

// ❌ 알림: "평가 점수 0.72" -> 좋은 건가? 나쁜 건가? // ✅ 먼저 기준선을 잡으세요 // 1-2주차: 알림 없이 평가 점수 수집 // 3주차: P50, P90, P99 기준선 계산 // 4주차+: 기준선 이탈 시 알림 const baseline = { accuracy: { p50: 0.85, p90: 0.92, p99: 0.97 }, relevance: { p50: 0.90, p90: 0.95, p99: 0.99 }, latency: { p50: 2000, p90: 5000, p99: 10000 }, }; function shouldAlert( dimension: string, value: number ): boolean { const b = baseline[dimension]; return value < b.p50 * 0.8; // 중간값보다 20% 낮으면 알림 }

최소 실행 가능 옵저버빌리티 스택

처음부터 시작한다면? 프로덕션 급까지 가장 빠르게 도달하는 경로를 알려드릴게요.

1일차: 기본 인스트루먼테이션

// 1. Langfuse 설치 (가장 빠르게 시작 가능) // npm install langfuse import Langfuse from 'langfuse'; const langfuse = new Langfuse(); // 2. 에이전트의 메인 함수를 래핑 async function runAgent(query: string, userId: string) { const trace = langfuse.trace({ name: 'agent-run', userId, input: query, }); // 기존 에이전트 코드... trace.update({ output: response }); await langfuse.flushAsync(); }

1주차: 비용 추적 추가

const generation = trace.generation({ name: 'main-llm-call', model: 'gpt-4.1-mini', input: messages, output: completion, usage: { promptTokens: usage.prompt_tokens, completionTokens: usage.completion_tokens, }, // Langfuse가 토큰 수로 비용 자동 계산 });

2주차: 결정론적 가드 추가

// 패턴 1의 서킷 브레이커 적용 // 빈 응답 감지 // 루프 감지 // 가드 위반 시 Slack/PagerDuty 알림 설정

4주차: LLM-as-Judge 평가 추가

// 프로덕션 트레이스의 10%에 실행 // 두 차원으로 시작: 정확성 + 관련성 // 알림 활성화 전에 기준선 확립

2개월차: 풀 스택으로 졸업

Langfuse (트레이싱 + 비용)
  + 커스텀 평가 파이프라인 (품질)
  + Grafana/Datadog (인프라)
  + PagerDuty (알림)

LLM 옵저버빌리티 체크리스트

AI 기능을 프로덕션에 배포하기 전 매번:

  • 모든 LLM 호출에 트레이스 컨텍스트 적용
  • 매 호출마다 토큰 수와 모델명 캡처
  • 툴 호출에 입출력 로깅
  • 요청 단위, 사용자 단위 비용 추적 활성화
  • 서킷 브레이커 한도 설정 (토큰, 호출 수, 실행 시간)
  • 결정론적 가드가 100% 트레이스에서 실행 중
  • LLM-as-Judge 평가가 샘플링된 트레이스에서 실행 중
  • 품질 메트릭 기준선 확립
  • 가드 위반과 품질 저하에 알림 설정
  • 전체 프롬프트/응답 로깅에 샘플링 적용 (100% 아님)
  • 프롬프트 로깅 전 PII 스크러빙 적용
  • 대시보드에서 실시간 비용, 품질, 레이턴시 추이 확인
  • 트레이스 보관 정책 정의 (보통 30-90일)

AI 에이전트는 결정론적 소프트웨어가 아니에요. 기존 API처럼 모니터링하면 에이전트가 조용히 폭주하는 그 날까지 거짓 안심만 주죠. 이 가이드에서 다룬 패턴들은 수백만 에이전트 인터랙션을 처리하는 프로덕션 시스템에서 실전 검증된 거예요. 핵심은 간단해요. 추론 과정을 추적 못 하면, 장애도 디버깅 못 해요. 전부 계측하고, 지속적으로 평가하고, 에이전트 출력 품질을 직접 재보기 전까지는 절대 초록불 대시보드를 믿지 마세요.

LLMobservabilityAI agentstracingLangSmithLangfusemonitoringproductiondebuggingMLOps

관련 도구 둘러보기

Pockit의 무료 개발자 도구를 사용해 보세요