오늘도 개발을 할 수 있음에 재밌다.
처음 기획은 논문을 볼 때 매번 GPT나 MoonLight에 업로드해서 보는게 귀찮아서였다.
그래서 내가 권한을 준 폴더 안의 pdf를 열면 자동으로 모델을 로드하고 청킹을 진행한 후 학습을 도와주는 에이전트를 만들기로 했다.
지금은 일단 streamlit에 pdf를 로드한 후 챗봇 형식으로 대화를 한다.
추후 mac 환경에서 바로 사용할 수 있게 디벨롭 할거다:
개발을 하면서 문제가 발생했다.
이 과정을 기록하려고 한다.

아주 간단한 질문인데도 내가 만든 챗봇은 대답도 못 한다.
프로젝트 설계를 잘 못한 것일까? 차근차근 원인을 찾아나섰다.

영문 PDF를 DB에 인덱싱 후 저장한 뒤 한국어로 질문을 하니 당연히 RAG의 첫 단계인 Retrival(검색)부터 막혔다.
기본적인 실수다..
| 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로 임베딩
한국어 쿼리 벡터 ≠ 영어 문서 벡터 ← 의미 공간 불일치
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 인덱스 구조 변경 없이 교체 가능했다.


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 자체 캐시와 중복, 실효성 낮음
앱 시작 시 워밍업 적용
def main():
_get_embeddings() # 최초 1회 로드
초기 로딩 UX 개선
with st.spinner("임베딩 모델 로딩 중..."):
embeddings = _load_embeddings()
👉 이후에는 캐시에서 즉시 반환
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
→ 유지보수 리스크
pdf_hash = hashlib.md5(pdf_bytes).hexdigest()[:12]
cache_path = f"./cache/{pdf_hash}"
👉 핵심 포인트

시나리오변경 전변경 후개선율
| 최초 실행 | ~27s | ~27s | - |
| 두 번째 PDF | ~27s | ~15s | 44% |
| 앱 재시작 후 | ~27s | ~2s | 93% |
이번 개선에서 얻은 핵심은 단순하다.
다음 단계는 macOS Quick Action 연동입니다.
PDF를 우클릭하면 챗봇이 바로 실행되는 UX를 만들어보겠습니다.
| RAG 미니 프로젝트 : 논문 읽어주는 챗봇 만들기 (0) | 2026.02.25 |
|---|---|
| [LangChain] Similarity vs MMR vs MultiQuery vs Hybrid (0) | 2026.02.20 |