
Gemini 서류 해석 전 분류층 삽입하기: 포맷 수에 따른 ML 분류와 LLM 분류의 활용법
요약
서류 종류가 많아질 때 Gemini의 추출 정밀도가 떨어지는 문제를 해결하기 위해 '분류층(Classification Layer)'을 삽입하는 2단계 구조를 제안합니다. 범용 프롬프트의 한계를 극복하고 비용과 정확도를 최적화하기 위해 ML 또는 LLM을 활용한 라우팅 패턴을 활용하는 방법을 다룹니다.
핵심 포인트
- 서류 타입 증가 시 단일 프롬프트의 정밀도 저하 및 비용 문제 발생
- Routing 패턴을 통해 경량 분류기와 전문화된 프롬프트로 분리 설계
- ML(Embedding)과 LLM(Gemini 1.5 Flash) 중 포맷 수에 따른 선택 필요
- Silent Failure 방지를 위해 분류 단계를 통한 입력 제어 중요
청구서 처리 현장에서 "서류 타입이 30종류를 넘어가면서부터 Gemini 단발 호출의 정밀도가 급격히 떨어졌다"는 순간을 경험한 적이 있습니다. 금액 추출 누락이 갑자기 늘어나고, 새로 추가된 벤더의 청구서가 기존 카테고리로 오분류됩니다. 월말에 한꺼번에 재작업이 발생하여, 비용은 크게 차이가 없는데 인건비만 치솟게 됩니다.
그로부터 시행착오를 거쳐 정착한 것이, Gemini를 호출하기 전에 "분류층 (Classification Layer)"을 삽입하는 2단계 구성이었습니다. 분류층을 ML (embedding + 클러스터링)로 만들 것인지, LLM (Gemini 1.5 Flash)로 만들 것인지. 이 선택지를 포맷 수와 성장성에 따라 나누는 것이 현재 시점에서 가장 효과적인 정리 방식입니다. 모델은 Gemini 1.5 세대를 전제로 하며, 일본어 서류를 다루며 겪었던 구체적인 함정도 몇 가지 포함하겠습니다.
처음에 모든 서류를 Gemini 1.5 Pro로 던졌던 시기에 부딪혔던 벽은 주로 다음 세 가지였습니다. 순서대로 작성하겠습니다.
첫 번째는, 범용 프롬프트 한 장으로는 서류 타입 고유의 특성을 모두 잡아낼 수 없다는 점입니다. A사 청구서는 비고란에 발주 번호가 수기로 혼입되고, B사 영수증은 세율이 세로 2단 구성으로 기재되며, C사 견적서는 합계란이 우측 상단이 아닌 좌측 하단에 있습니다. 이러한 암묵적인 규칙들을 "이 프롬프트 하나로 전부 대응하자"라고 하면, 프롬프트가 수천 토큰으로 비대해질 뿐만 아니라, 후반부의 클래스 설명에 대한 LLM 측의 주의력이 분산되어 놓치는 부분이 발생합니다. Lost in the Middle에서 지적된 것처럼, Gemini 1.5 세대에서도 완전히 해소되지 않은 경향입니다.
두 번째인 비용 문제는 은근히 타격이 큽니다. Gemini 1.5 Pro로 서류 1페이지를 추출하면, 이미지 토큰 약 560 (media_resolution=medium) + 프롬프트 약 500 + 출력 약 300의 구성으로, 건당 약 $0.006 내외입니다. 월 10,000건이면 $60, 월 100,000건이면 $600 규모입니다. Flash로 전환하면 월 $15 수준까지 떨어지지만, 이번에는 타입별 최적 프롬프트를 적용할 수 없어 추출 정밀도가 떨어지기 시작하는 또 다른 트레이드오프 (Trade-off)가 나타납니다.
세 번째, 그리고 가장 무서운 것은 사이런트 실패 (Silent Failure)였습니다. 새로운 벤더의 서류가 들어와도 LLM은 어딘가의 카테고리로 끼워 맞춰서 답변하기 때문에 분류 실수를 알아차릴 수 없습니다. 분개 실수나 청구 금액 불일치로 인한 업무 영향은 API 비용 차이를 순식간에 집어삼킵니다. "모르는 것은 모른다고 말해주길 바란다"라는, 일반적인 프로그램이라면 당연한 기대가 LLM에게 맡기면 성립되지 않습니다. 이것이 분류층을 삽입하게 된 가장 큰 동기가 되었습니다.
구성은 심플합니다. Gemini에 던지기 전에 타입 판정을 한 단계 넣는 것뿐입니다.
이 구성이 효과적인 이유는 추출 프롬프트를 서류 타입별로 육성할 수 있다는 점입니다. A사 청구서의 비고란 규칙도, B사 영수증의 세로 쓰기 세율도, 해당 서류 전용 프롬프트에 가두어 두면 되므로 다른 서류 타입에 부작용이 나타나지 않습니다. 프롬프트의 비대화도 막을 수 있습니다.
이 "분류기로 입력을 분류하여 전문화된 프롬프트로 전달하는" 구성은, Anthropic이 Building Effective Agents에서 정리한 LLM 워크플로우 패턴 중 Routing (프롬프트 라우팅)에 해당합니다. 범용 프롬프트 한 장으로 전부 처리하는 대신, 경량 분류기와 전문화된 서브 프롬프트로 분해하는 설계입니다. 본 기사는 이 Routing 패턴을 서류 AI에 특화했을 때 "분류기를 무엇으로 만들 것인가"를 다룹니다.
핵심인 "분류층을 무엇으로 만들 것인가"가 본 기사의 주제입니다. 후보는 세 가지입니다.
- Gemini 1.5 Flash로 분류시키는 LLM 방식
- embedding과 가벼운 분류기로 해결하는 ML 방식
- ML 중심 + 신뢰도가 낮은 경우에만 LLM으로 넘기는 하이브리드 (Hybrid) 방식
어느 것을 선택할지는 다루는 포맷 수와 6개월 후의 예상치를 기준으로 결정합니다.
7가지 관점을 나열하면 AI가 작성한 정형화된 표가 되므로, 실무에서 신경 쓰고 있는 순서대로 산문 형식으로 정리하겠습니다.
비용과 레이턴시 (Latency). Gemini 1.5 Flash로 1건을 분류하면 이미지 포함 약 $0.0003, 수백 ms ~ 1초 정도 소요됩니다. ML 측은 Gemini Embedding 2 (멀티모달)를 사용하면 1건당 약 $0.0002이며, 로컬에서 LayoutLMv3를 구동하면 embedding 계산이 수십 ms 내에 끝납니다 (GPU 상각비는 별도). 결정론적 (Deterministic)이라는 점도 ML 측의 강점으로, 동일한 입력에 대해 반드시 동일한 출력이 반환되므로 후속 테스트를 작성하기 용이합니다.
새 카테고리 추가와 미지 포맷(Unknown Format) 탐지의 적합성 여부가 운영 측면에서 가장 큰 차이를 만듭니다. LLM 분류는 "프롬프트에 새 카테고리를 추가"하는 것만으로 끝나기 때문에 간편합니다. 하지만 기존 카테고리와 유사한 서류가 들어오면 멋대로 분류해 버리기 때문에, 미지 포맷 탐지는 실질적으로 불가능합니다. 반면 ML 분류는 재학습(Re-training)이나 재클러스터링(Re-clustering)이 필요한 대신, 클러스터 중심으로부터의 거리나 HDBSCAN의 노이즈 레이블(-1)을 통해 "어느 곳에도 속하지 않는 서류"를 명시적으로 걸러낼 수 있습니다. 사이런트 실패(Silent failure)를 구조적으로 방지할 수 있는 것은 이 방식입니다.
템플릿 입도(Granularity)의 분리는 실운영에서 서서히 효과를 발휘하는 부분입니다. 같은 "송장(Invoice)" 카테고리라도 A사와 B사는 추출 필드(Extraction field)의 위치가 다릅니다. 이러한 "회사별 입도"를 LLM 프롬프트로 모두 기술하는 것은 솔직히 고된 일이며, 임베딩(Embedding) 공간의 기하학적 구조에 의해 자동으로 분리되도록 하는 것이 훨씬 편했습니다. Unsupervised Document and Template Clustering 연구에서도 서류 종류보다 더 세밀한 템플릿 단위의 비지도 학습(Unsupervised) 발견이 실운영에서 효과적이라고 보고되고 있습니다.
경계선은 실무적인 감각으로 다음과 같이 설정하고 있습니다.
| 포맷 수 | 권장 구성 |
|---|---|
| ~15 | LLM 분류로 충분 |
| ... |
15개까지는 솔직히 Gemini 3 Flash에 던지는 것이 편하며, ML 측의 운영 비용을 들일 의미가 적습니다. 30개를 넘어서는 시점부터 프롬프트 비대화와 유사 클래스 식별력 저하가 동시에 나타나기 시작하며, LLM 분류의 정밀도가 눈에 띄게 떨어집니다. 50개를 넘어가면 단일 분류기(Classifier)로는 한계가 오므로 계층화(Hierarchical)가 필요해집니다.
여기서 중요한 것은 "현재의 포맷 수"가 아니라 "6개월 후의 예측치"를 기준으로 결정하는 것입니다. 6개월 뒤에 30종류로 늘어날 10종류라면, 처음부터 ML로 구축하는 것이 나중에 전부 다시 쓰는 것보다 저렴했습니다. 적어도 제가 경험한 프로젝트에서는 LLM 분류에서 ML 분류로 전환하는 비용이 상당히 무거웠기 때문에, 가급적 일찍 전환하는 쪽을 택하고 있습니다.
필요한 의존성(Dependency)은 다음과 같습니다.
pip install transformers torch pillow scikit-learn hdbscan google-generativeai
google-generativeai는 구형 SDK입니다. 2026년 시점에는 google-genai (신형 SDK, from google import genai)로의 이행이 진행되고 있습니다. 본 기사의 코드는 구형 SDK를 전제로 작성되었으나, 신형 SDK로 읽어들일 경우에는 types.GenerateContentConfig 작성 방식으로 교체해 주십시오.
임베딩(Embedding) 모델은 용도에 따라 3가지 선택지 중 고르는 형태가 됩니다.
| 모델 | RVL-CDIP 정확도 | OCR | VRAM 기준 | 적합한 용도 |
|---|---|---|---|---|
| LayoutLMv3-base | 95.44% | 필요 | 8GB | OCR 품질이 높고, 텍스트와 레이아웃을 모두 사용하고 싶을 때 |
| ... |
LayoutLMv3를 사용할 경우, 임베딩 획득 코드는 다음과 같이 구성됩니다.
from transformers import LayoutLMv3Processor, LayoutLMv3Model
from PIL import Image
import torch
...
여기서 은근히 중요한 것이 .convert("RGB")입니다. 그레이스케일(Grayscale)이나 RGBA가 혼재된 PDF를 입력하면 조용히 에러가 발생하므로, 입력 정규화(Normalization)를 초기에 넣어두면 밤중에 에러로 인해 깨어나는 일을 방지할 수 있습니다. torch.no_grad()를 잊으면 VRAM이 불필요하게 팽창하며, truncation=True, max_length=512를 붙이지 않으면 긴 문서에서 암묵적 절단(Implicit truncation) 경고가 계속 발생하므로, 이런 부분은 처음에 해결해 두는 것이 나중에 편합니다.
GPU를 준비하고 싶지 않은 팀은 로컬 추론을 포기하고 Gemini Embedding 2 (멀티모달 MaaS)를 사용하는 방법도 있습니다. 건당 약 $0.0002로, 월 10,000건이라 해도 약 $2 정도입니다. VRAM 예산이 없는 상황이라면 솔직히 이쪽을 선택하는 것이 운영 측면에서 가볍습니다.
수중에 기존 서류가 1,000장 있고 몇 종류인지 정확히 모르는 상태에서 시작하는 경우, 클러스터 수를 사전에 지정하지 않고도 미지 서류를 노이즈 레이블(-1)로 명시적으로 잡아낼 수 있는 HDBSCAN이 구조적으로 적합합니다.
import hdbscan
import numpy as np
embeddings = np.vstack([embed(p).detach().numpy() for p in document_paths])
...
min_cluster_size는 서류 총수의 0.51% 정도(1,000건이라면 510)를 초기값의 기준으로 삼습니다. HDBSCAN의 강점은 클러스터(Cluster) 수를 사전에 지정하지 않고도 '유사한 서류 그룹'을 자동으로 추출해 준다는 점과, 어느 그룹에도 속하지 않는 서류가 -1 레이블로 분류된다는 점입니다. 이 -1 레이블을 그대로 '본 적 없는 서류'를 탐지하는 데 사용할 수 있습니다.
위의 코드는 768차원의 임베딩(Embedding)에 직접 HDBSCAN을 적용하는 최소 구성입니다. 실제 프로젝트에서는 UMAP을 사용하여 15차원 정도로 차원을 축소한 뒤 HDBSCAN을 적용하는 2단계 구성이 정석으로 통하며, BERTopic이 그 대표적인 구현 사례입니다. 고차원 상태 그대로 두면 밀도 추정(Density Estimation)이 제대로 작동하기 어려워, min_cluster_size를 낮추지 않으면 거의 모든 데이터가 노이즈로 분류되는 현상이 발생할 수 있습니다. 정밀도를 높이려면 UMAP을 중간에 포함하는 구성을 검토하십시오.
단, HDBSCAN은 운영 환경의 라우팅(Routing)에는 적합하지 않습니다. 비결정론적(Non-deterministic) 특성이 있어 동일한 데이터라도 실행할 때마다 결과가 달라질 수 있으며, 재현성(Reproducibility)이 요구되는 운영 환경에서는 디버깅이 어려워집니다. 발견(Discovery) 단계에서는 HDBSCAN을, 운영(Operation) 단계에서는 결정론적인 KNN을 사용하는 방식이 현실적이었습니다.
from sklearn.neighbors import KNeighborsClassifier
# metric='cosine'은 sklearn에서 algorithm='brute'를 강제함
knn = KNeighborsClassifier(n_neighbors=5, metric='cosine')
...
반환값의 두 번째 요소는 KNN이 선택한 레이블에 대한 근접 5개 데이터 중 다수결 비율이며, 이를 신뢰도(Confidence)로 취급합니다. 이 값 하나만으로 신뢰도가 낮은 케이스를 LLM 폴백(Fallback)으로 넘기는 라우팅을 작성할 수 있습니다.
신뢰도가 낮은 서류는 Gemini 3 Flash로, 더욱 모호한 경우에는 Gemini 3 Pro로 에스컬레이션(Escalation)하는 구성이 비용과 정밀도의 균형 측면에서 가장 효과적이었습니다.
# flash_classify / pro_classify는 Gemini의 분류 전용 프롬프트 호출
# 반환값은 (label: str, confidence: float)
def process(image_path: str) -> dict:
...
본인의 프로젝트(서류 종류 30종 이하, 임계값 0.7/0.6)에서는 전체의 70~90%가 ML 분류로 확정되고, 나머지는 Flash, 그리고 아주 극소수가 Pro로 넘어가는 분포로 안정되었습니다. 다만 클래스 간의 분리도(Separability)에 크게 의존하므로, 실제 데이터에서의 비율은 완전히 다를 수 있습니다. 특히 유사한 포맷(A사 청구서와 A사 발주서 등)이 많은 경우에는 ML 확정률이 낮아지므로, 운영 시작 후 임계값을 재조정한다는 전제하에 설계하는 것이 현실적입니다.
임계값 설계는 HDBSCAN의 노이즈 탐지(label == -1)와 KNN의 predict_proba를 조합하는 것이 가장 안정적이었습니다. 중심으로부터의 거리로 p95 임계값을 사용하는 방법도 있는데, 이 방식은 어떤 서류 종류로부터 얼마나 '멀리' 떨어져 있는지를 모니터링할 수 있어 데이터 드리프트(Data Drift) 탐지 역할도 겸할 수 있습니다.
포맷 수가 50개를 넘어가면 단일 분류기만으로는 클래스 경계가 너무 모호해지기 때문에, 단계적으로 범위를 좁혀가는 구성이 필요해졌습니다.
계층적 분류(Hierarchical Classification)에서 가장 많이 겪은 문제는 연쇄 오류(Cascading Error)였습니다. 첫 번째 단계에서 '청구서'를 '영수증'으로 오분류하면, 두 번째 단계의 벤더별 분류가 통째로 빗나가게 됩니다. 특히 '청구서 겸 발주서'와 같은 경계 사례에서는 첫 번째 단계의 신뢰도가 낮게 나오므로, 이러한 서류는 여러 개의 두 번째 단계 분류기를 병렬로 실행하여 정합성을 체크하는 방식으로 해결하고 있습니다. 모든 서류에 Gemini를 호출하는 것보다 저렴하면서도 경계 사례에 강해졌습니다.
가장 많은 시간을 허비했던 것은 인감이 숫자 위에 겹쳐 있는 청구서의 판독이었습니다. Gemini 2.5 Pro 시절에는 '¥38,500'의 '3'이 인감에 가려져 '¥8,500'으로 읽히는 경우가 있었고, 이는 분개 금액이 한 자릿수 차이 나는 것을 검산하는 과정에서 발견되었습니다. Gemini 3 Pro 세대에서는 media_resolution=high를 지정하면 상당히 개선되어, 인감 중첩으로 인한 오독은 거의 나타나지 않고 있습니다.
다음에 곤란했던 점은 연호(元号)의 날짜 변환이었습니다. 「令和6年3月(레이와 6년 3월)」을 「2024年3月(2024년 3월)」로 변환하는 처리는, 프롬프트(Prompt)로 명시적으로 지시하지 않으면 「R6-03」과 같은 중간 표기 형식으로 반환되는 경우가 있어, 후속 단계인 회계 시스템에서 거부되었습니다. 서류 타입별 프롬프트에 「날짜는 반드시 서기(西暦)로 반환할 것」이라는 한 줄을 넣어두는 것만으로 해결되지만, 범용 프롬프트 1장 구성으로는 이러한 서류별 규칙을 끼워 넣을 공간이 없습니다. 분류층(Classification Layer)을 사이에 두었을 때의 이점이 여기서 발휘되었습니다.
서류별 media_resolution
설정은 다음과 같이 전환하고 있습니다.
RESOLUTION = {
"invoice_vendor_a": "medium",
"invoice_vendor_b": "medium",
...
본 기사에서는 「Gemini 3 Pro」, 「Gemini 3 Flash」를 모델 세대명으로 참조하고 있으나, 2026-05 시점에서 구체적인 API 모델 ID는 아직 Preview 단계로 운영되고 있습니다. gemini-3-pro-preview는 2026-03-09에 shutdown되었으며, 현재는 gemini-3.1-pro-preview가 권장 ID입니다. 실서비스 투입 시에는 반드시 공식 모델 목록(Official Model List)에서 최신 정식 ID를 확인하십시오. 또한 media_resolution 파라미터 지정 방식도 SDK 버전에 따라 다르며, 신규 SDK(from google import genai)에서는 types.GenerateContentConfig(image_config=types.ImageConfig(media_resolution=...))와 같은 중첩 구조를 가집니다.
실제 구성별 월간 비용입니다. 전제 조건은 이미지 토큰 560(medium) + 프롬프트 500 + 출력 300, 2026년 4월 시점의 공식 요금(Gemini 3 Pro: 입력 $2/1M · 출력 $12/1M, Flash: 입력 $0.50/1M · 출력 $3/1M) 기준입니다.
| 구성 | 월간 비용 | 비고 |
|---|---|---|
| Gemini 3 Flash 단발 추출 | 약 $14 | 2.5 Pro 상당의 정밀도를 Flash 가격으로 |
| ... |
최저가 구성은 「ML 분류 + Gemini 3 Flash 추출 + 신뢰도에 따른 3 Pro 에스컬레이션(Escalation)」으로, 단발 Pro 구성의 약 1/3 수준입니다. 다만, 추출 누락 1건에 대한 재작업 공수(확인·수정·재처리)를 공수로 환산하면, 월 수천 엔~수만 엔의 API 비용 차이는 순식간에 상쇄될 수 있으므로, 최저가를 선택하는 것은 목적에 따라 달라져야 합니다.
제 경험상, 월간 처리 건수가 수만 건을 넘고 정밀도 SLA(Service Level Agreement)도 엄격한 프로젝트에서는 API 비용 최적화보다 추출 누락에 따른 업무 영향 억제가 더 중요해집니다. 이 부분은 회사의 감사 요건이나 회계 마감 스케줄과의 협의가 필요합니다.
서류 AI 파이프라인 설계를 몇 번이나 다시 해보며 최종적으로 효과를 본 것은 「포맷 수가 반년 뒤에 몇 개가 되어 있을 것인가」라는 예측이었습니다. 현재 10종류라서 문제가 없더라도, 반년 안에 30종류로 늘어날 전망이라면 처음부터 ML 기반으로 구축하는 것이 나중에 리라이트(Rewrite) 비용이 적게 듭니다. 반대로 10종류로 안정되어 있다면, Gemini 3 Flash 단발 구성으로도 충분히 경쟁력이 있습니다.
현재도 남아 있는 과제는, 일본어 수기 장부에서 인감의 농담(濃淡)이 강한 경우의 오독과, 계층 분류의 제2단계에서 벤더별 템플릿이 월 1~2건 페이스로 증가하는 운영상의 어려움입니다. 후자의 경우, HDBSCAN을 사용하여 월 1회 자동 재클러스터링(Re-clustering)을 돌려 새로운 템플릿 후보를 탐지하는 작업을 실행함으로써 안정화하고 있습니다.
Gemini 3 Pro를 사용할 수 있는 세대가 되더라도, 「LLM 한 번으로 전부 처리하기」보다는 「LLM에 도달하기 전의 전처리를 ML 측으로 넘기기」가, 적어도 정형 서류의 세계에서는 정직하게 정밀도와 비용 모두를 개선한다는 것이 현재까지의 결론입니다.
-
Liu, N. F. et al. (2023). Lost in the Middle: How Language Models Use Long Contexts. arXiv:2307.03172
-
Sampaio, P. R., Maxcici, H. (2025). Unsupervised Document and Template Clustering using Multimodal Embeddings. arXiv:2506.12116
-
Scius-Bertrand, A. et al. (2024). Zero-Shot Prompting and Few-Shot Fine-Tuning: Revisiting Document Image Classification Using Large Language Models. arXiv:2412.13859 (ICPR 2024)
-
McInnes, L., Healy, J., Astels, S. (2017). hdbscan: Hierarchical density based clustering. JOSS, 2(11), 205
-
Guo, C. et al. (2017). On Calibration of Modern Neural Networks. ICML 2017. arXiv:1706.04599
-
Huang, Y. et al. (2022). LayoutLMv3: Pre-training for Document AI with Unified Text and Image Masking. arXiv:2204.08387
-
microsoft/layoutlmv3-base (HuggingFace)
-
Kim, G. et al. (2022). OCR-free Document Understanding Transformer (Donut). arXiv:2111.15664
-
naver-clova-ix/donut-base (HuggingFace)
-
Faysse, M. et al. (2024). ColPali: Efficient Document Retrieval with Vision Language Models. arXiv:2407.01449
-
vidore/colpali-v1.3 (HuggingFace)
-
RVL-CDIP Dataset
-
Google AI: Gemini API Pricing
-
Google AI: Document Understanding (
media_resolution仕様含む) - Gemini Embedding 2 — Google Blog -
google-genai Python SDK (신(新) SDK)
-
google-generativeai Python SDK (구(舊) SDK)
-
scikit-learn: KNeighborsClassifier
-
HDBSCAN Python implementation (GitHub)
-
PyTorch
-
transformers (Hugging Face)
-
Anthropic: Building Effective Agents(라우팅 패턴의 1차 출처)
-
Lu, R., Liu, H., Hou, S. (2026). Evaluation of Embedding-Based and Generative Methods for LLM-Driven Document Classification. arXiv:2604.04997(지구과학 문서 영역이지만 방법론 비교 관점에서 참고)
-
Document Classification and Entity Extraction with Gemini — Google Cloud Community
-
Document Classification with LLM and ML — Andy Bosyi
-
Lessons from Running an LLM Document Processing Pipeline in Production
-
Gemini 2.5 Pro Japanese OCR (Zenn)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기