5년 전에는 인보이스에서 구조화된 데이터를 추출하려면 딥러닝 파이프라인이 필요했습니다. OCR 단계, 수천 개의 손으로 라벨링한 문서로 미세 조정한 레이아웃 인식 모델(LayoutLM, Donut), 그리고 공급업체가 템플릿을 변경할 때마다 발생하는 유지 관리 부담이 그것입니다. 2026년에는 전체 파이프라인이 하나의 비전 LLM API 호출로 축소됩니다. 이미지를 입력하면 스키마 검증 JSON이 출력되며, 학습 데이터도 호스팅 모델도 필요 없고 새로운 레이아웃도 제로샷으로 처리됩니다. Kunavo는 이 패턴에 사용되는 비전 모델에 대한 독립적인 OpenAI 호환 게이트웨이입니다. Kunavo가 제공하는 이 가이드는 전체 작동 패턴과 자체 공개 요금을 기준으로 측정한 인보이스당 비용, 운영 수준의 정확도를 유지하는 방법을 보여 줍니다.
인보이스 추출 방식 비교: 규칙, 머신러닝, 딥러닝, LLM
인보이스에서 구조화된 데이터를 얻기 위해 네 세대의 기술이 사용되어 왔습니다. 지난 20년 동안 이들 사이의 선택을 결정했던 절충점, 즉 라벨링된 데이터에 비용을 들여 정확도를 확보하는 관계가 더 이상 적용되지 않으므로, 이 기술들을 나란히 비교해 볼 가치가 있습니다.
| 접근 방식 | 작동 방식 | 설정 비용 | 새 공급업체 레이아웃 처리 |
|---|---|---|---|
| 템플릿 / 규칙 (1990년대–) | OCR 후 공급업체별 좌표 영역과 정규식(“‘TOTAL’ 오른쪽의 숫자가 총액”) 사용 | 공급업체당 수시간, 영구적 | 아니요 — 새 템플릿 필요 |
| 고전적 머신러닝 (2010년대) | OCR 후 직접 설계한 특징(위치, 글꼴 크기, 주변 단어)을 CRF 또는 SVM에 입력해 각 토큰에 태그 지정 | 특징 설계 + 수천 개의 라벨 | 부분적으로 — 보지 못한 레이아웃에서 성능 저하 |
| 딥러닝 (2020–) | 레이아웃 인식 트랜스포머(LayoutLM, Donut, DocTR)를 미세 조정해 텍스트, 위치, 픽셀을 함께 읽음 | 라벨링된 인보이스 3,000–50,000개 + GPU 서빙 | 대개 아님 — 일반적으로 재라벨링 및 재학습 필요 |
| 비전 LLM (2024–) | 한 번의 API 호출: 인보이스 이미지와 JSON 스키마를 보내고, 구조화된 출력을 해당 스키마로 제한 | 없음 — 스키마와 프롬프트만 필요; 인보이스당 $0.00248–$0.00497 | 예, 제로샷 |
앞의 세 방식은 모두 하나의 특성을 공유합니다. 정확도를 라벨링된 데이터로 확보한다는 점입니다. 모든 공급업체 템플릿, 모든 CRF 특징, 모든 LayoutLM 미세 조정은 주석 프로젝트입니다. 비전 LLM은 사전 학습 중에 이미 그 비용을 부담했으므로 새 레이아웃의 한계 비용은 0입니다. 이것이 전체적인 변화입니다. 이전 방식이 작동을 멈춘 것이 아니라, 이전 방식이 예산을 사용하던 항목이 무료가 된 것입니다.
미세 조정 딥러닝과 비전 LLM 상세 비교
| 미세 조정 레이아웃 모델(2021) | 비전 LLM(2026) | |
|---|---|---|
| 학습 데이터 | 라벨링된 인보이스 3,000–50,000개 | 없음(제로샷) — 예시는 일부 엣지 케이스에 도움 |
| 새 공급업체 레이아웃 | 대개 재라벨링 + 재학습 필요 | 제로샷으로 처리 |
| 인프라 | GPU 서빙 + OCR 단계 | HTTPS 호출 하나 |
| 출력 형식 | 토큰 태그 → 사용자 지정 디코딩 | 스키마 제약 JSON |
| 인보이스당 비용 | 엔지니어링 시간이 지배적 | $0.00248–$0.00497 |
단일 고정 고용량 레이아웃에서는 미세 조정 추출기가 여전히 우세할 수 있습니다. 그러나 실제 인보이스의 긴 꼬리, 즉 서로 다른 공급업체, 언어, 스캔 품질에서는 제로샷 비전 LLM이 실무상 더 정확하고 소유 비용도 크게 낮습니다. 이 작업 유형에 대한 더 많은 패턴은 데이터 추출 사용 사례에서 확인할 수 있습니다.
전체 패턴: 이미지 → 스키마 검증 JSON
아래의 모든 내용은 Kunavo의 OpenAI 호환 엔드포인트를 대상으로 실행됩니다. base_url을 https://api.kunavo.com/v1로 지정하면 문자열 하나만 변경해 동일한 코드로 Claude 또는 GPT를 호출할 수 있습니다.
from openai import OpenAI
import base64, json
client = OpenAI(
base_url="https://api.kunavo.com/v1",
api_key="sk-kn-...",
)
SCHEMA = {
"name": "invoice",
"schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"vendor_tax_id": {"type": ["string", "null"]},
"invoice_number": {"type": "string"},
"invoice_date": {"type": "string", "description": "ISO 8601"},
"due_date": {"type": ["string", "null"]},
"currency": {"type": "string", "description": "ISO 4217"},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
"amount": {"type": "number"},
},
"required": ["description", "amount"],
},
},
"subtotal": {"type": ["number", "null"]},
"tax": {"type": ["number", "null"]},
"total": {"type": "number"},
},
"required": ["vendor_name", "invoice_number", "invoice_date",
"currency", "line_items", "total"],
},
}
def extract(path: str) -> dict:
image_b64 = base64.b64encode(open(path, "rb").read()).decode()
resp = client.chat.completions.create(
model="claude-haiku-4-5", # the cheapest capable model for clean invoices
response_format={"type": "json_schema", "json_schema": SCHEMA},
messages=[
{"role": "system", "content":
"Extract the invoice into the schema. Copy values exactly as "
"printed; use null when a field is absent. Never invent data."},
{"role": "user", "content": [
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{image_b64}"}},
]},
],
)
return json.loads(resp.choices[0].message.content)
inv = extract("invoice_0231.png")
# Cheap arithmetic guardrail: reject when the line items don't add up.
delta = abs(sum(li["amount"] for li in inv["line_items"])
+ (inv.get("tax") or 0) - inv["total"])
if delta > 0.02:
raise ValueError(f"line items disagree with total by {delta:.2f}")
print(inv["vendor_name"], inv["total"], inv["currency"])대부분의 작업은 세 가지 세부 사항이 담당합니다. response_format의 JSON 스키마(Claude에서는 모델이 다른 것을 출력할 수 없습니다. 아래 연결된 참조의 GPT 관련 참고 사항 참조), 시스템 프롬프트 규칙 “값은 정확히 복사하고, 없으면 null로 하며, 절대 지어내지 말 것”(환각으로 만든 세금 ID 제거), 그리고 산술 안전장치입니다. 항목 합계와 세금이 총액과 일치해야 하며, 그렇지 않으면 문서를 더 강력한 모델이나 사람에게 보냅니다. 각 모델 계열에 대해 response_format가 무엇을 강제하는지는 채팅 완성 참조 문서를 확인하세요.
동일한 스키마를 타입이 지정된 Pydantic 모델로 표현
원시 JSON 스키마 딕셔너리는 엔드포인트 하나에는 괜찮지만 열 개에는 번거롭습니다. 운영 환경에서는 대부분의 팀이 스키마를 Pydantic 모델로 한 번만 정의하고 여기서 제약 조건과 파싱된 객체를 모두 도출합니다. 그러면 추출기와 나머지 코드베이스가 형태에 대해 불일치할 수 없습니다.
from pydantic import BaseModel, Field
from typing import Literal
from openai import OpenAI
client = OpenAI(base_url="https://api.kunavo.com/v1", api_key=KEY)
class LineItem(BaseModel):
description: str
quantity: float | None = None
unit_price: float | None = None
amount: float
class Invoice(BaseModel):
vendor_name: str
vendor_tax_id: str | None = None
invoice_number: str
invoice_date: str = Field(description="ISO 8601")
currency: str = Field(description="ISO 4217")
line_items: list[LineItem]
tax: float | None = None
total: float
# A confidence field the model fills in itself is a cheap, surprisingly
# reliable router: low self-reported confidence correlates well with the
# documents a human should see.
confidence: Literal["high", "medium", "low"]
resp = client.chat.completions.create(
model="claude-haiku-4-5",
response_format={
"type": "json_schema",
"json_schema": {"name": "invoice", "schema": Invoice.model_json_schema()},
},
messages=[...],
)
invoice = Invoice.model_validate_json(resp.choices[0].message.content)여기서 두 가지를 참고할 만합니다. float 대신 float | None으로 타입을 지정한 필드는 모델이 “없음”을 허용되는 형식으로 표현할 수 있게 합니다. 이것이 없으면 누락된 세금 항목은 0을 지어내도록 강하게 유도됩니다. 또한 자체 보고 confidence 필드는 출력 토큰 세 개만 사용하면서 라우팅 성능이 놀라울 정도로 좋습니다. 보정된 값은 아니지만 “low”는 사람이 확인해야 한다는 신뢰할 만한 신호입니다.
2단계 라우팅: 운영 환경에서 실제로 실행되는 방식
모든 문서에 단일 모델을 사용하는 방식은 적절하지 않습니다. 깨끗하게 인쇄된 인보이스 대부분은 사용 가능한 가장 저렴한 비전 모델로 처리하고, 손글씨, 불량 스캔, 특이한 레이아웃 같은 어려운 꼬리 부분에서 더 강력한 모델의 요금이 정당화됩니다. 추측이 아니라 검증 실패를 기준으로 승격하세요.
CHEAP, STRONG = "claude-haiku-4-5", "claude-sonnet-5"
def valid(inv: dict) -> bool:
"""Arithmetic is the guardrail no confidence score replaces."""
items = sum(li["amount"] for li in inv["line_items"])
return abs(items + (inv.get("tax") or 0) - inv["total"]) <= 0.02
def extract_routed(path: str) -> tuple[dict, str]:
inv = extract(path, model=CHEAP)
if valid(inv) and inv.get("confidence") != "low":
return inv, CHEAP
# Escalate: same prompt, same schema, stronger model. Only the failures
# pay the higher rate, which is what keeps the blended cost near the
# cheap tier's.
inv = extract(path, model=STRONG)
if not valid(inv):
raise NeedsHumanReview(path, inv)
return inv, STRONG
# Blended cost at a 90/10 split, per 10,000 invoices:
# 9,000 x $0.00248 + 1,000 x $0.00497
# = $27.29산술 검사가 실제 작업을 수행합니다. 모델 자체의 자기 평가와 독립적인 유일한 신호이기 때문입니다. 항목 합계가 명시된 총액과 일치하는 문서는 중요한 방식으로 잘못되었을 가능성이 거의 없고, 일치하지 않는 문서는 신뢰도 필드가 무엇을 주장하든 더 강력한 모델이나 사람에게 보낼 가치가 있습니다.
모델별 인보이스당 비용
한 페이지 인보이스 ≈ 1,800개의 입력 토큰(이미지로 전송) + 약 350개의 JSON 출력 토큰:
| 모델 | 인보이스당 | 인보이스 10,000개당 | 용도 |
|---|---|---|---|
claude-haiku-4-5 | $0.00248 | $24.80 | 기본값: 깨끗하게 인쇄된 인보이스 및 가장 저렴한 대량 실행 |
claude-sonnet-5 | $0.00497 | $49.70 | 승격 단계: 스캔, 손글씨, 실패 항목 |
요금은 가격 페이지의 실시간 정보이며(공급업체 목록 가격보다 약 30% 낮음), 실패한 요청에는 요금이 부과되지 않습니다. 2단계 라우팅, 즉 모든 요청을 Haiku로 처리하고 산술 검사 실패를 Sonnet으로 승격하는 방식을 사용하면 인보이스 10,000개/월의 비용은 일반적으로 $27.29 미만입니다.
정확도: 운영 환경에 도달하기 위한 체크리스트
- 스키마를 먼저 정의하세요. 정말 필수인 필드는
required으로 표시하고 나머지는 어디서나null를 허용하세요. 값을 강제하면 환각을 강제하게 됩니다. - OCR 텍스트가 아니라 이미지를 보내세요. 레이아웃에는 의미가 있습니다(열, 총액 상자). 비전 모델은 이를 직접 읽습니다. 깨끗한 텍스트 레이어를 추출할 수 있는 디지털 태생 PDF에서만 텍스트로 대체하세요.
- 코드에서 산술을 검증하세요. 항목 합계 + 세금 = 총액 검사는 대부분의 추출 오류를 무료로 잡아냅니다.
- 무작정 재시도하지 말고 승격하세요. 실패 항목은 검증 오류를 프롬프트에 포함해 Sonnet으로 보내고, 지속되는 실패는 사람 검토 대기열로 보냅니다.
- 실제 엣지 케이스에 퓨샷 예시를 사용하세요. 시스템 프롬프트에 최악의 레이아웃(신용 전표, 다중 통화) 예시 두세 개를 넣는 것이 어떤 양의 지시 튜닝보다 효과적입니다.
인보이스 외의 문서
동일한 스키마 제약 패턴은 영수증, 구매 주문서, 배송 메모, 세관 양식, 은행 거래 명세서에도 적용됩니다. 스키마를 바꾸고 안전장치는 유지하세요. RAG와 인접한 패턴은 더 폭넓은 구조화 데이터 추출 사용 사례에서, 물량이 증가한 후의 라우팅 전략은 비용 최적화 가이드에서 확인하세요. Kunavo 키 하나로 여기에서 사용하는 모든 모델 등급을 처리할 수 있습니다. 무료로 가입하고 키를 생성하면 스니펫을 그대로 실행할 수 있습니다.
자주 묻는 질문
인보이스에서 데이터를 추출하려면 딥러닝 모델이 필요한가요?
이제는 필요하지 않습니다. 기존의 OCR 후 LayoutLM 같은 레이아웃 모델을 수천 개의 라벨링된 인보이스로 미세 조정하던 방식은 대부분의 팀에서 단일 비전 LLM 호출로 대체되었습니다. 인보이스 이미지와 JSON 스키마를 보내면 검증된 구조화 데이터를 돌려받습니다. 라벨링할 학습 세트나 호스팅할 모델이 없으며, 미세 조정 모델을 무너뜨리던 레이아웃 변경도 제로샷으로 처리됩니다.
머신러닝 인보이스 추출은 LLM 사용과 비교해 어떤가요?
고전적 머신러닝 추출기(OCR 출력에 CRF 또는 SVM을 적용하고 직접 설계한 위치·글꼴 특징을 사용하는 방식)와 그 딥러닝 후속 모델(LayoutLM, Donut)은 모두 라벨링된 데이터로 정확도를 확보합니다. 배포마다 주석이 달린 인보이스 수천 개가 필요하고 레이아웃이 바뀌면 다시 라벨링해야 합니다. 비전 LLM은 사전 학습 중에 그 비용을 이미 부담했으므로, 보지 못한 공급업체 레이아웃도 제로샷으로 읽고 한 번의 API 호출에서 스키마 제약 JSON을 반환합니다. 추론 비용이 지배적인 단일 고정 초대량 레이아웃에서는 머신러닝이 여전히 우세하지만, 실제 인보이스의 긴 꼬리에서는 LLM이 실무상 더 정확하고 소유 비용도 훨씬 낮습니다.
인보이스 추출에 LayoutLM이나 Donut을 계속 사용할 수 있나요?
예. 안정적인 단일 레이아웃으로 수백만 개의 문서를 처리하고 라벨링 및 GPU 서빙 비용을 분산할 수 있다면 여전히 합리적인 선택입니다. 달라진 점은 기본값입니다. 새 프로젝트에서는 학습 세트 없이도 비전 LLM으로 오후 안에 운영 수준의 정확도에 도달할 수 있고 문서당 비용도 충분히 낮아 미세 조정의 경제성이 회수되는 경우가 드뭅니다. 흔한 절충안은 먼저 LLM을 실행하고, LLM이 승인한 자체 출력을 라벨링된 코퍼스로 사용해 나중에만 미세 조정하는 것입니다.
문서당 인보이스 추출 비용은 얼마인가요?
한 페이지 인보이스는 이미지로 약 1,800개의 입력 토큰과 약 350개의 JSON 출력 토큰을 사용합니다. Kunavo에서는 claude-haiku-4-5로 인보이스당 약 $0.00248, claude-sonnet-5로 $0.00497입니다. 즉 Sonnet 품질을 사용해도 인보이스 1,000개에 몇 달러만 듭니다. 실패한 요청에는 요금이 부과되지 않습니다.
인보이스 데이터 추출에는 어떤 모델을 사용해야 하나요?
claude-haiku-4-5로 시작하세요. 깨끗하게 인쇄된 인보이스를 최저 가격으로 거의 완벽하게 처리합니다. 손글씨, 저해상도 스캔, 이색적인 레이아웃 또는 산술 검사를 통과하지 못한 문서만 claude-sonnet-5로 라우팅하세요. 이 2단계 라우팅을 사용하면 일반적으로 전체 물량의 90% 이상을 저렴한 단계에서 처리할 수 있습니다.
출력이 유효한 JSON임을 어떻게 보장하나요?
구조화된 출력을 사용하세요. JSON 스키마와 함께 response_format을 전달하면 Claude 모델이 정확히 해당 형태만 출력하도록 제한되므로 정규식 후처리가 필요 없습니다. 여기에 저렴한 애플리케이션 수준의 안전장치를 추가하세요. 항목 합계와 세금이 총액과 일치하는지 확인하고, 불일치 항목은 더 강력한 모델이나 사람 검토 대기열로 보냅니다.
이미지뿐 아니라 PDF 인보이스에서도 데이터를 추출할 수 있나요?
예. 각 PDF 페이지를 PNG로 렌더링하고(예: pdf2image) 위와 같이 전송하세요. 디지털 태생 PDF라면 텍스트 레이어를 추출해 일반 텍스트로 보낼 수도 있어 더 저렴합니다. 스캔 문서에는 이미지 경로를 유지하세요.
LLM 인보이스 추출은 얼마나 정확한가요?
깨끗하게 인쇄된 인보이스에서는 산술 안전장치가 있는 스키마 제약 비전 추출이 필드 수준 정확도 98% 이상을 꾸준히 달성합니다. 미세 조정 모델과 달리 한 번도 본 적 없는 레이아웃에서도 정확도가 유지됩니다. 자체 문서로 측정하세요. 라벨링된 인보이스 100개면 탄탄한 평가 세트가 됩니다.
내 인보이스 데이터가 학습에 사용되나요?
아니요. Kunavo를 통한 요청은 소비자 앱 약관이 아니라 API 사용 약관에 따라 모델 공급업체로 전달되며 모델 학습에 사용되지 않습니다. 보존 세부 정보는 청구 및 데이터 문서를 참조하세요.