
처음 RAG 챗봇을 만들 때는 모든 게 싸 보입니다. OpenAI 임베딩 1만 토큰에 $0.0002, GPT-4o-mini 답변 한 번에 $0.001. "이 정도면 무한히 써도 되겠는데?" 그런데 사용자가 100명, 1,000명, 10,000명으로 늘어나면 다른 세상이 펼쳐집니다.
월 청구서를 보고 깜짝 놀랍니다. OpenAI에서 $5,000, Pinecone에서 $800, Cohere Rerank에서 $300. 한 답변이 평균 8초씩 걸려서 사용자 이탈도 늘어납니다. 어디서부터 손을 대야 할까요?
이 글은 RAG 챗봇의 비용과 레이턴시를 단계별로 해부하고, 각 단계에서 적용할 수 있는 실무 검증된 최적화 기법을 정리합니다. 막연한 "최적화하세요"가 아니라, 코드와 함께 어디를 어떻게 줄일 수 있는지 구체적으로 다룹니다.
목차
- RAG 한 답변의 비용 구조 해부
- Latency 분석 — 어디가 진짜 병목인가
- 임베딩 비용 줄이기 — 캐싱과 배치 처리
- LLM Prompt Caching — Anthropic의 90% 할인 마법
- Semantic Cache — 비슷한 질문은 답변 재사용
- 모델 라우팅 — 쉬운 질문엔 싼 모델
- 컨텍스트 압축 — 토큰을 절약하는 기술
- 검색 인프라 최적화 — 양자화와 인덱스 튜닝
- 스트리밍과 비동기 — 체감 속도 개선
- 통합 최적화 전략 — 비용 80% 절감 시나리오
1. RAG 한 답변의 비용 구조 해부
먼저 RAG 한 번의 답변에 드는 비용을 세분화해 봅시다. 무엇이 비싼지 알아야 어디를 줄일지 보입니다.
┌────────────────────────────────────────────────────────┐
│ RAG 한 답변의 비용 구조 │
│ │
│ Step 1: 질문 임베딩 │
│ - OpenAI text-embedding-3-small │
│ - 평균 50 토큰 → $0.000001 │
│ │
│ Step 2: 벡터 검색 (Qdrant/Pinecone) │
│ - Pinecone Serverless: 검색당 $0.0000125 │
│ - Self-hosted Qdrant: $0 (운영비 별도) │
│ │
│ Step 3: Rerank (선택적) │
│ - Cohere Rerank 3.5: 검색당 $0.002 │
│ - 자체 호스팅: GPU 비용 │
│ │
│ Step 4: LLM 생성 │
│ - GPT-4o: 입력 $2.5/1M + 출력 $10/1M │
│ - 평균 RAG 호출: │
│ Input 4,000 토큰 (질문 + 5청크) │
│ Output 500 토큰 │
│ - 비용: $0.01 + $0.005 = $0.015 │
│ │
│ 총합: 약 $0.018/답변 │
└────────────────────────────────────────────────────────┘
답변 1만 건당 비용
임베딩: $0.01
벡터 검색: $0.125
Rerank: $20
LLM: $150
→ 1만 답변 = $170
→ 100만 답변 = $17,000
비용의 95%는 LLM
위 숫자에서 분명한 사실 하나 — 비용의 90% 이상이 LLM 호출입니다. 그래서 최적화의 첫 번째 우선순위는 LLM 비용입니다. 하지만 다른 부분도 무시할 수 없습니다.
┌──────────────────────────────────────────────────────┐
│ 비용 비중 (GPT-4o RAG 기준) │
│ │
│ LLM 호출: 88% ████████████████████████░░ │
│ Rerank (Cohere): 12% ████░ │
│ Vector 검색: <1% ░ │
│ 임베딩: <1% ░ │
│ │
│ → LLM 50% 줄이면 전체의 44% 절감 │
│ → 임베딩 100% 줄여도 1% 미만 │
└──────────────────────────────────────────────────────┘
이 분포가 GPT-4o-mini, Claude Haiku 같은 저렴한 모델로 바뀌면 비중이 달라집니다.
GPT-4o-mini 사용 시:
- LLM: $0.0007/답변
- Rerank: $0.002/답변
- → Rerank가 LLM보다 비싸짐!
- → 이때는 자체 호스팅 BGE Reranker가 유리
2. Latency 분석 — 어디가 진짜 병목인가
비용만큼 중요한 것이 레이턴시입니다. 사용자는 8초짜리 답변을 기다리지 못합니다. 어디가 느린지 측정해보면 의외의 결과가 나옵니다.
┌────────────────────────────────────────────────────────┐
│ RAG 답변 레이턴시 분해 (실측 평균) │
│ │
│ Step 시간 비중 │
│ ──────────── ───── ──── │
│ 질문 임베딩 80ms 1% │
│ 벡터 검색 50ms 1% │
│ Rerank (Cohere) 400ms 6% │
│ LLM 첫 토큰 1,200ms 18% │
│ LLM 전체 생성 5,000ms 74% │
│ ──────────── │
│ 총합 6,730ms 100% │
│ │
│ → LLM이 92% 차지 │
│ → 검색 최적화는 사용자가 거의 못 느낌 │
│ → 진짜 병목은 LLM의 토큰 생성 속도 │
└────────────────────────────────────────────────────────┘
TTFT vs TPOT — 두 가지 지표
LLM 레이턴시는 두 부분으로 나뉩니다.
TTFT (Time To First Token)
- 요청 전송 ~ 첫 토큰 도착까지
- 보통 500ms ~ 2,000ms
- "사용자가 답변이 시작됨을 보는 순간"
TPOT (Time Per Output Token)
- 각 토큰을 생성하는 속도
- 보통 20~50 ms/token
- 답변이 길수록 누적
총 시간 = TTFT + (TPOT × 출력 토큰 수)
예: TTFT 1,200ms, TPOT 30ms, 출력 200토큰
= 1,200 + 30 × 200 = 7,200ms
모델별 속도 비교
┌──────────────────────────────────────────────────────┐
│ 주요 모델 속도 (2025 기준, 평균) │
│ │
│ 모델 TTFT TPOT 속도 │
│ ────────────── ────── ────── ────── │
│ GPT-4o 800ms 25ms 빠름 │
│ GPT-4o mini 400ms 20ms 매우 빠름 │
│ Claude Sonnet 4 600ms 30ms 빠름 │
│ Claude Haiku 4 300ms 15ms 매우 빠름 │
│ Claude Opus 4 1,500ms 50ms 느림 │
│ Gemini 2 Flash 300ms 18ms 매우 빠름 │
│ │
│ → 같은 답변 길이여도 모델별 2~5배 차이 │
└──────────────────────────────────────────────────────┘
레이턴시 개선의 첫 단계는 모델 선택입니다. GPT-4o로 답변하던 것을 GPT-4o-mini로 바꾸면 레이턴시는 절반, 비용은 1/15로 줄어듭니다. 단, 품질도 따져봐야 합니다.
3. 임베딩 비용 줄이기 — 캐싱과 배치 처리
임베딩 비용은 작지만 줄일 수 있는 명확한 방법이 있습니다.
임베딩 캐싱
같은 질문/문서는 한 번만 임베딩하고 결과를 저장합니다.
import hashlib
import json
import redis
from openai import OpenAI
openai = OpenAI()
cache = redis.Redis(host='localhost', port=6379, decode_responses=True)
def embed_with_cache(text: str, model: str = "text-embedding-3-small") -> list[float]:
"""임베딩을 Redis에 캐싱"""
# 캐시 키: 모델 + 텍스트의 해시
cache_key = f"emb:{model}:{hashlib.md5(text.encode()).hexdigest()}"
cached = cache.get(cache_key)
if cached:
return json.loads(cached)
# 캐시 미스 → API 호출
embedding = openai.embeddings.create(
model=model,
input=text
).data[0].embedding
# 캐시 저장 (1주일 TTL)
cache.setex(cache_key, 7 * 86400, json.dumps(embedding))
return embedding
사용자 질문 캐싱의 효과
자주 묻는 질문(FAQ 패턴)은 똑같이 반복되는 경우가 많습니다.
실측: 1주일간 사용자 질문 분석
- 고유 질문: 8,000개
- 전체 질문: 25,000회
- 중복률: 68%
→ 캐싱으로 임베딩 호출 68% 감소
→ 임베딩 비용 68% 절감 + 80ms 레이턴시 절약
배치 임베딩 — 인덱싱 시 비용 절감
OpenAI Batch API를 쓰면 임베딩 비용이 50% 할인됩니다. 단, 24시간 이내 처리 보장(즉시 응답 X).
# 인덱싱 작업처럼 시간 여유가 있는 경우
# Batch API 사용
batch_request = {
"input": chunks, # 한 번에 수만 개도 가능
"model": "text-embedding-3-small",
"encoding_format": "float"
}
# Batch API 비용: 일반 API의 50%
더 작은 임베딩 모델 사용
text-embedding-3-large (3,072차원): $0.13/1M
text-embedding-3-small (1,536차원): $0.02/1M (6배 저렴)
text-embedding-ada-002 (구형, 1,536): $0.10/1M
→ small이 large보다 6배 저렴
→ 일반 RAG에서는 품질 차이 미미
차원 축소 옵션 (text-embedding-3)
text-embedding-3 모델은 출력 차원을 줄일 수 있습니다.
# 1,536 → 512 차원으로 줄여서 저장
embedding = openai.embeddings.create(
model="text-embedding-3-small",
input=text,
dimensions=512 # 기본 1,536 → 512
).data[0].embedding
# 이점:
# - 벡터 DB 저장 공간 1/3
# - 검색 속도 3배
# - 품질 손실 5% 이내
4. LLM Prompt Caching — Anthropic의 90% 할인 마법
LLM 비용 절감의 가장 강력한 무기는 Prompt Caching입니다.
Prompt Caching이란
같은 프롬프트의 앞부분을 반복해서 보낼 때, 서버가 그 부분을 캐싱하여 비용을 줄여주는 기능입니다.
일반 호출:
[시스템 프롬프트 2000토큰] + [사용자 질문 100토큰]
- 매번 2,100토큰 입력 비용
Prompt Caching:
[시스템 프롬프트 2000토큰 - 캐시] + [사용자 질문 100토큰]
- 첫 호출: 2,100토큰 (캐시 생성)
- 이후 호출: 캐시된 부분은 10% 비용
Anthropic Prompt Caching 비용
Claude Sonnet 4:
- 일반 입력: $3/1M tokens
- 캐시 쓰기: $3.75/1M (25% 비싸짐)
- 캐시 읽기: $0.30/1M (90% 할인)
→ 같은 프롬프트를 4번 이상 반복하면 절감
→ RAG 챗봇은 보통 시스템 프롬프트가 일정 → 큰 절감
RAG에서의 활용
RAG는 항상 같은 시스템 프롬프트를 사용합니다. 이걸 캐싱하면 큰 절감입니다.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=[
{
"type": "text",
"text": "당신은 회사 내부 문서를 기반으로 답변하는 AI 어시스턴트입니다.",
},
{
"type": "text",
"text": LARGE_DOCUMENT_CONTEXT, # 큰 컨텍스트 (예: 매뉴얼 전체)
"cache_control": {"type": "ephemeral"} # ← 이 부분 캐싱
}
],
messages=[
{"role": "user", "content": user_question}
]
)
효과 계산
시나리오: 사내 매뉴얼 50,000토큰을 컨텍스트로
사용자 질문 평균 100토큰, 답변 500토큰
Prompt Caching 없이:
- 매번 50,100 input + 500 output
- 비용 (Claude Sonnet 4): $0.158/답변
Prompt Caching 적용:
- 첫 호출: $0.190 (캐시 쓰기)
- 이후 호출: 50,000 캐시 읽기 + 100 일반 입력 + 500 출력
= $0.015 + $0.0003 + $0.0075 = $0.023/답변
→ 7배 절감 (87% 비용 감소)
OpenAI의 자동 Prompt Caching
OpenAI도 2024년 10월부터 자동 캐싱을 지원합니다 (별도 설정 불필요).
- 1024토큰 이상의 프롬프트 자동 캐싱
- 캐시 히트 시 50% 할인 (입력만)
- 5~10분 TTL (자동 갱신)
→ Anthropic만큼 강력하진 않지만 자동
→ 별도 코드 변경 없이 효과
5. Semantic Cache — 비슷한 질문은 답변 재사용
같은 질문이 아니라 비슷한 질문도 캐시할 수 있습니다.
기본 아이디어
질문 A: "환불 절차가 어떻게 되나요?"
질문 B: "환불 어떻게 신청해요?"
질문 C: "구매한 거 돌려받으려면?"
→ 셋 다 같은 답변
→ A의 답변을 만든 후, B와 C는 캐시 활용
Semantic Cache 구현
from openai import OpenAI
import numpy as np
import json
import redis
openai = OpenAI()
cache = redis.Redis(host='localhost', port=6379, decode_responses=True)
class SemanticCache:
def __init__(self, similarity_threshold: float = 0.95):
self.threshold = similarity_threshold
def _embed(self, text: str) -> list[float]:
return openai.embeddings.create(
model="text-embedding-3-small",
input=text
).data[0].embedding
def _cosine(self, a, b):
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def get(self, query: str) -> str | None:
"""비슷한 질문이 캐시에 있으면 답변 반환"""
query_emb = self._embed(query)
# 모든 캐시 키 검사 (실무에선 벡터 DB 활용)
for key in cache.scan_iter("semcache:*"):
cached = json.loads(cache.get(key))
cached_emb = cached["embedding"]
sim = self._cosine(query_emb, cached_emb)
if sim >= self.threshold:
print(f"[Cache HIT] sim={sim:.4f}")
return cached["answer"]
return None
def put(self, query: str, answer: str, ttl: int = 86400):
"""질문-답변 쌍을 캐시에 저장"""
query_emb = self._embed(query)
key = f"semcache:{hash(query)}"
cache.setex(key, ttl, json.dumps({
"query": query,
"answer": answer,
"embedding": query_emb,
}))
사용
sem_cache = SemanticCache(similarity_threshold=0.95)
def rag_answer(query: str) -> str:
# 1. 캐시 확인
cached = sem_cache.get(query)
if cached:
return cached
# 2. 캐시 미스 → 정상 RAG
docs = retrieve(query)
answer = generate_with_llm(query, docs)
# 3. 캐시 저장
sem_cache.put(query, answer)
return answer
Threshold 튜닝
┌──────────────────────────────────────────────────────┐
│ Similarity Threshold 설정 가이드 │
│ │
│ 0.99 매우 엄격 │
│ - 거의 같은 질문만 캐시 히트 │
│ - 안전, Hit Rate 낮음 (5~10%) │
│ │
│ 0.95 일반적 │
│ - 의미가 매우 유사한 질문만 │
│ - Hit Rate 20~30% │
│ │
│ 0.90 관대 │
│ - 비슷한 주제 질문 │
│ - Hit Rate 40~50% │
│ - 잘못된 캐시 히트 위험 │
│ │
│ 0.85 이하 위험 │
│ - 다른 의도 질문이 같은 답변 받을 위험 │
└──────────────────────────────────────────────────────┘
전용 솔루션 — GPTCache, Redis VL
직접 구현 대신 전용 라이브러리를 쓸 수 있습니다.
# GPTCache 예시
from gptcache import cache
from gptcache.adapter import openai
from gptcache.embedding import Onnx
from gptcache.manager import CacheBase, VectorBase, get_data_manager
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation
onnx = Onnx()
data_manager = get_data_manager(
CacheBase("sqlite"),
VectorBase("faiss", dimension=onnx.dimension),
)
cache.init(
embedding_func=onnx.to_embeddings,
data_manager=data_manager,
similarity_evaluation=SearchDistanceEvaluation(),
)
cache.set_openai_key()
# 이제 openai.ChatCompletion.create() 사용 시 자동 캐싱
6. 모델 라우팅 — 쉬운 질문엔 싼 모델
모든 질문에 GPT-4o가 필요한 건 아닙니다. 80%의 질문은 GPT-4o-mini로 충분합니다.
Router 패턴
from openai import OpenAI
client = OpenAI()
def classify_complexity(query: str) -> str:
"""질문의 복잡도를 판단 (Haiku 또는 mini로 빠르게)"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """질문의 복잡도를 분류하세요:
- "simple": 단순 정보 검색, FAQ형 질문
- "complex": 다단계 추론, 비교, 분석이 필요한 질문
JSON 형식으로 응답: {"complexity": "simple"} or {"complexity": "complex"}"""},
{"role": "user", "content": query}
],
temperature=0,
response_format={"type": "json_object"}
)
import json
return json.loads(response.choices[0].message.content)["complexity"]
def route_and_answer(query: str, context: str) -> str:
"""복잡도에 따라 다른 모델 사용"""
complexity = classify_complexity(query)
if complexity == "simple":
model = "gpt-4o-mini"
else:
model = "gpt-4o"
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "다음 컨텍스트를 기반으로 답변하세요."},
{"role": "user", "content": f"[컨텍스트]\n{context}\n\n[질문]\n{query}"}
]
)
return response.choices[0].message.content
Router의 효과
시나리오: 1만 질문 처리
Without Router (전부 GPT-4o):
- 1만 × $0.015 = $150
With Router (80% mini, 20% 4o):
- 8,000 × $0.001 = $8
- 2,000 × $0.015 = $30
- Router 비용: 1만 × $0.0001 = $1
- 총: $39
→ 비용 74% 절감
더 정교한 라우팅
┌──────────────────────────────────────────────────────┐
│ 3단계 모델 라우팅 │
│ │
│ Tier 1: GPT-4o mini, Claude Haiku │
│ - 단순 정보 조회 │
│ - FAQ │
│ - 정형화된 답변 │
│ → 60~70% 질문 │
│ │
│ Tier 2: GPT-4o, Claude Sonnet │
│ - 일반적 분석 │
│ - 다단계 추론 시작 │
│ → 25~35% 질문 │
│ │
│ Tier 3: GPT-4 / Claude Opus / o1 │
│ - 복잡한 추론 │
│ - 코드/수학 │
│ - 정확성이 결정적인 답변 │
│ → 5~10% 질문 │
│ │
│ → 평균 비용 80% 절감 │
└──────────────────────────────────────────────────────┘
7. 컨텍스트 압축 — 토큰을 절약하는 기술
LLM 비용은 입력 토큰 × 출력 토큰입니다. RAG에서는 입력이 매우 깁니다 (검색된 청크들). 이를 줄이는 것이 비용 절감의 핵심입니다.
청크 압축 (Contextual Compression)
검색된 청크에서 질문에 정말 관련 있는 부분만 추출합니다.
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
# 사용
docs = compression_retriever.invoke("환불 절차는?")
# 각 청크에서 "환불 절차"와 직접 관련된 부분만 추출됨
LLMLingua — 프롬프트 압축
Microsoft의 LLMLingua는 프롬프트 자체를 토큰 단위로 압축합니다.
from llmlingua import PromptCompressor
compressor = PromptCompressor()
compressed_prompt = compressor.compress_prompt(
long_context,
instruction="다음 문서를 기반으로 답하세요",
question=user_query,
target_token=500 # 목표 토큰 수
)
# 원래 5,000 → 500 토큰으로 압축 (10배)
# 정보 손실은 최소화됨
청크 크기 최적화
큰 청크 5개 vs 작은 청크 10개. 어느 게 효율적일까요?
시나리오 A: 1,000토큰 × 5청크 = 5,000토큰 컨텍스트
시나리오 B: 500토큰 × 5청크 = 2,500토큰 컨텍스트
GPT-4o 비용 차이: $0.0125 vs $0.00625
→ B가 절반 비용
→ 단, B는 컨텍스트 부족할 수 있음
균형점: 사용 사례에 따라 600~800토큰 청크 권장
Top-K 줄이기
검색 결과 Top-10을 LLM에 주는 대신 Top-3만 주면 토큰 70% 감소합니다.
Top-10: 10 × 800토큰 = 8,000토큰
Top-3: 3 × 800토큰 = 2,400토큰
→ Reranker로 Top-3 정확도 보장하면 안전하게 줄일 수 있음
8. 검색 인프라 최적화 — 양자화와 인덱스 튜닝
검색 자체의 비용/속도도 최적화 여지가 있습니다.
벡터 양자화 (Quantization)
벡터 크기를 줄여서 저장 공간과 검색 속도를 향상.
원본 (float32): 1,536 × 4바이트 = 6,144바이트/벡터
Scalar (int8): 1,536 × 1바이트 = 1,536바이트 (75% 절감)
Binary (1-bit): 1,536 / 8 = 192바이트 (97% 절감)
# Qdrant Scalar Quantization
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig
client.update_collection(
collection_name="docs",
quantization_config=ScalarQuantization(
scalar=ScalarQuantizationConfig(
type="int8",
quantile=0.99,
always_ram=True
)
)
)
# 효과:
# - 메모리 사용 75% 감소
# - 검색 속도 2~4배 빨라짐
# - 정확도 손실 < 2%
HNSW 파라미터 튜닝
# m을 줄여서 메모리/속도 향상
client.update_collection(
collection_name="docs",
hnsw_config={
"m": 8, # 기본 16 → 8 (메모리 50% 감소)
"ef_construct": 64,
}
)
# 검색 시 ef 줄이기
results = client.search(
collection_name="docs",
query_vector=q,
limit=10,
search_params={"hnsw_ef": 64} # 기본 128 → 64 (속도 2배)
)
자체 호스팅으로 전환
Pinecone, Weaviate Cloud 등 관리형은 편하지만 비쌉니다.
Pinecone Serverless (1M vectors, 1536-dim):
- 저장: $0.02/GB/월 = ~$120/월
- 쿼리: $0.0125/1000 queries
- 월 100만 쿼리: $12.5
- 합계: ~$135/월
Self-hosted Qdrant on AWS:
- t3.large 인스턴스: $60/월
- 동일 데이터 처리 가능
- 합계: $60/월 (절반 이하)
규모가 클수록 자체 호스팅이 압도적으로 저렴합니다.
9. 스트리밍과 비동기 — 체감 속도 개선
실제 속도가 빠르지 않아도, 체감 속도는 개선할 수 있습니다.
스트리밍
LLM 답변을 토큰 단위로 사용자에게 보여줍니다. 답변 전체가 완성되기 전에 첫 단어가 나옵니다.
# OpenAI 스트리밍
from openai import OpenAI
client = OpenAI()
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "..."}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
효과 — TTFT가 모든 것
사용자가 느끼는 속도는 TTFT(Time To First Token)입니다. 답변 전체 시간이 8초여도, TTFT가 0.8초면 "빠르다"고 느낍니다.
없는 스트리밍:
사용자 입장: ............8초.....답변
"왜 이렇게 느려?"
있는 스트리밍:
사용자 입장: ..0.8초.."환불 절차는..." (답변이 흘러나옴)
"실시간으로 답하네!"
비동기 검색 — 검색과 다른 작업 병렬화
import asyncio
async def parallel_retrieve_and_classify(query: str):
"""검색과 의도 분류를 동시에"""
retrieval_task = asyncio.create_task(retrieve_async(query))
classify_task = asyncio.create_task(classify_intent(query))
docs, intent = await asyncio.gather(retrieval_task, classify_task)
return docs, intent
Background Reranker
레이턴시가 가장 큰 Reranker를 백그라운드로 보낼 수 있습니다 (Top-K 결과를 보여준 후 순위 조정).
# 1단계: 빠른 결과 우선 표시
fast_results = vector_search(query, k=20)
return_to_user_streaming(fast_results[:5]) # 일단 보여줌
# 2단계: 백그라운드에서 Rerank
async def background_rerank():
reranked = await cohere_rerank(query, fast_results)
cache_for_next_query(reranked)
10. 통합 최적화 전략 — 비용 80% 절감 시나리오
지금까지의 기법을 모두 적용한 통합 시나리오를 봅시다.
Before — 단순 RAG
구성:
- text-embedding-3-large 임베딩
- Pinecone Serverless 벡터 DB
- Cohere Rerank (Top-20 → Top-5)
- GPT-4 답변
사용량: 월 100만 쿼리
비용 (월):
- 임베딩: $1,500 (캐싱 없음)
- 벡터 검색: $1,250 (Pinecone)
- Rerank: $2,000 (Cohere)
- LLM (GPT-4): $30,000
- 합계: $34,750/월
레이턴시: 평균 8초
After — 최적화 적용
적용한 것:
✅ 임베딩 캐싱 (Redis, 70% 히트율)
✅ 임베딩 모델 small + 차원 축소 (1536 → 512)
✅ Self-hosted Qdrant (AWS t3.xlarge)
✅ BGE-Reranker 자체 호스팅 (RTX 4090)
✅ Anthropic Prompt Caching (시스템 프롬프트)
✅ Semantic Cache (Hit rate 30%)
✅ 모델 라우팅 (75% Sonnet, 25% Opus)
✅ 컨텍스트 압축 (LLMLingua)
✅ 스트리밍 적용
✅ Top-K 5 → 3
비용 (월):
- 임베딩: $30 (90% 캐싱)
- 벡터 검색: $200 (자체 호스팅 인프라)
- Rerank: $300 (GPU 비용)
- LLM (Claude Sonnet 4 + Caching): $5,500
- 합계: $6,030/월
레이턴시: 평균 3.5초 (TTFT 0.6초 - 체감 매우 빠름)
→ 비용 83% 절감 ($34,750 → $6,030)
→ 레이턴시 56% 단축
→ 사용자 만족도 향상
우선순위 가이드
모든 최적화를 한 번에 할 필요 없습니다. 효과 큰 것부터:
┌──────────────────────────────────────────────────────────┐
│ RAG 최적화 우선순위 (효과 vs 노력) │
│ │
│ ❶ Anthropic Prompt Caching (또는 OpenAI 자동 캐싱) │
│ 효과: ⭐⭐⭐⭐⭐ 노력: ⭐ → 즉시 도입 │
│ │
│ ❷ 모델 다운그레이드 (4o → 4o-mini) │
│ 효과: ⭐⭐⭐⭐⭐ 노력: ⭐ → 품질 비교 후 적용 │
│ │
│ ❸ 스트리밍 활성화 │
│ 효과: ⭐⭐⭐⭐ (체감) 노력: ⭐ → 무조건 적용 │
│ │
│ ❹ Semantic Cache │
│ 효과: ⭐⭐⭐⭐ 노력: ⭐⭐ │
│ │
│ ❺ Top-K 줄이기 (10 → 5 → 3) │
│ 효과: ⭐⭐⭐⭐ 노력: ⭐ │
│ │
│ ❻ 모델 라우팅 │
│ 효과: ⭐⭐⭐⭐ 노력: ⭐⭐⭐ │
│ │
│ ❼ 임베딩 캐싱 │
│ 효과: ⭐⭐ 노력: ⭐⭐ │
│ │
│ ❽ 자체 호스팅 전환 │
│ 효과: ⭐⭐⭐⭐⭐ (큰 규모) 노력: ⭐⭐⭐⭐ │
└──────────────────────────────────────────────────────────┘
측정 — 안 하면 의미 없다
모든 최적화의 첫 단계는 현재 측정입니다.
import time
class RAGMetrics:
def __init__(self):
self.events = []
def measure(self, name):
return MetricContext(self, name)
def report(self):
for event in self.events:
print(f"{event['name']}: {event['duration_ms']}ms, ${event['cost']:.5f}")
class MetricContext:
def __init__(self, metrics, name):
self.metrics = metrics
self.name = name
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
duration = (time.time() - self.start) * 1000
self.metrics.events.append({
"name": self.name,
"duration_ms": duration,
})
# 사용
metrics = RAGMetrics()
with metrics.measure("embed"):
q_emb = embed(query)
with metrics.measure("search"):
docs = search(q_emb)
with metrics.measure("rerank"):
reranked = rerank(query, docs)
with metrics.measure("llm"):
answer = llm.generate(query, reranked)
metrics.report()
마무리 — 최적화는 측정으로 시작한다
RAG 챗봇의 비용과 레이턴시는 단순한 문제가 아닙니다. 임베딩, 검색, Rerank, LLM 호출 — 각 단계에 다른 최적화 기법이 적용됩니다.
핵심 정리:
- 비용의 88%는 LLM — 그곳부터 최적화
- Prompt Caching이 가장 강력한 무기 — 쉽고 효과 큼
- 모델 라우팅으로 80% 절감 가능 — 모든 질문에 GPT-4o 필요 없음
- 체감 속도는 TTFT가 결정 — 스트리밍은 무조건
- 측정 없이 최적화 없음 — 추측 말고 데이터로
오늘 글의 모든 기법을 적용하면, 비용은 80% 이상 절감되고 레이턴시는 절반 이하로 줄일 수 있습니다. 그것도 답변 품질을 거의 유지하면서 말이죠.
작은 것부터 시작하세요. Prompt Caching 한 줄과 스트리밍 한 줄만 추가해도 즉시 효과가 보입니다.
'프로그래밍 PROGRAMMING > 인공지능 AI' 카테고리의 다른 글
| RRF(Reciprocal Rank Fusion)란 무엇인가 - 가장 우아한 검색 결과 결합방식 (0) | 2026.04.27 |
|---|---|
| BM25란 무엇인가 — 30년 묵은 알고리즘이 여전히 강한 이유 (1) | 2026.04.26 |
| 왜 PDF를 넣으면 답변 품질이 떨어질까 — 표, 이미지, 페이지 구조를 AI가 못 읽는 문제 (1) | 2026.04.23 |
| Claude Design 시작하기: 말로 만들고, 대화로 다듬는 새로운 디자인 방식 (1) | 2026.04.22 |
| 출처가 있어도 답변이 틀리는 이유 — Citation이 신뢰를 보장하지 않는 순간들 (0) | 2026.04.21 |