PDF 파싱은 RAG 파이프라인의 숨겨진 병목 구간입니다
요약
RAG 시스템의 성능 저하 원인이 리트리버나 LLM이 아닌 PDF 파싱 단계의 구조적 정보 손실에 있음을 분석합니다. 특히 표(Table)가 포함된 한국어 문서에서 발생하는 구조 붕괴 문제를 해결하기 위한 pdfplumber 활용법과 후처리 전략을 제시합니다.
핵심 포인트
- 잘못된 PDF 파싱은 문맥 오염과 구조 붕괴를 야기하여 검색 정밀도를 저하시킴
- 표 데이터를 평면화하면 헤더와 셀 간의 관계가 파괴되어 LLM 환각의 원인이 됨
- pdfplumber를 활용해 행/열 좌표를 보존하고 마크다운 형태로 재구성 가능
- 병합된 셀 처리 및 셀 내부 줄바꿈 제거 등 커스텀 후처리가 필수적임
RAG (Retrieval-Augmented Generation) 시스템이 환각 (Hallucination)을 일으키거나 명백히 틀린 답변을 내놓을 때, 우리의 첫 번째 본능은 보통 리트리버 (Retriever)를 탓하거나 더 큰 LLM (Large Language Model)으로 교체하는 것입니다.
하지만 표 (Table)가 많은 한국어 문서를 대상으로 엔드 투 엔드 (End-to-end) RAG 파이프라인을 스트레스 테스트하던 중, 저는 진짜 범인이 한 단계 앞선 곳에 있다는 것을 깨달았습니다. 바로 **PDF 파서 (PDF Parser)**입니다.
만약 파서가 표를 평면적인 텍스트 스트림으로 조각내 버린다면, 그 어떤 프롬프트 엔지니어링 (Prompt Engineering)이나 고차원 임베딩 (High-dimensional Embeddings)도 손실된 구조적 문맥 (Structural Context)을 복구할 수 없습니다.
잘못된 파싱이 검색을 망치는 방법
- 문맥 오염 (Context Pollution): 기본적인 파서들이 텍스트 청크 (Chunk)에 깨진 인코딩, 누락된 공백, 또는 쓰레기 메타데이터를 주입하면, 임베딩 모델은 결국 노이즈를 벡터화하게 됩니다. 이로 인해 검색 정밀도가 즉시 떨어집니다.
- 구조 붕괴 (Structure Collapse): 이는 표가 많은 PDF에 있어 치명적인 문제입니다. 다중 열(Multi-column) 표를 평면화하면 헤더 (Header), 셀 (Cell), 값 (Value) 사이의 명시적인 관계가 파괴됩니다. 그러면 LLM은 서로 관련 없는 숫자들 사이의 연결 고리를 환각해냅니다.
제 테스트 케이스에서 수치 임계값과 관련된 모든 실패는 데이터 수집 (Ingestion) 단계에서 평면화된 표로부터 직접적으로 기인했습니다.
로컬 PDF 파서 벤치마킹 (한국어 + 표)
병합된 셀과 다중 행 헤더를 포함하는 복잡한 한국어 PDF 문서를 대상으로 표준 오픈 소스 도구들을 벤치마킹했습니다:
| 파서 / 라이브러리 | 표 구조 보존 | 한국어 인코딩 | 보안 / 로컬 | 비고 |
|---|---|---|---|---|
| PyPDFLoader (LangChain) | ⭐ | ❌ 빈번한 손상 | ✅ 완전 로컬 | 빠르지만, 표 텍스트를 단순히 한 줄씩 덤프(Dump) 처리합니다. 구조가 완전히 상실됩니다. |
| ... |
제가 pdfplumber 파이프라인을 구축한 이유
- 구조 인식 (Structure-Aware): 행/열 좌표 경계를 보존하여 마크다운 (Markdown) 표를 정확하게 재구성할 수 있게 해줍니다.
- 로컬 및 경량화 (Local & Lightweight): 외부 API 의존성이 없으며, 데이터 노출 위험이 제로입니다.
후처리를 통한 pdfplumber의 한계 극복
pdfplumber가 단순 텍스트 덤프(raw text dumps)보다는 훨씬 뛰어나지만, 별도의 설정 없이 바로 마법처럼 작동하는 것은 아닙니다. 출력 결과물을 LLM이 사용하기 적합한 상태(LLM-ready)로 만들기 위해 몇 가지 커스텀 후처리 규칙을 구현해야 했습니다.
import pdfplumber
def extract_and_clean_tables(pdf_path):
...
- 병합된 셀 처리 (Merged Cell Handling): 병합된 스팬(merged spans)으로 인해 남겨진 빈
None값들을 공백으로 채웁니다. - 셀 내부 줄바꿈 (In-cell Newlines): 셀 내부의
\n을 공백으로 대체하여, 여러 줄로 된 텍스트 셀이 새로운 행 경계인 것처럼 오인되지 않도록 합니다.
결과: 골든 세트 평가 (Golden Set Evaluation)
두 파이프라인을 대상으로 도메인 특화 질의(domain-specific queries) 테스트 세트를 다시 실행했습니다.
| 질의 유형 | 기본 PyPDFLoader 파이프라인 | 커스텀 pdfplumber 파이프라인 |
|---|---|---|
| 수치 임계값 (참조 테이블) | ❌ 실패 (잘못된 행을 검색함) | ✅ 정확히 일치 |
| ... |
핵심 요약 (Key Takeaways)
- 파싱 품질이 곧 검색 품질입니다: 파싱 단계에서 이미 입력 텍스트가 손상되었다면, 청크 크기(chunk sizes)를 조정하거나 임베딩(embeddings)을 미세 조정(fine-tuning)하는 데 몇 주를 허비하지 마세요.
- 컨텍스트 오염 최소화: 검색기(retriever)와 LLM이 성공할 확률을 높이려면 테이블 경계를 보존해야 합니다 (예: 청킹(chunking) 전에 깨끗한 마크다운(Markdown) 테이블로 변환).
다음 단계
데이터를 외부로 전송하지 않고도 매우 복잡하고 중첩된 이미지 기반 테이블을 파싱하기 위해, 로컬 오픈 소스 시각 언어 모델 (Vision Language Models, 예: Qwen2-VL 또는 Florence-2)을 탐색하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기