<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pydantic on RockB</title><link>https://baeseokjae.github.io/tags/pydantic/</link><description>Recent content in Pydantic on RockB</description><image><title>RockB</title><url>https://baeseokjae.github.io/images/og-default.png</url><link>https://baeseokjae.github.io/images/og-default.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 11 May 2026 15:04:22 +0000</lastBuildDate><atom:link href="https://baeseokjae.github.io/tags/pydantic/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM Structured Outputs Guide 2026: JSON Mode, Instructor &amp; Outlines</title><link>https://baeseokjae.github.io/posts/llm-structured-output-guide-2026/</link><pubDate>Mon, 11 May 2026 15:04:22 +0000</pubDate><guid>https://baeseokjae.github.io/posts/llm-structured-output-guide-2026/</guid><description>2026년 LLM 구조화 출력 완벽 가이드: JSON mode의 한계, Instructor vs Outlines 선택법, 프로덕션 신뢰성 확보 전략.</description><content:encoded><![CDATA[<p>LLM 구조화 출력(Structured Outputs)은 AI 모델이 임의의 텍스트 대신 정해진 스키마를 따르는 JSON을 반환하도록 강제하는 기술이다. 2026년 현재, 80% 이상의 기업이 구조화 출력 기반의 생성형 AI 앱을 배포 중이며, 프로덕션에서 안정적인 JSON을 얻는 것은 모든 AI 시스템의 핵심 과제가 되었다.</p>
<h2 id="what-are-llm-structured-outputs-and-why-do-they-matter-in-2026">What Are LLM Structured Outputs and Why Do They Matter in 2026?</h2>
<p>LLM 구조화 출력(Structured Outputs)은 언어 모델이 자유 형식 텍스트가 아닌, 사전에 정의된 JSON 스키마를 완전히 준수하는 데이터를 반환하도록 보장하는 기술을 말한다. 단순히 JSON을 요청하는 것과는 다르다 — 스키마 강제(schema enforcement)는 필드 누락, 타입 불일치, 마크다운 펜스 오류 같은 파싱 실패를 원천 차단한다. 2026년 기준, 전 세계 67%의 기업이 LLM을 운영에 활용하고 있으며, 80% 이상이 구조화 출력 기반 앱을 배포할 것으로 예상된다. 이 수치는 단순한 실험이 아닌 실제 비즈니스 로직이 LLM 출력에 의존한다는 의미다. 프로덕션에서 구조화 출력 없이 나이브한 프롬프팅만으로 JSON을 추출하면 5~20%의 확률로 파싱 에러가 발생하고, 이는 조용한 버그(silent bug)로 이어져 데이터 파이프라인을 무너뜨린다. OpenAI, Anthropic, Google Gemini 모두 2026년 초 기준 네이티브 구조화 출력을 지원하며, 생태계는 제약적 디코딩(constrained decoding) 방식으로 수렴하고 있다. 추출(extraction), 분류(classification), 에이전트 도구 호출(tool calls) 등 거의 모든 LLM 워크플로가 신뢰할 수 있는 구조화 출력에 의존하므로, 이를 올바르게 구현하는 것은 2026년 AI 개발의 필수 기본기다.</p>
<h3 id="왜-구조화-출력이-ai-에이전트의-핵심인가">왜 구조화 출력이 AI 에이전트의 핵심인가?</h3>
<p>AI 에이전트는 도구 호출(tool calls), 라우팅 결정, 데이터 추출 등 모든 단계에서 파싱 가능한 출력에 의존한다. 구조화 출력 없이는 에이전트 파이프라인의 어느 한 단계에서 LLM이 예상치 못한 형식을 반환할 때 전체 워크플로가 중단된다. Langchain이나 LlamaIndex 같은 에이전트 프레임워크들이 구조화 출력을 핵심 기능으로 통합하는 이유가 여기에 있다.</p>
<h2 id="the-three-levels-of-structured-output-from-prompting-to-constrained-decoding">The Three Levels of Structured Output: From Prompting to Constrained Decoding</h2>
<p>구조화 출력에는 신뢰성이 다른 세 가지 수준이 존재하며, 각 수준은 실패율과 구현 복잡도 사이의 트레이드오프를 가진다. 첫 번째는 <strong>프롬프트 엔지니어링(Prompt Engineering)</strong> 수준으로, &ldquo;JSON 형식으로 반환하라&quot;고 지시하는 방식이다. 성공률은 80<del>95%에 불과하며, 복잡한 스키마일수록 실패율이 높아진다. 두 번째는 <strong>함수 호출(Function Calling / Tool Use)</strong> 수준으로, 모델이 사전에 정의된 함수 시그니처에 맞는 인수를 생성한다. 성공률은 95</del>99%이며 대부분의 API 기반 앱에서 충분하다. 세 번째는 <strong>네이티브 구조화 출력(Native Structured Output)</strong> 수준으로, <code>type: json_schema</code>와 <code>strict: true</code>를 사용해 모델이 제약적 디코딩으로 스키마를 100% 준수한다. 프로덕션 데이터 파이프라인, 에이전트 아키텍처, 금융/의료 등 고신뢰도 도메인에서는 반드시 세 번째 수준을 사용해야 한다.</p>
<table>
  <thead>
      <tr>
          <th>수준</th>
          <th>방법</th>
          <th>신뢰성</th>
          <th>사용 사례</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>프롬프트 엔지니어링</td>
          <td>80~95%</td>
          <td>프로토타입, 단순 추출</td>
      </tr>
      <tr>
          <td>2</td>
          <td>함수 호출 / Tool Use</td>
          <td>95~99%</td>
          <td>일반 API 앱, 에이전트</td>
      </tr>
      <tr>
          <td>3</td>
          <td>네이티브 구조화 출력</td>
          <td>~100%</td>
          <td>프로덕션, 데이터 파이프라인</td>
      </tr>
  </tbody>
</table>
<h3 id="어떤-수준을-선택해야-하는가">어떤 수준을 선택해야 하는가?</h3>
<p>실험과 프로토타입에는 레벨 1이 빠르다. 그러나 출력이 다른 시스템의 입력이 되거나 스키마가 5개 이상의 필드를 가진다면 레벨 3으로 직접 이동하라. 레벨 2는 레벨 3을 지원하지 않는 오래된 모델을 다룰 때의 중간 선택지다.</p>
<h2 id="json-mode-vs-structured-outputs-vs-function-calling--whats-the-difference">JSON Mode vs Structured Outputs vs Function Calling — What&rsquo;s the Difference?</h2>
<p>JSON mode, 구조화 출력, 함수 호출은 혼용되는 경우가 많지만 동작 방식과 신뢰성이 근본적으로 다르다. <strong>JSON mode</strong>는 모델이 유효한 JSON을 반환하도록 요청하는 가장 단순한 방법이다. 구조적으로 올바른 JSON을 반환하지만, 스키마 준수는 보장하지 않는다 — 필드가 누락되거나 타입이 틀릴 수 있다. 2026년 현재 JSON mode는 레거시로 간주되며, 스키마 바인딩 사용 사례에서는 deprecated된 것으로 봐야 한다. **함수 호출(Function Calling)**은 모델이 특정 함수를 호출하기 위한 인수를 생성하는 방식이다. JSON 스키마로 함수 시그니처를 정의하고, 모델은 이를 맞춰 JSON을 생성한다. <strong>네이티브 구조화 출력</strong>은 <code>response_format: {type: &quot;json_schema&quot;, json_schema: {...}, strict: true}</code>를 사용해 제약적 디코딩으로 스키마를 100% 강제한다. OpenAI gpt-4o, Anthropic Claude 3.5+, Google Gemini 1.5+ 모두 이 방식을 지원한다. 2026년 프로덕션 표준은 <code>strict: true</code>를 사용한 네이티브 구조화 출력이다.</p>
<table>
  <thead>
      <tr>
          <th>방식</th>
          <th>스키마 보장</th>
          <th>지원 범위</th>
          <th>권장 사용</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JSON mode</td>
          <td>없음 (유효 JSON만)</td>
          <td>넓음 (레거시)</td>
          <td>사용 지양</td>
      </tr>
      <tr>
          <td>함수 호출</td>
          <td>부분적</td>
          <td>대부분의 API</td>
          <td>에이전트 도구 호출</td>
      </tr>
      <tr>
          <td>구조화 출력 (strict)</td>
          <td>100%</td>
          <td>OpenAI, Anthropic, Gemini</td>
          <td>프로덕션 기본값</td>
      </tr>
  </tbody>
</table>
<h2 id="how-constrained-decoding-works-finite-state-machines-under-the-hood">How Constrained Decoding Works: Finite State Machines Under the Hood</h2>
<p>제약적 디코딩(Constrained Decoding)은 LLM이 토큰을 생성할 때 스키마를 위반하는 토큰을 생성 단계에서 마스킹(masking)하여 유효하지 않은 출력을 원천적으로 불가능하게 만드는 기술이다. 구체적으로는 JSON 스키마를 유한 상태 머신(Finite State Machine, FSM)으로 변환하고, 각 생성 스텝에서 현재 FSM 상태를 기반으로 다음에 올 수 있는 유효 토큰 집합을 계산한다. LLM의 소프트맥스 출력에서 유효하지 않은 토큰의 로짓(logit)을 음의 무한대로 설정해 선택을 불가능하게 만든다. Outlines 라이브러리의 Rust 기반 <code>outlines-core</code> 0.1.0은 이 방식을 구현하여 생성 스텝당 O(1) 유효 토큰 조회를 달성했다. vLLM, TGI(Text Generation Inference), LoRAX, xinference, SGLang 등 주요 LLM 서빙 프레임워크가 Outlines를 통해 제약적 디코딩을 지원한다. 클라우드 API(OpenAI, Anthropic)도 내부적으로 동일한 원리를 적용하지만, 구현 세부사항은 공개되지 않는다. 실질적인 의미는 이렇다 — 제약적 디코딩을 사용하면 모델이 &ldquo;틀린 JSON&quot;을 생성하는 것 자체가 물리적으로 불가능해지므로, 파싱 로직이나 재시도 루프가 필요 없어진다. 이것이 Outlines가 고처리량 자체 호스팅 환경에서 선택되는 핵심 이유다.</p>
<h3 id="fsm-vs-후처리post-processing-접근법-비교">FSM vs 후처리(Post-processing) 접근법 비교</h3>
<p>FSM 기반 사전 제약(pre-generation)은 생성 자체가 스키마를 따르므로 재시도가 필요 없다. 후처리(post-generation) 검증은 Instructor 같은 라이브러리가 사용하는 방식으로, 생성 후 Pydantic으로 검증하고 실패 시 자동 재시도한다. 클라우드 API에서는 post-generation이 실용적이며, 로컬/자체 호스팅 모델에서는 pre-generation이 성능과 신뢰성 모두에서 우위다.</p>
<h2 id="provider-guide-openai-anthropic-google-gemini-and-mistral-structured-outputs">Provider Guide: OpenAI, Anthropic, Google Gemini, and Mistral Structured Outputs</h2>
<p>2026년 현재 OpenAI, Anthropic, Google Gemini, Mistral 모두 네이티브 구조화 출력을 지원하지만 API 인터페이스와 지원 수준은 다르다. <strong>OpenAI</strong>는 <code>response_format: {type: &quot;json_schema&quot;, json_schema: schema, strict: true}</code>로 가장 완성도 높은 구현을 제공한다. gpt-4o, gpt-4o-mini, o1 계열 모두 지원하며, 재귀 스키마와 복잡한 <code>$ref</code> 참조도 처리한다. <strong>Anthropic Claude</strong>는 <code>tool_choice: {type: &quot;tool&quot;, name: &quot;extract&quot;}</code>와 함께 도구 정의를 통해 구조화 출력을 강제한다. Claude 3.5 Sonnet 이후로 JSON 스키마 바인딩의 신뢰성이 크게 향상되었다. <strong>Google Gemini</strong>는 <code>response_mime_type: &quot;application/json&quot;</code>과 <code>response_schema</code>를 통해 네이티브 지원한다. Gemini 1.5 Pro와 Flash 모두 지원하며, Google AI Studio에서 시각적으로 스키마를 테스트할 수 있다. <strong>Mistral</strong>은 <code>response_format: {type: &quot;json_object&quot;}</code>를 지원하지만 strict 모드가 없어 신뢰성은 OpenAI보다 낮다. Mistral Large 2 이상에서 함수 호출과 결합 시 신뢰성이 개선된다.</p>
<table>
  <thead>
      <tr>
          <th>프로바이더</th>
          <th>Strict 모드</th>
          <th>스키마 방식</th>
          <th>복잡 스키마</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OpenAI gpt-4o</td>
          <td>✅</td>
          <td>json_schema + strict</td>
          <td>✅</td>
      </tr>
      <tr>
          <td>Anthropic Claude 3.5+</td>
          <td>✅ (도구 강제)</td>
          <td>tool_choice 강제</td>
          <td>✅</td>
      </tr>
      <tr>
          <td>Google Gemini 1.5+</td>
          <td>✅</td>
          <td>response_schema</td>
          <td>✅</td>
      </tr>
      <tr>
          <td>Mistral Large 2</td>
          <td>❌</td>
          <td>json_object</td>
          <td>부분적</td>
      </tr>
  </tbody>
</table>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># OpenAI 구조화 출력 예시</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> openai <span style="color:#f92672">import</span> OpenAI
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAI()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>schema <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;product_extract&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;strict&#34;</span>: <span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;schema&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;object&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;properties&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;name&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;string&#34;</span>},
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;price&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;number&#34;</span>},
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;in_stock&#34;</span>: {<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;boolean&#34;</span>}
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;required&#34;</span>: [<span style="color:#e6db74">&#34;name&#34;</span>, <span style="color:#e6db74">&#34;price&#34;</span>, <span style="color:#e6db74">&#34;in_stock&#34;</span>],
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;additionalProperties&#34;</span>: <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>response <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>chat<span style="color:#f92672">.</span>completions<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gpt-4o&#34;</span>,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>[{<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;Extract product info: Blue Widget $29.99, available&#34;</span>}],
</span></span><span style="display:flex;"><span>    response_format<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;json_schema&#34;</span>, <span style="color:#e6db74">&#34;json_schema&#34;</span>: schema}
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>product <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>loads(response<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>message<span style="color:#f92672">.</span>content)
</span></span></code></pre></div><h2 id="instructor-the-python-library-for-post-generation-structured-output">Instructor: The Python Library for Post-Generation Structured Output</h2>
<p>Instructor는 Pydantic 모델을 사용해 LLM 출력을 검증하고, 검증 실패 시 자동으로 재시도하는 Python 라이브러리다. 2026년 기준 월 300만 다운로드 이상, GitHub 스타 11,000+, 기여자 100명 이상으로 파이썬 LLM 생태계의 사실상 표준이 되었다. Instructor의 핵심 가치는 <strong>프로바이더 중립성</strong>이다 — OpenAI, Anthropic, Google Gemini, Ollama, Mistral, Cohere 등 15개 이상의 프로바이더를 동일한 코드 패턴으로 사용할 수 있다. <code>instructor.patch()</code>로 기존 OpenAI 클라이언트를 래핑하거나, Anthropic용 <code>instructor.from_anthropic()</code>을 사용하는 방식이다. Instructor는 생성 후 Pydantic ValidationError를 LLM 프롬프트로 변환해 자동 재시도(automatic retry)를 수행한다. 기본 재시도 횟수는 3회이며, <code>max_retries</code> 파라미터로 조정할 수 있다. 스트리밍, 비동기(async), 부분 응답(partial streaming) 등 고급 기능도 지원한다. 클라우드 API 기반 앱에서 프로바이더를 유연하게 전환해야 한다면 Instructor가 최적의 선택이다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> instructor
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> anthropic <span style="color:#f92672">import</span> Anthropic
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel, Field
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> List
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ResearchSummary</span>(BaseModel):
</span></span><span style="display:flex;"><span>    key_findings: List[str] <span style="color:#f92672">=</span> Field(min_length<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, max_length<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>    confidence_score: float <span style="color:#f92672">=</span> Field(ge<span style="color:#f92672">=</span><span style="color:#ae81ff">0.0</span>, le<span style="color:#f92672">=</span><span style="color:#ae81ff">1.0</span>)
</span></span><span style="display:flex;"><span>    recommended_action: str
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> instructor<span style="color:#f92672">.</span>from_anthropic(Anthropic())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>summary <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>messages<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;claude-opus-4-7&#34;</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span>,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>[{
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;Summarize the key findings from this Q1 2026 AI adoption report...&#34;</span>
</span></span><span style="display:flex;"><span>    }],
</span></span><span style="display:flex;"><span>    response_model<span style="color:#f92672">=</span>ResearchSummary
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(summary<span style="color:#f92672">.</span>key_findings)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Confidence: </span><span style="color:#e6db74">{</span>summary<span style="color:#f92672">.</span>confidence_score<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span></code></pre></div><h3 id="instructor의-자동-재시도-메커니즘">Instructor의 자동 재시도 메커니즘</h3>
<p>Pydantic 검증이 실패하면 Instructor는 검증 오류 메시지를 LLM에 다시 전달하며 &ldquo;이전 출력에서 다음 오류가 발생했다, 수정해 달라&quot;는 형식으로 재시도한다. 이는 단순 JSON 파싱보다 훨씬 강력한 오류 복구 루프를 제공한다. <code>Iterable[Model]</code>을 사용한 스트리밍 추출과 <code>Partial[Model]</code>을 통한 부분 완료 스트리밍도 지원해 대용량 문서 처리에서 UX를 개선할 수 있다.</p>
<h2 id="outlines-pre-generation-constraints-for-local-and-self-hosted-models">Outlines: Pre-Generation Constraints for Local and Self-Hosted Models</h2>
<p>Outlines는 dottxt-ai가 개발한 오픈소스 라이브러리로, LLM 생성 단계에서 JSON 스키마, 정규식, 열거형(Enum)을 FSM으로 변환하여 유효하지 않은 토큰 생성을 원천 차단한다. Instructor와 달리 Outlines는 <strong>사전 생성(pre-generation)</strong> 제약 방식을 사용하므로 재시도가 전혀 필요 없다. 2026년 기준 vLLM, TGI, LoRAX, xinference, SGLang을 포함한 수백 개 조직이 Outlines를 프로덕션에서 사용 중이다. Rust로 작성된 <code>outlines-core</code> 0.1.0은 생성 스텝당 O(1) 유효 토큰 조회를 제공해 대용량 배치 처리에서 Instructor보다 현저히 빠른 성능을 낸다. Outlines는 Hugging Face Transformers, vLLM, llama.cpp와 통합되며, 로컬 모델이나 자체 호스팅 LLM 서버에 특화되어 있다. JSON 스키마 외에도 정규식 패턴, 파이썬 타입 힌트, 선택형 텍스트(choices) 등 다양한 제약 방식을 지원한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> outlines
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>model <span style="color:#f92672">=</span> outlines<span style="color:#f92672">.</span>models<span style="color:#f92672">.</span>transformers(<span style="color:#e6db74">&#34;mistralai/Mistral-7B-v0.1&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ProductReview</span>(BaseModel):
</span></span><span style="display:flex;"><span>    sentiment: str  <span style="color:#75715e"># &#34;positive&#34;, &#34;negative&#34;, &#34;neutral&#34;</span>
</span></span><span style="display:flex;"><span>    score: int      <span style="color:#75715e"># 1-5</span>
</span></span><span style="display:flex;"><span>    summary: str
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>generator <span style="color:#f92672">=</span> outlines<span style="color:#f92672">.</span>generate<span style="color:#f92672">.</span>json(model, ProductReview)
</span></span><span style="display:flex;"><span>review <span style="color:#f92672">=</span> generator(<span style="color:#e6db74">&#34;Review: This product exceeded my expectations, would buy again.&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(review<span style="color:#f92672">.</span>sentiment)  <span style="color:#75715e"># 보장된 유효값</span>
</span></span><span style="display:flex;"><span>print(review<span style="color:#f92672">.</span>score)      <span style="color:#75715e"># 보장된 정수</span>
</span></span></code></pre></div><h3 id="outlines-vs-instructor-언제-무엇을-선택하는가">Outlines vs Instructor: 언제 무엇을 선택하는가?</h3>
<p>핵심 구분 기준은 모델 접근 방식이다. 클라우드 API(OpenAI, Anthropic)를 사용한다면 토큰 수준 제어가 불가능하므로 Instructor를 사용한다. 로컬 모델이나 vLLM 같은 자체 호스팅 서빙 스택을 사용한다면 Outlines가 성능과 신뢰성 모두에서 우위다. 처리량(throughput)이 중요한 고볼륨 배치 파이프라인에서는 재시도 오버헤드가 없는 Outlines가 확실히 유리하다.</p>
<h2 id="langchain-and-pydanticai-structured-outputs-in-agent-frameworks">LangChain and PydanticAI: Structured Outputs in Agent Frameworks</h2>
<p>에이전트 프레임워크들은 구조화 출력을 일급 시민으로 통합하고 있으며, 이는 복잡한 멀티 스텝 워크플로에서 LLM 출력 신뢰성을 프레임워크 수준에서 보장함을 의미한다. <strong>LangChain</strong>은 <code>with_structured_output()</code> 메서드를 통해 Pydantic 모델이나 TypedDict를 사용한 구조화 출력을 네이티브 지원한다. <code>method=&quot;json_schema&quot;</code> 또는 <code>method=&quot;function_calling&quot;</code>을 선택할 수 있으며, LCEL(LangChain Expression Language) 체인에 자연스럽게 통합된다. <strong>PydanticAI</strong>는 Pydantic을 개발한 팀이 만든 에이전트 프레임워크로, 구조화 출력이 아키텍처의 중심이다. <code>result_type</code> 파라미터로 Pydantic 모델을 직접 지정하며, 타입 안전성(type safety)과 IDE 자동완성을 완전히 지원한다. 2026년 기준 PydanticAI는 빠르게 성장하고 있으며, 특히 타입 안전성을 중시하는 팀에서 선호된다. LangChain의 <code>with_structured_output()</code>은 기존 LangChain 코드베이스와의 호환성이 중요할 때 최적의 선택이다. PydanticAI는 새로운 에이전트 프로젝트에서 타입 안전성과 Pydantic 생태계와의 긴밀한 통합이 필요할 때 권장된다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># LangChain 구조화 출력</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain_openai <span style="color:#f92672">import</span> ChatOpenAI
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TicketClassification</span>(BaseModel):
</span></span><span style="display:flex;"><span>    category: str
</span></span><span style="display:flex;"><span>    priority: int  <span style="color:#75715e"># 1-5</span>
</span></span><span style="display:flex;"><span>    requires_human: bool
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>llm <span style="color:#f92672">=</span> ChatOpenAI(model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gpt-4o&#34;</span>)
</span></span><span style="display:flex;"><span>structured_llm <span style="color:#f92672">=</span> llm<span style="color:#f92672">.</span>with_structured_output(TicketClassification, method<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;json_schema&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>result <span style="color:#f92672">=</span> structured_llm<span style="color:#f92672">.</span>invoke(<span style="color:#e6db74">&#34;User can&#39;t log in after password reset, production system&#34;</span>)
</span></span><span style="display:flex;"><span>print(result<span style="color:#f92672">.</span>category, result<span style="color:#f92672">.</span>priority, result<span style="color:#f92672">.</span>requires_human)
</span></span></code></pre></div><h2 id="production-best-practices-validation-retries-and-fallback-chains">Production Best Practices: Validation, Retries, and Fallback Chains</h2>
<p>프로덕션 LLM 앱에서 구조화 출력의 신뢰성을 극대화하려면 단순한 스키마 강제 이상의 전략이 필요하다. 검증(validation), 재시도(retry), 폴백 체인(fallback chain)을 체계적으로 구성해야 한다. 첫째, <strong>입력 스키마 설계</strong>: <code>additionalProperties: false</code>를 항상 설정하고, 선택 필드는 <code>default</code> 값을 지정하라. 중첩이 깊은 스키마는 성능 저하와 실패율 증가를 유발한다 — 3단계 이하로 유지하라. 둘째, <strong>Pydantic 검증 레이어</strong>: 스키마 통과 후에도 Pydantic <code>@field_validator</code>로 비즈니스 로직 검증을 추가하라. 예를 들어 <code>price</code>가 양수인지, <code>email</code>이 올바른 형식인지 LLM 레이어에만 의존하지 말라. 셋째, <strong>재시도 전략</strong>: Instructor의 자동 재시도를 사용하되, 최대 3회로 제한하고 재시도 간 지수 백오프를 적용하라. 프롬프트에 구체적인 오류 메시지를 포함하면 재시도 성공률이 크게 높아진다. 넷째, <strong>폴백 체인</strong>: 메인 모델 실패 시 더 저렴한 모델이나 단순화된 스키마로 폴백하는 체인을 구성하라. 마지막으로, <strong>옵저버빌리티</strong>: 구조화 출력 실패율, 재시도 횟수, 폴백 발생률을 메트릭으로 추적하라 — 이 지표들이 5%를 넘으면 프롬프트나 스키마를 재검토해야 한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> instructor
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> openai <span style="color:#f92672">import</span> OpenAI
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel, field_validator
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> tenacity <span style="color:#f92672">import</span> retry, stop_after_attempt, wait_exponential
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderExtract</span>(BaseModel):
</span></span><span style="display:flex;"><span>    product_id: str
</span></span><span style="display:flex;"><span>    quantity: int
</span></span><span style="display:flex;"><span>    unit_price: float
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@field_validator</span>(<span style="color:#e6db74">&#34;quantity&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@classmethod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quantity_positive</span>(cls, v):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> v <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">&#34;Quantity must be positive&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> v
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@field_validator</span>(<span style="color:#e6db74">&#34;unit_price&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@classmethod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">price_positive</span>(cls, v):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> v <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">&#34;Price must be positive&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> v
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> instructor<span style="color:#f92672">.</span>patch(OpenAI())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>order <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>chat<span style="color:#f92672">.</span>completions<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gpt-4o&#34;</span>,
</span></span><span style="display:flex;"><span>    max_retries<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span>,
</span></span><span style="display:flex;"><span>    response_model<span style="color:#f92672">=</span>OrderExtract,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>[{<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;Extract: 5 units of SKU-429 at $12.50 each&#34;</span>}]
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h2 id="choosing-the-right-tool-decision-matrix-for-structured-output-libraries">Choosing the Right Tool: Decision Matrix for Structured Output Libraries</h2>
<p>어떤 구조화 출력 도구를 선택할지는 모델 접근 방식, 팀의 기술 스택, 처리량 요구사항, 그리고 프로바이더 유연성 필요 여부에 따라 결정된다. 2026년 기준 생태계는 세 가지 명확한 패턴으로 정착했다. 첫 번째 패턴은 <strong>클라우드 API + Instructor</strong>: OpenAI, Anthropic, Gemini API를 사용하며 여러 프로바이더를 전환하거나 빠른 개발이 필요한 팀에 최적이다. Instructor는 프로바이더 중립적이며 Pydantic 생태계와 완벽히 통합된다. 두 번째 패턴은 <strong>로컬/자체 호스팅 + Outlines</strong>: 데이터 보안, 비용 절감, 또는 특화된 파인튜닝 모델 사용이 필요한 경우다. vLLM + Outlines 조합은 고처리량 배치 추론에서 업계 표준이 되고 있다. 세 번째 패턴은 <strong>에이전트 프레임워크 + 빌트인 지원</strong>: LangChain이나 PydanticAI 위에 구축 중이라면 각 프레임워크의 네이티브 구조화 출력 지원을 사용하라 — 별도 라이브러리 없이도 충분하다.</p>
<table>
  <thead>
      <tr>
          <th>상황</th>
          <th>권장 도구</th>
          <th>이유</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>클라우드 API, 다중 프로바이더</td>
          <td>Instructor</td>
          <td>프로바이더 중립, Pydantic 통합</td>
      </tr>
      <tr>
          <td>로컬 모델 (vLLM, TGI)</td>
          <td>Outlines</td>
          <td>FSM 사전 제약, 재시도 없음, O(1) 성능</td>
      </tr>
      <tr>
          <td>LangChain 기반 앱</td>
          <td>LangChain with_structured_output</td>
          <td>네이티브 통합</td>
      </tr>
      <tr>
          <td>PydanticAI 에이전트</td>
          <td>PydanticAI result_type</td>
          <td>타입 안전성 최우선</td>
      </tr>
      <tr>
          <td>단일 프로바이더 (OpenAI)</td>
          <td>네이티브 API (strict mode)</td>
          <td>추가 의존성 없음</td>
      </tr>
      <tr>
          <td>고볼륨 배치 추론</td>
          <td>Outlines + vLLM</td>
          <td>처리량 최적화</td>
      </tr>
  </tbody>
</table>
<h3 id="마이그레이션-경로-json-mode에서-벗어나기">마이그레이션 경로: JSON mode에서 벗어나기</h3>
<p>기존 코드베이스에서 <code>response_format: {type: &quot;json_object&quot;}</code>를 사용 중이라면 단계적 마이그레이션을 권장한다. 먼저 현재 JSON 출력의 실제 구조를 분석해 Pydantic 모델을 정의하고, <code>strict: true</code>와 <code>json_schema</code> 방식으로 전환한다. Instructor를 도입해 기존 파싱 코드를 제거하면 코드베이스가 단순해지고 신뢰성이 높아진다.</p>
<hr>
<h2 id="faq">FAQ</h2>
<p><strong>Q: JSON mode와 구조화 출력(Structured Outputs)의 차이는 무엇인가요?</strong></p>
<p>JSON mode는 유효한 JSON 형식을 반환하도록 요청하지만 스키마 준수를 보장하지 않습니다. 구조화 출력(특히 <code>strict: true</code>와 함께)은 사전에 정의한 JSON 스키마를 100% 준수하는 출력을 제약적 디코딩으로 강제합니다. 2026년 프로덕션에서는 JSON mode 사용을 피하고 네이티브 구조화 출력을 사용하세요.</p>
<p><strong>Q: Instructor와 Outlines 중 어떤 것을 선택해야 하나요?</strong></p>
<p>클라우드 API(OpenAI, Anthropic, Gemini)를 사용한다면 Instructor를 선택하세요. 로컬 모델이나 vLLM 같은 자체 호스팅 스택을 사용한다면 Outlines를 선택하세요. Instructor는 사후 검증 + 자동 재시도 방식이고, Outlines는 생성 단계에서 유효하지 않은 토큰을 차단하는 사전 제약 방식입니다.</p>
<p><strong>Q: LLM 구조화 출력 실패율이 얼마나 되나요?</strong></p>
<p>프롬프트 엔지니어링만 사용하는 경우 프로덕션에서 5<del>20%의 파싱 실패율이 보고됩니다. 함수 호출을 사용하면 1</del>5%로, 네이티브 구조화 출력(strict mode)을 사용하면 사실상 0%에 가깝게 줄어듭니다. 파싱 실패는 조용한 버그(silent bug)로 이어지기 때문에 프로덕션에서는 반드시 스키마 강제를 사용해야 합니다.</p>
<p><strong>Q: 재귀적이거나 매우 복잡한 JSON 스키마도 지원되나요?</strong></p>
<p>OpenAI와 Google Gemini는 복잡한 중첩 스키마와 <code>$ref</code> 참조를 지원하지만, 스키마가 복잡해질수록 성능이 떨어질 수 있습니다. 실무에서는 스키마를 3단계 이하의 중첩으로 유지하고, 재귀 스키마는 반드시 프로덕션에서 철저히 테스트하세요. Outlines는 FSM 변환 단계에서 복잡도가 높은 스키마의 컴파일 시간이 길어질 수 있습니다.</p>
<p><strong>Q: LLM 구조화 출력을 TypeScript/JavaScript에서 사용할 수 있나요?</strong></p>
<p>네. OpenAI JavaScript SDK는 <code>zodResponseFormat()</code>을 통해 Zod 스키마와 결합한 구조화 출력을 지원합니다. Instructor JS(<code>@instructor-ai/instructor</code>)도 Zod 기반 검증과 자동 재시도를 지원합니다. TypeScript 사용자는 Zod + OpenAI SDK 또는 Instructor JS를 파이썬의 Pydantic + Instructor와 동일한 패턴으로 사용할 수 있습니다.</p>
]]></content:encoded></item></channel></rss>