PDF를 쏟아붓는 대신 문서와 대화하기 시작한 방법
요약
문서 기반 질문 답변을 위한 RAG 시스템 구축 과정에서 겪은 시행착오와 해결 방법을 다룹니다. 단순 PDF 임베딩의 한계를 극복하기 위해 계층적 청킹과 하이브리드 검색 방식을 도입하여 정확도를 높이는 과정을 설명합니다.
핵심 포인트
- 고정 크기 청킹은 문맥 단절과 코드 블록 파손 문제를 야기함
- 단순 시맨틱 검색만으로는 복잡한 질문에 대한 정확한 매칭이 어려움
- 문서 요약과 세밀한 청킹을 결합한 계층적 구조가 효과적임
- 밀집(Dense) 검색과 희소(Sparse) 검색을 결합한 하이브리드 방식 권장
몇 달 전, 저는 문서의 홍수에 빠져 있었습니다. 저희 팀은 내부 마이크로서비스 (microservices), 설정 가이드, 그리고 배포 절차에 대해 수백 페이지에 달하는 문서를 작성했습니다. 좋게 생각하면 훌륭한 일이죠? 문제는 아무도 그것을 읽지 않는다는 것이었습니다. 매주 Slack에는 똑같은 질문들이 올라왔습니다. "스테이징 DB (staging DB)를 어떻게 초기화하나요?" "그 웹훅 (webhook)의 구문은 무엇인가요?"
저는 위키 (wiki) 위에 기본적인 검색 인덱스 (search index)를 얹어보려고 시도했습니다. 결과는 끔찍했습니다. 사람들이 "reset staging database"라고 입력하면, 운영 환경 자격 증명 (production credentials)을 초기화하는 페이지가 결과로 나왔습니다. 문맥 (Context)? 사라졌습니다. 유의어 (Synonyms)? 쓸모가 없었습니다.
그래서 저는 모든 개발자가 그렇듯, 두 번의 주말을 바쳐 RAG (Retrieval-Augmented Generation, 검색 증강 생성) 시스템을 처음부터 구축했습니다. 시간을 낭비하게 만든 막다른 길들을 포함하여, 제가 배운 것들을 공유하겠습니다.
순진한 접근 방식: 벡터 데이터베이스 (vector database)에 PDF 쏟아붓기
저는 전형적인 레시피로 시작했습니다: PDF → 텍스트 분할기 (text splitter) → OpenAI 임베딩 (embeddings) → Pinecone. 간단했습니다. 한 가지 질문에는 작동했습니다... 하지만 그 외의 모든 것에 대해서는 관련 없는 쓰레기 정보만을 반환했습니다.
문제는 청킹 (chunking)이었습니다. 저는 겹침 (overlap)이 없는 고정된 512-토큰 (token) 청크 크기를 사용했습니다. 문장들이 중간에 잘려 나갔습니다. 코드 블록 (code blocks)은 갈가리 찢겼습니다. 검색 (retrieval) 단계에서 벡터 유사도 (vector-similar)가 높아 보이는 텍스트 조각들을 찾아냈지만, 그것들은 LLM (Large Language Model)에게 아무런 의미가 없었습니다.
효과가 없었던 것: 시맨틱 검색 (semantic search)만 사용하기
더 발전된 임베딩 모델 (text-embedding-3-large)로 교체하고 메타데이터 필터 (metadata filters)를 추가해 보았습니다. 여전히 별로였습니다. 문제는 "스테이징 DB를 어떻게 초기화하나요?"와 같은 질문은 동사 (reset)와 명사 (staging DB)를 관련 절차와 매칭시켜야 한다는 점입니다. 단일 청크 (single chunk)에 동작과 대상이 모두 포함되는 경우는 드물었습니다.
슬라이딩 윈도우 겹침 (sliding window overlap)과 더 큰 청크 크기 (1024 토큰)를 실험하기도 했습니다. 그것이 약간 도움이 되긴 했지만, 이번에는 LLM이 너무 많은 문맥 (context) 때문에 주의가 산만해졌습니다.
마침내 성공한 방법: 계층적 청킹 (hierarchical chunking) + 하이브리드 검색 (hybrid search)
수십 개의 블로그 포스트와 논문을 읽은 끝에, 저는 두 가지 계층 구조 접근 방식을 선택했습니다:
- 문서 수준 요약 (Document-level summary): 각 문서에 대해 (LLM을 통해) 짧은 요약을 생성하고, 이를 별도의 청크 (chunk)로 저장합니다.
- 세밀한 청크 (Fine-grained chunks): 각 문서 내부를 논리적 섹션(제목)별로 분할하며, 청크 크기는 작게(256 토큰) 유지하고 50 토큰의 중첩 (overlap)을 둡니다.
- 하이브리드 검색 (Hybrid retrieval): 먼저 밀집 임베딩 (dense embeddings)을 사용하여 요약본들을 검색한 다음, 상위 문서 내에서 밀집 (dense) + 희소 (sparse, BM25) 검색을 결합하여 수행합니다.
제가 최종적으로 구현한 핵심 검색 함수는 다음과 같습니다:
import chromadb
from sentence_transformers import CrossEncoder
...
이 하이브리드 접근 방식은 마침내 일관되게 관련성 높은 청크를 제공해 주었습니다. 교차 인코더 재순위화 모델 (cross-encoder reranker)은 느리지만, 상위 10개의 후보군에 대해서만 실행하기 때문에 감내할 만한 수준입니다.
배운 점 (그리고 트레이드오프)
- 청킹 (Chunking)이 가장 어려운 부분입니다. 이를 과소평가하지 마세요. 논리적 분할 (마크다운 제목 기준, 함수 정의 기준 등)이 그 어떤 고정된 토큰 창 (fixed token window) 방식보다 뛰어납니다.
- 메타데이터 (Metadata)는 당신의 친구입니다. 출처를 인용할 수 있도록 문서 제목, 섹션, URL을 저장하세요.
- 임베딩 모델 (Embedding models)은 검색 전략 (retrieval strategy)보다 덜 중요합니다. OpenAI, Cohere, 그리고 로컬 모델들을 시도해 보았습니다. 청킹 + 재순위화 (reranking) 파이프라인과 비교했을 때 그 차이는 미미했습니다.
- 절대로 벡터 검색 (vector search)만 단독으로 사용하지 마세요. BM25는 임베딩이 놓치는 키워드 일치를 잡아냅니다.
- 호스팅된 솔루션들이 존재합니다 – Interwest Info (https://ai.interwestinfo.com/)와 같은 서비스들은 이 모든 과정을 API로 묶어서 제공하지만, 내부 동작 원리를 이해하고 싶다면 먼저 직접 구축해 보세요.
다음에 다시 한다면 다르게 할 점
직접 파이프라인을 구축하는 대신 LangChain이나 LlamaIndex로 시작할 것입니다. 이들은 제가 디버깅하는 데 며칠을 소비했던 수많은 예외 케이스들 (코드 블록 분할, 테이블 처리 등)을 처리해 줍니다. 또한, 좋은 평가 데이터셋 (evaluation set) 구축에 더 일찍 투자할 것입니다. 수십 개의 테스트 쿼리가 없다면, 여러분의 변경 사항이 실제로 성능을 개선하고 있는지 결코 알 수 없을 것입니다.
현재 이 시스템은 저희 팀원 20명을 위해 운영 환경 (production)에서 실행되고 있습니다. 하루에 약 50개의 질문이 들어오고 있으며, 저는 여전히 리랭커 임계값 (reranker threshold)을 미세 조정하고 있습니다. 완벽하지는 않습니다. 매우 모호한 질문에는 실패하기도 하지만, 슬랙 (Slack)에서의 반복적인 질문을 70%나 줄였습니다.
기술 문서 (technical documentation)를 위해 어떤 청킹 전략 (chunking strategies)이 효과적이라고 느끼셨나요? 저는 여전히 배우는 중입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기