LLM API 비용은 일반 서버 비용과 달리 예측하기 어렵고 순식간에 폭발합니다. 비용을 통제하려면 어디서 토큰이 낭비되는지 보여야 하고, 그것이 AI Observability입니다 — AI 버전의 APM(Application Performance Monitoring).
@Service
public class SemanticCacheService {
private final EmbeddingService embedding;
private final VectorCacheRepo cache; // pgvector
private static final float THRESHOLD = 0.92f;
public Optional<String> get(String query) {
float[] vec = embedding.embed(query);
// 유사도 0.92 이상인 캐시 조회
return cache.findSimilar(vec, THRESHOLD)
.map(CacheEntry::getResponse);
}
public void put(String query, String response) {
float[] vec = embedding.embed(query);
cache.save(new CacheEntry(query, vec, response,
Instant.now().plus(Duration.ofHours(24))));
}
}
// 사용
public String chat(String userMessage) {
// 1. 캐시 먼저 확인
var cached = semanticCache.get(userMessage);
if (cached.isPresent()) {
metrics.recordCacheHit(); // 비용 0
return cached.get();
}
// 2. 캐시 미스 → LLM 호출
String response = llm.call(userMessage);
semanticCache.put(userMessage, response);
return response;
}
@Service
public class ModelRouter {
public String route(String prompt) {
int complexity = assessComplexity(prompt);
return switch (complexity) {
case 1 -> "claude-haiku-4-5"; // $0.25/1M
case 2 -> "claude-sonnet-4-6"; // $3/1M
default -> "claude-opus-4-8"; // $15/1M
};
}
private int assessComplexity(String prompt) {
// 간단한 휴리스틱
boolean isSimple =
prompt.length() < 100 &&
!prompt.contains("코드") &&
!prompt.contains("분석");
boolean isComplex =
prompt.length() > 500 ||
prompt.contains("아키텍처") ||
prompt.contains("설계");
if (isSimple) return 1;
if (isComplex) return 3;
return 2;
}
}
| 측정 항목 | 왜 중요한가 | 이상 신호 |
|---|---|---|
| 토큰 사용량 / 비용 | 어느 엔드포인트가 비용을 많이 쓰는지 파악 | 특정 사용자가 일반의 10배 이상 사용 |
| 레이턴시 (Time to First Token) | 사용자 체감 응답 속도 | p99 > 10초 → 모델 변경 또는 캐싱 검토 |
| Agent 스텝 수 | 루프 가능성 탐지 | 평균 3스텝인데 특정 요청이 8스텝 이상 |
| Tool 호출 성공률 | 어느 Tool이 자주 오류 나는지 파악 | 특정 Tool 오류율 > 5% |
| Cache Hit Rate | Semantic Cache 효과 측정 | Hit Rate < 20% → 쿼리 다양성 높음, 캐시 TTL 조정 |
// Langfuse: LLM Observability 오픈소스 툴
// 각 LLM 호출을 Trace로 기록
Langfuse langfuse = new Langfuse(
System.getenv("LANGFUSE_PUBLIC_KEY"),
System.getenv("LANGFUSE_SECRET_KEY"));
public String callLLM(String userId, String prompt) {
// Trace 시작
Trace trace = langfuse.trace(
TraceParams.builder()
.name("chat-completion")
.userId(userId)
.build());
Generation gen = trace.generation(
GenerationParams.builder()
.name("llm-call")
.model("claude-sonnet-4-6")
.input(prompt)
.build());
String response = llm.call(prompt);
// 토큰 사용량 기록
gen.end(GenerationEndParams.builder()
.output(response)
.usage(Usage.of(inputTokens, outputTokens))
.build());
return response;
}
// 일일 비용 초과 알람
@Scheduled(cron = "0 * * * * *") // 매분 체크
public void checkDailyCost() {
double todayCost = metricsRepo
.getDailyCost(LocalDate.now());
// 일일 예산 80% 도달 시 슬랙 알람
if (todayCost > DAILY_BUDGET * 0.8) {
slack.send(
"#ai-ops",
String.format(
"⚠️ AI 일일 비용 경고: $%.2f / $%.2f (%.0f%%)",
todayCost, DAILY_BUDGET,
todayCost / DAILY_BUDGET * 100
)
);
}
// 예산 초과 시 Circuit Breaker OPEN
if (todayCost >= DAILY_BUDGET) {
circuitBreaker.open();
slack.send("#ai-ops", "🚨 일일 AI 예산 초과. 서비스 일시 중단.");
}
}
두 단계로 나눕니다. 모니터링: Langfuse 같은 AI Observability 툴로 토큰 사용량·비용·레이턴시·Agent 스텝 수를 추적합니다. Grafana 대시보드에 일별 비용 그래프를 올리고, 예산 80%에서 슬랙 알람을 겁니다. 통제: Semantic Cache(40% 절감), Prompt Cache(25%), Model Routing(간단 쿼리는 Haiku), max_tokens 제한을 계층화합니다. 예산 초과 시 Circuit Breaker로 LLM 호출을 차단합니다.
트레이드오프 문제입니다. 임계값이 낮으면(0.80): 더 많은 질문이 캐시 히트 → 비용 절감 크지만 다른 의미의 질문에 엉뚱한 답변 반환 위험. 임계값이 높으면(0.98): 거의 동일한 질문만 히트 → 안전하지만 절감 효과 작음. 실무에서는 0.90~0.93 사이에서 시작해서 A/B 테스트로 조정합니다. 질문 유형에 따라 다르게 설정하기도 합니다 — FAQ처럼 정답이 정해진 경우는 낮게, 복잡한 분석 쿼리는 높게.
비용 최적화 = Semantic Cache → Prompt Cache → Model Routing → Output Control 순서로 적용. Observability 없이는 어디서 돈이 새는지 모른다 — Langfuse로 토큰·레이턴시·스텝 수를 추적하라.