토니의 연습장
RAG 주의점 (Splitter) 본문
RAG 시스템을 구축했을 때, pdf 페이지를 로드하고 임베딩해서 저장하고 있다가 이를 기반으로 답변할 때 답변 내용의 출처 페이지 번호도 답하게 하면 실제 원문 pdf 파일의 페이지와 잘 일치하지 않을 때가 있습니다. 이 때, 이를 잘 일치하게끔 하려면 각 단계마다 어떤 라이브러리를 쓰는 게 좋을까요? 참고로 RAG 주요 단계로는 pdf 문서 load, 문서/텍스트 split, embedding, DB store 단계들이 있습니다.
RAG 단계를 거치면서 원문의 페이지 번호를 유지하려면, 각 단계에서 페이지 번호 등 메타데이터를 함께 관리할 수 있는 방식을 택하는 것이 중요합니다. 보통 다음과 같은 순서로 RAG 파이프라인을 구성합니다.
1) PDF 문서 로드(Loading)
핵심 포인트
- PDF에서 텍스트를 추출할 때, 각 페이지 정보를 함께 가져올 수 있는 라이브러리를 사용하는 것이 좋습니다.
- Python에서 자주 쓰이는 PDF 로더로는 다음이 있습니다.
- PyMuPDF (fitz 라이브러리)
- 페이지별 텍스트 추출이 편리하고, 페이지 번호 등 메타정보를 쉽게 얻을 수 있습니다.
- pdfplumber
- PDF 구조화가 잘 되어 있는 경우에 주로 사용합니다. 페이지별 텍스트 추출 가능, 레이아웃 정보도 어느 정도 활용 가능.
- PyPDF2 / pypdf
- 비교적 단순하게 텍스트를 뽑아내는 데 사용. 페이지별로 읽을 수 있지만, 텍스트 추출 품질은 PDF 구조에 따라 조금 편차가 있을 수 있음.
- LangChain DocumentLoaders
- 내부적으로 PyPDF2나 pdfplumber 등을 활용하여 페이지 단위로 Document를 생성해 주는 로더가 있습니다.
- 예: from langchain.document_loaders import PyPDFLoader
- 이 방식을 쓰면 LangChain에서 바로 Document 객체에 metadata={"page": <페이지번호>} 같은 식으로 넣어주어, 이후 파이프라인 전체에서 페이지 번호를 추적하기가 수월합니다.
추천: LangChain의 PyPDFLoader(또는 pdfplumber loader)를 활용하면, 별도 코드 작성 없이도 Document 객체에 페이지 정보를 자동으로 넣어줘서 페이지 번호 관리가 용이합니다.
2) 문서 분할(Splitting)
핵심 포인트
- 페이지별로 바로 나누는 경우가 많지만, 내용이 긴 경우에는 한 페이지 내에서도 적당한 길이로 쪼개야 하는 경우가 있습니다.
- 분할 후에도 “원래 페이지 번호”를 메타데이터로 함께 보존해야 최종적으로 출처를 정확히 표시할 수 있습니다.
방법 1) 페이지 단위 그대로 사용
- 먼저 페이지별로 Document를 생성한다. (LangChain의 Loader가 이미 해줌)
- 한 페이지가 너무 길면, LangChain의 TextSplitter(예: RecursiveCharacterTextSplitter)로 추가 분할
- 이 때 원본 Document의 metadata에 있던 페이지 번호를 부분 chunk에도 그대로 복사해 넣으면 됩니다.
- LangChain의 TextSplitter.split_documents([...]) 호출 시, 원본 Document.metadata를 chunk들의 metadata에 복사해줍니다.
방법 2) 직접 PyMuPDF 등으로 페이지 단위 텍스트 추출 + custom 분할
- PyMuPDF를 사용해 각 페이지의 텍스트를 가져온 뒤, 직접 Python 코드로 페이지 내용을 일정 길이(예: 토큰 수, 문장 수 등)로 나눕니다.
- 이 경우에도 페이지 번호를 담은 메타데이터(예: {"page_number": i}}를 각 chunk에 저장해줘야 합니다.
추천: LangChain에서 제공하는 TextSplitter를 활용하면, 분할 후에도 자동으로 메타데이터를 넘겨주기 때문에 페이지 번호 관리를 쉽게 할 수 있습니다.
3) 임베딩(Embedding)
핵심 포인트
- 분할된 텍스트(chunk)를 벡터로 변환할 때, 각 chunk가 어느 페이지에서 왔는지 메타데이터로 함께 관리해야 합니다.
- 이후 쿼리에 대한 Retrieval을 할 때, 해당 chunk의 메타데이터에서 page 번호를 뽑아 최종 답변에 “출처: x페이지”와 같이 표시합니다.
임베딩에 자주 쓰이는 라이브러리/서비스
- OpenAI Embeddings
- 예: text-embedding-ada-002
- LangChain의 OpenAIEmbeddings를 사용하면 코드가 간단.
- Hugging Face Transformers / SentenceTransformers
- 예: sentence-transformers/all-MiniLM-L6-v2 등
- 로컬 환경에서 API 비용 없이 사용 가능.
- Cohere Embeddings, Anthropic 등 기타 API 기반 모델들.
추천: 손쉽게 시작하려면 LangChain + OpenAI Embeddings 또는 LangChain + HuggingFaceEmbeddings 조합이 가장 편리합니다.
벡터 저장소로는 FAISS, Chroma, Pinecone, Weaviate, Milvus 등이 많이 쓰입니다.
LangChain에서는 VectorStore 추상화를 제공하기 때문에, 어떤 벡터DB를 쓰든 로직은 비슷합니다.
4) RAG 질의응답 단계에서의 페이지 번호 출력
- Retriever(또는 VectorStore)에서 유사도가 높은 chunk들을 가져오면, 각 chunk의 metadata["page"] 혹은 metadata["page_number"]를 꺼냅니다.
- LLM(예: ChatGPT)에 “원문 chunk들과 함께 답변을 생성하라”고 프롬프트를 줄 때, chunk의 내용 + 페이지 번호를 컨텍스트로 함께 전달합니다.
- 모델이 답안을 생성한 뒤, 근거(출처)로서 어떤 페이지에서 해당 내용이 나왔는지 보여줄 수 있습니다.
요약
- 문서 로드:
- 권장권장 LangChain의 PyPDFLoader (혹은 pdfplumber 기반 DocumentLoader)
- 페이지 단위로 Document를 생성해주고, metadata에 페이지 번호를 저장해 줌
- 문서 분할(Splitting):
- 권장권장 LangChain의 TextSplitter
- chunk로 분할되더라도 원본 Document의 metadata를 그대로 이어받을 수 있음
- 임베딩:
- 예시예시 LangChain의 OpenAIEmbeddings, HuggingFaceEmbeddings
- 각 chunk에 대해 벡터를 만든 뒤, 해당 chunk의 메타데이터(페이지 번호)를 함께 저장
- Retrieval & QA:
- 권장권장 LangChain의 Retrieval QA 체인(RetrievalQA) 혹은 유사한 RAG 구조
- 검색된 chunk의 metadata["page"] 정보를 모델에 함께 전달하여 “출처 페이지 번호”를 답안에 표기
이런 식으로 각 단계마다 문서의 페이지 정보를 잃어버리지 않고(meta data) 관리하면, 최종적으로 원문 PDF의 “실제 페이지 번호”와 답변에서 제시되는 번호가 일치하게 됩니다.
정리하면 “LangChain의 PDF 로더 + TextSplitter + Embeddings + (벡터DB) + RetrievalQA” 조합이 가장 편하고, 페이지 번호 같은 중요한 정보를 metadata로 지속적으로 넘겨주는 것이 핵심입니다.