RAG 문서 검색 시스템
완료문서 청킹부터 임베딩·벡터 검색·답변 생성까지의 파이프라인을 순수 Python으로 한 번, LangChain으로 한 번 구현해 두 방식을 비교했습니다. 벡터 검색에 BM25 키워드 검색을 더한 하이브리드 검색도 붙였습니다.
BACKGROUND
RAG를 라이브러리 호출로만 익히면 문제가 생겼을 때 어디를 봐야 할지 모른다고 생각했습니다. 그래서 프레임워크 없이 직접 한 번 만들고, 같은 파이프라인을 LangChain으로 다시 만들어 각 컴포넌트가 무엇을 대신해주는지 대조했습니다.
WHAT I BUILT
프레임워크 없이 먼저 구현
sentence-transformers로 임베딩을 만들고 NumPy 코사인 유사도로 직접 검색했습니다. 청킹 크기와 겹침이 검색 품질을 어떻게 바꾸는지, 유사도 상위 문서가 왜 엉뚱하게 잡히는지를 코드를 직접 뜯어보며 확인했습니다.
같은 파이프라인을 LangChain으로 재구현
TextSplitter·Embeddings·VectorStore(FAISS)·Retriever가 앞서 직접 짠 어느 부분을 대체하는지 1:1로 대응시켰습니다. 프레임워크가 감춘 것과 대신해준 것을 구분할 수 있게 됐습니다.
BM25 하이브리드 검색
임베딩 검색이 숫자나 고유명사가 들어간 질문에 약한 지점을 보완하려고 BM25 키워드 검색을 붙였습니다. 코사인 유사도와 BM25 점수는 범위가 달라 그대로 더할 수 없어서, 각각 0~1로 정규화한 뒤 벡터 0.7 : 키워드 0.3으로 가중합했습니다. 같은 질문에 대해 벡터 단독 결과와 하이브리드 결과를 나란히 출력해 비교했습니다.
로컬 모델로 전 구간 실행
외부 API 없이 로컬 모델로 임베딩(ko-sroberta)과 생성(Qwen2.5-3B-Instruct)을 모두 처리했습니다. 비용 없이 반복 실험할 수 있었고, 모델 크기와 응답 품질의 트레이드오프를 직접 체감했습니다.
생성 품질 문제 진단
답변 중간에 다른 언어가 섞여 나오는 현상을 만났습니다. 검색 결과 문제인지 생성 파라미터 문제인지 분리해서 확인했고, repetition_penalty를 1.15 부근으로 조정해 안정화했습니다.
WHAT I LEARNED
RAG의 답변 품질은 대부분 생성이 아니라 검색 단계에서 갈렸습니다. 엉뚱한 답이 나오면 모델보다 먼저 청킹과 검색 결과를 봐야 했습니다.
증상만 보고 파라미터를 만지면 재현이 안 됩니다. 검색 단계와 생성 단계를 나눠 원인을 좁히는 습관이 남았습니다.
서로 다른 검색 점수를 섞을 때는 범위부터 맞춰야 했습니다. 정규화 없이 더하면 범위가 큰 BM25 점수가 결과를 좌우합니다.