RAG (Retrieval-Augmented Generation) - 검색으로 환각을 막다
ChatGPT에게 “우리 회사 휴가 정책이 뭐야?”라고 물어본 적 있나요? 아마 이런 답변을 받으셨을 겁니다:
“죄송하지만, 귀사의 내부 정책에 대한 정보는 가지고 있지 않습니다.”
당연합니다. GPT는 공개 인터넷 데이터로 학습했지, 여러분 회사의 내부 문서는 본 적이 없으니까요.
그렇다면 “회사 문서로 Fine-tuning 하면 되지 않을까?”라고 생각할 수 있습니다. 하지만 현실적으로 어렵습니다:
- Fine-tuning 비용이 매우 높음
- 문서가 업데이트될 때마다 재학습 필요
- 수천 개의 문서를 모두 모델에 넣을 수 없음
여기서 RAG (Retrieval-Augmented Generation)가 등장합니다. 모델을 재학습시키지 않고, 필요한 정보를 검색해서 답변에 활용하는 방법이죠.
왜 RAG를 이해해야 할까요?
1. LLM의 가장 큰 문제 해결
LLM의 가장 큰 문제는 환각(Hallucination)입니다. 없는 사실을 그럴듯하게 만들어내는 것이죠.
2023년 한 변호사가 ChatGPT가 만들어낸 가짜 판례를 법정에 제출했다가 징계를 받은 사건(출처: 중앙일보)이 있었습니다. 이런 문제를 방지하려면 모델이 사실 기반으로 답변하도록 해야 합니다.
RAG는 이 문제의 현실적인 해결책입니다:
- 모델에게 “상상하지 말고, 이 문서를 바탕으로 답해”라고 지시
- 검증 가능한 출처 제공
- 최신 정보 반영 (재학습 없이)
2. 실무에서 가장 많이 쓰이는 패턴
2024년 기준, 실무에서 배포된 LLM 애플리케이션의 약 70% 이상이 RAG를 사용한다는 통계가 있습니다. (Gartner 보고서)
왜일까요? RAG는 실용적이기 때문입니다:
- Fine-tuning보다 훨씬 저렴 (1/100 비용)
- 데이터 업데이트가 즉시 반영
- 구현이 상대적으로 간단
제가 본 대부분의 기업 AI 챗봇, 문서 검색 시스템, 고객 지원 도구들은 모두 RAG 기반이었습니다.
3. 도메인 특화 AI의 핵심
“우리 회사 데이터로 AI를 만들고 싶어요”라는 요구사항을 자주 들으셨나요?
Fine-tuning은 비싸고 느리지만, RAG는:
- 내부 문서만 벡터 DB에 저장
- 질문이 오면 관련 문서 검색
- 검색된 내용을 바탕으로 답변
이 과정이 몇 초 안에 완료됩니다. 모델 재학습 없이요.
먼저, 기초부터 이해하기
RAG란 무엇인가?
RAG (Retrieval-Augmented Generation)는 두 단계로 동작합니다:
- Retrieval (검색): 관련 있는 문서를 찾음
- Generation (생성): 찾은 문서를 바탕으로 답변 생성
비유하자면, 오픈북 시험과 비슷합니다. 모든 걸 외우지 않고, 교과서를 보면서 답을 작성하는 거죠.
사용자 질문
↓
[1. Retrieval]
검색 → 관련 문서 찾기
↓
[2. Generation]
질문 + 문서 → LLM → 답변
Fine-tuning vs RAG - 어떤 차이가 있을까?
RAG를 이해하려면 먼저 Fine-tuning과 비교해야 합니다. 둘 다 “LLM을 우리 데이터에 맞게 만든다”는 목표는 같지만, 접근 방식이 완전히 다릅니다.
Fine-tuning이란?
Fine-tuning(미세 조정)은 이미 학습된 모델을 추가 학습시키는 방법입니다.
[GPT-3.5 기본 모델]
↓
[우리 회사 데이터로 추가 학습]
(몇 시간~며칠)
↓
[회사 특화 모델]
비유하자면, 의사 면허가 있는 사람을 소아과 전문의로 만드는 “전문의 과정”과 비슷합니다.
예시:
# OpenAI Fine-tuning
training_data = [
{"messages": [
{"role": "user", "content": "휴가 정책은?"},
{"role": "assistant", "content": "연차 15일입니다"}
]},
# ... 수백~수천 개
]
# 모델 학습 (시간 + 비용 소요)
fine_tuned_model = openai.FineTuning.create(
training_file="data.jsonl",
model="gpt-3.5-turbo"
)
Fine-tuning의 특징:
장점:
- 모델이 회사 용어, 문체, 답변 스타일을 “학습”함
- 반복적인 패턴이 많으면 효과적
- 추론 시 추가 컨텍스트 불필요
단점:
- 비용이 높음: $100~$1000+ (데이터 양에 따라)
- 시간이 오래 걸림: 몇 시간~며칠
- 업데이트가 어려움: 데이터 변경 시 재학습 필요
- 환각 여전히 발생: 모델은 여전히 “상상”할 수 있음
RAG의 차이점
RAG는 모델을 전혀 학습시키지 않습니다. 대신 질문할 때마다 필요한 정보를 찾아서 제공합니다.
[GPT-3.5 기본 모델] ← 그대로 사용
+
[벡터 DB: 회사 문서들] ← 여기만 준비
↓
질문 → 검색 → 문서 + 질문 → 답변
비유하자면, 의사에게 “이 의학 교과서를 참고해서 답해주세요”라고 하는 것과 비슷합니다.
RAG의 특징:
장점:
- 저렴함: Fine-tuning의 1/100 비용
- 빠름: 몇 분~몇 시간이면 구축 가능
- 업데이트 쉬움: 문서만 추가/수정하면 즉시 반영
- 환각 방지: 문서에 없으면 “모르겠습니다” 답변 가능
- 출처 표시: “이 문서에 따르면…” 명시 가능
단점:
- 매번 검색 비용 발생
- 컨텍스트 길이 제한 (검색된 문서가 너무 길면 문제)
- 복잡한 추론에는 Fine-tuning보다 약할 수 있음
언제 무엇을 사용할까?
Fine-tuning을 선택해야 할 때:
- 특정 형식/스타일 답변이 중요 (예: 법률 문서 작성)
- 반복적인 패턴이 많음 (예: 고객 응대 스크립트)
- 추론 속도가 매우 중요 (검색 지연 없이)
- 데이터가 자주 변하지 않음
RAG를 선택해야 할 때:
- 지식 기반 Q&A (예: 회사 내부 문서 검색)
- 데이터가 자주 업데이트됨 (예: 뉴스, 정책 변경)
- 출처 표시가 중요 (예: 의료, 법률)
- 빠른 프로토타입 필요
- 대부분의 경우 ← 실무에서는 RAG가 훨씬 실용적
두 개를 함께 사용하기:
가장 강력한 조합은 Fine-tuned 모델 + RAG입니다!
[Fine-tuned GPT-3.5]
(회사 톤앤매너 학습)
+
[RAG: 최신 문서 검색]
(실시간 정보 제공)
=
최고의 성능
예: Fine-tuning으로 “친절한 고객 지원 톤”을 학습시키고, RAG로 최신 제품 정보를 제공.
전통적 방식 vs RAG
차이를 명확히 이해하기 위해 비교해볼까요?
❌ 전통적 LLM 방식
# 질문만 던짐
question = "2023년 우리 회사 매출은?"
response = llm.generate(question)
# 결과
→ "죄송하지만 해당 정보를 모릅니다."
# 또는 더 나쁜 경우
→ "약 150억원입니다." (환각 - 틀린 정보!)
문제:
- LLM은 학습 시점까지의 데이터만 알고 있음
- 회사 내부 정보는 전혀 모름
- 없는 정보를 만들어낼 위험
✅ RAG 방식
# 1단계: 관련 문서 검색
question = "2023년 우리 회사 매출은?"
relevant_docs = vector_db.search(question, top_k=3)
# 검색 결과:
# - "2023 Annual Report.pdf" (관련도: 0.92)
# - "Q4 2023 Financial Summary.pdf" (관련도: 0.87)
# 2단계: 문서와 함께 LLM에게 질문
prompt = f"""
다음 문서를 바탕으로 질문에 답하세요:
문서:
{relevant_docs[0].content}
{relevant_docs[1].content}
질문: {question}
"""
response = llm.generate(prompt)
# 결과
→ "2023 Annual Report에 따르면, 2023년 매출은 157억원입니다."
장점:
- 실제 문서 기반 답변
- 출처 명시 가능
- 환각 방지
RAG의 핵심 구성 요소
RAG 시스템을 이해하려면 세 가지 핵심 요소를 알아야 합니다.
1. 임베딩 (Embedding) - 텍스트를 숫자로
RAG의 첫 번째 마법은 임베딩입니다. 텍스트를 숫자 벡터로 변환하는 과정이죠.
왜 임베딩이 필요한가?
컴퓨터는 “유사도”를 직접 계산할 수 없습니다. 하지만 숫자 벡터로 바꾸면 가능합니다.
# 텍스트 → 벡터 변환
text1 = "강아지가 공원에서 놀고 있다"
text2 = "개가 야외에서 뛰어놀고 있다"
text3 = "주식 시장이 급락했다"
# 임베딩 (1536차원 벡터로 변환)
vec1 = embedding_model.encode(text1) # [0.234, -0.123, 0.456, ...]
vec2 = embedding_model.encode(text2) # [0.221, -0.118, 0.447, ...]
vec3 = embedding_model.encode(text3) # [-0.567, 0.890, -0.234, ...]
# 유사도 계산 (코사인 유사도)
similarity(vec1, vec2) # 0.95 (매우 유사)
similarity(vec1, vec3) # 0.12 (거의 무관)
의미가 비슷한 텍스트는 벡터 공간에서 가까이 위치합니다!
임베딩 모델의 예
OpenAI text-embedding-3-small:
- 1536차원 벡터
- 비용: $0.00002 / 1K tokens
- 다국어 지원
OpenAI text-embedding-3-large:
- 3072차원 벡터
- 더 정확하지만 비싸고 느림
오픈소스: sentence-transformers:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
embedding = model.encode("검색할 텍스트")
무료이고, 로컬에서 실행 가능!
2. 벡터 데이터베이스 (Vector Database) - 유사한 것을 빠르게 찾기
임베딩 벡터를 저장하고, 빠르게 검색하려면 특별한 데이터베이스가 필요합니다.
전통적 DB vs 벡터 DB
전통적 데이터베이스 (MySQL, PostgreSQL):
SELECT * FROM documents WHERE title = '매출 보고서';
-- 정확히 일치하는 것만 찾음
벡터 데이터베이스:
vector_db.search(
query_vector,
top_k=5,
metric='cosine'
)
# "의미상 유사한" 문서 5개를 찾음
인기 있는 벡터 DB
Pinecone (클라우드):
- 관리형 서비스
- 확장성 좋음
- 비용: $70/month부터
Chroma (오픈소스):
import chromadb
client = chromadb.Client()
collection = client.create_collection("my_docs")
# 문서 저장
collection.add(
documents=["문서 내용 1", "문서 내용 2"],
ids=["doc1", "doc2"]
)
# 검색
results = collection.query(
query_texts=["질문"],
n_results=3
)
무료이고 간단!
Weaviate, Qdrant, Milvus:
- 각각 장단점이 있음
- 대규모 프로덕션에 적합
3. 청킹 (Chunking) - 문서를 적절한 크기로 나누기
긴 문서를 통째로 임베딩하면 문제가 생깁니다. 문서가 “매출, 인사, 복지” 모두 다루면, 정작 필요한 부분을 찾기 어렵죠.
청킹 전략
방법 1: 고정 크기
def chunk_by_size(text, chunk_size=500):
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size):
chunks.append(' '.join(words[i:i+chunk_size]))
return chunks
장점: 간단 단점: 문장 중간에 끊길 수 있음
방법 2: 의미 단위 (추천)
# 문단, 섹션 단위로 분할
chunks = text.split('\n\n') # 빈 줄 기준
# 또는 LangChain 사용
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200, # 중복 허용 (맥락 유지)
separators=['\n\n', '\n', '.', ' ']
)
chunks = splitter.split_text(long_document)
장점: 의미가 유지됨 단점: 청크 크기가 불균일
최적 청크 크기는?
경험상:
- 너무 작으면 (< 200 tokens): 맥락 부족
- 너무 크면 (> 1000 tokens): 노이즈 증가, 관련 없는 내용 포함
- 추천: 500-800 tokens
실험을 통해 도메인에 맞게 조정해야 합니다.
RAG 시스템 구축하기
이론은 충분합니다. 실제로 만들어봅시다!
예제 1: 가장 간단한 RAG (50줄 이하)
from openai import OpenAI
import chromadb
client = OpenAI()
# 1. 벡터 DB 초기화
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("knowledge_base")
# 2. 문서 저장 (임베딩 자동 생성)
documents = [
"우리 회사 휴가 정책: 연차 15일, 병가 10일",
"재택근무는 주 2회 가능합니다",
"점심시간은 12시부터 1시까지입니다"
]
collection.add(
documents=documents,
ids=["doc1", "doc2", "doc3"]
)
# 3. RAG 함수
def rag_query(question):
# 3-1. 관련 문서 검색
results = collection.query(
query_texts=[question],
n_results=2
)
context = '\n'.join(results['documents'][0])
# 3-2. LLM에게 질문 + 문서 제공
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "다음 문서를 바탕으로 답하세요. 문서에 없는 내용은 '모르겠습니다'라고 답하세요."
},
{
"role": "user",
"content": f"문서:\n{context}\n\n질문: {question}"
}
]
)
return response.choices[0].message.content
# 4. 사용
answer = rag_query("재택근무 가능한가요?")
print(answer)
# → "네, 주 2회 재택근무가 가능합니다."
answer = rag_query("회사 주소가 어디인가요?")
print(answer)
# → "죄송하지만, 제공된 문서에 회사 주소 정보가 없습니다."
이게 RAG의 전부입니다! 검색 → 문서 + 질문 → LLM → 답변.
예제 2: PDF 문서 RAG 시스템
실무에서는 PDF, Word 등 다양한 파일을 처리해야 합니다.
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQA
# 1. PDF 로드
loader = PyPDFLoader("company_handbook.pdf")
pages = loader.load()
# 2. 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = text_splitter.split_documents(pages)
print(f"총 {len(chunks)}개의 청크 생성")
# 3. 임베딩 + 벡터 DB 저장
embeddings = OpenAIEmbeddings()
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 디스크에 저장
)
# 4. RAG 체인 구성
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 문서를 모두 프롬프트에 넣기
retriever=vector_store.as_retriever(search_kwargs={"k": 3})
)
# 5. 사용
result = qa_chain.run("퇴직금 정책은 어떻게 되나요?")
print(result)
핵심:
- PDF → 텍스트 추출
- 긴 텍스트 → 청크로 분할
- 청크 → 임베딩 → 벡터 DB
- 질문 → 검색 → LLM
예제 3: 출처 표시하기 (Citation)
신뢰성을 위해 “어디서 찾았는지” 출처를 보여줘야 합니다.
def rag_with_citation(question):
# 검색
results = vector_store.similarity_search_with_score(
question,
k=3
)
# 문서 + 출처 정보 수집
context_parts = []
sources = []
for i, (doc, score) in enumerate(results):
context_parts.append(f"[출처 {i+1}]\n{doc.page_content}")
sources.append({
"id": i+1,
"file": doc.metadata.get("source", "unknown"),
"page": doc.metadata.get("page", "unknown"),
"score": score
})
context = '\n\n'.join(context_parts)
# LLM에게 출처 번호 사용하도록 지시
prompt = f"""
다음 문서를 바탕으로 질문에 답하세요.
답변할 때 반드시 출처 번호를 명시하세요. (예: [출처 1]에 따르면...)
{context}
질문: {question}
"""
response = llm.generate(prompt)
return {
"answer": response,
"sources": sources
}
# 사용
result = rag_with_citation("재택근무 정책은?")
print(result["answer"])
# → "[출처 1]에 따르면, 주 2회 재택근무가 가능합니다."
print("\n출처:")
for source in result["sources"]:
print(f" {source['id']}. {source['file']} (페이지 {source['page']})")
결과:
[출처 1]에 따르면, 주 2회 재택근무가 가능합니다.
출처:
1. company_handbook.pdf (페이지 15)
2. hr_policy.pdf (페이지 3)
사용자가 직접 확인할 수 있어 신뢰도가 높아집니다!
고급 RAG 기법
기본 RAG는 간단하지만, 실무에서는 더 정교한 기법이 필요합니다.
1. Hybrid Search - 키워드 + 의미 검색
벡터 검색만으로는 부족할 때가 있습니다.
문제 상황:
질문: "GPT-4의 가격은?"
문서: "GPT-4 pricing: $0.03/1K tokens"
벡터 검색: "GPT-4"와 "가격"의 의미를 이해하지만...
→ "AI 가격 정책", "모델 비용" 같은 문서도 검색됨 (노이즈)
해결: Hybrid Search
벡터 검색(의미) + BM25(키워드) 조합
from langchain.retrievers import BM25Retriever, EnsembleRetriever
# 벡터 검색기
vector_retriever = vector_store.as_retriever(search_kwargs={"k": 5})
# 키워드 검색기
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# 앙상블 (가중 평균)
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.5, 0.5] # 50:50 비율
)
# 사용
results = ensemble_retriever.get_relevant_documents("GPT-4 가격")
결과:
- “GPT-4”라는 키워드가 정확히 있는 문서 우선
- 의미상 관련 있는 문서도 포함
2. Re-ranking - 검색 후 재정렬
초기 검색에서 10개를 가져온 후, 더 정확하게 재정렬합니다.
from sentence_transformers import CrossEncoder
# 1단계: 벡터 검색 (빠르지만 덜 정확)
candidates = vector_store.similarity_search(question, k=20)
# 2단계: Re-ranking (느리지만 정확)
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
pairs = [[question, doc.page_content] for doc in candidates]
scores = reranker.predict(pairs)
# 점수 기준 재정렬
ranked_docs = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)
# 상위 3개만 사용
top_docs = [doc for doc, score in ranked_docs[:3]]
효과:
- 검색 품질 10-30% 향상
- 비용: 약간의 지연 시간 증가 (0.5초 정도)
3. Query Transformation - 질문 개선하기
사용자의 질문이 항상 명확하지는 않습니다.
문제:
사용자: "그거 어떻게 되는거야?"
→ 맥락 부족, 검색 불가능
해결 1: Query Expansion (질문 확장)
def expand_query(question, chat_history):
# LLM에게 이전 대화 맥락 포함하여 질문 재작성 요청
prompt = f"""
대화 기록:
{chat_history}
사용자 질문: {question}
위 질문을 검색 가능하도록 구체적으로 재작성하세요.
"""
expanded_query = llm.generate(prompt)
return expanded_query
# 예시
chat_history = """
사용자: 재택근무 정책 알려줘
AI: 주 2회 재택근무가 가능합니다.
사용자: 그거 언제부터 시행된거야?
"""
expanded = expand_query("그거 언제부터 시행된거야?", chat_history)
# → "재택근무 정책의 시행 날짜는 언제인가요?"
해결 2: Multi-Query (여러 질문 생성)
def generate_multiple_queries(question):
prompt = f"""
다음 질문에 대해 3가지 다른 방식으로 물어보세요:
원본 질문: {question}
1.
2.
3.
"""
queries = llm.generate(prompt)
return queries.split('\n')
# 예시
queries = generate_multiple_queries("GPT-4 가격")
# 1. GPT-4의 비용은 얼마인가요?
# 2. GPT-4 API 요금은?
# 3. GPT-4 사용료는 어떻게 되나요?
# 각 질문으로 검색 → 결과 합치기
all_results = []
for query in queries:
results = vector_store.search(query, k=2)
all_results.extend(results)
# 중복 제거 + 상위 5개
unique_results = deduplicate(all_results)[:5]
다양한 관점에서 검색하므로 놓치는 문서가 줄어듭니다.
4. Contextual Compression - 문서 압축하기
검색된 문서가 너무 길면, LLM 컨텍스트 낭비 + 비용 증가.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
# 압축기 설정
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vector_store.as_retriever()
)
# 사용
compressed_docs = compression_retriever.get_relevant_documents(
"재택근무 정책"
)
# 원본: 1000 tokens
# 압축: 200 tokens (질문과 관련된 부분만 추출)
효과:
- 토큰 사용량 50-80% 감소
- 답변 품질 유지 또는 향상 (노이즈 제거)
함정과 주의사항
RAG가 만능은 아닙니다. 실무에서 주의할 점들이 있습니다.
1. 검색 실패 - 관련 문서를 못 찾는 경우
RAG의 성능은 검색 품질에 달려 있습니다. 검색이 실패하면 답변도 틀립니다.
원인 1: 임베딩 모델의 한계
# 도메인 특화 용어
question = "우리 시스템의 QPS는?"
document = "초당 쿼리 수(Queries Per Second)는 10,000입니다."
# 일반 임베딩 모델은 "QPS"와 "Queries Per Second"의 관계를 모를 수 있음
해결:
- 도메인 특화 임베딩 모델 사용
- Fine-tuning (가능하면)
- 용어집 추가 (QPS = Queries Per Second)
원인 2: 청킹 문제
# 잘못된 청킹
청크 1: "우리 회사의 매출은 2022년에 100억,"
청크 2: "2023년에는 150억원을 기록했습니다."
# 질문: "2023년 매출은?"
# → 청크 2만 검색되면 문맥 부족
해결:
- Chunk overlap 사용 (예: 200 tokens)
- 더 큰 청크 크기
- Parent Document Retriever 사용
# Parent Document Retriever
from langchain.retrievers import ParentDocumentRetriever
# 작은 청크로 검색 (정확도↑)
# 하지만 원본 문서 반환 (맥락↑)
retriever = ParentDocumentRetriever(
vectorstore=vector_store,
docstore=InMemoryDocstore(),
child_splitter=small_splitter, # 검색용
parent_splitter=large_splitter # 반환용
)
2. 컨텍스트 길이 제한
LLM은 컨텍스트 길이에 제한이 있습니다.
GPT-3.5-turbo: 16K tokens
GPT-4: 8K tokens (기본) ~ 128K tokens (확장)
Claude 3: 200K tokens
문제는, 검색된 문서 5개가 각각 2000 tokens면:
- 문서: 10,000 tokens
- 질문 + 시스템 프롬프트: 500 tokens
- 총: 10,500 tokens
GPT-3.5의 절반 이상을 사용합니다!
해결 방법:
1) Map-Reduce 패턴
# 1. 각 문서를 개별적으로 요약
summaries = []
for doc in retrieved_docs:
summary = llm.generate(f"요약: {doc}")
summaries.append(summary)
# 2. 요약들을 합쳐서 최종 답변
final_answer = llm.generate(f"""
요약들:
{summaries}
질문: {question}
""")
2) Refine 패턴
# 첫 문서로 초기 답변 생성
answer = llm.generate(f"{docs[0]}\n\n질문: {question}")
# 다음 문서들로 답변 개선
for doc in docs[1:]:
answer = llm.generate(f"""
현재 답변: {answer}
추가 정보: {doc}
위 정보를 바탕으로 답변을 개선하세요.
""")
3) Contextual Compression (앞서 언급)
3. 비용 - 임베딩과 벡터 DB 비용
RAG는 “공짜”가 아닙니다.
임베딩 비용
# 1만 개 문서, 각 500 tokens
total_tokens = 10000 * 500 = 5,000,000 tokens
# OpenAI text-embedding-3-small: $0.00002 / 1K tokens
embedding_cost = (5000 / 1000) * 0.00002 = $0.1
# 한 번만 하면 됨 (업데이트 제외)
저렴합니다!
벡터 DB 비용
Pinecone:
- Standard: $70/month (1GB 저장)
- 1만 개 문서 (1536차원 벡터): 약 60MB
- → $70/month
오픈소스 (Chroma, Weaviate):
- 무료
- 하지만 서버 운영 비용 발생
검색 비용
매 질문마다:
- 임베딩 (질문): $0.00002 (거의 무료)
- LLM 생성 (문서 + 질문): $0.001 ~ $0.01 (모델에 따라)
# 하루 1000 질문
# 각 질문당 평균 2000 tokens (문서 포함)
# GPT-3.5-turbo: $0.0005 / 1K input tokens
daily_cost = 1000 * (2000/1000) * 0.0005 = $1/day
monthly_cost = $30/month
Fine-tuning ($1000+)보다는 훨씬 저렴하지만, 무시할 수 없는 비용입니다.
최적화:
# 1. 캐싱
cache = {}
def cached_rag(question):
if question in cache:
return cache[question]
answer = rag_query(question)
cache[question] = answer
return answer
# 2. 더 작은 모델 사용
# GPT-4 → GPT-3.5 (10배 저렴)
# 3. 청크 수 제한
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
# k=5 → k=3 (40% 비용 절감)
4. 데이터 프라이버시 - 내부 문서 보안
회사 내부 문서를 외부 API(OpenAI, Pinecone)에 보내는 것이 안전한가요?
우려 사항:
- 문서가 OpenAI 서버를 거침
- 학습 데이터로 사용될 수 있음
- 규제 준수 (GDPR, HIPAA 등)
해결 방법:
1) API 설정 확인
# OpenAI는 API 데이터를 학습에 사용하지 않음 (명시)
# 하지만 서버를 거치는 건 사실
2) 온프레미스 솔루션
# 로컬 임베딩 모델
from sentence_transformers import SentenceTransformer
embeddings = SentenceTransformer('all-MiniLM-L6-v2')
# 로컬 벡터 DB
import chromadb
chroma_client = chromadb.Client()
# 로컬 LLM (오픈소스)
from langchain.llms import LlamaCpp
llm = LlamaCpp(model_path="./llama-2-7b.bin")
장점: 데이터가 외부로 나가지 않음 단점: 성능 저하, 인프라 비용
3) Azure OpenAI Service
- 전용 인스턴스
- 데이터 격리 보장
- 엔터프라이즈 계약 가능
5. 최신성 vs 정확성 트레이드오프
RAG의 장점은 “최신 정보”인데, 여기에도 함정이 있습니다.
시나리오:
오늘: 2025-11-26
질문: "현재 CEO는 누구인가요?"
데이터베이스:
- 2023-01-01 문서: "CEO는 김철수입니다"
- 2024-05-15 문서: "신임 CEO 박영희 취임"
- 2025-11-20 문서: "CEO 박영희, 사임 발표"
검색 결과: 3개 문서 모두 검색됨
LLM 답변: "CEO는 박영희입니다" (틀림! 이미 사임함)
해결:
1) 메타데이터 필터링
# 최신 문서만 검색
results = vector_store.similarity_search(
question,
k=5,
filter={"date": {"$gte": "2025-01-01"}} # 2025년 이후만
)
2) 날짜 가중치
from datetime import datetime
def rerank_by_date(docs):
now = datetime.now()
scored_docs = []
for doc in docs:
doc_date = datetime.fromisoformat(doc.metadata['date'])
days_old = (now - doc_date).days
# 최신일수록 높은 점수
recency_score = 1 / (1 + days_old / 30) # 한 달마다 감소
# 유사도 + 최신성
final_score = doc.score * 0.7 + recency_score * 0.3
scored_docs.append((doc, final_score))
return sorted(scored_docs, key=lambda x: x[1], reverse=True)
3) 명시적 지시
prompt = f"""
다음 문서들을 바탕으로 답하세요.
주의: 날짜를 확인하고 가장 최신 정보를 사용하세요.
{context}
질문: {question}
"""
실전 활용: RAG 시스템 구축 체크리스트
이제 실무에서 RAG 시스템을 구축한다면, 어떤 순서로 해야 할까요?
Phase 1: MVP (Minimum Viable Product)
목표: 1-2주 안에 작동하는 프로토타입
# 1. 간단한 구성
- 임베딩: OpenAI text-embedding-3-small
- 벡터 DB: Chroma (로컬)
- LLM: GPT-3.5-turbo
- 프레임워크: LangChain
# 2. 핵심 기능만
- 문서 업로드
- 질문-답변
- 출처 표시
# 3. 평가
- 10개 질문 셋 준비
- 수동으로 답변 품질 확인
Phase 2: 품질 개선
목표: 답변 정확도 80% 이상
# 1. 검색 개선
- Hybrid search 도입
- Re-ranking 추가
- 청킹 전략 최적화
# 2. 프롬프트 엔지니어링
- System prompt 튜닝
- Few-shot examples 추가
- 답변 형식 가이드
# 3. 평가 자동화
def evaluate_rag():
test_cases = [
{"question": "...", "expected": "...", "source": "..."},
# 50개 이상
]
correct = 0
for case in test_cases:
answer = rag_query(case["question"])
if is_correct(answer, case["expected"]):
correct += 1
accuracy = correct / len(test_cases)
print(f"정확도: {accuracy:.1%}")
Phase 3: 프로덕션 준비
목표: 안정적인 서비스
# 1. 인프라
- 벡터 DB: Pinecone / Weaviate (확장성)
- 모니터링: LangSmith, Weights & Biases
- 로깅: 모든 질문-답변 저장
# 2. 성능 최적화
- 캐싱 (Redis)
- 비동기 처리
- 배치 임베딩
# 3. 안전장치
- Rate limiting
- 컨텐츠 필터링
- Fallback 메커니즘
Phase 4: 지속적 개선
# 1. 사용자 피드백 수집
def collect_feedback(question, answer, feedback):
# 엄지 up/down
db.save({
"question": question,
"answer": answer,
"rating": feedback,
"timestamp": now()
})
# 2. 주기적 분석
- 낮은 평가 받은 답변 검토
- 자주 묻는 질문 파악
- 문서 갭 발견
# 3. 모델 업그레이드
- 새 임베딩 모델 A/B 테스트
- LLM 모델 업그레이드
마치며
RAG는 LLM을 실용적으로 만드는 핵심 기술입니다. Fine-tuning처럼 비싸지도 않고, Prompt Engineering처럼 불안정하지도 않습니다.
핵심을 정리하면:
- 검색 + 생성: 필요한 정보를 찾아서 답변에 활용
- 환각 방지: 문서 기반 답변으로 신뢰성 확보
- 최신성: 재학습 없이 새 정보 반영
- 비용 효율: Fine-tuning의 1/100 비용
실무에서 성공하려면:
- 좋은 검색이 80% - 임베딩, 청킹, 벡터 DB에 집중
- 프롬프트 엔지니어링이 15% - LLM에게 명확한 지시
- 나머지 5% - 모니터링과 지속적 개선
RAG는 “설정하고 잊기” 방식이 아닙니다. 계속 모니터링하고, 사용자 피드백을 받고, 개선해야 합니다. 하지만 제대로 구축하면, 회사 지식을 AI로 만드는 가장 현실적인 방법입니다.
다음 단계:
- 간단한 프로토타입 만들어보기 (예제 1 활용)
- 실제 문서 10개로 테스트
- 검색 품질 평가
- 점진적으로 고도화
RAG는 이론보다 실습이 중요합니다. 직접 만들어보면서 체득하는 것이 가장 빠른 학습 방법입니다.
참고 자료
논문
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks - Meta AI, 2020 (RAG 원조 논문)
- Dense Passage Retrieval for Open-Domain Question Answering - Facebook AI, 2020
- In-Context Retrieval-Augmented Language Models - 2023
공식 문서 & 튜토리얼
벡터 데이터베이스 비교
- Pinecone - 관리형 클라우드
- Weaviate - 오픈소스, 확장성 좋음
- Chroma - 오픈소스, 간단함
- Qdrant - 오픈소스, 러시아산
- Milvus - 오픈소스, 대규모 데이터
실습 자료
- Building RAG from Scratch - LangChain 공식
- RAG Techniques - 고급 기법 모음
블로그 & 케이스 스터디
- How Notion AI works - Notion의 RAG 활용
- Building RAG at Scale - Pinecone 시리즈
댓글