Contents
see ListRetrieval-Augmented Generation(RAG)은 LLM의 환각(hallucination) 문제를 해결하는 가장 실용적인 방법이다. 사내 문서, 기술 위키, 고객 FAQ 등 자체 데이터를 기반으로 정확한 답변을 생성하는 RAG 시스템을 LangChain과 벡터 데이터베이스를 활용하여 구축하는 방법을 단계별로 설명한다.
RAG 아키텍처 개요
RAG 시스템은 크게 3단계로 구성된다.
- 문서 수집 및 청킹(Chunking): 원본 문서를 적절한 크기로 분할
- 임베딩 및 인덱싱: 텍스트를 벡터로 변환하여 벡터DB에 저장
- 검색 및 생성: 쿼리와 유사한 문서를 검색하고 LLM에 컨텍스트로 전달
환경 설정
# 필요한 패키지 설치
pip install langchain langchain-community langchain-anthropic
pip install chromadb sentence-transformers
pip install unstructured pypdf docx2txt
# 환경 변수 설정
export ANTHROPIC_API_KEY="your-api-key"
1단계: 문서 로딩 및 청킹
다양한 형식의 문서를 로드하고 적절한 크기로 분할하는 것이 RAG 성능의 핵심이다.
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
Docx2txtLoader,
DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os
# 다양한 문서 형식 로딩
def load_documents(docs_dir: str):
"""디렉토리에서 PDF, TXT, DOCX 문서를 모두 로드한다."""
loaders = []
# PDF 파일
pdf_loader = DirectoryLoader(
docs_dir, glob="**/*.pdf", loader_cls=PyPDFLoader
)
loaders.append(pdf_loader)
# 텍스트 파일
txt_loader = DirectoryLoader(
docs_dir, glob="**/*.txt", loader_cls=TextLoader
)
loaders.append(txt_loader)
# Word 문서
docx_loader = DirectoryLoader(
docs_dir, glob="**/*.docx", loader_cls=Docx2txtLoader
)
loaders.append(docx_loader)
all_docs = []
for loader in loaders:
try:
docs = loader.load()
all_docs.extend(docs)
except Exception as e:
print(f"로딩 오류: {e}")
return all_docs
# 청킹 설정 - 성능에 가장 큰 영향을 미치는 부분
def chunk_documents(documents, chunk_size=1000, chunk_overlap=200):
"""문서를 재귀적으로 분할한다.
chunk_size: 각 청크의 최대 문자 수
chunk_overlap: 청크 간 겹치는 문자 수 (문맥 유지)
"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", ".", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"원본 문서 {len(documents)}개 -> {len(chunks)}개 청크로 분할")
return chunks
# 실행
docs = load_documents("./company_docs")
chunks = chunk_documents(docs)
2단계: 벡터 임베딩 및 저장
ChromaDB를 사용하여 로컬에서 벡터 검색을 수행한다. 프로덕션에서는 Pinecone, Weaviate, Qdrant 등의 관리형 벡터DB를 고려할 수 있다.
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# 임베딩 모델 설정
# 한국어 문서에는 multilingual 모델 사용
embeddings = HuggingFaceEmbeddings(
model_name="intfloat/multilingual-e5-large",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
# ChromaDB에 저장
PERSIST_DIR = "./chroma_db"
def create_vectorstore(chunks, persist_directory=PERSIST_DIR):
"""청크를 벡터로 변환하여 ChromaDB에 저장한다."""
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_directory,
collection_name="company_docs"
)
print(f"{len(chunks)}개 청크를 벡터DB에 저장 완료")
return vectorstore
# 기존 DB 로드
def load_vectorstore(persist_directory=PERSIST_DIR):
"""저장된 벡터DB를 로드한다."""
return Chroma(
persist_directory=persist_directory,
embedding_function=embeddings,
collection_name="company_docs"
)
vectorstore = create_vectorstore(chunks)
3단계: RAG 체인 구성
from langchain_anthropic import ChatAnthropic
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# LLM 설정
llm = ChatAnthropic(
model="claude-sonnet-4-20250514",
temperature=0,
max_tokens=4096
)
# 커스텀 프롬프트
qa_prompt = PromptTemplate(
input_variables=["context", "question"],
template="""다음 문서 내용을 기반으로 질문에 답변하세요.
문서에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답변하세요.
참고 문서:
{context}
질문: {question}
답변:"""
)
# 검색 설정
retriever = vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance
search_kwargs={
"k": 5, # 반환할 문서 수
"fetch_k": 20, # MMR 후보 문서 수
"lambda_mult": 0.7 # 다양성 vs 관련성 (0~1)
}
)
# RAG 체인 생성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
chain_type_kwargs={"prompt": qa_prompt},
return_source_documents=True
)
# 질의
result = qa_chain.invoke({"query": "우리 회사의 휴가 정책은?"})
print("답변:", result["result"])
print("\n참조 문서:")
for doc in result["source_documents"]:
print(f" - {doc.metadata.get("source", "unknown")}")
4단계: 성능 최적화
하이브리드 검색
벡터 검색과 키워드 검색을 결합하면 정확도가 크게 향상된다.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# BM25 키워드 검색기
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# 벡터 검색기
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 하이브리드 (앙상블) 검색기
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6] # 키워드 40% + 벡터 60%
)
Reranking
# Cohere Reranker로 검색 결과 재정렬
from langchain.retrievers import ContextualCompressionRetriever
from langchain_community.document_compressors import CohereRerank
compressor = CohereRerank(model="rerank-v3.5", top_n=3)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=ensemble_retriever
)
프로덕션 고려사항
- 청크 크기 실험: 문서 유형에 따라 500~2000자 사이에서 최적값을 찾을 것
- 메타데이터 활용: 문서 출처, 날짜, 카테고리 등을 메타데이터로 저장하여 필터링에 활용
- 주기적 재인덱싱: 문서 변경 시 자동으로 벡터DB를 업데이트하는 파이프라인 구축
- 평가 체계: RAGAS 프레임워크로 faithfulness, relevancy, precision 등을 정량 평가
RAG는 LLM을 실무에 적용하는 가장 현실적인 방법이다. 위 코드를 기반으로 자사 문서에 맞게 청킹 전략과 검색 파라미터를 조정하면 높은 정확도의 사내 AI 검색 시스템을 구축할 수 있다.