구조화 출력(Structured Output) — JSON Schema, Pydantic, Zod, 제약 디코딩(Constrained Decoding)

"모델에게 JSON으로 답해 달라고 정중히 부탁하기"는 최전선(frontier) 모델에서도 5~15% 정도는 실패합니다. 구조화 출력(Structured Outputs)은 제약 디코딩(Constrained Decoding)으로 이 간극을 메웁니다. 모델이 스키마(schema)를 위반하는 토큰을 애초에 출력하지 못하도록 막는 방식입니다. OpenAI의 strict 모드, Anthropic의 스키마가 부여된 tool_use, Gemini의 responseSchema, Pydantic AI의 output_type, 그리고 Zod의 .parse는 모두 같은 아이디어를 다섯 가지 표면 형태로 드러낸 것입니다. 이 강의에서는 학습자가 앞으로 모든 프로덕션 추출(extraction) 파이프라인에 사용할 스키마 검증기(validator)와 strict 모드 계약(contract)을 직접 만들어 봅니다.

유형: Build 언어: Python (표준 라이브러리, JSON Schema 2020-12 일부) 선수 지식: Phase 13 · 02 (함수 호출 심화, function calling deep dive) 예상 시간: 약 75분

학습 목표

  • 추출 대상(extraction target)에 알맞은 제약 조건(enum, min/max, required, pattern)을 활용해 JSON Schema 2020-12를 작성할 수 있습니다.
  • strict 모드와 제약 디코딩이 "생성 후 검증(validate after generation)"과 어떤 보장(guarantee)에서 다른지 설명할 수 있습니다.
  • 세 가지 실패 모드인 파싱 오류(parse error), 스키마 위반(schema violation), 모델 거절(refusal)을 구분할 수 있습니다.
  • 타입이 부여된 복구(repair)와 거절(refusal) 처리를 갖춘 추출 파이프라인을 실제로 출시(ship)할 수 있습니다.

문제

구매 주문서(purchase order) 이메일을 읽는 에이전트(agent)는 자유 텍스트를 {customer, line_items, total_usd} 같은 구조로 바꿔야 합니다. 이를 풀어가는 접근은 크게 세 가지가 있습니다.

접근 1: JSON으로 답해 달라고 프롬프트로 요청하기. "customer, line_items, total_usd 필드를 가진 JSON으로 답해 주세요" 같은 방식입니다. 최전선 모델에서는 85~95% 정도 잘 작동합니다. 그러나 여섯 가지 방식으로 실패합니다. 빠진 중괄호, 끝에 붙은 쉼표(trailing comma), 타입 불일치, 환각된 필드(hallucinated fields), 토큰 한도에서 잘려 나간 출력, 그리고 "Here is your JSON:" 같은 산문이 섞여 새어 나오는 경우입니다.

접근 2: 생성 후 검증. 자유롭게 생성하게 두고, 파싱한 뒤 스키마로 검증하고, 실패하면 재시도합니다. 신뢰성은 있지만 비용이 큽니다. 모든 재시도에 비용이 들고, 잘림 버그(truncation bug)가 발생할 때마다 한 턴(turn)이 더 소모됩니다.

접근 3: 제약 디코딩(Constrained Decoding). 프로바이더(provider)가 디코딩 시점에 스키마를 강제로 적용합니다. 유효하지 않은 토큰은 샘플링 분포(sampling distribution)에서 마스킹(mask)됩니다. 결과적으로 출력이 항상 파싱 가능하고 검증을 통과한다는 점이 보장됩니다. 실패는 단 하나의 모드, 즉 모델 거절(refusal)로 수렴합니다. 모델이 입력이 스키마에 맞지 않는다고 판단하는 경우입니다.

2026년의 모든 최전선 프로바이더는 어떤 형태로든 접근 3을 제공합니다.

  • OpenAI. response_format: {type: "json_schema", strict: true}와, 모델이 거절할 때 응답에 들어가는 refusal 필드.
  • Anthropic. tool_use 입력(input)에 대한 스키마 강제. stop_reason: "refusal" 같은 별도 신호는 없지만, 도구 호출 없이 end_turn으로 끝나는 것이 사실상의 거절 신호입니다.
  • Gemini. 요청 수준의 responseSchema. 2026년 Gemini는 선택된 타입에 대해 토큰 수준 문법 제약(token-level grammar constraint)도 함께 제공합니다.
  • Pydantic AI. output_type=InvoiceModel로 지정하면 InvoiceModel 타입을 갖는 구조화된 RunResult를 돌려줍니다.
  • Zod (TypeScript). 프로바이더 출력을 Zod 스키마로 검증하는 런타임 파서(runtime parser)입니다. OpenAI의 beta.chat.completions.parse와 짝을 이뤄 사용합니다.

공통점은 스키마를 한 번만 선언하고, 그 스키마를 처음부터 끝까지(end-to-end) 강제한다는 것입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.구조화된 출력가 해결하는 핵심 과제는?

2.구조화된 출력 이전의 주요 한계는?

0/2 답변 완료

개념

JSON Schema 2020-12 — 공통 언어

모든 프로바이더는 JSON Schema 2020-12를 받아들입니다. 가장 자주 쓰는 구성 요소는 다음과 같습니다.

  • type: object, array, string, number, integer, boolean, null 중 하나.
  • properties: 필드 이름에서 하위 스키마(subschema)로 가는 매핑.
  • required: 반드시 존재해야 하는 필드 이름 목록.
  • enum: 허용된 값들의 닫힌 집합.
  • minimum / maximum (숫자), minLength / maxLength / pattern (문자열).
  • items: 배열의 모든 요소에 적용되는 하위 스키마.
  • additionalProperties: false이면 추가 필드를 금지합니다. 기본값은 모드에 따라 다릅니다.

OpenAI strict 모드는 여기에 세 가지 요구사항을 더합니다. 모든 속성(property)이 required에 포함되어야 하고, 모든 객체에 additionalProperties: false가 붙어 있어야 하며, 해결되지 않은(unresolved) $ref가 없어야 합니다. 이를 어기면 API는 요청 시점에 400 응답을 반환합니다.

Pydantic, 파이썬 바인딩

Pydantic v2는 데이터클래스(dataclass) 형태의 모델로부터 model_json_schema()를 통해 JSON Schema를 생성합니다. Pydantic AI는 이를 한 번 더 감싸 다음처럼 쓸 수 있게 해 줍니다.

class Invoice(BaseModel):
    customer: str
    line_items: list[LineItem]
    total_usd: Decimal

에이전트 프레임워크는 가장자리(edge)에서 이 스키마를 OpenAI strict 모드, Anthropic의 input_schema, Gemini의 responseSchema 형식으로 변환해 줍니다. 모델의 출력은 타입이 부여된 Invoice 인스턴스로 돌아옵니다. 검증 오류는 타입이 부여된 오류 경로(error path)를 가진 ValidationError로 발생합니다.

Zod, 타입스크립트 바인딩

Zod (z.object({customer: z.string(), ...}))는 TypeScript에서 같은 역할을 합니다. OpenAI Node SDK는 zodResponseFormat(Invoice)를 제공하며, 이를 API의 JSON Schema 페이로드(payload)로 변환해 줍니다.

거절(Refusal)

strict 모드라고 해도 모델이 억지로 답하도록 강제할 수는 없습니다. 입력이 스키마에 맞을 수 없는 경우("이 이메일은 송장이 아니라 시였습니다" 같은 상황) 모델은 이유가 담긴 refusal 필드를 출력합니다. 코드는 이 거절을 실패가 아니라 일급(first-class) 결과로 다뤄야 합니다. 거절은 안전(safety) 신호로도 유용합니다. 보호된 콘텐츠(protected content)가 담긴 이메일에서 신용카드 번호를 추출하라는 요청을 받으면, 모델은 안전 사유가 붙은 거절을 반환합니다.

오픈 모델에서의 제약 디코딩

오픈 가중치(open-weights) 구현은 세 가지 기법을 사용합니다.

  1. 문법 기반 디코딩(grammar-based decoding): outlines, guidance, lm-format-enforcer가 대표적입니다. 스키마에서 결정적 유한 오토마타(deterministic finite automaton; DFA)를 만들고, 매 스텝(step)마다 이 유한 상태 기계(FSM)를 위반할 토큰의 로짓(logit)을 마스킹합니다.
  2. JSON 파서와 로짓 마스킹: 스트리밍 JSON 파서를 모델과 같은 보폭(lockstep)으로 실행하면서, 매 스텝마다 다음 단계에서 허용되는 토큰 집합을 계산합니다.
  3. 검증자가 붙은 추측 디코딩(speculative decoding with a verifier): 가벼운 초안(draft) 모델이 토큰을 제안하면, 검증자(verifier)가 스키마를 강제합니다.

상용 프로바이더들은 내부적으로 이 가운데 하나를 선택해 사용합니다. 2026년 기준 최신 구현은 짧은 구조화 출력에서는 일반 생성보다 빠르고, 긴 출력에서는 거의 같은 속도를 보입니다.

세 가지 실패 모드

  1. 파싱 오류(Parse error). 출력이 유효한 JSON이 아닌 경우입니다. strict 모드에서는 발생할 수 없지만, 비-strict 프로바이더에서는 여전히 일어납니다.
  2. 스키마 위반(Schema violation). 출력은 파싱되지만 스키마를 위반합니다. strict 모드에서는 발생할 수 없으나, 그 밖의 환경에서는 흔합니다.
  3. 거절(Refusal). 모델이 거절합니다. 타입이 부여된 결과(typed outcome)로 처리해야 합니다.

재시도 전략

strict 모드 바깥(예: Anthropic tool_use, 비-strict OpenAI, 이전 버전 Gemini)에서는 다음과 같은 복구 패턴을 사용합니다.

generate -> parse -> validate -> if fail, inject error and retry, max 3x

대개 한 번의 재시도(retry)면 충분합니다. 세 번의 재시도는 약한 모델의 흔들림(flake)을 잡아냅니다. 세 번을 넘기는 경우는 스키마가 잘못되었다는 신호입니다. 모델이 어떤 입력에서는 만족시킬 수 없다는 뜻이므로, 프롬프트나 스키마를 고쳐야 합니다.

작은 모델 지원

제약 디코딩은 작은 모델에서도 잘 작동합니다. 문법 강제(grammar enforcement)가 적용된 3B 파라미터 오픈 모델은, 구조화 과제에서 단순 프롬프트(raw prompting)만 사용하는 70B 파라미터 모델보다 더 나은 성능을 보입니다. 이것이 구조화 출력이 프로덕션에서 중요한 가장 큰 이유입니다. 신뢰성을 모델 크기와 분리해 주기 때문입니다.

사용해보기

code/main.py는 표준 라이브러리만으로 만든 최소한의 JSON Schema 2020-12 검증기를 제공합니다. 지원 범위는 type, required, enum, min/max, pattern, items, additionalProperties입니다. 이 검증기는 Invoice 스키마를 감싸고 있고, 가짜 LLM 출력을 통과시켜 파싱 오류, 스키마 위반, 거절 경로(path)를 차례로 보여줍니다. 프로덕션에서는 가짜 출력을 실제 프로바이더 응답으로 교체하기만 하면 됩니다.

주의해서 볼 지점은 다음과 같습니다.

  • 검증기는 경로(path)와 메시지를 가진 타입화된 [ValidationError] 리스트를 반환합니다. 이것이 바로 재시도 프롬프트에 다시 전달하고 싶은 형태입니다.
  • 거절 분기(branch)는 재시도하지 않습니다. 로그를 남기고 타입이 부여된 거절을 반환합니다. Phase 14 · 09에서는 이 거절을 안전 신호로 활용합니다.
  • additionalProperties: false 검사는 적대적(adversarial) 테스트 입력에서 발동합니다. strict 모드가 환각된 필드를 막아 주는 이유를 직접 확인할 수 있습니다.

산출물 만들기

이 강의에서는 outputs/skill-structured-output-designer.md를 만듭니다. 송장, 지원 티켓, 이력서 같은 자유 텍스트 추출 대상이 주어지면, strict 모드와 호환되는(strict-mode-compatible) JSON Schema 2020-12와 이를 그대로 반영한 Pydantic 모델을 만들어 주는 스킬(skill)입니다. 여기에 타입이 부여된 거절 처리와 재시도 처리(stub)가 함께 포함됩니다.

연습문제

  1. (쉬움) code/main.py를 실행합니다. total_usd가 음수인 네 번째 테스트 케이스를 추가합니다. 검증기가 minimum 제약 경로를 이유로 이를 거절하는지 확인합니다.

  2. (중간) 식별자(discriminator)가 있는 oneOf를 지원하도록 검증기를 확장합니다. 흔한 예시는 kind 필드로 태깅된 상품(product) 또는 서비스(service) line_item입니다. strict 모드에는 미묘한 규칙들이 있으므로 OpenAI의 structured outputs 가이드를 함께 확인합니다.

  3. (중간) 동일한 Invoice 스키마를 Pydantic BaseModel로 작성하고, model_json_schema()의 출력을 직접 작성한(hand-rolled) 스키마와 비교합니다. Pydantic이 기본값으로 설정하지만 직접 작성한 버전에서는 빠져 있는 필드 하나를 찾아냅니다.

  4. (어려움) 거절률(refusal rate)을 측정합니다. 추출이 불가능해야 하는 입력 10개(예: 노래 가사, 수학 증명, 빈 이메일)를 만들고, 실제 프로바이더의 strict 모드에 통과시킵니다. 거절(refusal)과 환각된 출력(hallucinated output)의 개수를 셉니다. 이것이 거절 인지 재시도(refusal-aware retry)의 기준 자료(ground truth)가 됩니다.

  5. (어려움) OpenAI의 structured outputs 가이드를 처음부터 끝까지 읽습니다. 일반 JSON Schema에서는 허용되지만 strict 모드에서는 명시적으로 금지하는 구성 요소(construct) 하나를 찾습니다. 그 금지된 구성 요소를 꼭 필요하지 않은 방식으로 사용하는 스키마를 만든 뒤, strict 호환 형태로 리팩터링(refactor)합니다.

핵심 용어

용어흔한 설명실제 의미
JSON Schema 2020-12"스키마 명세"최신 프로바이더들이 공통으로 사용하는 IETF 초안 스키마 방언(dialect)
엄격 모드(Strict mode)"보장된 스키마"제약 디코딩으로 스키마를 강제하는 OpenAI 플래그(flag)
제약 디코딩(Constrained decoding)"로짓 마스킹"유효하지 않은 다음 토큰을 마스킹하는 디코드 시점(decode-time) 강제
거절(Refusal)"모델이 거절함"입력이 스키마에 맞을 수 없을 때 반환되는 타입이 부여된 결과
파싱 오류(Parse error)"유효하지 않은 JSON"JSON으로 파싱되지 않는 출력. strict 모드에서는 불가능
스키마 위반(Schema violation)"형태가 잘못됨"파싱은 되지만 타입 / required / enum / 범위를 위반한 출력
additionalProperties: false"추가 필드 금지"알 수 없는 필드를 금지함. OpenAI strict 모드에서 필수
Pydantic BaseModel"타입이 있는 출력"JSON Schema를 생성하고 검증해 주는 Python 클래스
Zod 스키마"TypeScript 출력 타입"프로바이더 출력을 검증하기 위한 TS 런타임 스키마
문법 강제(Grammar enforcement)"오픈 가중치 제약 디코드"outlines / guidance처럼 FSM 기반 로짓 마스킹을 사용하는 방식

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

structured-output-designer

Design a strict-mode-compatible JSON Schema plus Pydantic model for a free-text extraction target, with typed refusal and retry handling stubbed in.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.프로덕션에서 구조화된 출력의 가장 중요한 설계 원칙은?

2.구조화된 출력가 올바른 선택이 아닌 경우는?

3.구조화된 출력는 AI 생태계에 어떻게 들어맞나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다