RAG 시스템 최적화: 성능을 결정짓는 핵심 요소 완벽 가이드
📌 들어가며
안녕하세요, AI 개발자 여러분! 오늘은 취업 포트폴리오에 강력한 임팩트를 줄 수 있는 RAG(Retrieval-Augmented Generation) 시스템 구현의 핵심 요소들을 깊이 있게 살펴보려 합니다. 단순히 LLM API를 호출하는 것보다 한 단계 더 나아가, 여러분만의 데이터로 정확하고 신뢰할 수 있는 AI 애플리케이션을 구축하는 방법을 공유합니다.
이 글은 특히:
- 취업을 준비 중인 개발자
- RAG 기반 프로젝트를 시작하려는 분들
- 차별화된 사이드 프로젝트를 찾고 있는 개발자
분들에게 실질적인 도움이 될 것입니다.
영상 링크 : https://youtu.be/j6i94AC6Prs
🔍 RAG란 무엇이며 왜 중요한가?
RAG는 Large Language Model(LLM)이 외부 지식(문서, DB 등)에 접근하여 더 정확하고 최신 정보를 바탕으로 응답을 생성하는 방식입니다. 기존의 LLM은 학습된 데이터에만 의존하기 때문에 다음과 같은 한계가 있습니다:
- 학습 시점 이후의 정보 부재
- 전문적이거나 특정 도메인의 정보 부족
- 환각(hallucination) 문제
RAG는 이런 한계를 극복하고 여러분의 데이터를 활용하여 정확하고 신뢰할 수 있는 응답을 생성할 수 있게 해줍니다.
🛠️ RAG 성능을 결정짓는 3가지 핵심 요소
RAG 시스템의 성능은 세 가지 핵심 요소의 최적화에 달려 있습니다. 각 요소를 제대로 이해하고 설정하는 것만으로도 여러분의 프로젝트는 일반적인 RAG 시스템과 차별화될 수 있습니다.
1️⃣ 임베딩 모델 (Embedding Model)
임베딩 모델은 텍스트를 벡터로 변환하여 의미적 유사성을 계산할 수 있게 해주는 RAG의 두뇌와 같은 존재입니다.
📊 임베딩 모델의 중요성
저는 PDF 문서 기반 Q&A 시스템을 개발하는 과정에서 두 가지 다른 임베딩 모델을 테스트했습니다:
사용한 PDF 문서
- Hugging Face
sentence-transformers/all-mpnet-base-v2
- OpenAI
text-embedding-3-small
같은 질문, 같은 문서, 같은 LLM 모델을 사용했음에도 결과는 극적으로 달랐습니다.
🔄 실제 성능 비교
💻 임베딩 모델 변경 코드
# Hugging Face 임베딩 모델 설정
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/all-mpnet-base-v2")
Settings.embed_model = embed_model
# OpenAI 임베딩 모델 설정
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding(
model="text-embedding-3-small",
api_key=openai_api_key,
embed_batch_size=10, # 비용 최적화를 위한 배치 처리
dimensions=1536
)
Settings.embed_model = embed_model
🔑 취업 준비생을 위한 팁
면접에서 "인공지능 프로젝트에서 가장 중요한 결정은 무엇이었나요?"라는 질문을 받는다면, 임베딩 모델 선택과 비교 실험을 통한 성능 최적화 경험을 공유하세요. 데이터 기반의 의사결정 능력을 보여줄 수 있는 인상적인 사례가 됩니다.
2️⃣ 청크 사이즈 (Chunk Size)
청크 사이즈는 원본 문서를 얼마나 작은 조각으로 나눌지 결정하는 중요한 파라미터입니다.
📏 청크 사이즈의 영향
청크 사이즈는 다음과 같은 트레이드오프를 갖습니다:
- 작은 청크 (256 토큰): 검색 정밀도가 높지만, 문맥 유지가 어려움
- 큰 청크 (512-1024 토큰): 풍부한 문맥이 유지되지만, 불필요한 정보가 포함될 수 있음
한국어와 같이 문맥 의존성이 높은 언어는 더 큰 청크 사이즈가 유리한 경향이 있습니다.
📈 실험 결과
퇴직연금 문서를 다양한 청크 사이즈로 분할하여 테스트한 결과:
청크 사이즈 | 정확한 답변 비율 | 메모리 사용량 | 벡터 DB 항목 수 |
---|---|---|---|
256 토큰 | 65% | 낮음 | 많음 (약 450개) |
512 토큰 | 82% | 중간 | 중간 (약 225개) |
1024 토큰 | 78% | 높음 | 적음 (약 115개) |
한국어 퇴직연금 문서의 경우 512 토큰이 최적의 균형점이었습니다.
💻 청크 사이즈 설정 코드
from llama_index.node_parser import SentenceSplitter
# 문장 단위로 분할하며 청크 사이즈 조정
text_splitter = SentenceSplitter(
chunk_size=512,
chunk_overlap=50,
separator="\n",
paragraph_separator="\n\n",
)
# 문서 분할 적용
nodes = text_splitter.get_nodes_from_documents(documents)
🚀 프로젝트 차별화 포인트
GitHub에서 fork하여 수정할 수 있는 오픈소스 RAG 프로젝트 대부분은 영어 문서에 최적화된 청크 사이즈(보통 256 토큰)를 사용합니다. 한국어 문서에 특화된 청크 사이즈 최적화 코드를 PR하면 오픈소스 기여 실적을 쌓을 수 있습니다.
3️⃣ 청크 오버랩 (Chunk Overlap)
청크 오버랩은 인접한 청크 간에 겹치는 텍스트의 양을 결정합니다.
🔄 오버랩의 중요성
오버랩이 없으면 다음과 같은 문제가 발생합니다:
- 문장이 중간에 잘려 의미 손실
- 청크 경계에 걸친 정보 검색 불가능
- 문맥 연결성 부재
📊 오버랩 설정 실험
다양한 오버랩 비율로 테스트한 결과:
오버랩 비율 | 정확한 답변 비율 | 스토리지 사용량 증가 |
---|---|---|
0 (오버랩 없음) | 65% | 0% |
10% (약 50 토큰) | 75% | +10% |
25% (약 128 토큰) | 86% | +25% |
50% (약 256 토큰) | 88% | +50% |
25% 오버랩이 성능과 효율성 측면에서 최적의 균형점이었습니다.
💻 청크 오버랩 설정 코드
from llama_index.node_parser import RecursiveCharacterTextSplitter
# 청크 크기의 25%를 오버랩으로 설정
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 토큰 대신 문자 수로 지정
chunk_overlap=128, # 약 25% 오버랩
separators=["\n\n", "\n", ". ", " ", ""]
)
nodes = text_splitter.get_nodes_from_documents(documents)
💡 실무 개발 인사이트
면접에서 "RAG 시스템 개발 시 마주친 도전과 해결책은 무엇이었나요?"라는 질문에 청크 오버랩의 최적화 실험을 통해 사용자 질문 응답 정확도를 20% 이상 향상시킨 경험을 공유하면 깊이 있는 기술적 이해를 증명할 수 있습니다.
🔬 세 가지 요소의 조합과 최적화
이 세 가지 요소는 독립적으로 작용하지 않고 서로 영향을 미칩니다.
최적 조합 실험
다양한 조합을 실험한 결과, 다음의 설정이 한국어 문서에 가장 효과적이었습니다:
- 임베딩 모델: OpenAI
text-embedding-3-small
- 청크 사이즈: 512 토큰 (한국어의 경우)
- 청크 오버랩: 25% (128 토큰)
성능 측정 결과
조합 | 정확도 | 응답 생성 시간 | 비용 효율성 |
---|---|---|---|
HF 임베딩 + 256 토큰 + 0% 오버랩 | 45% | 빠름 | 매우 높음 |
HF 임베딩 + 512 토큰 + 25% 오버랩 | 62% | 중간 | 높음 |
OpenAI 임베딩 + 256 토큰 + 0% 오버랩 | 72% | 빠름 | 중간 |
OpenAI 임베딩 + 512 토큰 + 25% 오버랩 | 88% | 중간 | 중간 |
OpenAI 임베딩 + 1024 토큰 + 50% 오버랩 | 85% | 느림 | 낮음 |
💻 전체 소스 코드 : app.py
@st.cache_resource # 캐싱 추가
def get_huggingface_token():
"""환경 변수 또는 streamlit secrets에서 토큰을 가져오는 함수"""
token = os.environ.get("HUGGINGFACE_API_TOKEN")
if token is None: # 환경 변수에 없으면 streamlit secrets에서 시도
try:
token = st.secrets["HUGGINGFACE_API_TOKEN"]
except:
st.error("HUGGINGFACE_API_TOKEN이 설정되지 않았습니다.")
return None
return token
@st.cache_resource # 캐싱 추가
def get_openai_api_key():
"""환경 변수 또는 streamlit secrets에서 OpenAI API 키를 가져오는 함수"""
api_key = os.environ.get("OPENAI_API_KEY")
if api_key is None: # 환경 변수에 없으면 streamlit secrets에서 시도
try:
api_key = st.secrets["OPENAI_API_KEY"]
except:
st.error("OPENAI_API_KEY가 설정되지 않았습니다.")
return None
return api_key
@st.cache_resource # 캐싱 추가
def initialize_models():
"""모델 초기화 함수"""
# model_name = "mistralai/Mistral-7B-Instruct-v0.2"
model_name = "google/gemma-2-2b-it"
# Hugging Face 토큰 가져오기
hf_token = get_huggingface_token()
if hf_token is None:
return None, None
# OpenAI API 키 가져오기
openai_api_key = get_openai_api_key()
if openai_api_key is None:
st.error("OpenAI API 키가 없습니다. API 키를 설정해주세요.")
return None, None
llm = HuggingFaceInferenceAPI(
model_name=model_name,
max_new_tokens=512,
temperature=0.01,
model_type="text_completion",
system_prompt="당신은 주어진 문서 내용만 기반으로 답변하는 AI 어시스턴트입니다. 검색된 문서 조각들 내용을 활용하여 질문에 답변하세요. 검색된 문서에 관련 정보가 있다면 반드시 그 내용을 기반으로 답변을 생성하세요. 문서에 명확히 언급된 내용을 우선으로 답변하고, 문서에 있는 내용만 참고하여 한국어로 명확하고 정확하게 답변해주세요. 답변을 생성할 때 관련 문서 내용을 직접 인용하고, 문서에 없는 내용은 추가하지 마세요.",
token=hf_token
)
# OpenAI 임베딩 모델
embed_model = OpenAIEmbedding(
model="text-embedding-3-small", # OpenAI의 최신 임베딩 모델 사용
api_key=openai_api_key,
embed_batch_size=10, # 배치 처리 크기 설정
dimensions=1536 # 임베딩 벡터 차원 수
)
# 전역 설정 업데이트
Settings.llm = llm
Settings.embed_model = embed_model
return llm, embed_model
@st.cache_resource # 캐싱 추가
def process_pdf_and_create_index(pdf_path):
"""PDF 파일을 처리하고 인덱스를 생성하는 함수"""
# PDF 파일 로드 및 텍스트 추출
pdf_reader = PdfReader(pdf_path)
text_chunks = []
# 모든 페이지의 텍스트 추출
for page_num, page in enumerate(pdf_reader.pages):
text = page.extract_text()
if text.strip(): # 빈 페이지 제외
# 페이지 번호 정보 포함
text_with_metadata = f"[페이지 {page_num + 1}] {text}"
text_chunks.append(text_with_metadata)
# LlamaIndex Document 객체 생성
documents = [Document(text=chunk) for chunk in text_chunks]
# 문서 분할 (청킹)
node_parser = SentenceSplitter(
chunk_size=512, # 청크 크기
chunk_overlap=50, # 오버랩
paragraph_separator="\n\n",
secondary_chunking_regex="(?<=\. )",
include_metadata=True,
include_prev_next_rel=True # 이전/다음 청크 관계 포함
)
# 모델 초기화 및 설정
llm, embed_model = initialize_models()
# Settings 설정
Settings.llm = llm
Settings.embed_model = embed_model
Settings.node_parser = node_parser
# 노드 파싱
nodes = node_parser.get_nodes_from_documents(documents)
# 인덱스 생성
index = VectorStoreIndex(nodes)
# 인덱스 저장
index_dir = "pdf_index_storage"
os.makedirs(index_dir, exist_ok=True)
index.storage_context.persist(persist_dir=index_dir)
return index
@st.cache_resource
def load_saved_index():
"""저장된 인덱스 로드"""
index_dir = "pdf_index_storage"
if os.path.exists(index_dir):
# 인덱스 로드
storage_context = StorageContext.from_defaults(persist_dir=index_dir)
index = load_index_from_storage(storage_context)
return index
return None
def create_optimized_query_engine(index):
"""최적화된 쿼리 엔진 생성"""
# 검색기 설정 (Top-k 파라미터 조정)
retriever = VectorIndexRetriever(
index=index,
similarity_top_k=5, # 관련성 높은 문서 5개로 조정
vector_store_query_mode="default"
)
# 유사도 임계값 설정
node_postprocessors = [SimilarityPostprocessor(similarity_cutoff=0.2)]
# 쿼리 엔진 생성
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
node_postprocessors=node_postprocessors,
response_mode="compact", # 모든 관련 문서를 한번에 고려
response_kwargs={"verbose": True} # 디버깅을 위한 verbose 모드
)
return query_engine
def main():
st.title('PDF 문서 기반 질의응답 시스템')
st.write("선진기업복지 업무메뉴얼을 기반으로 질의응답을 제공합니다.")
# 모델 초기화
llm, embed_model = initialize_models()
if llm is None or embed_model is None:
st.error("모델 초기화에 실패했습니다. API 토큰을 확인해주세요.")
return
# PDF 경로 설정
pdf_path = "선진기업복지_업무매뉴얼.pdf"
# 인덱스 로드 또는 생성
index = load_saved_index()
if index is None:
with st.spinner('PDF 파일을 처리하고 인덱스를 생성하고 있습니다...'):
index = process_pdf_and_create_index(pdf_path)
if index is not None:
# 전역 설정 업데이트
Settings.llm = llm
Settings.embed_model = embed_model
# 최적화된 쿼리 엔진 생성
query_engine = create_optimized_query_engine(index)
# 사용자 입력 받기
user_question = st.text_input("질문을 입력해주세요:")
if user_question:
try:
with st.spinner('답변을 생성하고 있습니다...'):
# 검색된 문서 조각 표시 설정
st.subheader("검색된 관련 문서:")
with st.expander("문서 내용 보기", expanded=False):
retriever = VectorIndexRetriever(
index=index,
similarity_top_k=5
)
nodes = retriever.retrieve(user_question)
for i, node in enumerate(nodes):
st.markdown(f"**관련 문서 {i+1}** (유사도: {node.score:.4f})")
st.text(node.node.text[:500] + "..." if len(node.node.text) > 500 else node.node.text)
st.markdown("---")
# 질문에 대한 답변 생성
response = query_engine.query(user_question)
st.subheader("답변:")
st.info(response.response)
# 소스 문서 정보 표시
if hasattr(response, 'source_nodes') and response.source_nodes:
st.subheader("참조 소스:")
for i, source_node in enumerate(response.source_nodes):
with st.expander(f"소스 {i+1}", expanded=True): # 소스를 기본적으로 확장하여 표시
st.text(source_node.node.text[:500] + "..." if len(source_node.node.text) > 500 else source_node.node.text)
st.markdown(f"**유사도 점수**: {source_node.score:.4f}")
except Exception as e:
st.error(f"답변 생성 중 오류가 발생했습니다: {str(e)}")
st.error("잠시 후 다시 시도해주세요.")
if __name__ == "__main__":
main()
🚀 취업 준비생을 위한 프로젝트 활용법
포트폴리오 차별화 전략
- A/B 테스트 결과 시각화
- 다양한 임베딩 모델, 청크 사이즈, 오버랩 조합의 성능 비교 그래프
- 사용자 쿼리별 정확도 측정 방법론 제시
- 도메인 특화 최적화
- 법률, 의료, 금융 등 특정 도메인에 맞춘 최적 파라미터 발견
- 도메인별 성능 차이 분석 및 이유 설명
- 비용 효율성 분석
- API 비용 vs 성능 트레이드오프 분석
- ROI(Return on Investment) 관점에서의 의사결정 프로세스
기술 면접 대비 포인트
다음 질문들에 대한 답변을 준비하세요:
- "RAG 시스템에서 가장 중요한 성능 요소는 무엇이라고 생각하나요?"
- "임베딩 모델 선택 시 고려한 요소는 무엇인가요?"
- "청크 사이즈와 오버랩을 어떻게 최적화했나요?"
- "한국어 문서 처리에서 마주친 특별한 도전과 해결 방법은?"
💡 사이드 프로젝트 아이디어
이 기술을 활용한 사이드 프로젝트 아이디어:
- 개인 문서 기반 AI 비서
- 학습 노트, 개인 문서를 기반으로 질문에 답변하는 서비스
- 개인화된 지식 검색 엔진
- 전문 도메인 Q&A 시스템
- 법률 문서, 의료 정보, 학술 논문 기반 질의응답 시스템
- 전문가를 위한 지식 검색 도구
- 다국어 지원 문서 분석기
- 여러 언어로 된 문서를 동시에 처리하는 RAG 시스템
- 언어별 최적 파라미터 자동 감지 및 적용
📚 참고 자료
궁금한점 : macro@prag-ai.com 으로 주세요~
'DeepLearining' 카테고리의 다른 글
Facebook MMS 모델로 한국어 TTS 웹 서비스 개발하기 (2) | 2025.03.08 |
---|---|
TheoremExplainAgent: Towards Multimodal Explanations for LLM Theorem Understanding (0) | 2025.02.28 |
GHOST 2.0: generative high-fidelity one shot transfer of heads (0) | 2025.02.28 |
Kanana: Compute-efficient Bilingual Language Model (2) | 2025.02.28 |
딥러닝 하드웨어 : GPU와 TPU란? (0) | 2025.02.24 |