RAG (Retrieval-Augmented Generation) 완벽 가이드
이 글의 핵심
RAG로 LLM에 실시간 기업 데이터를 제공하여 정확하고 최신 답변을 생성하세요. 벡터 DB, 임베딩, 청크 전략, 하이브리드 검색, 실전 구현까지 단계별로 설명합니다.
들어가며
RAG (Retrieval-Augmented Generation)는 LLM(대규모 언어 모델)의 한계를 극복하는 핵심 기술입니다. LLM은 학습 데이터 시점까지의 정보만 알고 있고, 기업 내부 문서나 최신 정보는 모릅니다. RAG는 외부 데이터베이스에서 관련 정보를 검색하여 프롬프트에 추가함으로써, LLM이 최신·정확한 답변을 생성하도록 합니다. 이 글은 RAG의 동작 원리, 벡터 데이터베이스 선택, 청크 전략, 하이브리드 검색, Python 구현 예제, 실무 최적화를 단계별로 다룹니다.
RAG란?
기본 개념
전통적인 LLM 사용:
사용자 질문 → LLM → 답변 (학습 데이터 기반)
- 한계: 최신 정보 없음, 기업 내부 데이터 모름 RAG 사용:
사용자 질문 → 관련 문서 검색 → 문서 + 질문 → LLM → 답변
- 장점: 최신 정보, 기업 데이터 활용, 출처 제공 가능
RAG가 해결하는 문제
1. 지식 시점 문제
# ❌ LLM만 사용
질문: "우리 회사 2026년 1분기 매출은?"
답변: "죄송합니다. 해당 정보를 알 수 없습니다."
# ✅ RAG 사용
질문: "우리 회사 2026년 1분기 매출은?"
검색: [2026 Q1 재무 보고서] → "매출 150억원"
답변: "2026년 1분기 매출은 150억원입니다. (출처: Q1 재무보고서)"
2. 환각(Hallucination) 문제
# ❌ LLM만 사용 (지어낸 답변)
질문: "제품 X의 보증 기간은?"
답변: "일반적으로 1년입니다." (실제는 2년)
# ✅ RAG 사용 (문서 기반)
질문: "제품 X의 보증 기간은?"
검색: [제품 X 매뉴얼] → "보증 기간: 2년"
답변: "제품 X의 보증 기간은 2년입니다."
3. 도메인 지식 부족
# ❌ LLM만 사용
질문: "우리 회사 휴가 정책은?"
답변: "일반적인 기업은..." (일반론)
# ✅ RAG 사용
질문: "우리 회사 휴가 정책은?"
검색: [인사 규정 문서]
답변: "연차 15일, 병가 10일, 경조사 휴가 3일입니다."
RAG 아키텍처
전체 흐름
┌─────────────────────────────────────────────┐
│ 1. 데이터 준비 (Offline) │
└─────────────────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
[문서 수집] [청크 분할] [임베딩 생성]
│ │ │
└────────────┼────────────┘
▼
[벡터 DB 저장]
┌─────────────────────────────────────────────┐
│ 2. 검색 (Online) │
└─────────────────────────────────────────────┘
│
사용자 질문 → [임베딩 변환]
│
▼
[벡터 유사도 검색]
│
▼
[상위 K개 문서 반환]
┌─────────────────────────────────────────────┐
│ 3. 생성 (Online) │
└─────────────────────────────────────────────┘
│
[질문 + 검색된 문서] → [LLM]
│
▼
[답변 생성]
핵심 구성 요소
1. 문서 로더 (Document Loader)
- PDF, Word, HTML, Markdown 등 다양한 포맷 지원
- 텍스트 추출 및 전처리 2. 텍스트 스플리터 (Text Splitter)
- 긴 문서를 작은 청크로 분할
- 맥락 유지를 위한 오버랩 설정 3. 임베딩 모델 (Embedding Model)
- 텍스트를 벡터로 변환
- OpenAI, Cohere, Sentence-BERT 등 4. 벡터 데이터베이스 (Vector Database)
- 고차원 벡터 저장 및 검색
- Pinecone, Weaviate, Qdrant, ChromaDB 등 5. LLM (Large Language Model)
- 검색된 문서를 기반으로 답변 생성
- GPT-4, Claude, Llama 등
벡터 임베딩
임베딩이란?
텍스트를 고차원 벡터로 변환하여 의미적 유사도를 계산 가능하게 합니다:
"강아지는 귀엽다" → [0.2, 0.8, 0.1, ..., 0.4] (1536차원)
"개는 사랑스럽다" → [0.3, 0.7, 0.2, ..., 0.5] (유사도 높음)
"자동차는 빠르다" → [0.9, 0.1, 0.8, ..., 0.2] (유사도 낮음)
주요 임베딩 모델
| 모델 | 차원 | 성능 | 비용 | 특징 |
|---|---|---|---|---|
| OpenAI text-embedding-3-large | 3072 | ⭐⭐⭐⭐⭐ | $$$ | 최고 성능 |
| OpenAI text-embedding-3-small | 1536 | ⭐⭐⭐⭐ | $$ | 가성비 좋음 |
| Cohere embed-multilingual-v3 | 1024 | ⭐⭐⭐⭐ | $$ | 다국어 강점 |
| Sentence-BERT (오픈소스) | 768 | ⭐⭐⭐ | Free | 자체 호스팅 |
Python 예제
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
def get_embedding(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# 사용 예
text = "C++26의 Static Reflection은 메타프로그래밍을 혁신합니다."
embedding = get_embedding(text)
print(f"차원: {len(embedding)}") # 1536
print(f"첫 5개 값: {embedding[:5]}")
유사도 계산
import numpy as np
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
"""코사인 유사도 계산 (0~1, 높을수록 유사)"""
a = np.array(vec1)
b = np.array(vec2)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 예제
query = "C++ 리플렉션"
doc1 = "C++26 Static Reflection 가이드"
doc2 = "Python 웹 크롤링 튜토리얼"
emb_query = get_embedding(query)
emb_doc1 = get_embedding(doc1)
emb_doc2 = get_embedding(doc2)
print(f"query vs doc1: {cosine_similarity(emb_query, emb_doc1):.3f}") # 0.85
print(f"query vs doc2: {cosine_similarity(emb_query, emb_doc2):.3f}") # 0.32
벡터 데이터베이스
주요 벡터 DB 비교
| DB | 타입 | 특징 | 추천 용도 |
|---|---|---|---|
| Pinecone | 관리형 | 쉬운 시작, 자동 스케일링 | 프로토타입, 빠른 출시 |
| Weaviate | 오픈소스/관리형 | 하이브리드 검색 강력 | 복잡한 검색 요구사항 |
| Qdrant | 오픈소스 | Rust 기반, 빠른 성능 | 자체 호스팅, 대용량 |
| ChromaDB | 오픈소스 | 간단한 API, 로컬 개발 | 개발/테스트 환경 |
| Milvus | 오픈소스 | 엔터프라이즈급 | 대규모 프로덕션 |
Pinecone 예제
import pinecone
from openai import OpenAI
# 초기화
pinecone.init(api_key="your-pinecone-key", environment="us-west1-gcp")
# 인덱스 생성
index_name = "my-rag-index"
if index_name not in pinecone.list_indexes():
pinecone.create_index(
name=index_name,
dimension=1536, # OpenAI embedding 차원
metric="cosine"
)
index = pinecone.Index(index_name)
# 문서 추가
openai_client = OpenAI(api_key="your-openai-key")
def add_document(doc_id: str, text: str, metadata: dict):
embedding = openai_client.embeddings.create(
model="text-embedding-3-small",
input=text
).data[0].embedding
index.upsert(vectors=[{
"id": doc_id,
"values": embedding,
"metadata": {"text": text, **metadata}
}])
# 검색
def search(query: str, top_k: int = 3):
query_embedding = openai_client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
results = index.query(
vector=query_embedding,
top_k=top_k,
include_metadata=True
)
return [(match.metadata[text], match.score)
for match in results.matches]
# 사용
add_document("doc1", "C++26은 Static Reflection을 도입했습니다.",
{"category": "cpp", "date": "2026-03-31"})
add_document("doc2", "Python은 동적 타이핑 언어입니다.",
{"category": "python", "date": "2026-03-30"})
results = search("C++ 리플렉션")
for text, score in results:
print(f"[{score:.3f}] {text}")
ChromaDB 예제 (로컬 개발)
import chromadb
from chromadb.config import Settings
# 클라이언트 생성
client = chromadb.Client(Settings(
chroma_db_impl="duckdb+parquet",
persist_directory="./chroma_db"
))
# 컬렉션 생성
collection = client.get_or_create_collection(
name="my_documents",
metadata={"description": "RAG document store"}
)
# 문서 추가 (자동 임베딩)
collection.add(
documents=[
"C++26 Static Reflection은 컴파일 타임 타입 정보를 제공합니다.",
"Rust는 메모리 안전성을 보장하는 시스템 프로그래밍 언어입니다.",
"WebAssembly는 브라우저에서 네이티브 수준 성능을 제공합니다."
],
metadatas=[
{"category": "cpp", "level": "advanced"},
{"category": "rust", "level": "intermediate"},
{"category": "web", "level": "intermediate"}
],
ids=["doc1", "doc2", "doc3"]
)
# 검색
results = collection.query(
query_texts=["C++ 타입 정보"],
n_results=2
)
print(results[documents])
print(results[distances])
청크 전략
청크 크기 결정
고려 사항:
- LLM 컨텍스트 윈도우: GPT-4는 128K 토큰, Claude는 200K 토큰
- 검색 정확도: 너무 작으면 맥락 부족, 너무 크면 노이즈 증가
- 비용: 토큰 수에 비례한 API 비용 권장 크기:
- 일반 문서: 500-1000 토큰 (약 2000-4000자)
- 코드: 함수/클래스 단위 (100-500 토큰)
- FAQ: 질문-답변 쌍 단위
청크 분할 전략
1. 고정 크기 분할 (Fixed-size)
def chunk_by_tokens(text: str, chunk_size: int = 500, overlap: int = 50):
# tiktoken으로 토큰 계산
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode(text)
chunks = []
for i in range(0, len(tokens), chunk_size - overlap):
chunk_tokens = tokens[i:i + chunk_size]
chunk_text = enc.decode(chunk_tokens)
chunks.append(chunk_text)
return chunks
2. 문장 경계 분할 (Sentence-based)
# 필요한 모듈 import
import nltk
nltk.download('punkt')
def chunk_by_sentences(text: str, max_sentences: int = 5):
sentences = nltk.sent_tokenize(text)
chunks = []
for i in range(0, len(sentences), max_sentences):
chunk = ' '.join(sentences[i:i + max_sentences])
chunks.append(chunk)
return chunks
3. 의미 단위 분할 (Semantic)
# 필요한 모듈 import
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""] # 우선순위
)
chunks = splitter.split_text(long_document)
4. 마크다운 구조 기반
def chunk_by_markdown_sections(markdown_text: str):
import re
# ## 헤딩으로 분할
sections = re.split(r'\n## ', markdown_text)
chunks = []
for i, section in enumerate(sections):
if i > 0:
section = "## " + section
# 너무 긴 섹션은 추가 분할
if len(section) > 2000:
sub_chunks = chunk_by_sentences(section, max_sentences=10)
chunks.extend(sub_chunks)
else:
chunks.append(section)
return chunks
오버랩 전략
# 오버랩 없음: 맥락 손실 위험
chunks = ["A B C", "D E F", "G H I"]
# 오버랩 있음: 맥락 유지
chunks = ["A B C D", "C D E F", "E F G H I"]
# ^^^ ^^^
권장 오버랩: 청크 크기의 10-20%
검색 최적화
기본 검색 (Semantic Search)
from langchain.vectorstores import Pinecone
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Pinecone.from_existing_index(
index_name="my-index",
embedding=embeddings
)
# 검색
query = "C++ 메모리 관리 방법"
docs = vectorstore.similarity_search(query, k=3)
for doc in docs:
print(doc.page_content)
print(doc.metadata)
메타데이터 필터링
# 특정 카테고리만 검색
docs = vectorstore.similarity_search(
query,
k=3,
filter={"category": "cpp", "level": "advanced"}
)
# 날짜 범위 필터
docs = vectorstore.similarity_search(
query,
k=3,
filter={
"date": {"$gte": "2026-01-01", "$lte": "2026-03-31"}
}
)
MMR (Maximal Marginal Relevance)
문제: 유사한 문서만 반환되어 다양성 부족 해결: MMR로 관련성과 다양성 균형
docs = vectorstore.max_marginal_relevance_search(
query,
k=5,
fetch_k=20, # 후보 20개 중
lambda_mult=0.5 # 0=다양성 우선, 1=관련성 우선
)
Python 구현
기본 RAG 시스템
from langchain.document_loaders import DirectoryLoader, TextLoader
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. 문서 로드
loader = DirectoryLoader('./docs', glob="**/*.md", loader_cls=TextLoader)
documents = loader.load()
# 2. 청크 분할
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)
# 3. 임베딩 및 벡터 DB 저장
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# 4. RAG 체인 생성
llm = ChatOpenAI(model="gpt-4", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 모든 문서를 하나의 프롬프트에
retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
)
# 5. 질문
query = "C++26 Static Reflection의 주요 장점은?"
answer = qa_chain.run(query)
print(answer)
커스텀 프롬프트
from langchain.prompts import PromptTemplate
template = """다음 문서를 참고하여 질문에 답변하세요.
답변은 한국어로 작성하고, 출처를 명시하세요.
문서:
{context}
질문: {question}
답변:"""
prompt = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(),
chain_type_kwargs={"prompt": prompt}
)
출처 포함 답변
from langchain.chains import RetrievalQAWithSourcesChain
qa_chain = RetrievalQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever()
)
result = qa_chain({"question": query})
print("답변:", result[answer])
print("출처:", result[sources])
하이브리드 검색
Semantic + Keyword 검색
문제: 벡터 검색만으로는 정확한 키워드 매칭 부족 해결: BM25(키워드 검색) + 벡터 검색 결합
from langchain.retrievers import BM25Retriever, EnsembleRetriever
# BM25 검색기 (키워드 기반)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# 벡터 검색기
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 하이브리드 검색기
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6] # BM25: 40%, Vector: 60%
)
# 검색
docs = ensemble_retriever.get_relevant_documents(query)
Reranking
문제: 초기 검색 결과의 순서가 최적이 아닐 수 있음 해결: Reranker 모델로 재정렬
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank
# Cohere Rerank 사용
compressor = CohereRerank(
model="rerank-english-v2.0",
top_n=3
)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vector_retriever
)
# 검색 + 재정렬
docs = compression_retriever.get_relevant_documents(query)
성능 최적화
1. 캐싱
from langchain.cache import InMemoryCache
from langchain.globals import set_llm_cache
# LLM 응답 캐싱
set_llm_cache(InMemoryCache())
# 같은 질문 반복 시 캐시 사용 (비용 절감)
answer1 = qa_chain.run("C++ 메모리 관리")
answer2 = qa_chain.run("C++ 메모리 관리") # 캐시에서 반환
2. 배치 임베딩
# ❌ 비효율적: 하나씩 임베딩
for text in texts:
embedding = get_embedding(text)
# ✅ 효율적: 배치 임베딩
embeddings = client.embeddings.create(
model="text-embedding-3-small",
input=texts # 리스트 전체
).data
# 100개 문서 기준: 단일 요청 시간 vs 100회 요청 시간
# 배치: 2초 vs 개별: 50초
3. 인덱스 최적화
# Pinecone: 파티션 활용
index.upsert(
vectors=vectors,
namespace="cpp-docs" # 네임스페이스로 분리
)
# 특정 네임스페이스만 검색
results = index.query(
vector=query_embedding,
top_k=3,
namespace="cpp-docs"
)
4. 청크 메타데이터 활용
# 메타데이터에 요약 추가
chunks_with_summary = []
for chunk in chunks:
summary = llm.predict(f"다음 텍스트를 한 문장으로 요약:\n{chunk}")
chunks_with_summary.append({
"text": chunk,
"summary": summary,
"char_count": len(chunk)
})
# 검색 시 요약으로 먼저 필터링
실무 사례
1. 고객 지원 챗봇
요구사항:
- 제품 매뉴얼, FAQ, 정책 문서 기반 답변
- 실시간 업데이트 반영
- 출처 제공 구현:
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
qa_chain = ConversationalRetrievalChain.from_llm(
llm=ChatOpenAI(model="gpt-4"),
retriever=vectorstore.as_retriever(search_kwargs={"k": 5}),
memory=memory,
return_source_documents=True
)
# 대화
result = qa_chain({"question": "제품 보증 기간은?"})
print(result[answer])
print("출처:", [doc.metadata[source] for doc in result[source_documents]])
# 후속 질문 (이전 대화 맥락 유지)
result = qa_chain({"question": "연장 가능한가요?"})
2. 사내 문서 검색
요구사항:
- 수천 개의 내부 문서 검색
- 부서별, 프로젝트별 필터링
- 권한 관리 구현:
def search_with_permissions(query: str, user_id: str, department: str):
# 사용자 권한에 따른 필터
filter_dict = {
"$or": [
{"department": department},
{"public": True}
]
}
docs = vectorstore.similarity_search(
query,
k=5,
filter=filter_dict
)
return docs
# 사용
docs = search_with_permissions(
"2026년 예산 계획",
user_id="user123",
department="finance"
)
3. 코드 검색 및 설명
요구사항:
- 대규모 코드베이스에서 관련 코드 찾기
- 함수 설명 자동 생성
- API 사용 예제 제공 구현:
from langchain.document_loaders import GitLoader
# Git 저장소 로드
loader = GitLoader(
repo_path="./my-repo",
file_filter=lambda x: x.endswith(('.cpp', '.h', '.py'))
)
documents = loader.load()
# 함수 단위로 청크
def chunk_by_functions(code: str, language: str):
# tree-sitter로 AST 파싱
import tree_sitter
# ....함수 단위 추출
return function_chunks
# 코드 검색
query = "HTTP 요청을 보내는 함수"
docs = vectorstore.similarity_search(query, k=3)
# LLM으로 설명 생성
for doc in docs:
explanation = llm.predict(
f"다음 코드를 설명하세요:\n\n{doc.page_content}"
)
print(explanation)
4. 논문/연구 자료 QA
요구사항:
- 수백 페이지 PDF 논문 분석
- 수식, 그래프 처리
- 인용 추적 구현:
from langchain.document_loaders import PyPDFLoader
# PDF 로드
loader = PyPDFLoader("research_paper.pdf")
pages = loader.load()
# 페이지별 메타데이터 추가
for i, page in enumerate(pages):
page.metadata[page] = i + 1
page.metadata[source] = "research_paper.pdf"
# 벡터 DB 저장
vectorstore = Chroma.from_documents(pages, embeddings)
# 질문 (페이지 번호 포함)
result = qa_chain({"question": "Transformer 아키텍처의 핵심은?"})
print(result[answer])
print("페이지:", [doc.metadata[page] for doc in result[source_documents]])
고급 기법
1. Query Expansion (질문 확장)
def expand_query(query: str) -> list[str]:
"""LLM으로 질문을 여러 형태로 확장"""
prompt = f"""다음 질문을 3가지 다른 방식으로 다시 작성하세요:
원본: {query}
1.
2.
3.
"""
response = llm.predict(prompt)
expanded = [query] + response.strip().split('\n')
return expanded
# 확장된 질문으로 검색
query = "C++ 성능 최적화"
expanded_queries = expand_query(query)
all_docs = []
for q in expanded_queries:
docs = vectorstore.similarity_search(q, k=2)
all_docs.extend(docs)
# 중복 제거 후 사용
unique_docs = list({doc.page_content: doc for doc in all_docs}.values())
2. HyDE (Hypothetical Document Embeddings)
def hyde_search(query: str, k: int = 3):
"""가상 답변을 생성하여 검색"""
# 1. LLM으로 가상 답변 생성
hypothetical_answer = llm.predict(
f"다음 질문에 대한 답변을 작성하세요:\n{query}"
)
# 2. 가상 답변으로 검색 (질문이 아닌 답변으로 검색!)
docs = vectorstore.similarity_search(hypothetical_answer, k=k)
# 3. 실제 문서로 최종 답변 생성
context = "\n\n".join([doc.page_content for doc in docs])
final_answer = llm.predict(
f"문서:\n{context}\n\n질문: {query}\n\n답변:"
)
return final_answer
# 사용
answer = hyde_search("C++ 컴파일 타임 최적화 방법")
3. Self-Query (자동 필터 추출)
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
# 메타데이터 스키마 정의
metadata_field_info = [
AttributeInfo(
name="category",
description="문서 카테고리 (cpp, python, java 등)",
type="string"
),
AttributeInfo(
name="level",
description="난이도 (beginner, intermediate, advanced)",
type="string"
),
AttributeInfo(
name="date",
description="작성 날짜",
type="date"
)
]
retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents="프로그래밍 튜토리얼 및 가이드",
metadata_field_info=metadata_field_info
)
# 자연어 질문에서 자동으로 필터 추출
docs = retriever.get_relevant_documents(
"2026년 3월에 작성된 고급 C++ 가이드를 찾아줘"
)
# 자동으로 filter={"category": "cpp", "level": "advanced", "date": "2026-03"} 생성
트러블슈팅
문제 1: 검색 결과가 관련 없음
원인:
- 청크 크기가 너무 작거나 큼
- 임베딩 모델이 도메인에 맞지 않음
- 쿼리가 너무 모호함 해결:
# 1. 청크 크기 조정
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1500, # 1000에서 증가
chunk_overlap=300 # 200에서 증가
)
# 2. 쿼리 확장
expanded_queries = expand_query(original_query)
# 3. 하이브리드 검색 사용
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5]
)
문제 2: LLM이 문서를 무시하고 일반 지식으로 답변
원인:
- 프롬프트가 약함
- 문서가 질문과 관련 없음 해결:
template = """당신은 제공된 문서만을 기반으로 답변하는 어시스턴트입니다.
문서에 정보가 없으면 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답변하세요.
절대로 문서 외의 지식을 사용하지 마세요.
문서:
{context}
질문: {question}
답변 (문서 기반):"""
# temperature를 0으로 설정 (창의성 최소화)
llm = ChatOpenAI(model="gpt-4", temperature=0)
문제 3: 토큰 한도 초과
원인:
- 검색된 문서가 너무 많거나 큼
- 컨텍스트 윈도우 초과 해결:
# 1. Map-Reduce 체인 사용
from langchain.chains.question_answering import load_qa_chain
chain = load_qa_chain(
llm,
chain_type="map_reduce" # 각 문서 개별 처리 후 병합
)
# 2. Refine 체인 사용
chain = load_qa_chain(
llm,
chain_type="refine" # 순차적으로 답변 개선
)
# 3. 검색 결과 제한
retriever = vectorstore.as_retriever(
search_kwargs={"k": 3} # 5에서 3으로 감소
)
문제 4: 느린 검색 속도
원인:
- 벡터 DB 인덱스 최적화 부족
- 임베딩 생성 병목 해결:
# 1. 인덱스 타입 변경 (Pinecone)
pinecone.create_index(
name=index_name,
dimension=1536,
metric="cosine",
pod_type="p2" # 성능 향상 (비용 증가)
)
# 2. 로컬 캐시
import diskcache
cache = diskcache.Cache('./embedding_cache')
def get_embedding_cached(text: str):
if text in cache:
return cache[text]
embedding = get_embedding(text)
cache[text] = embedding
return embedding
# 3. 비동기 처리
import asyncio
async def search_async(queries: list[str]):
tasks = [vectorstore.asimilarity_search(q, k=3) for q in queries]
results = await asyncio.gather(*tasks)
return results
평가 지표
Retrieval 평가
Precision@K: 상위 K개 중 관련 문서 비율
def precision_at_k(retrieved_docs, relevant_docs, k):
retrieved_k = retrieved_docs[:k]
relevant_count = sum(1 for doc in retrieved_k if doc in relevant_docs)
return relevant_count / k
Recall@K: 전체 관련 문서 중 검색된 비율
# 함수 정의 및 구현
def recall_at_k(retrieved_docs, relevant_docs, k):
retrieved_k = retrieved_docs[:k]
relevant_count = sum(1 for doc in retrieved_k if doc in relevant_docs)
return relevant_count / len(relevant_docs)
MRR (Mean Reciprocal Rank): 첫 관련 문서의 순위
def mrr(retrieved_docs, relevant_docs):
for i, doc in enumerate(retrieved_docs):
if doc in relevant_docs:
return 1 / (i + 1)
return 0
Generation 평가
자동 평가:
from langchain.evaluation import load_evaluator
# 정확성 평가
evaluator = load_evaluator("qa")
result = evaluator.evaluate_strings(
prediction=answer,
reference=ground_truth,
input=query
)
# 관련성 평가
evaluator = load_evaluator("criteria", criteria="relevance")
result = evaluator.evaluate_strings(
prediction=answer,
input=query
)
사람 평가:
- 정확성: 답변이 사실과 일치하는가?
- 완전성: 필요한 정보를 모두 포함하는가?
- 간결성: 불필요한 정보가 없는가?
- 출처: 출처가 정확한가?
비용 최적화
비용 구조
OpenAI 기준 (2026년 3월):
- GPT-4: $30/1M input tokens, $60/1M output tokens
- GPT-3.5-turbo: $0.5/1M input tokens, $1.5/1M output tokens
- text-embedding-3-small: $0.02/1M tokens
- text-embedding-3-large: $0.13/1M tokens
비용 절감 전략
1. 임베딩 모델 선택
# 프로토타입: 작은 모델
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 프로덕션: 성능 필요 시
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 오픈소스: 비용 제로
from langchain.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
2. LLM 모델 선택
# 간단한 질문: GPT-3.5-turbo
llm_simple = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# 복잡한 질문: GPT-4
llm_complex = ChatOpenAI(model="gpt-4", temperature=0)
# 라우팅
def route_query(query: str):
if is_simple_query(query):
return qa_chain_simple.run(query)
else:
return qa_chain_complex.run(query)
3. 캐싱
from langchain.cache import SQLiteCache
from langchain.globals import set_llm_cache
set_llm_cache(SQLiteCache(database_path=".langchain.db"))
# 동일 질문 반복 시 API 호출 없음
4. 검색 결과 제한
# ❌ 비용 높음: 10개 문서 전달
retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# ✅ 비용 절감: 3개만 전달
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
마무리
RAG는 LLM을 실무에 적용하는 핵심 기술입니다: 핵심 포인트:
- 검색 증강: 외부 지식을 프롬프트에 추가
- 실시간 업데이트: 문서 변경 즉시 반영
- 출처 제공: 답변의 신뢰성 향상
- 비용 효율: Fine-tuning보다 저렴 구현 단계:
- 문서 수집 및 청크 분할
- 임베딩 생성 및 벡터 DB 저장
- 검색 파이프라인 구축
- LLM 통합 및 프롬프트 최적화
- 평가 및 개선 시작 가이드:
- 프로토타입: ChromaDB + OpenAI + LangChain
- 프로덕션: Pinecone + GPT-4 + 커스텀 파이프라인
- 엔터프라이즈: Weaviate + Azure OpenAI + 권한 관리 다음 학습:
- Python 시리즈에서 Python 기초
- 알고리즘 시리즈에서 검색 알고리즘
- 데이터베이스 비교 참고 자료:
- LangChain 문서
- Pinecone 가이드
- OpenAI Embeddings API
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「RAG (Retrieval-Augmented Generation) 완벽 가이드 | 기업 데이터로 AI 강화하기」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「RAG (Retrieval-Augmented Generation) 완벽 가이드 | 기업 데이터로 AI 강화하기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. RAG로 LLM에 실시간 기업 데이터를 제공하여 정확하고 최신 답변을 생성하세요. 벡터 DB, 임베딩, 청크 전략, 하이브리드 검색, 실전 구현까지 단계별로 설명합니다. RAG·LLM·AI 중심으로 설명합니다. St… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- RAG 구현 완벽 가이드 | 검색 증강 생성으로 LLM 성능 향상
- LangChain.js 완벽 가이드 — LLM 애플리케이션 개발·RAG·Next.js
- Pinecone 완벽 가이드 | Vector Database·임베딩·유사도 검색·RAG·실전 활용
이 글에서 다루는 키워드 (관련 검색어)
RAG, LLM, AI, Vector Database, Embedding, LangChain, OpenAI, Retrieval, Augmented Generation 등으로 검색하시면 이 글이 도움이 됩니다.