프로그래밍 PROGRAMMING/인공지능 AI

RAG 챗봇이 느리고 비싸지는 이유 — 검색, 임베딩, LLM 호출 비용 줄이는 방법

매운할라피뇨 2026. 4. 24. 09:10
반응형

RAG 챗봇이 느리고 비싸지는 이유 — 검색, 임베딩, LLM 호출 비용 줄이는 방법

 
처음 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 챗봇의 비용과 레이턴시를 단계별로 해부하고, 각 단계에서 적용할 수 있는 실무 검증된 최적화 기법을 정리합니다. 막연한 "최적화하세요"가 아니라, 코드와 함께 어디를 어떻게 줄일 수 있는지 구체적으로 다룹니다.
 

목차

  1. RAG 한 답변의 비용 구조 해부
  2. Latency 분석 — 어디가 진짜 병목인가
  3. 임베딩 비용 줄이기 — 캐싱과 배치 처리
  4. LLM Prompt Caching — Anthropic의 90% 할인 마법
  5. Semantic Cache — 비슷한 질문은 답변 재사용
  6. 모델 라우팅 — 쉬운 질문엔 싼 모델
  7. 컨텍스트 압축 — 토큰을 절약하는 기술
  8. 검색 인프라 최적화 — 양자화와 인덱스 튜닝
  9. 스트리밍과 비동기 — 체감 속도 개선
  10. 통합 최적화 전략 — 비용 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 패턴)은 똑같이 반복되는 경우가 많습니다.

320x100
실측: 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 호출 — 각 단계에 다른 최적화 기법이 적용됩니다.
핵심 정리:

  1. 비용의 88%는 LLM — 그곳부터 최적화
  2. Prompt Caching이 가장 강력한 무기 — 쉽고 효과 큼
  3. 모델 라우팅으로 80% 절감 가능 — 모든 질문에 GPT-4o 필요 없음
  4. 체감 속도는 TTFT가 결정 — 스트리밍은 무조건
  5. 측정 없이 최적화 없음 — 추측 말고 데이터로

오늘 글의 모든 기법을 적용하면, 비용은 80% 이상 절감되고 레이턴시는 절반 이하로 줄일 수 있습니다. 그것도 답변 품질을 거의 유지하면서 말이죠.
작은 것부터 시작하세요. Prompt Caching 한 줄스트리밍 한 줄만 추가해도 즉시 효과가 보입니다.

반응형