한 오후 만에 PDF 폴더를 질의응답 API로 변환하기
요약
다량의 PDF 문서를 기반으로 질의응답이 가능한 RAG(검색 증강 생성) API를 구축하는 전 과정을 설명합니다. 텍스트 추출, 청킹, 임베딩, 저장 및 답변 생성의 4단계 핵심 메커니즘과 실무적인 주의사항을 다룹니다.
핵심 포인트
- RAG의 4단계: 수집, 청킹, 임베딩/저장, 답변 생성 과정을 이해합니다.
- PDF 추출 시 스캔된 이미지 파일은 OCR 처리가 필수적입니다.
- 효과적인 검색을 위해 문맥을 유지하는 적절한 청크 크기와 중첩(overlap) 설정이 중요합니다.
- 임베딩을 통해 텍스트를 벡터로 변환하여 의미적 유사성을 기반으로 검색합니다.
누군가 당신에게 200개의 PDF를 건네며 "이 내용들을 다 알고 있는 챗봇"을 만들어 달라고 요청합니다. 당신은 200개를 전부 읽을 수도 있습니다. 아니면 당신 대신 그것들을 읽고 인용(citation)과 함께 질문에 답하는 작은 API를 구축할 수도 있습니다. 이 포스트는 두 번째 옵션에 대한 처음부터 끝까지의 과정이며, 보통 문제가 발생하는 부분들을 짚어줍니다.
먼저 어떤 일이 일어나고 있는지 이해할 수 있도록 메커니즘을 보여드린 후, 직접 배관 작업(plumbing)을 하고 싶지 않은 분들을 위한 더 짧은 경로를 안내하겠습니다.
실제로 구축하게 되는 것
자신의 문서에 대한 질의응답(question-answering) API는 네 가지 구성 요소로 이루어집니다:
- 수집 (Ingest). 각 PDF를 읽고 텍스트를 추출합니다.
- 청킹 (Chunk). 해당 텍스트를 검색하기에 충분히 작은 조각으로 나눕니다.
- 임베딩 및 저장 (Embed and store). 각 청크를 벡터(vector)로 변환하고, 쿼리할 수 있는 저장소에 보관합니다.
- 답변 (Answer). 질문 시점에 가장 유사한 청크를 찾아 질문과 함께 모델에 전달합니다.
사람들은 이 전체 과정을 RAG (retrieval-augmented generation, 검색 증강 생성)라고 부릅니다. 이름 때문에 실제보다 더 무겁게 느껴질 수 있지만, 이는 검색(search) 후 프롬프트(prompt)를 넣는 과정입니다.
1단계: 텍스트 추출하기
PDF는 엉망진창입니다. 어떤 것은 깨끗한 텍스트이고, 어떤 것은 스캔된 이미지이며, 어떤 것은 두 개의 열(column)이 있어 잘못된 순서로 추출되기도 합니다. pypdf로 간단하게 시작해 보세요:
from pypdf import PdfReader
def read_pdf(path: str) -> str:
...
문제가 발생하는 지점: 스캔된 PDF는 텍스트 레이어가 없고 픽셀만 있기 때문에 빈 문자열을 반환합니다. 만약 extract_text()가 아무것도 반환하지 않는다면, 해당 파일들은 OCR (Tesseract 또는 호스팅된 비전 모델)이 필요합니다. 빈 페이지가 조용히 인덱싱되지 않도록 초기에 빈 출력을 확인하세요.
2단계: 텍스트 청킹하기
문서 전체를 하나의 벡터로 임베딩하고 좋은 답변을 기대할 수는 없습니다. 검색(Retrieval)은 조각 단위로 작동합니다. 이 조각들의 크기는 대부분의 튜토리얼에서 인정하는 것보다 더 중요합니다.
너무 작으면(문장 하나) 각 청크가 문맥(context)을 잃게 됩니다. 너무 크면(페이지 전체) 모델이 대부분 관련 없는 텍스트의 벽을 마주하게 되어 답변이 빗나갑니다. 합리적인 시작점은 청크당 500~800 토큰(token)이며, 경계선에서 나뉜 문장이 손실되지 않도록 약간의 중첩(overlap)을 두는 것입니다.
def chunk(text: str, size: int = 600, overlap: int = 80) -> list[str]:
words = text.split()
step = size - overlap
...
이 방식은 공백(whitespace)을 기준으로 분할하는데, 다소 투박하지만 시작 단계에서는 괜찮습니다. 만약 문서에 명확한 구조(제목, 섹션 등)가 있다면, 이러한 경계선을 기준으로 분할하는 것이 고정된 윈도우(fixed window) 방식보다 훨씬 낫습니다. 섹션과 일치하는 청크(chunk)는 그 자체로 의미를 갖는 청크가 되기 때문입니다.
3단계: 임베딩(embed) 및 저장
임베딩(embedding)은 청크를 그 의미를 포착하는 숫자 리스트로 변환합니다. 유사한 텍스트는 서로 가까운 곳에 위치하게 됩니다. 질문을 동일한 방식으로 임베딩하여 쿼리(query)를 날리고, 가장 가까운 청크들을 찾아냅니다.
200개의 PDF가 담긴 폴더 정도라면 관리형 벡터 데이터베이스(managed vector database)는 필요하지 않습니다. 로컬 저장소만으로도 충분합니다:
import chromadb
from chromadb.utils import embedding_functions
...
메타데이터(metadata)에 원본 파일명을 포함하세요. 그래야 답변이 어디에서 왔는지 인용(cite)할 수 있으며, 이것이 단순한 데모와 사람들이 신뢰할 수 있는 서비스 사이의 차이를 만듭니다.
4단계: 인용을 포함한 답변 생성
질문 시점에는 다음과 같이 진행합니다: 질문을 임베딩하고, 상위 몇 개의 청크를 가져와 프롬프트(prompt)에 넣은 뒤, 모델에게 주어진 정보로만 답변하도록 요청합니다.
def answer(question: str) -> str:
hits = col.query(query_texts=[question], n_results=5)
context = "\n\n".join(hits["documents"][0])
...
여기서 중요한 두 가지가 있습니다:
n_results. 5개가 적당한 기본값입니다. 컨텍스트(context)가 많다고 해서 무조건 좋은 것은 아닙니다. 일정 수준을 넘어서면 토큰(token) 비용만 발생하고 신호(signal)가 희석됩니다.- "말할 수 있는 것만 말하라"는 지침. 이 지침이 없으면 모델은 빈틈을 그럴듯한 허구로 채워버립니다. 이 지침이 있으면 "문서에 해당 내용이 포함되어 있지 않습니다"라는 답변을 얻게 되는데, 이것이 정직한 답변이며 사람들이 시스템을 계속 신뢰하게 만드는 방법입니다.
answer() 함수를 어떤 웹 프레임워크(web framework)로든 감싸면 API가 됩니다:
from fastapi import FastAPI
app = FastAPI()
...
프로덕션 환경에서 어려워지는 지점들
위에서 설명한 '오후 버전'은 잘 작동합니다. 이를 실제 프로젝트로 만드는 요소들은 다음과 같습니다:
- 재색인 (Re-indexing). 문서는 변경됩니다. 모든 것을 다시 구축하지 않고도 청크 (chunks)를 업데이트하거나 삭제할 수 있는 방법이 필요합니다.
- 잘못된 검색 (Bad retrieval). 답변이 틀렸을 때는 모델의 문제라기보다 대개 검색의 문제입니다. 올바른 청크가 컨텍스트 (context)에 포함되지 않은 것입니다. 각 질문에 대해 무엇이 검색되었는지 기록하는 것이 여러분이 가장 먼저 사용하게 될 디버깅 도구입니다.
- 액세스 제어 (Access control). 서로 다른 사용자가 서로 다른 문서를 봐야 한다면, 검색 과정에서 순위를 매기기 전에 권한에 따라 필터링을 수행해야 합니다. 이를 나중에 덧붙이는 것은 매우 고통스러운 작업입니다.
- 비용 (Cost). 임베딩 (Embeddings)은 저렴합니다. 하지만 생성 (generation) 호출은 그렇지 않으며, 트래픽에 따라 비용이 증가합니다. 자주 묻는 질문을 캐싱 (Caching)하는 것이 도움이 됩니다.
더 짧은 경로
데이터 수집 (ingest), 청킹 (chunking), 벡터 스토어 (vector store), 그리고 프롬프트 루프 (prompt loop)를 직접 실행하고 싶지 않다면, 이것이 바로 LLMGraph가 구축하는 형태입니다. 문서를 지정하기만 하면, 서비스가 청킹과 검색을 처리하며, 결과물로 REST API와 임베드 가능한 채팅 위젯을 제공합니다. 위의 네 가지 단계는 여전히 발생하지만, 여러분이 직접 관리할 필요는 없습니다.
어떤 방식이든 사고 모델 (mental model)은 동일합니다: 읽기, 청킹, 임베딩, 검색, 답변. 이 원리가 이해되고 나면, "우리 데이터를 잘 아는 챗봇"은 연구 프로젝트처럼 들리지 않고, 단지 어느 오후에 끝낼 수 있는 작업처럼 들리기 시작할 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기