AI API 비용 60% 절감: 시맨틱 캐싱으로 중복 질문 잡는 실전 방법
AI API 비용 60% 절감: 시맨틱 캐싱으로 중복 질문 잡는 실전 방법
AI 서비스를 운영하다 보면 어느 순간 API 비용이 폭발한다. 사용자는 느는데 비용은 더 빨리 늘고, 레이턴시도 점점 길어진다. 원인의 상당 부분은 같은 질문을 반복해서 LLM에 보내고 있다는 데 있다.
업계 사례를 종합하면, 고객 지원 챗봇에서 25~45%의 쿼리가 의미상(semantically) 중복이다. “프랑스 수도가 뭐야?”와 “프랑스의 수도는?”은 다른 문자열이지만 같은 질문이다. 이걸 캐싱하면 비용을 20~60% 줄일 수 있다.
프로바이더 프롬프트 캐싱 vs 시맨틱 캐싱 — 둘 다 써야 한다
먼저 헷갈리기 쉬운 두 가지를 구분하자.
프로바이더 프롬프트 캐싱은 OpenAI, Anthropic이 제공하는 기능이다. 시스템 프롬프트처럼 긴 접두사가 반복될 때, 입력 토큰을 캐시해서 재처리하지 않는다. Anthropic은 90% 할인, OpenAI는 50% 할인을 적용한다.
# Anthropic 프롬프트 캐싱 예시
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = """
당신은 계약서 검토 전문가입니다.
다음 지침에 따라 분석하세요... (3000+ 토큰의 상세 지침)
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2000,
system=[{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}
}],
messages=[{"role": "user", "content": "이 계약서를 분석해줘: ..."}]
)
# 첫 요청: 전체 입력 토큰 과금
# 이후 요청: 캐시된 프롬프트 부분 90% 할인
시맨틱 캐싱은 애플리케이션 레벨에서 동작한다. 사용자 질문을 임베딩으로 변환하고, 벡터 유사도가 임계값 이상인 기존 응답을 반환한다. 질문의 의미가 같으면 캐시 히트다.
둘은 상호 보완적이다. 시스템 프롬프트는 프로바이더 캐싱으로, 사용자 질문은 시맨틱 캐싱으로 처리하면 된다.
시맨틱 캐싱 작동 원리
사용자 질문 → 임베딩 모델 → 벡터 변환
↓
벡터 DB에서 유사도 검색
↓
유사도 ≥ 임계값 → 캐시된 응답 반환 (25~60ms)
유사도 < 임계값 → LLM 호출 후 캐시에 저장 (500~3000ms)
핵심은 임계값 설정이다. 너무 낮으면 관련 없는 질문이 같은 응답을 받고, 너무 높으면 캐시 히트율이 떨어진다. 실전에서는 0.85~0.92 사이가 적당하다.
구현: GPTCache로 5분 만에 적용하기
가장 빠르게 도입할 수 있는 오픈소스 라이브러리는 GPTCache다.
from gptcache import Cache
from gptcache.adapter import openai
from gptcache.embedding import OpenAI as EmbedOpenAI
from gptcache.similarity_evaluation import Cosine
from gptcache.manager import manager_factory
# 캐시 초기화
cache = Cache()
cache.init(
pre_embedding_func=lambda x: x, # 질문 전처리
embedding_func=EmbedOpenAI(), # 임베딩 모델
data_manager=manager_factory("sqlite,faiss",
data_dir="./cache_data"),
similarity_evaluation=Cosine(),
config={"similarity_threshold": 0.85}
)
# OpenAI 호출 대신 캐시 래퍼 사용
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=[{"role": "user", "content": "환불 규정이 어떻게 되나요?"}],
cache_obj=cache # 이 한 줄로 캐싱 활성화
)
# 첫 호출: LLM API 호출 (정상 과금)
# "환불 정책 알려주세요" 같은 유사 질문: 캐시 히트 (API 비용 0원)
구현: Redis + 임베딩으로 커스텀 캐싱
GPTCache보다 더 세밀한 제어가 필요하면 Redis + 임베딩을 직접 조합한다.
import redis
import numpy as np
import json, hashlib, time
from openai import OpenAI
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
client = OpenAI()
SIMILARITY_THRESHOLD = 0.88
def get_embedding(text: str) -> list[float]:
resp = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return resp.data[0].embedding
def cosine_similarity(a: list[float], b: list[float]) -> float:
a_np, b_np = np.array(a), np.array(b)
return np.dot(a_np, b_np) / (np.linalg.norm(a_np) * np.linalg.norm(b_np))
def cached_chat(user_query: str) -> str:
query_emb = get_embedding(user_query)
# Redis에서 모든 캐시 키 조회 (실제로는 HNSW 인덱스 권장)
for key in r.scan_iter("cache:*"):
stored_emb = json.loads(r.hget(key, "embedding"))
sim = cosine_similarity(query_emb, stored_emb)
if sim >= SIMILARITY_THRESHOLD:
# 캐시 히트 — 히트 카운트 증가
r.hincrby(key, "hits", 1)
return r.hget(key, "response")
# 캐시 미스 — LLM 호출
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": user_query}]
).choices[0].message.content
# 캐시에 저장
cache_id = hashlib.md5(user_query.encode()).hexdigest()[:12]
r.hset(f"cache:{cache_id}", mapping={
"query": user_query,
"embedding": json.dumps(query_emb),
"response": response,
"hits": 0,
"created": str(int(time.time()))
})
return response
Redis Stack을 쓰면 FT.SEARCH로 HNSW 벡터 인덱스를 만들어 O(log n)으로 유사도 검색이 가능하다. 캐시가 수만 개 이상이면 필수다.
실전 히트율과 비용 절감 데이터
| 유스케이스 | 히트율 | 비용 절감 | 참고 |
|---|---|---|---|
| 고객 지원 챗봇 | 35~45% | 30~50% | 자주 묻는 질문 패턴이 뚜렷함 |
| 제품 FAQ | 40~55% | 35~60% | 질문 범위가 좁아 히트율 최고 |
| 코드 어시스턴트 | 15~25% | 10~20% | 질문이 다양하지만 일부 반복 존재 |
| 창의적 글쓰기 | 5~15% | 3~10% | 거의 매번 다른 질문 |
100K 쿼리/월 Claude Sonnet 기준 계산:
- 캐시 없음: 입력 50M × $3.00 + 출력 30M × $15.00 = $600/월
- 35% 히트율: 65K 유니크 + 35K 캐시(임베딩 비용만) = $395/월 (34% 절감)
- 55% 히트율(FAQ 서비스): 45K 유니크 + 55K 캐시 = $270/월 (55% 절감)
- 여기에 프롬프트 캐싱까지 결합하면 최대 60~70% 절감 가능
임계값 튜닝: A/B 테스트로 최적값 찾기
임계값은 서비스마다 다르다. 실전에서는 이렇게 접근한다:
- 초기값 0.90에서 시작 — 보수적으로 시작하는 게 안전
- 캐시 히트 로그 수집 — 어떤 질문이 히트되는지, 응답이 실제로 적절한지 1주일 관찰
- 오답률 5% 미만 유지하면서 임계값을 점진적으로 하향
- 도메인별로 다른 임계값 적용 — FAQ는 0.85, 상담은 0.92
# 도메인별 임계값 설정 예
THRESHOLDS = {
"faq": 0.85, # 단답형, 높은 허용
"support": 0.88, # 일반 지원
"billing": 0.92, # 결제 관련은 엄격
"legal": 0.95 # 법률 자문은 거의 캐싱 안 함
}
자주 하는 실수
1. TTL 없이 캐시 무한 누적 시간이 지나면 정보가 바뀐다. “오늘 날씨” 캐시를 영구 저장하면 안 된다. 유스케이스에 따라 1시간~7일 TTL을 설정하자.
2. 임베딩 모델과 LLM을 다르게 쓸 때 차이 무시
임베딩 모델이 한국어에 약하면 한국어 질문의 유사도 판별이 부정확해진다. 다국어 서비스라면 text-embedding-3-large나 Cohere의 multilingual 모델을 쓰자.
3. 캐시 히트율만 보고 판단하기 히트율이 높아도 캐시된 응답이 틀리면 의미 없다. 정확도 메트릭을 병행 측정해야 한다. 샘플링해서 human evaluation을 주기적으로 수행하자.
결론
시맨틱 캐싱은 AI API 비용 최적화에서 가장 과소평가된 기법이다. 구현 난이도에 비해 효과가 압도적이다.
- 5분: GPTCache 한 줄 추가로 시작
- 하루: Redis + 임베딩 커스텀 구현
- 일주일: 임계값 튜닝 + 모니터링 대시보드 구축
핵심은 프로바이더 프롬프트 캐싱과 시맨틱 캐싱을 같이 쓰는 것이다. 시스템 프롬프트는 프로바이더가, 사용자 질문은 시맨틱 캐시가 잡는 구조를 만들면 비용을 50~70%까지 줄일 수 있다.
오늘 당장 해볼 것: 기존 API 호출 로그에서 중복 질문 비율을 확인해보자. 20%만 넘어도 시맨틱 캐싱 도입의 근거가 충분하다.