AI 문서 인텔리전스: PDF·계약서·영수증에서 정형 데이터 자동 추출

AI 기술

문서 인텔리전스AI 데이터 추출PDF 처리OCR문서 자동화

이 글은 누구를 위한 것인가

  • PDF 계약서·영수증을 수동으로 입력하는 팀
  • OCR만으로는 구조화된 데이터 추출이 안 되는 팀
  • LLM으로 문서에서 특정 정보를 자동으로 뽑고 싶은 개발자

들어가며

영수증 사진에서 금액·날짜·상호를 뽑고, 계약서에서 계약 조건·만료일·당사자를 추출하는 것 — 이것을 LLM이 한다. OCR이 텍스트를 읽어주고, LLM이 의미를 이해하고 구조화한다.

이 글은 bluefoxdev.kr의 AI 문서 처리 가이드 를 참고하여 작성했습니다.


1. 문서 처리 파이프라인

[문서 인텔리전스 처리 흐름]

1. 문서 입력:
   PDF, DOCX, 이미지(JPG, PNG), 스캔본

2. 전처리:
   이미지: 기울기 보정, 품질 향상
   PDF: 레이어 분리 (텍스트 vs 이미지)
   보안 PDF: 비밀번호 처리

3. 텍스트 추출:
   디지털 PDF: pdfminer/pypdf 직접 추출
   스캔/이미지: Tesseract OCR 또는 AWS Textract
   표/양식: Amazon Textract (레이아웃 인식)

4. LLM 구조화 추출:
   Claude Vision 또는 GPT-4V (이미지 직접)
   또는 텍스트 → Claude (텍스트만)
   
5. 검증:
   필수 필드 존재 여부
   데이터 타입 검증 (날짜, 금액 형식)
   신뢰도 점수 산출

6. 후처리:
   정규화 (날짜 형식 통일)
   DB 저장
   수동 검토 큐 (신뢰도 낮은 건)

2. 영수증·문서 데이터 추출

import anthropic
import base64
from pathlib import Path
from pydantic import BaseModel

class ReceiptData(BaseModel):
    merchant_name: str
    merchant_address: str | None
    date: str
    total_amount: float
    tax_amount: float | None
    items: list[dict]
    payment_method: str | None
    confidence: float

class ContractData(BaseModel):
    contract_type: str
    parties: list[dict]
    effective_date: str
    expiration_date: str | None
    key_clauses: list[dict]
    payment_terms: str | None
    termination_conditions: str | None
    confidence: float

client = anthropic.Anthropic()

async def extract_receipt(image_path: str) -> ReceiptData:
    """영수증 이미지에서 데이터 추출"""
    
    with open(image_path, "rb") as f:
        image_data = base64.standard_b64encode(f.read()).decode("utf-8")
    
    ext = Path(image_path).suffix.lower()
    media_type = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}.get(ext, "image/jpeg")
    
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1500,
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": media_type,
                        "data": image_data,
                    },
                },
                {
                    "type": "text",
                    "text": """이 영수증에서 다음 정보를 JSON으로 추출하세요.
찾을 수 없는 필드는 null로 설정하세요.

{
  "merchant_name": "상호명",
  "merchant_address": "주소",
  "date": "YYYY-MM-DD 형식",
  "total_amount": 총금액 (숫자),
  "tax_amount": 부가세 (숫자 또는 null),
  "items": [{"name": "품목명", "quantity": 수량, "price": 가격}],
  "payment_method": "카드/현금/간편결제",
  "confidence": 0.0-1.0 (추출 신뢰도)
}"""
                }
            ]
        }]
    )
    
    import json
    raw = json.loads(response.content[0].text)
    return ReceiptData(**raw)

async def extract_contract(pdf_text: str) -> ContractData:
    """계약서 텍스트에서 핵심 정보 추출"""
    
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=3000,
        messages=[{
            "role": "user",
            "content": f"""다음 계약서에서 핵심 정보를 JSON으로 추출하세요.

계약서 내용:
{pdf_text[:8000]}

추출할 정보:
{{
  "contract_type": "계약 유형 (임대차/매매/서비스/용역 등)",
  "parties": [
    {{"role": "甲/乙/공급자/수요자", "name": "이름/상호", "registration_number": "사업자번호"}}
  ],
  "effective_date": "YYYY-MM-DD",
  "expiration_date": "YYYY-MM-DD 또는 null",
  "key_clauses": [
    {{"clause": "조항명", "summary": "한 줄 요약", "is_critical": true/false}}
  ],
  "payment_terms": "대금 지급 조건",
  "termination_conditions": "계약 해지 조건",
  "confidence": 0.0-1.0
}}"""
        }]
    )
    
    import json
    raw = json.loads(response.content[0].text)
    return ContractData(**raw)

async def batch_process_documents(documents: list[dict]) -> list[dict]:
    """문서 일괄 처리 파이프라인"""
    
    results = []
    
    for doc in documents:
        doc_type = doc.get("type", "unknown")
        
        try:
            if doc_type == "receipt":
                data = await extract_receipt(doc["file_path"])
            elif doc_type == "contract":
                text = await extract_pdf_text(doc["file_path"])
                data = await extract_contract(text)
            else:
                data = None
            
            if data and data.confidence < 0.7:
                # 낮은 신뢰도는 수동 검토 큐
                await queue_for_manual_review(doc["id"], data.dict())
            elif data:
                await save_extracted_data(doc["id"], data.dict())
            
            results.append({
                "doc_id": doc["id"],
                "status": "success",
                "confidence": data.confidence if data else 0,
            })
            
        except Exception as e:
            results.append({
                "doc_id": doc["id"],
                "status": "error",
                "error": str(e),
            })
    
    return results

마무리

AI 문서 추출에서 가장 중요한 것은 신뢰도 검증이다. LLM이 "있다고 생각하는" 데이터와 실제 데이터가 다를 수 있다. 신뢰도 70% 이상만 자동 저장하고, 나머지는 인간 검토를 거치는 구조가 안전하다. 계약서의 경우 법적 효력이 있으므로 100% 인간 검토를 권장한다.