상세 컨텐츠

본문 제목

RAG 미니 프로젝트 - 모델 성능 개선

본문

 

오늘도 개발을 할 수 있음에 재밌다.

 

처음 기획은 논문을 볼 때 매번 GPT나 MoonLight에 업로드해서 보는게 귀찮아서였다.

그래서 내가 권한을 준 폴더 안의 pdf를 열면 자동으로 모델을 로드하고 청킹을 진행한 후 학습을 도와주는 에이전트를 만들기로 했다.

지금은 일단 streamlit에 pdf를 로드한 후 챗봇 형식으로 대화를 한다.

추후 mac 환경에서 바로 사용할 수 있게 디벨롭 할거다:

 

개발을 하면서 문제가 발생했다.

이 과정을 기록하려고 한다.

 


상황 : 저자를 묻는 질문에 대답도 못하는 챗봇.

 

아주 간단한 질문인데도 내가 만든 챗봇은 대답도 못 한다.

프로젝트 설계를 잘 못한 것일까? 차근차근 원인을 찾아나섰다.

 

원인 01 : English에 대해 한국어로 질문한다?

번역 오류

 

영문 PDF를 DB에 인덱싱 후 저장한 뒤 한국어로 질문을 하니 당연히 RAG의 첫 단계인 Retrival(검색)부터 막혔다.

기본적인 실수다..

 

✅ 해결 : 인덱싱 모델 변경 -> paraphrase-multilingual-MiniLM-L12-v2

 

⏭️ 모델비교

all-MiniLM-L6-v2 영어 전용 22M 384 ~90MB 초기 모델, 교체 대상
paraphrase-multilingual-MiniLM-L12-v2 50+ 언어 118M 384 ~470MB 채택
paraphrase-multilingual-mpnet-base-v2 50+ 언어 278M 768 ~1.1GB 품질 높지만 RAM 2.4배
intfloat/multilingual-e5-small 100+ 언어 118M 384 ~470MB query: 프리픽스 필수
intfloat/multilingual-e5-base 100+ 언어 278M 768 ~1.1GB 고품질, 느림

 

 

초기 all-MiniLM-L6-v2 사용 시 발생한 실패 사례:

[사용자 질문]  "이 논문의 저자는 누구인가?"  (한국어)
                          │
                          ▼
          all-MiniLM-L6-v2로 임베딩
          한국어 쿼리 벡터 ≠ 영어 문서 벡터   ← 의미 공간 불일치
          FAISS 유사도 낮음 + BM25 토큰 히트 0
                          │
                          ▼
          LLM이 빈 컨텍스트를 받아 답변 불가

 

paraphrase-multilingual-MiniLM-L12-v2는 50개 이상 언어의 병렬 코퍼스(같은 의미의 다국어 문장 쌍)로 학습되어 언어가 달라도 같은 의미의 문장을 가까운 벡터로 매핑한다.

 

"이 논문의 저자는 누구인가?" (한국어)  →  [0.21, -0.44, ...]
"Who are the authors of this paper?"  →  [0.23, -0.41, ...]
                    두 벡터 간 코사인 유사도: ~0.94  ← FAISS 검색 성공

 

paraphrase-multilingual-mpnet-base-v2보다 RAM이 절반이고 속도가 빠르면서 벡터 차원(384)이 동일하여 FAISS 인덱스 구조 변경 없이 교체 가능했다.

 

 


📌 문제 2 : 초기 로딩이 너무 오래걸린다.

어떤 상황이었나

Streamlit 앱에서 PDF를 업로드하면 PDFChatbot 인스턴스가 새로 생성.

이 과정에서 임베딩 모델이 매번 다시 로드되고 있다.

# rag_engine.py — 변경 전
embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    model_kwargs={"device": "cpu"},
)

이 한 줄이 실행될 때마다 118MB 모델을 디스크에서 메모리로 로드하고 있었다.

[상황 재현]
PDF A 업로드 → 모델 로드(~12s) + FAISS 빌드(~15s) = 27s
PDF B 업로드 → 모델 로드(~12s) + FAISS 빌드(~15s) = 27s

👉 같은 모델인데 매번 다시 로드하는 건 명백한 낭비이다.


해결 방법

1️⃣ session_state 활용

if "embeddings" not in st.session_state:
    st.session_state.embeddings = HuggingFaceEmbeddings(...)

→ 탭 새로고침 시 초기화되는 한계 존재

 

2️⃣ @st.cache_resource (선택)

@st.cache_resource
def _load_embeddings():
    return HuggingFaceEmbeddings(...)

 프로세스 레벨 캐싱 (가장 안정적)

 

3️⃣ pickle 저장

→ HuggingFace 자체 캐시와 중복, 실효성 낮음


최종 선택: @st.cache_resource

  • ✔ 프로세스 단위 1회 로드
  • ✔ 코드 변경 최소
  • ✔ 재사용 안정성 높음

 

앱 시작 시 워밍업 적용

def main():
    _get_embeddings()  # 최초 1회 로드

 

초기 로딩 UX 개선

with st.spinner("임베딩 모델 로딩 중..."):
    embeddings = _load_embeddings()

👉 이후에는 캐시에서 즉시 반환


📌 별도 개선 : index 캐싱

어떤 상황이었나

faiss_db = FAISS.from_documents(docs, embeddings)

👉 메모리에만 존재 → 앱 종료 시 삭제

[상황 재현]
오전: PDF 업로드 → FAISS 빌드(~15s)
저녁: 같은 PDF → 다시 빌드(~15s)

해결 방법 탐색

1️⃣ FAISS save/load (선택)

faiss_db.save_local("./cache")
faiss_db = FAISS.load_local("./cache", embeddings)

✔ 공식 지원 기능
✔ 빠르고 안정적

 

2️⃣ Chroma DB

→ 구조 변경 필요 (과도한 리팩토링)

 

3️⃣ pickle

→ 유지보수 리스크


최종 선택: FAISS + MD5 캐시

pdf_hash = hashlib.md5(pdf_bytes).hexdigest()[:12]
cache_path = f"./cache/{pdf_hash}"

👉 핵심 포인트

  • 파일명이 아닌 내용 기반 캐싱
  • 같은 파일 → 재사용
  • 내용 변경 → 자동 재빌드

🚀 최종 성능 개선 결과

 

시나리오변경 전변경 후개선율

최초 실행 ~27s ~27s -
두 번째 PDF ~27s ~15s 44%
앱 재시작 후 ~27s ~2s 93%

💡 마무리

이번 개선에서 얻은 핵심은 단순하다.

  • 결과물의 오류 -> 사용한 모델과 방식을 재검토해라 [설계]
  • "느리다" → 원인을 분해하라
  • 모델 문제 vs 인덱스 문제 → 분리 해결
  • 기존 라이브러리 기능을 먼저 활용하라

 

다음 단계는 macOS Quick Action 연동입니다.
PDF를 우클릭하면 챗봇이 바로 실행되는 UX를 만들어보겠습니다.

관련글 더보기