13.2.2 목표 출력 스키마(Target Output Schema) 정의
앞서 13.2.1절에서 규명한 극단적인 4가지의 무질서한 이종 입력 문서들(인보이스, 영수증, 발주서, 견적서)을 하나의 빈틈없는 백엔드 파이프라인으로 아름답게 관통시키기 위해서는, LLM 에이전트 컴포넌트가 어떤 적대적인 문서 타입을 마주하든 최종적으로 뱉어내야 할 **‘단일하고 확정적인 목표 구조(Target Structure)’**를 파이썬(Python)의 메타 클래스 위상으로 하드코딩(Hard-coding)하여 선언해 두어야만 한다.
이 목표 출력 스키마(Target Output Schema)는 인공지능이 멋대로 텍스트를 만들어낼 서술형 응답의 자유도를 0%로 완벽하게 수축시키고, 모델이 내뿜는 모든 출력 토큰을 우리가 통제하는 깔끔하고 융통성 없는 JSON 컨테이너 안으로 무자비하게 욱여넣는 **‘결정론적 감옥(Deterministic Frame)’**이 될 것이다. 본 실전 예제 시나리오에서는 최신 파이썬 생태계의 표준이자 가장 엄격하고 성능이 강력한 자가 검증 라이브러리인 Pydantic을 사용하여, 데이터 계층의 강타입(Strong Type) 심장부를 설계한다.
1. Pydantic 컴파일러를 이용한 스키마 기반 객체 정의
우리가 설계할 재무 데이터 모델의 구조적 위상은 크게 세 가지 계층으로 정교하게 나뉜다. 문서 전체의 식별자를 담는 전역(Global) Header 계층, 다수의 세부 구매 품목을 분할 배열로 나열하는 Line Item List 계층, 그리고 최종 결제액 계산의 진실(Ground Truth)을 담는 Summary 계층이다.
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
from datetime import date
class DocumentType(str, Enum):
""" LLM이 프롬프트를 벗어나 스스로 문서의 다형성 타입을
4개의 닫힌 세상에서만 분류하도록 강제하는 Enum """
INVOICE = "INVOICE"
RECEIPT = "RECEIPT"
PURCHASE_ORDER = "PURCHASE_ORDER"
QUOTE = "QUOTE"
class LineItem(BaseModel):
""" 단일 품목(Row) 단위로 쪼개진 강타입(Strong Type) 하위 스키마 """
item_description: str = Field(..., description="원문에 기재된 구매 품목의 상세 명칭")
# 수량은 무조건 1개 이상이어야 함. 마이너스나 0은 백엔드가 에러로 요격함
quantity: int = Field(..., ge=1, description="구매 수량 (1 이상의 정수 강제)")
unit_price: float = Field(..., ge=0.0, description="단일 품목의 단가")
line_total: float = Field(..., ge=0.0, description="품목 단위의 총금액 (수량 * 단가)")
class FinancialDocumentExtraction(BaseModel):
""" 파이프라인 최상위 노드: LLM 에이전트가 반드시 반환해야 할 최종 JSON 컨테이너 """
document_type: DocumentType = Field(..., description="추론 및 분류된 문서의 Enum 종류")
# [1계층] Header (메타데이터)
vendor_name: str = Field(..., description="공급자/판매자의 공식 상호명")
# ISO-8601 포맷 강제 변환. Date 캐스팅 실패 시 즉각 Validation Exception 발생
document_date: date = Field(..., description="문서 발행 일자")
# 영수증처럼 공식 식별 번호가 없는 문서 다형성을 포용하기 위한 선택적 값 허용
document_id: Optional[str] = Field(None, description="인보이스 번호 혹은 PO 번호")
# [2계층] Line Items (배열 객체)
# 품목이 텅 빈 JSON 껍데기 반환 환각을 사전에 차단하기 위한 배열 크기 통제
items: List[LineItem] = Field(..., min_items=1, description="추출된 개별 품목 리스트")
# [3계층] Summary (관계 대수 검증의 핵심 타겟)
subtotal: float = Field(..., ge=0.0, description="세금 포함 전 총 공급가액")
tax_amount: float = Field(0.0, ge=0.0, description="부과된 부가가치세(VAT) 세액")
total_amount: float = Field(..., ge=0.0, description="최종 청구/결제 텐서 총액")
2. Pydantic 스키마 설계에 숨겨진 오라클의 통제 철학
위 파이썬 코드는 단순하고 얌전한 API 데이터 선언문 구조체가 결코 아니다. 코드를 자세히 들여다보면 Pydantic의 Field() 파라미터 내부에 ge=0.0(Greater than or equal to 0, 마이너스 값 불허), min_items=1(배열 크기는 최소 1개 보장)과 같은 매우 원초적이고 폭력적인 **단위 유효성 검사 트리거(Unit Validation Trigger)**가 스키마 뼛속까지 덕지덕지 박혀있다.
이 견고한 Pydantic 스키마가 메모리에 선언되는 바로 그 순간, 거대 언어 모델이 어텐션 붕괴 환각으로 인해 수량(quantity) 필드에 한글로 “없음“을 넣거나, 단가(unit_price) 필드에 -5라는 치명적인 계산 오류를 집어넣으려는 찰나의 순간에, Pydantic 런타임 컴파일러는 가차 없이 ValidationError 익셉션(Exception)을 격발 시키며 해당 페이로드를 품은 파이프라인의 숨통을 즉시 끊어버리고 강제 종료(Fast-fail)시킨다.
우리는 프롬프트 텍스트에 기구하게 매달려 *“제발 LLM님, 수량에 마이너스 값은 내보내지 말아 주세요”*라고 시적으로 호소하는 멍청한 짓을 거부했다. 그 대신, 예외가 터지면 곧바로 모델에게 재시도(Retry) 채찍을 때릴 수 있도록, 환각으로 인해 발생할 타입 파괴 에러들을 시스템 아키텍처 레벨(Pydantic Schema)에서 수학적이고 대수적으로 강제 틀어막아 버리는 가장 절대적인 ’1단계 구문 오라클’의 입구 지뢰밭을 완벽하게 완성해 낸 것이다.