
프로덕션 RAG의 숨은 영웅: 작은 분류기(Classifier)와 Redis를 통해 비용과 지연 시간(Latency)을 25~28% 절감한 방법
요약
프로덕션 RAG 파이프라인에서 반복되는 질문으로 인한 비용과 지연 시간을 줄이기 위해, 무거운 LLM 대신 가벼운 분류기(Classifier)와 Redis를 활용한 최적화 방법을 소개합니다. 이를 통해 불필요한 임베딩 및 LLM 호출을 방지하여 성능을 개선했습니다.
핵심 포인트
- 반복되는 질문에 대해 매번 전체 RAG 파이프라인을 실행하는 것은 비용과 지연 시간 낭비임
- LLM을 의도 탐지기로 사용하는 것은 라우팅 결정 자체에 과도한 지연 시간을 초래함
- 미세 조정된 작은 분류기(Classifier)를 사용하여 구조화된 의도 파악 수행
- 분류기와 캐싱(Redis)을 결합하여 비용과 지연 시간을 25~28% 절감
우리의 프로덕션 RAG 파이프라인이 느렸던 이유는 임베딩 (Embedding) 때문이 아니었습니다.
벡터 검색 (Vector Search) 때문도 아니었습니다.
우리가 동일한 질문에 수백 번씩 계속해서 답변하고 있었기 때문이었습니다.
이것이 세 줄로 요약한 전체 이야기입니다. 우리가 어떻게 이를 발견했는지, 개선하기 전에 어떻게 상황을 더 악화시켰는지, 그리고 솔직히 말해 조금은 지루하지만 바로 그 점 때문에 효과적이었던 해결책에 도달하게 된 과정을 소개합니다.
문제점: 모든 쿼리가 전체 비용을 지불함
우리의 원래 흐름은 다음과 같았습니다:
flowchart LR
U[User Query] --> E[Embedding]
E --> V[Vector DB Search]
...
깔끔한 다이어그램입니다. 하지만 실제로는 비용이 많이 듭니다. 각 단계마다 시간과 비용이 발생합니다:
- 지연 시간 (Latency): 쿼리당 매번 2~6초의 왕복 시간 소요
- 비용 (Cost): 이미 그날 500번이나 답변한 질문이라 할지라도, 모든 LLM 호출마다 토큰이 소모됨
- 반복성 (Repetition): 사용자들이 질문을 일관된 방식으로 표현하지 않음
우리의 로그에서 확인한 사례는 다음과 같습니다:
"What is the FD rate?"
"Fixed deposit interest?"
"Current FD interest?"
...
네 개의 문장이지만 의도 (Intent)는 하나입니다. 우리의 파이프라인은 이를 알지 못했습니다. 각 문장은 그날 아침 이후로 변하지 않은 사실임에도 불구하고, 각각 별도의 임베딩 호출, 별도의 벡터 검색, 그리고 잠재적으로 별도의 LLM 호출을 트리거했습니다.
시도 #1: LLM 문제에 더 많은 LLM으로 대응하다
우리의 첫 번째 해결책은 쿼리가 벡터 DB (Vector DB)에 닿기 전에, 파이프라인 앞단에 의도 및 컨텍스트 탐지기 (Intent-and-context detector)로서 LLM을 추가하는 것이었습니다.
flowchart LR
U[User Query] --> I[LLM as Intent + Context Detector]
I --> E[Embedding]
...
의도를 정확하게 식별한다는 측면에서는 효과가 있었습니다. 하지만 우리는 단지 다음에 무엇을 할지 결정하기 위해 전체 LLM 호출을 추가한 셈이었고, 그 결정 자체만으로도 실제 작업이 시작되기도 전에 모든 쿼리마다 추가로 100~300ms의 시간이 소요되었습니다.
마치 신분증 확인 시간이 실제 회의 시간만큼이나 오래 걸리는 보안 요원을 고용한 것과 같았습니다.
| 접근 방식 (Approach) | 추가 지연 시간 (Latency added) | 문제점 (Problem) |
|---|---|---|
| 의도 탐지 없음 (No intent detection) | 0 ms | 모든 쿼리가 비용이 많이 드는 전체 파이프라인을 통과함 |
| LLM 기반 의도 탐지 (LLM-based intent detection) | +100–300 ms | 정확하지만, 라우팅 결정을 내리기에는 너무 무거움 |
우리 팀의 시니어 중 한 명이 회고(Retro)에서 이렇게 요약했습니다. "문 앞에 온 사람을 맞이하는 데 대포를 사용하지 마세요."
실제 해결책: 작고 평범한 분류기 (Classifier)
우리는 LLM 기반의 탐지기를 작고 미세 조정된(Fine-tuned) 분류 모델로 교체했습니다. 생성(Generation)도, 추론(Reasoning)도, 토큰 단위의 출력(Token-by-token output)도 없습니다. 오직 구조화된 분류(Structured classification)만 수행합니다.
{
"intent": "RATE_QUERY",
"entity": "FIXED_DEPOSIT",
...
우리가 실제로 사용한 것:
- 증류된 트랜스포머 인코더 (DistilBERT/MiniLM 급, 약 3,000만~6,000만 개의 파라미터) — 생성형 디코더(Generative decoder)가 아닌 인코더(Encoder)
- 의도/엔티티 쌍(
RATE_QUERY,BALANCE_QUERY,BRANCH_LOCATOR등)으로 라벨링된 자체 과거 쿼리 로그를 통해 미세 조정(Fine-tuned) - CPU 추론을 위해 ONNX로 내보내고 INT8로 양자화(Quantized) — 이 단계에서는 GPU가 필요하지 않음
- 메인 LLM 호출 체인 외부의 경량 사이드카(Sidecar) 서비스로 배포
특정 모델보다 더 중요했던 것은 카테고리의 전환이었습니다. 분류(Classification)와 생성(Generation)은 서로 다른 작업입니다. "이 질문이 무엇에 관한 것인가"를 묻는 데는 에세이를 쓸 수 있는 모델이 필요한 것이 아니라, 우편물을 분류할 수 있는 모델이 필요합니다. 우리는 그동안 우편물을 분류하기 위해 소설가를 고용해 왔던 것입니다. 실제 분류기로 전환함으로써 우리는 100~300ms에서 한 자릿수 밀리초(ms)로 시간을 단축할 수 있었습니다.
의도에서 Redis 키로
이 부분은 사람들이 RAG 캐싱(Caching)을 설명할 때 보통 대충 넘어가는 부분입니다. 우리는 사용자의 가공되지 않은 문장(Raw sentence)을 캐싱하지 않습니다. 가공되지 않은 문장은 취약하기 때문입니다. 대신 의도(Intent) + 엔티티(Entity)로부터 정형화된 키(Canonical key)를 구축합니다.
"What is the FD rate?" → faq:rate:fixed_deposit
"Fixed deposit interest?" → faq:rate:fixed_deposit
"Current FD interest?" → faq:rate:fixed_deposit
...
그리고 Redis에서 해당 키 뒤에 실제로 저장되는 내용은 다음과 같습니다:
Key:
faq:rate:fixed_deposit
...
네 개의 서로 다른 문장이 하나의 키로 압축됩니다. 해당 질문을 처음 던진 첫 번째 사용자는 전체 파이프라인 비용을 지불합니다. 그 이후의 모든 사용자는 해당 키에 대해, 질문을 어떻게 표현했는지와 관계없이 밀리초(milliseconds) 단위로 Redis에서 즉시 응답을 받게 됩니다.
이로 인해 즉각적으로 발생한 한 가지 문제
답변을 캐싱(Caching)하는 것은 쉽습니다. 하지만 정확한 답변을 캐싱하는 것이 실제 과제입니다.
우리가 거의 즉시 맞닥뜨린 과제 중 하나는 다음과 같습니다: 정기예금(FD) 금리는 변동되지만, 우리의 캐시(Cache)는 그 사실을 알지 못했습니다. 사용자가 월요일에 금리를 물어보고 캐싱이 되었다면, 은행이 이미 금리를 수정한 목요일에도 여전히 월요일의 숫자를 보게 될 수 있습니다. 자신 있게 틀린 캐시 답변은 느리지만 정확한 답변보다 더 나쁩니다.
우리는 짧은 TTL(Time To Live, 위 값에서 볼 수 있듯이 3600초)과 상위 소스의 금리 데이터가 변경될 때마다 실행되는 명시적인 무효화 훅(Invalidation hook)을 통해 이 문제를 해결했습니다. 이는 그 자체로 별도의 주제이며, 다음 글에서 캐시 무효화 전략(Cache invalidation strategy)에 대해 더 자세히 다루겠습니다. 하지만 캐시 레이어가 예외 케이스 없는 완벽한 승리였던 것처럼 꾸미기보다는 여기서 미리 짚고 넘어갈 가치가 있습니다.
전체 최적화 흐름
flowchart TD
U[User Query] --> C[Small Intent + Entity Classifier]
C --> K[Build Canonical Redis Key]
...
각각 다른 케이스를 잡아내는 두 개의 캐시 레이어:
- Redis 키 캐시 (Redis key cache) — 표준화된 키(Canonical key)에 대한 정확한 일치(Exact-match)를 통해 "질문은 같지만 단어가 다른" 경우를 잡아냅니다.
- 시맨틱 캐시 (Semantic cache) — 임베딩 유사도(Embedding similarity)를 통해 벡터 DB(Vector DB)에 도달하기 전 "유사하지만 동일하지 않은" 쿼리를 잡아냅니다.
진정으로 새로운 질문만이 비용이 많이 드는 경로인 벡터 검색(Vector search) + 메인 LLM 호출 단계에 도달하게 됩니다.
실제로 이를 어떻게 검증했는가
이를 배포하기 전에, 우리는 이것이 더 빠르다는 단순한 직감 이상의 것이 필요했습니다. 테스트 프로세스는 다음과 같습니다:
- Replay testing (재생 테스트) — 프로덕션 로그(익명화됨)에서 100만 개의 실제 과거 쿼리를 추출하여, 기존 파이프라인과 새로운 파이프라인을 나란히 놓고 동시에 재생했습니다.
- Shadow mode (섀도 모드) — 분류기(Classifier)를 실제 응답에 영향을 주지 않도록 라이브 파이프라인과 병행하여 실행했습니다. 오분류를 조기에 발견하기 위해 분류기의 예측값과 실제 다운스트림 LLM (Large Language Model)의 동작을 비교했습니다.
- Confidence threshold (신뢰도 임계값) — 하한선을 0.85로 설정했습니다. 이보다 낮은 값은 캐시(Cache)를 사용하도록 신뢰하지 않았으며, 전체 파이프라인으로 전달되었습니다. '느리지만 정확한 것'이 '빠르지만 틀린 것'보다 낫기 때문입니다.
- Live A/B test (라이브 A/B 테스트) — 섀도 모드가 안정적이라고 판단되면, 실제 트래픽을 분할하여 테스트 환경(Test harness)이 아닌 프로덕션 환경에서 지연 시간(Latency)과 비용을 직접 측정했습니다.
100만 건의 테스트 케이스에 대한 결과:
| 지표 | 이전 | 이후 | 변화 |
|---|---|---|---|
| 평균 지연 시간 (Avg latency) | 모든 쿼리에 대해 전체 파이프라인 실행 | 반복되는 의도(Intent)에 대해 캐시 히트(Cache hit) 발생 | ~25–28% 더 빠름 |
| ... | |||
![]() |
25–28%라는 수치는 100만 개의 쿼리로 곱해지기 전까지는 극적으로 들리지 않을 수 있습니다. 하지만 곱해지는 순간, 이는 클라우드 청구서의 실제 항목이 되며 사용자가 답변을 받는 속도에서 실질적인 차이를 만들어냅니다.
누가 이것을 만들었는가
이것은 혼자만의 노력이 아니었습니다. AI 팀은 분류기(Classifier)를 담당했습니다 — 학습 데이터, 라벨링(Labeling), 모델 선택, 임계값 조정(Threshold tuning) 등을 수행했습니다. 백엔드 팀은 기반 시설(Plumbing)을 담당했습니다 — 정규 키 생성(Canonical key generation), Redis 아키텍처, 캐시 무효화(Cache invalidation), 그리고 기존의 지연 시간 SLA (Service Level Agreement)를 깨뜨리지 않고 이 모든 것을 연결하는 작업을 수행했습니다. 어느 한 쪽도 다른 쪽 없이는 작동할 수 없습니다.
다음에 이것을 구축할 사람에게 해주고 싶은 말
- LLM의 지연 시간(Latency)을 또 다른 LLM으로 해결하려 하지 마세요. 작업에 맞는 도구를 선택해야 합니다. 분류(Classification)는 생성(Generation)이 아닙니다.
- 영리한 프롬프트보다 정형화된 키(Canonical key)가 더 강력합니다. 정형화된 키는 서로 다른 네 개의 문장을 하나의 캐시 히트(Cache hit)로 전환해 주는 역할을 합니다.
- 무효화(Invalidation) 전략이 없는 캐시는 잠재적인 버그입니다. TTL(Time To Live)을 결정하기 전에 데이터의 신선도 허용 범위(Staleness tolerance)를 먼저 결정하세요.
- 주장하기 전에 측정하세요. 백만 건의 재실행된 쿼리, 섀도 모드(Shadow mode), 그리고 신뢰도 임계값(Confidence threshold)이 있어야만 "25~28% 더 빠름"이라는 결과가 단순한 슬라이드 속 수치가 아니라, 화이트보드 위에서도 방어할 수 있는 근거가 됩니다.
프로덕션 환경에서 주인공은 RAG가 아닙니다. 캐싱(Caching)이 주인공입니다. 그리고 의도 분류기(Intent classifier)는 캐싱을 가능하게 만드는 핵심 요소입니다.
다음 내용: 캐시 무효화(Cache invalidation) 심층 분석 — 상류(Upstream)의 변화를 감지하고, Redis가 모두에게 자신 있게 거짓말을 하기 전에 어떻게 적절한 키를 제거(Evict)하는가.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기