
PyPDFLoader, LlamaParse, Custom Regex — 인도 정부 PDF를 위해 모든 방법을 시도해 보았습니다. 실제로 작동한
요약
인도 정부의 복잡한 PDF 문서를 처리하기 위해 다양한 파싱 도구를 테스트한 사후 분석 글입니다. LlamaParse와 같은 고성능 도구도 밀도 높은 법률 텍스트의 각주와 본문을 구분하지 못해 발생하는 RAG 환각 문제를 다룹니다.
핵심 포인트
- LlamaParse는 구조화된 양식에는 강하나 밀도 높은 텍스트에서는 청킹 오류 발생 가능
- 각주와 본문의 번호가 유사할 경우 임베딩 모델이 혼동하여 환각 유발
- 복잡한 문서 처리를 위해 단순 파싱을 넘어선 커스텀 로직과 멱등성 계층 필요
6개월 전, 저는 여러분이 지금 던지고 있는 것과 똑같은 질문들을 했습니다. "병합된 셀(merged cells)은 어떻게 처리하나요?" "왜 테이블 추출이 깨지는 거죠?" "어떤 파서(parser)를 사용해야 하나요?"
저는 상상할 수 있는 가장 까다로운 PDF들, 즉 인도 정부 예산 문서, 재무 법안, 그리고 인도 헌법(매 페이지마다 각주가 달린 400페이지 이상의 밀도 높은 법률 텍스트)을 대상으로 모든 인기 있는 방식 — PyPDFLoader, Unstructured, LlamaParse, 커스텀 정규표현식(custom regex) — 을 시도해 보았습니다.
이 글은 무엇이 잘못되었는지, 왜 그랬는지, 그리고 실제로 프로덕션(production) 환경에서 살아남은 유일한 아키텍처가 무엇인지에 대한 솔직한 사후 분석(post-mortem)입니다.
🤯 지옥에서 온 문서
대부분의 RAG 튜토리얼은 깔끔하고 단순한 PDF를 사용합니다. 인도 헌법은 그렇지 않습니다.
여러분이 매 페이지마다 마주하게 될 상황은 다음과 같습니다:
19. Protection of certain rights regarding freedom of speech, etc.—
(1) All citizens shall have the right—
(a) to freedom of speech and expression;
...
모든 페이지는 세 가지 구역으로 나뉩니다:
조항 내용 (Article content) (사용자가 실제로 원하는 것)
구분선 (A separator line) (________)
각주 (Footnotes) (다음과 같이 숫자로 시작하는 개정 인용문)
1., 19., 34.)
이 각주들은 실제 조항(Article)과 동일한 숫자로 시작합니다. 임베딩 모델(Embedding models)은 이들을 동일한 가중치로 인코딩합니다. 바로 이 지점에서 환각(hallucinations)이 발생합니다.
❌ 시도 1: LlamaParse (Agentic Tier) — 값비싼 실패
저의 초기 설정: LlamaParse Agentic 티어 (페이지당 10 크레딧) + LangChain의 MarkdownHeaderTextSplitter.
기대했던 결과: 조항(Article)별로 계층적으로 분리된 깔끔한 청크(chunks).
실제 결과: 402페이지 문서에서 624개의 거대한 청크가 생성됨.
LlamaParse는 테이블, 인보이스, 구조화된 양식에는 탁월합니다. 하지만 수백 개의 번호가 매겨진 항목이 있는 밀도 높은 연속 법률 텍스트의 경우, 여러 페이지를 단일 Markdown 블록으로 병합해 버렸습니다. 조항 19는 독립된 청크가 아니었습니다. 조항 17, 18, 20 및 수십 개의 각주와 함께 5,000자짜리 덩어리(blob) 안에 파묻혀 있었습니다.
환각 테스트:
질의(Query): "조항 19는 무엇인가요?"
벡터 유사도(Vector similarity)가 실제 제19조(Article 19) 본문보다 각주(footnote)와 더 높게 매칭되었습니다.
(19. Ins. by Constitution (Forty-fourth Amendment)...)
LLM은 쓰레기 같은 컨텍스트(garbage context)를 전달받았고, 쓰레기 같은 출력(garbage output)을 반환했습니다.
비용 손실: 402페이지 × 10 크레딧 = 동기화당 4,020 크레딧. 여러 번의 디버깅 반복 = 30K 이상의 크레딧 소진.
🛡️ 멱등성 계층 (The Idempotency Layer): API 호출을 두 번 낭비하지 마세요
검색(retrieval) 문제를 해결하기 전에, 저는 안전망을 구축했습니다. 디버깅에 30K 이상의 크레딧을 태운 후, 저는 맹세했습니다: 다시는 그러지 않겠다고.
SHA-256 파일 해싱 (SHA-256 File Hashing)
# sync.py — 처리 전 모든 PDF를 해싱합니다
...
모든 PDF는 처리 전 SHA-256으로 해싱됩니다. 해시는 Supabase에 저장됩니다. 재동기화 시 해시가 일치하면 → 파일 전체를 건너뜁니다. 파싱(parsing) 제로, 임베딩(embedding) 제로, Pinecone 호출 제로.
결정론적 청크 ID (Deterministic Chunk IDs)
python
# chunker.py — 동일한 입력 = 항상 동일한 ID
parent_id = f"{source_file}_{page_number}_{parent_index}"
child_id = hashlib.md5(f"{parent_id}_{child_index}".encode()).hexdigest()
무작위 UUID를 사용하지 않습니다. 청크 ID는 파일 이름 + 페이지 + 위치에서 유도됩니다. 동일한 파일을 재동기화하면 = 동일한 ID가 생성됩니다. Pinecone의 upsert는 중복 생성 대신 덮어쓰기를 수행합니다.
이것이 한 번은 작동하는 스크립트와, 매일 프로덕션(production) 환경에서 안전하게 실행할 수 있는 시스템 사이의 차이입니다.
✅ 시도 2: 결정론적 파이프라인 (실제로 작동한 방식)
저는 근본적인 질문을 던졌습니다: "이 특정 문서를 파싱하는 데 실제로 LLM이 필요한가?"
아니요. 헌법은 완전히 예측 가능한 구조를 가지고 있습니다:
조항(Articles)은 항상 다음과 같이 시작합니다:
\n[번호]. [제목]—
각주는 항상 언더스코어(_) 뒤에 위치합니다.
페이지 헤더는 항상 "THE CONSTITUTION OF INDIA"라고 적혀 있습니다.
이것은 LLM의 영역이 아니라, 정규 표현식 (regex)의 영역입니다.
1단계: 공격적인 각주 제거 (Aggressive Footnote Removal)
python
# parser.py
for page_num in range(doc.page_count):
text = doc[page_num].get_text("text")
...
결과: 벡터 인덱스(vector index) 내에 각주가 전혀 없음.
2단계: 조항 경계 기반 청킹 (Article-Boundary Chunking)
# chunker.py — 글자 수가 아닌 조항 경계에서 분할
...
결과: 624개의 지저분한 덩어리(blobs) → 3,248개의 정밀한 청크(chunks), 각 청크는 하나의 조항(Article)입니다.
3단계: Pinecone으로의 메타데이터 주입 (Metadata Injection)
python
chunk_metadata = {
"source_file": "constitution of india.pdf",
"chunk_type": "parent_child",
...
모든 청크는 Pinecone 내에서 자신의 조항(Article) 정체성을 보유합니다. 추론하거나 추측한 것이 아닙니다. 결정론적으로 태깅(tagged)되었습니다.
4단계: 스마트 LangGraph 라우팅 (Smart LangGraph Routing)
# graph.py — LangGraph Retriever Node
...
이것이 바로 SQL에서의
article_number = '19'
이 작동하는 지점입니다. 벡터 인덱스(vector index)는 다른 조항(Article)의 청크를 절대로 반환할 수 없습니다.
🎯 검증: 환각 테스트 스위트 (The Hallucination Test Suite)
제3자 LLM 평가자에 의해 독립적으로 점수가 매겨진 결과입니다:
질의 (Query)
제20조(Article 20)는 무엇인가요?
주요 동작 (Key Behavior)
3가지 보호 장치(소급 입법 금지(Ex Post Facto), 일사부재리(Double Jeopardy), 자기부죄 거부(Self-Incrimination))를 정확하게 반환함
점수 (Score) 9/10
제34조(Article 34)는 무엇인가요?
주요 동작 (Key Behavior)
부속서(Schedule)의 노이즈 없이 계엄령(martial law) 조항을 정확하게 검색함
점수 (Score) 9/10
질의 (Query)
제31C조(Article 31C) + Kesavananda Bharati?
주요 동작 (Key Behavior)
31C를 정확하게 검색함; 판례(case law)에 대해 환각(hallucinate)하기를 올바르게 거부함
점수 (Score) 92/100
질문 (Query)
기본 구조 원칙(Basic Structure Doctrine)?
주요 동작 (Key Behavior)
사법적 원칙으로 식별했으며, 헌법 조항에 나타나지 않는다고 언급함. 통과
질문 (Query)
제31B조 + 제9개장(Ninth Schedule)?
주요 동작 (Key Behavior)
기본 구조 대 제9개장의 긴장 관계를 정확하게 설명함 8.8/10
가장 중요한 결과는 질문 3에서 나왔습니다. 시스템은 다음과 같이 응답했습니다:
_
article_number: "19"
. 현재 영향: 낮음 — LLM이 생성 과정에서 이를 구분합니다.
-
"일반적인 개념적 질문 (General Conceptual Queries)" — _"모든 기본권(Fundamental Rights)은 무엇인가요?"_와 같은 질문은 메타데이터 필터(metadata filter)를 트리거하지 않습니다. 대신 시맨틱 검색 (semantic search)으로 전환됩니다.
-
문서 간 관계 부재 (No Cross-Article Relationships) — 시스템은 제32조가 제19조를 집행한다는 관계를 모델링하지 않습니다. 각 조항은 독립적으로 인덱싱(indexed)됩니다.
🔧 기술 스택 (Tech Stack)
파서 (Parser): PyMuPDF (무료, 로컬)
청커 (Chunker): 커스텀 정규 표현식 (regex) 기반 계층적 청커
임베딩 (Embeddings): Jina AI v3 (MRL: 1024→256 차원, 저장 공간 75% 절감)
벡터 DB (Vector DB): Pinecone Serverless (메타데이터 필터링 포함)
오케스트레이션 (Orchestration): LangGraph (8개 노드 에이전트 파이프라인)
LLM: Google Gemini
레지스트리 (Registry): Supabase (파일 해싱 + 동기화 추적)
모니터링 (Monitoring): Langfuse (LLM 관측성)
💡 세 가지 핵심 요약 (Three Takeaways)
파서를 선택하기 전에 문서 구조를 먼저 평가하세요. LlamaParse는 반구조화된 (semi-structured) 문서에 매우 탁월합니다. 예측 가능한 패턴을 가진 연속적인 법률 텍스트의 경우, 커스텀 정규 표현식 (regex) 파서를 사용하면 비용 부담 없이 더 많은 제어권을 가질 수 있습니다.
첫날부터 메타데이터를 고려하여 설계하세요. 벡터 유사도 (Vector similarity)는 최우선 선택지가 아닌, 차선책(fallback)이어야 합니다.
정상적인 경로(happy path)뿐만 아니라 환각 경계(hallucination boundary)를 테스트하세요. RAG 시스템에 문서에 있는 내용을 묻는 것만큼, 문서에 없는 내용을 물어보는 것도 중요합니다.
📊 커뮤니티 반응 (Community Response)
이 접근 방식은 AI 커뮤니티에서 상당한 호응을 얻었습니다:
Reddit (r/LangChain): 게시물 2개에 걸쳐 50,000회 이상의 조회수, 500회 이상의 공유
GitHub: 별(star) 64개, 포크(fork) 22개
HuggingFace: 3개의 파인튜닝(fine-tuned) 모델(1B, 3B, 8B) 게시, 5,500회 이상의 다운로드
🔗 링크
GitHub (전체 소스 코드): github.com/Ambuj123-lab/agentic-rag-financial-parser
라이브 데모: ambuj-portfolio-v2.netlify.app
LinkedIn: linkedin.com/in/ambuj-tripathi-042b4a118
다른 분들도 각주가 많은 PDF를 다루어 보았거나 LlamaParse 시도에 실패한 경험이 있으신가요? 어떻게 해결하셨나요? 여러분의 접근 방식을 댓글로 남겨주세요 — 함께 의견을 나누고 싶습니다.
이 내용이 유익했다면 ❤️를 눌러주시고, 더 많은 프로덕션 RAG (Production RAG) 콘텐츠를 위해 팔로우해 주세요!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기





