RAG란 무엇인가?
RAG(Retrieval-Augmented Generation)는 AI가 답변을 생성하기 전에, 관련 문서를 먼저 검색하여 참고 자료로 활용하는 기술입니다. 일반적인 LLM은 학습 데이터에만 의존하지만, RAG를 적용하면 최신 정보나 개인 문서를 기반으로 정확한 답변을 생성할 수 있습니다.
RAG의 동작 원리
사용자 질문 → 문서 검색(Retrieval) → 관련 문서 추출 → LLM에 전달 → 답변 생성(Generation)
왜 로컬 RAG인가?
| 항목 | 클라우드 LLM | 로컬 LLM (Ollama) |
|---|---|---|
| 비용 | API 호출당 과금 | 무료 |
| 프라이버시 | 데이터 외부 전송 | 로컬에서만 처리 |
| 속도 | 네트워크 의존 | GPU 성능에 의존 |
| 모델 선택 | 제공사 모델만 | 오픈소스 모델 자유 선택 |
환경 설정
1. Ollama 설치
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh
# 모델 다운로드
ollama pull llama3
ollama pull nomic-embed-text
2. Python 패키지 설치
pip install langchain langchain-community chromadb
pip install langchain-ollama
pip install pypdf docx2txt
기본 구조 설계
RAG 시스템의 핵심 구성 요소는 세 가지입니다.
# rag_system.py
from langchain_ollama import OllamaLLM, OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
Step 1: 문서 로딩
다양한 형식의 문서를 읽어 텍스트로 변환합니다.
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
Docx2txtLoader,
DirectoryLoader,
)
import os
def load_documents(directory_path):
"""디렉토리 내 모든 문서를 로드합니다."""
documents = []
for filename in os.listdir(directory_path):
filepath = os.path.join(directory_path, filename)
if filename.endswith('.pdf'):
loader = PyPDFLoader(filepath)
elif filename.endswith('.txt'):
loader = TextLoader(filepath, encoding='utf-8')
elif filename.endswith('.docx'):
loader = Docx2txtLoader(filepath)
else:
continue
documents.extend(loader.load())
print(f"로드 완료: {filename}")
print(f"총 {len(documents)}개 문서 로드됨")
return documents
Step 2: 텍스트 분할 (Chunking)
문서를 적절한 크기의 조각으로 나눕니다. 청크 크기는 RAG 성능에 큰 영향을 미칩니다.
def split_documents(documents):
"""문서를 검색에 적합한 크기로 분할합니다."""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 각 청크의 최대 문자 수
chunk_overlap=200, # 청크 간 겹치는 문자 수
length_function=len,
separators=["\n\n", "\n", ".", " "],
)
chunks = text_splitter.split_documents(documents)
print(f"총 {len(chunks)}개 청크 생성됨")
return chunks
청크 크기 선택 가이드
| 청크 크기 | 장점 | 단점 | 적합한 용도 |
|---|---|---|---|
| 200-500 | 정밀한 검색 | 문맥 부족 | FAQ, 사전 |
| 500-1000 | 균형 잡힌 성능 | - | 일반 문서 |
| 1000-2000 | 풍부한 문맥 | 검색 정밀도 저하 | 논문, 보고서 |
Step 3: 벡터 데이터베이스 생성
텍스트를 벡터(숫자 배열)로 변환하여 유사도 검색이 가능한 데이터베이스에 저장합니다.
def create_vector_store(chunks, persist_directory="./chroma_db"):
"""청크를 벡터로 변환하여 ChromaDB에 저장합니다."""
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_directory,
)
print(f"벡터 DB 생성 완료: {persist_directory}")
return vector_store
def load_vector_store(persist_directory="./chroma_db"):
"""기존 벡터 DB를 로드합니다."""
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vector_store = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings,
)
return vector_store
Step 4: RAG 체인 구성
검색과 생성을 연결하는 체인을 만듭니다.
from langchain.prompts import PromptTemplate
def create_rag_chain(vector_store):
"""RAG 체인을 생성합니다."""
llm = OllamaLLM(model="llama3", temperature=0.3)
# 검색기 설정 (상위 4개 관련 문서 검색)
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 4},
)
# 프롬프트 템플릿
prompt_template = PromptTemplate(
template="""다음 참고 자료를 바탕으로 질문에 답변하세요.
참고 자료에 답이 없으면 "관련 정보를 찾을 수 없습니다"라고 답하세요.
참고 자료:
{context}
질문: {question}
답변:""",
input_variables=["context", "question"],
)
# RetrievalQA 체인 생성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt_template},
)
return qa_chain
Step 5: 전체 통합
def main():
docs_path = "./my_documents"
# 1. 문서 로딩 및 처리
documents = load_documents(docs_path)
chunks = split_documents(documents)
# 2. 벡터 DB 생성
vector_store = create_vector_store(chunks)
# 3. RAG 체인 생성
qa_chain = create_rag_chain(vector_store)
# 4. 대화 루프
print("\nRAG 시스템이 준비되었습니다. 질문을 입력하세요.")
print("종료하려면 'quit'을 입력하세요.\n")
while True:
question = input("질문: ")
if question.lower() == 'quit':
break
result = qa_chain.invoke({"query": question})
print(f"\n답변: {result['result']}")
# 참고 문서 출력
print("\n참고 문서:")
for doc in result['source_documents']:
source = doc.metadata.get('source', '알 수 없음')
print(f" - {source}")
print()
if __name__ == "__main__":
main()
성능 최적화 팁
1. 임베딩 모델 선택
# 한국어 문서가 많은 경우
embeddings = OllamaEmbeddings(model="bge-m3")
# 영어 중심 문서
embeddings = OllamaEmbeddings(model="nomic-embed-text")
2. 검색 방식 비교
# 유사도 검색 (기본)
retriever = vector_store.as_retriever(search_type="similarity")
# MMR (Maximal Marginal Relevance) - 다양성 확보
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 10},
)
3. 대화 기록 유지
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True,
)
결론
로컬 RAG 시스템을 구축하면 프라이버시를 지키면서도 개인 문서를 AI와 대화할 수 있습니다. LangChain은 각 컴포넌트를 모듈화하여 유연한 구성이 가능하고, Ollama는 다양한 오픈소스 모델을 손쉽게 교체할 수 있게 해줍니다.
실제로 이 기술 스택을 활용하여 Odin 프로젝트를 개발했으며, 내 컴퓨터의 모든 문서를 이해하는 AI 비서를 만들 수 있었습니다.