나는 RAG 파이프라인을 구축했다. 그리고 나서 Retrieval이 진짜 모델이라는 것을 깨달았다.
Source: Dev.to
모두가 LLM에 대해 이야기합니다. GPT‑4, Claude, Gemini – 그것이 바로 스타죠. 하지만 첫 번째 실제 RAG 파이프라인을 구축한 뒤 겸손해지는 사실을 깨달았습니다: LLM은 교체 가능한 부품이고, 검색 시스템이 실제 작업자입니다.
제가 무슨 말인지 보여드릴게요.
우리 모두가 복사하는 4‑Step 파이프라인
- Ingest – 문서를 청크로 나누기
- Embed – 청크를 벡터로 변환
- Retrieve – 상위 k개의 유사 청크 찾기
- Generate – 해당 컨텍스트로 LLM이 답변 생성
작동합니다. 내 봇은 회사 정책 질문에 인용과 함께 답변할 수 있었습니다. 나는 똑똑해진 느낌이 들었습니다.
그때 나는 물었습니다: “디지털 제품에 대한 환불을 받을 수 있나요?”
LLM은 아름답고 자신감 넘치는 답변을 제공했지만 완전히 틀렸습니다. 왜냐하면 검색 단계에서 물리적 반품(30일, 원래 포장)과 관련된 청크만 반환했고, 두 단락 떨어진 곳에 있는 디지털 제품 예외를 전혀 놓쳤기 때문입니다.
LLM은 자신의 역할을 완벽히 수행했습니다. 검색 단계가 실패했습니다.
왜 검색이 진정한 모델인가
| 당신이 생각하기에 중요한 것 | 실제로 중요한 것 |
|---|---|
| 어떤 LLM을 사용하는가 | 문서를 어떻게 청크하는가 |
| 프롬프트 엔지니어링 | 임베딩 품질 |
| 시스템 프롬프트 | 검색 후 재정렬 |
LLM은 단지 답변을 포맷할 뿐입니다. 검색이 답변이 진실인지 여부를 결정합니다.
파이프라인을 고친 코드
시맨틱 검색만으로는 “non‑refundable after download”(다운로드 후 환불 불가)와 같은 정확한 구문을 놓칠 수 있습니다. 키워드 검색만으로는 의미를 놓칩니다. 하이브리드 검색은 두 가지를 결합합니다. 여기 핵심 구현입니다 (FAISS + BM25 사용):
from sentence_transformers import SentenceTransformer
import faiss, numpy as np
from rank_bm25 import BM25Okapi
# 1. Load documents and embed
docs = [
"Refund within 30 days, physical items only.",
"Digital products: non-refundable after download.",
"Contact support for defective digital items."
]
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(docs)
index = faiss.IndexFlatL2(embeddings.shape[1])
index.add(np.array(embeddings, dtype='float32'))
# 2. BM25 keyword index (tokenized)
tokenized_docs = [doc.lower().split() for doc in docs]
bm25 = BM25Okapi(tokenized_docs)
# 3. Hybrid search function
def hybrid_search(query, top_k=2, alpha=0.5):
# Semantic score (distance -> similarity)
query_vec = model.encode([query])
distances, indices = index.search(query_vec, top_k)
semantic_scores = 1 / (1 + distances[0])
# Keyword score
query_tokens = query.lower().split()
bm25_scores = bm25.get_scores(query_tokens)
top_bm25_idx = np.argsort(bm25_scores)[-top_k:][::-1]
keyword_scores = [bm25_scores[i] for i in top_bm25_idx]
# Combine (normalized)
combined = {}
for i, idx in enumerate(indices[0]):
combined[idx] = alpha * semantic_scores[i]
for i, idx in enumerate(top_bm25_idx):
combined[idx] = combined.get(idx, 0) + (1 - alpha) * (keyword_scores[i] / max(keyword_scores))
return sorted(combined.items(), key=lambda x: x[1], reverse=True)[:top_k]
# 4. Test
query = "Can I get my money back for a digital product?"
results = hybrid_search(query)
for idx, score in results:
print(f"Score: {score:.2f} | {docs[idx]}")
# Output: Score: 0.92 | Digital products: non-refundable after download.
alpha=0.5는 의미와 정확한 문구를 균형 있게 조정합니다. 하이브리드 검색이 없으면 디지털 제품에 대한 문장은 무시되었습니다. 하이브리드 검색을 사용하면 최상위 결과로 나타납니다.
Three Changes That 10x’ed My Pipeline
- 청크 크기는 기본값이 아니다 – 겹치는 청크(200 토큰, 50‑토큰 겹침)로 전환했습니다.
- 시맨틱 검색만으로는 거짓 – BM25 하이브리드 검색을 추가했습니다(위 코드 참조).
- 재순위가 모든 것을 바꾼다 – 작은 크로스‑인코더가 상위 10개 청크를 재점수화하여 정확도를 72 %에서 91 %로 끌어올렸습니다.
대부분의 사람들이 저지르는 실수
우리는 RAG를 LLM 문제로 간주합니다. 그래서 프롬프트를 조정하고, 모델을 교체하고, 시스템 지시문을 추가합니다.
하지만 LLM은 강제로 제공된 컨텍스트를 사용해야 합니다. 잘못된 청크를 제공하면 자신 있게 환상을 만들어냅니다. 올바른 청크를 제공하면, 작은 모델이라도 정확히 답합니다.
병목 현상은 거의 LLM이 아닙니다. 검색기가 원인입니다.
지금 내가 다르게 하는 일
에이전트 코드를 한 줄이라도 작성하기 전에 나는 세 가지 질문을 한다:
- “내가 직접 벡터 데이터베이스를 검색한다면, 이 질문에 정확히 답하는 문장을 찾을 수 있을까?”
- “내 검색이 동의어 와 정확한 키워드 모두에 대해 작동하는가?” → 아니면 하이브리드 검색을 사용한다.
- “상위 1개의 검색된 청크가 실제로 가장 좋은가?” → 아니면 재‑랭커를 추가한다.
핵심 요약
AI 산업은 모델 자체를 팔아줍니다. 하지만 실제 운영되는 RAG 시스템에서는 모델이 가장 저렴하고 가장 쉽게 교체할 수 있는 구성 요소입니다. 어려운 부분—작동하는 봇과 데모 수준의 소프트웨어를 구분 짓는 부분—은 올바른 정보를 컨텍스트 윈도우에 넣는 일입니다.
LLM은 펜이고, 검색은 기억이며, 기억이 시스템을 유용하게 만드는 요소입니다.
다음에 RAG 봇이 실패한다면, GPT를 탓하지 말고 검색한 내용을 살펴보세요. 진짜 문제는 거기에 있습니다.