
**LLM 구조화된 출력(structured output)**은 언어 모델의 응답이 미리 정의된 스키마를 준수하도록 보장하는 메커니즘입니다. 단순히 유효한 JSON을 넘어, 사용자가 지정한 정확한 필드, 타입 및 제약 조건을 갖춘 스키마 유효(schema-valid) JSON을 보장합니다. 모든 주요 제공자가 이제 이를 네이티브로 지원하며, 이는 프로덕션 LLM 애플리케이션이 구축되는 방식을 변화시켰습니다.
빠른 요약: 한눈에 보는 구조화된 출력
시간이 부족하다면 2026년의 현황은 다음과 같습니다:
| 측면 | 세부 사항 |
|---|---|
| 정의 | LLM으로부터 스키마가 강제된 응답, "최선의 노력"이 아닌 보장된 구조 |
| 지원 업체 | OpenAI, Anthropic, Gemini, Cohere, xAI(Grok), plus Ollama/vLLM을 통한 로컬 환경 |
| 핵심 메커니즘 | 제약 디코딩(constrained decoding), 샘플링 전에 유효하지 않은 토큰 마스킹 |
| JSON 모드 vs 엄격 모드 | JSON 모드 = 유효한 구문만 보장. 엄격 모드 = 전체 스키마 준수 |
| Python 라이브러리 | 스키마 정의를 위한 Pydantic (BaseModel + Field) |
| TypeScript 라이브러리 | 스키마 정의를 위한 Zod (z.object + .describe) |
| 추천 시작 방법 | 네이티브 SDK를 통해 Pydantic 또는 Zod와 함께 OpenAI 사용 |
| 추천 프로덕션 라이브러리 | Instructor (Python) 또는 네이티브 SDK (TypeScript) |
| 가장 흔한 함정 | 답변 필드 뒤에 추론(reasoning) 필드를 배치하면, 모델이 생각하기 전에 결정함 |
| 지연 시간 오버헤드 | 첫 호출 시 50-200ms (스키마 컴파일), 이후 캐싱됨 |
이제 각 부분을 자세히 살펴보겠습니다.
LLM 구조화된 출력이란 무엇인가?
구조화된 출력은 LLM이 유효한 JSON을 반환하기를 희망하는 것과 그것을 보장하는 것의 차이입니다. 구조화된 출력을 활성화하면 모델은 물리적으로 스키마를 위반하는 토큰을 생성할 수 없습니다. JSON 스키마(또는 Pydantic 모델, Zod 스키마)를 정의하여 API에 전달하면, 매번 일치하는 응답을 받게 됩니다.
왜 이것이 중요할까요? 구조화된 출력 이전에는 개발자들이 취약한 정규식 파서를 작성하고, 모든 LLM 호출을 try/catch JSON.parse 블록으로 감싸며, 여전히 "거의 맞는" 응답(필드가 누락되거나 타입이 잘못된 유효한 JSON)을 처리해야 했습니다. 이러한 버그 클래스는 이제 사라졌습니다.
구조 강제에는 세 가지 수준이 있으며, 이는 명확한 진화를 나타냅니다:
- 프롬프트 엔지니어링, "이 필드로 JSON을 반환해주세요." 신뢰할 수 없습니다. 모델은 80-90%의 확률로 따를 뿐입니다.
- JSON 모드, 구문적으로 유효한 JSON을 보장하지만 스키마는 강제하지 않습니다.
{"name": string, "age": number}를 기대했을 때{"foo": "bar"}를 받을 수 있습니다. - 엄격 모드 / 제약 디코딩, 100% 스키마 준수를 보장합니다. 모델은 문자 그대로 유효하지 않은 토큰을 출력할 수 없습니다. 이것이 2026년에서의 "구조화된 출력"이 의미하는 바입니다.
2026년 초 기준, OpenAI, Anthropic, 그리고 Google Gemini 모두 네이티브 구조화된 출력을 지원합니다. 생태계가 수렴되었습니다.
결론: 프로덕션에서 정규식이나 JSON.parse로 LLM 응답을 파싱하고 있다면, 어려운 길을 가고 있는 것입니다. 네이티브 구조화된 출력은 이러한 실패 모드를 완전히 제거합니다.
JSON 모드 vs 엄격 모드: 실제로 무엇이 달라졌나?
이 구분은 이름이 비슷해 보이기 때문에 많은 개발자들을 혼란스럽게 합니다. 하지만 둘은 다릅니다.
| 기능 | JSON 모드 | 엄격 모드 (구조화된 출력) |
|---|---|---|
| API 파라미터 | type: "json_object" | strict: true인 type: "json_schema" |
| 유효한 JSON 보장 | 예 | 예 |
| 스키마 준수 보장 | 아니오 | 예 |
| 메커니즘 | 사후 토큰 바이어스 | 제약 디코딩 (FSM) |
| 예상치 못한 필드 반환 가능 | 예 | 아니오 |
| 필수 필드 누락 가능 | 예 | 아니오 |
| 타입 강제 | 없음 | 완전함 (string, number, array 등) |
| 사용 시기 | 사전에 스키마가 없을 때 | 프로덕션의 모든 경우 |
타임라인: OpenAI는 2023년 말에 JSON 모드를 도입했습니다. 이는 진전이었지만, 개발자들은 곧 "유효한 JSON"만으로는 충분하지 않으며 스키마 유효 JSON이 필요하다는 것을 깨달았습니다. 2024년 8월, OpenAI는 스키마 준수를 보장하기 위해 제약 디코딩을 사용하는 Strict Mode와 함께 구조화된 출력을 출시했습니다. 2025-2026년까지 모든 주요 제공자가 동일한 접근 방식을 채택했습니다.
JSON 모드는 여전히 좁은 용도가 있습니다: 응답의 형태를 미리 알 수 없고 비구조화된 탐색을 위해 어떤 유효한 JSON이든 원할 때입니다. 하지만 프로덕션에서는 드문 경우입니다.
결론: 프로덕션의 모든 것에 엄격 모드를 사용하세요. 스키마 기반 사용 사례에서 JSON 모드는 사실상 폐기되었습니다. 스키마가 있다면(그리고 있어야 합니다), strict: true와 함께 type: "json_schema"를 사용하세요.
제약 디코딩은 실제로 어떻게 작동하는가?
99.9%가 아니라 문자 그대로 100% 스키마 준수를 가능하게 하는 메커니즘은 다음과 같습니다.
Strict Mode가 활성화된 상태에서 제공자에게 JSON 스키마를 보내면, 스키마는 **유한 상태 기계(FSM, finite state machine)**로 컴파일됩니다. 이 FSM은 스키마를 통한 모든 유효한 경로를 나타냅니다. 각 토큰 생성 단계에서 추론 엔진은 어떤 토큰이 출력을 유효한 경로로 유지하고 어떤 토큰이 그렇지 않은지 확인합니다. 유효하지 않은 토큰은 샘플링 전에 로짓(logit)이 음의 무한대로 설정되므로, 선택될 확률이 0이 됩니다.
<!-- IMAGE: constrained-decoding diagram showing FSM token masking during structured output generation -->강력해진 자동 완성이라고 생각하면 됩니다. 모델이 방금 {"rating":을 출력했고 스키마에서 rating이 정수라고 명시했다면, 다음에 허용되는 토큰은 숫자 토큰뿐입니다. 따옴표, 문자, 괄호 등은 모두 마스킹됩니다. 모델이 "원한다" 해도 "five"를 출력할 수 없습니다.
이는 XGrammar(vLLM, SGLang 및 대부분의 로컬 추론 서버 뒤의 엔진)와 Outlines(제약 생성을 위한 오픈 소스 Python 라이브러리)에서 사용되는 동일한 핵심 메커니즘입니다. API 제공자는 이를 추론 인프라에 통합했을 뿐입니다.
알아두어야 할 trade-off가 하나 있습니다: 새로운 스키마를 사용한 첫 요청은 FSM이 빌드되는 동안 컴파일 지연 시간 손실(일반적으로 50-200ms)이 발생합니다. 동일한 스키마를 사용한 후속 요청은 캐시된 FSM을 사용하며 오버헤드가 거의 0에 가깝습니다. 또한 미묘한 품질 고려 사항이 있는데, 토큰 어휘를 제한하면 창의적이거나 자유 형식의 필드에서 출력 품질이 가끔 감소할 수 있으므로, 스키마는 진정한 구조화된 데이터에 집중하세요.
결론: 제약 디코딩은 "대부분 작동"과 "항상 작동"을 구분합니다. 이것이 구조화된 출력을 프로덕션 준비 상태로 만드는 엔지니어링입니다.
다중 제공자 구현: OpenAI, Anthropic 및 Gemini
다른 가이드에서는 보여주지 않는 내용이 있습니다: 세 가지 주요 제공자 전반에 걸쳐 구현된 동일한 추출 작업입니다. 비구조화된 텍스트에서 구조화된 제품 리뷰를 추출해 보겠습니다.
모든 제공자에서 공유되는 Pydantic 스키마:
from pydantic import BaseModel, Field
from typing import Literal
class ProductReview(BaseModel):
reasoning: str = Field(description="Think through the review before scoring")
rating: int = Field(description="Rating from 1-5", ge=1, le=5)
sentiment: Literal["positive", "negative", "neutral"]
pros: list[str] = Field(description="Key positive points")
cons: list[str] = Field(description="Key negative points")
summary: str = Field(description="One-sentence summary")OpenAI 구현
from openai import OpenAI
client = OpenAI()
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extract a structured review from the text."},
{"role": "user", "content": review_text}
],
response_format=ProductReview, # Pydantic model directly
)
review = response.choices[0].message.parsed # Typed ProductReview objectOpenAI의 구현은 가장 성숙합니다. parse() 메서드는 Pydantic 모델을 직접 받아 타입이 지정된 객체를 반환합니다. 한 가지 제약 사항: OpenAI의 Strict Mode는 JSON 스키마의 하위 집합만 지원합니다. $ref 없음, 제한된 anyOf, 그리고 모든 필드는 additionalProperties: false와 함께 필수여야 합니다.
Anthropic 구현
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": f"Extract a structured review:\n\n{review_text}"}
],
output_config={
"format": {
"type": "json_schema",
"json_schema": ProductReview.model_json_schema()
}
}
)
import json
review_data = json.loads(response.content[0].text)
review = ProductReview(**review_data)Anthropic의 네이티브 구조화된 출력은 JSON 스키마와 함께 output_config.format을 사용합니다. 2026년 초 GA(일반 공개)에 도달했습니다. Anthropic은 또한 "가짜" 도구를 정의하고 tool_use를 통해 추출하는 이전 패턴도 지원하는데, 이는 여전히 작동하지만 순수 추출을 위해서는 네이티브 구조화된 출력이 더 깔끔합니다.
Gemini 구현
from google import genai
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=f"Extract a structured review:\n\n{review_text}",
config={
"response_mime_type": "application/json",
"response_schema": ProductReview, # Pydantic model directly
}
)
import json
review = ProductReview(**json.loads(response.text))Gemini는 Python SDK에서 response_schema를 통해 Pydantic 모델을 직접 지원합니다. 독특한 기능: Gemini는 스키마의 propertyOrdering을 존중하므로, 필드 출력 순서를 제어할 수 있습니다(추론 우선 패턴에 유용).
제공자 비교
| 기능 | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| API 파라미터 | response_format | output_config.format | response_schema |
| 스키마 입력 | Pydantic 또는 JSON 스키마 | JSON 스키마 | Pydantic 또는 JSON 스키마 |
| 엄격 모드 | strict: true | json_schema와 함께 암시적 | 암시적 |
| 스트리밍 | 예 (부분 JSON) | 예 | 예 |
| 거부 처리 | message.refusal 필드 | 오류 응답 | 오류 응답 |
| 도구 사용 대안 | 예 | 예 (원래 방법) | 예 |
| 스키마 컴파일 캐시 | 예 (서버 측) | 예 | 예 |
| 속성 순서 | 네이티브 지원 안 함 | 아니오 | 예 (propertyOrdering) |
결론: OpenAI는 parse() 메서드로 가장 세련된 개발자 경험(DX)을 제공합니다. Anthropic은 가장 유능한 기본 모델을 제공합니다. Gemini의 속성 순서 지정은 독특하게 유용합니다. 세 곳 모두 작업을 완료하므로, 기존 제공자 관계에 따라 선택하세요.
Python 개발자를 위한 Pydantic 패턴
Pydantic은 Python에서 구조화된 출력 스키마를 정의하는 사실상의 표준입니다. 중요한 패턴들은 다음과 같습니다.
설명이 포함된 기본 스키마
from pydantic import BaseModel, Field
from typing import Literal, Optional
class ExtractedEntity(BaseModel):
reasoning: str = Field(description="Think step by step about the entity")
name: str = Field(description="Full name of the entity")
entity_type: Literal["person", "company", "location"]
confidence: float = Field(description="Confidence score 0.0-1.0", ge=0.0, le=1.0)
context: Optional[str] = Field(description="Surrounding context, if relevant")那些 description 문자열은 문서화용만이 아닙니다. 이들은 모델에 전송되는 JSON 스키마의 일부가 되어 모델이 생성하는 내용에 직접적인 영향을 미칩니다. 이를 스키마 내부의 프롬프트 엔지니어링이라고 생각하세요.
중첩 모델
class Address(BaseModel):
street: str
city: str
country: str
postal_code: Optional[str] = None
class Company(BaseModel):
reasoning: str = Field(description="Analysis of the company details")
name: str
industry: Literal["tech", "finance", "healthcare", "retail", "other"]
headquarters: Address # Nested model
key_products: list[str] = Field(description="Top 3 products or services")중첩은 최대 2-3단계로 유지하세요. 깊게 중첩된 스키마는 오류율을 높이고 스키마 컴파일을 느리게 합니다.
추론 우선(Reasoning-First) 패턴
これは単一の 가장 영향력 있는 스키마 설계 패턴입니다. 답변 필드 앞에 reasoning 필드를 배치하세요:
# Reliable JSON from Any LLM: Pydantic + Zod Patterns for 2026
class ClassificationBad(BaseModel):
category: Literal["spam", "ham"]
confidence: float
# Good -- model reasons through the problem first
class ClassificationGood(BaseModel):
reasoning: str = Field(description="Analyze the text before classifying")
category: Literal["spam", "ham"]
confidence: float = Field(ge=0.0, le=1.0)LLM은 토큰을 왼쪽에서 오른쪽으로 생성합니다. category가 먼저 오면 모델은 카테고리를 선택한 후 이를 합리화합니다. reasoning이 먼저 오면 모델은 문제를 해결한 후 카테고리에commit합니다. 이는 스키마에 내장된 연쇄 사고(chain-of-thought)입니다.
JSON 스키마 export
# Generate the JSON Schema for any Pydantic model
schema = ProductReview.model_json_schema()
# Pass this to any provider that accepts raw JSON Schema결론: Pydantic + 설명 필드 + 추론 우선 순서는 Python 구조화된 출력의 삼각편대입니다. 이 세 가지 패턴을 마스터하면 사용 사례의 90%를 처리할 수 있습니다.
TypeScript 개발자를 위한 Zod 패턴
Zod는 Pydantic의 TypeScript 버전이며, 구조화된 출력 워크플로우에서 마찬가지로 중심적입니다.
설명이 포함된 기본 스키마
import { z } from "zod";
const ProductReview = z.object({
reasoning: z.string().describe("Think through the review before scoring"),
rating: z.number().int().min(1).max(5),
sentiment: z.enum(["positive", "negative", "neutral"]),
pros: z.array(z.string()).describe("Key positive points"),
cons: z.array(z.string()).describe("Key negative points"),
summary: z.string().describe("One-sentence summary"),
});
// Infer the TypeScript type automatically
type ProductReview = z.infer<typeof ProductReview>;Pydantic의 Field(description=...)와 마찬가지로, Zod의 .describe()는 JSON 스키마의 일부가 되어 모델의 출력을 안내합니다.
OpenAI Node SDK와의 통합
import OpenAI from "openai";
import { zodResponseFormat } from "openai/helpers/zod";
const client = new OpenAI();
const response = await client.beta.chat.completions.parse({
model: "gpt-4o-2024-08-06",
messages: [
{ role: "system", content: "Extract a structured review." },
{ role: "user", content: reviewText },
],
response_format: zodResponseFormat(ProductReview, "product_review"),
});
const review = response.choices[0].message.parsed; // Typed!Vercel AI SDK와의 통합
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
const { object: review } = await generateObject({
model: openai("gpt-4o"),
schema: ProductReview,
prompt: `Extract a structured review:\n\n${reviewText}`,
});
// review is fully typed as ProductReviewVercel AI SDK는 generateObject()와 함께 Zod를 네이티브로 사용하여 가장 깔끔한 TypeScript 통합을 제공합니다. 통합 API를 통해 OpenAI, Anthropic, Gemini 및 기타 제공자와 함께 작동합니다.
JSON 스키마 변환
import { zodToJsonSchema } from "zod-to-json-schema";
const jsonSchema = zodToJsonSchema(ProductReview);
// Use with any provider that accepts raw JSON Schema결론: Zod + .describe() + Vercel AI SDK는 TypeScript 구조화된 출력 스택입니다. Node/Next.js 생태계에 있다면 이것이 최소 저항의 경로입니다.
구조화된 출력 vs 함수 호출: 언제 각각 사용하는가?
이는 가장 흔한 혼란의 원인 중 하나입니다. 둘 다 스키마를 포함하고 구조화된 데이터를 반환하지만, 서로 다른 문제를 해결합니다.
구조화된 출력은 말합니다: "데이터를 이 정확한 형태로 주세요." 이는 추출, 분류 및 포맷팅을 위한 것입니다. 비구조화된 텍스트에서 구조화된 정보를 끌어내는 것입니다.
함수 호출(도구 사용)은 말합니다: "다음은 수행할 수 있는 작업들입니다. 어떤 것을 실행할지 결정하고 인수를 제공하세요." 이는 모델이 여러 도구 중에서 선택하고 작업을 트리거하는 에이전트 워크플로우를 위한 것입니다.
역사적으로 혼란이 생긴 것은 이해갑니다. Anthropic의 원래 "구조화된 출력"은 문자 그대로 함수 호출이었습니다. extract_review라는 가짜 도구를 정의하고 인수를 가져오는 방식이었습니다. 이는 여전히 작동하지만, 순수 추출을 위해서는 네이티브 구조화된 출력이 더 간단합니다.
| 시나리오 | 최선의 접근법 | 이유 |
|---|---|---|
| 텍스트에서 데이터 추출 | 구조화된 출력 | 직접적, 낮은 지연 시간, 단일 스키마 |
| 카테고리 분류 | 구조화된 출력 | 하나의 응답, 하나의 스키마 |
| 에이전트가 호출할 도구 결정 | 함수 호출 | 모델이 여러 도구 중 선택 |
| 다단계 오케스트레이션 | 함수 호출 | 순차적 도구 호출 |
| 데이터 추출 AND 다음 작업 결정 | 둘 다 | 추출에는 구조화된 출력, 오케스트레이션에는 함수 호출 |
구조화된 출력은 AI 에이전트 시스템의 도구 호출 파이프라인을 구동합니다. 이러한 것이 프로덕션 워크플로우에 어떻게 적합하는지 알아보려면 비즈니스용 AI 에이전트 가이드를 참조하세요.
결론: 데이터의 형태를 알고 있을 때는 구조화된 출력을 사용하세요. 모델이 작업을 선택해야 할 때는 함수 호출을 사용하세요. 실제로 대부분의 애플리케이션은 둘 다 사용합니다. 데이터 추출에는 구조화된 출력, 에이전트 오케스트레이션에는 함수 호출을 사용합니다.
프로덕션 패턴: 오류, 재시도 및 스트리밍
데모에서 구조화된 출력을 작동시키는 것은 쉽습니다. 프로덕션에서 신뢰성을 유지하려면 거부, 검증 실패 및 스트리밍이라는 세 가지 사항을 처리해야 합니다.
거부 처리
때로는 모델이 요청한 출력을 생성하는 것을 거부합니다. 일반적으로 안전 필터가 입력을 플래그했기 때문입니다. 이때 구조화된 출력 API는 스키마를 반환하지 않습니다. 거부를 반환합니다.
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=messages,
response_format=ProductReview,
)
# ALWAYS check for refusal before accessing parsed content
if response.choices[0].message.refusal:
print(f"Model refused: {response.choices[0].message.refusal}")
else:
review = response.choices[0].message.parsed거부 확인을 건너뛰고 거부 상태에서 .parsed에 액세스하려고 하면 None과 혼란스러운 하류 오류가 발생합니다. 항상 먼저 확인하세요.
검증 피드백을 통한 재시도 패턴
스키마 준수는 제약 디코딩에 의해 보장되지만, semantic 정확성은 보장되지 않습니다. 모델은 {"rating": 1, "sentiment": "positive"}를 반환할 수 있습니다. 유효한 스키마이지만 내용이 모순됩니다. 이때 검증 + 재시도가 필요합니다.
import instructor
client = instructor.from_openai(OpenAI())
# Instructor handles retries automatically
review = client.chat.completions.create(
model="gpt-4o",
response_model=ProductReview,
max_retries=3, # Retries with validation error feedback
messages=[
{"role": "user", "content": review_text}
],
)Instructor는 재시도 시 검증 오류를 모델에 다시 피드백하여 자체 수정할 수 있게 합니다. Instructor 없이.manual 재시도 패턴을 위한 코드:
from pydantic import ValidationError
for attempt in range(3):
try:
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=messages,
response_format=ProductReview,
)
review = response.choices[0].message.parsed
# Run additional semantic validation here
break
except ValidationError as e:
messages.append({"role": "assistant", "content": str(response)})
messages.append({"role": "user", "content": f"Validation error: {e}. Fix it."})구조화된 출력 스트리밍
큰 구조화된 응답, 긴 배열, 많은 필드, 복잡한 중첩 객체의 경우, 스트리밍을 통해 부분 결과를 점진적으로 렌더링할 수 있습니다.
import instructor
client = instructor.from_openai(OpenAI())
# Stream partial results as fields populate
review_stream = client.chat.completions.create_partial(
model="gpt-4o",
response_model=ProductReview,
messages=[{"role": "user", "content": review_text}],
)
for partial_review in review_stream:
# Fields populate one by one as tokens stream in
if partial_review.summary:
print(f"Summary so far: {partial_review.summary}")주의할 점: 개별 스트리밍 청크는 자체적으로 스키마 유효하지 않습니다. rating이 아직 None인 동안 reasoning 필드가 채워질 수 있습니다. 이에 맞춰 UI를 계획하고, 채워지지 않은 필드에 대해 로딩 상태를 표시하세요.
결론: 거부 확인은 필수입니다. 검증 피드백을 통한 재시도는 semantic 오류를 catch합니다. 몇 초 이상かかる 응답에는 스트리밍이 가치 있습니다.
구조화된 출력 라이브러리 비교
네이티브 API를 통해 구조화된 출력을 사용할 수 있지만, 라이브러리는 검증, 재시도, 스트리밍 및 다중 제공자 지원을 추가합니다. 현황은 다음과 같습니다.
**Instructor**는 11K+ GitHub 스타와 월 3M+ 다운로드로 가장 인기 있는 옵션입니다. OpenAI, Anthropic, Gemini, Cohere, Ollama 등을 통합된 Pydantic 기반 인터페이스로 감쌉니다. 주요 기능: 검증 피드백을 통한 자동 재시도, create_partial()을 통한 스트리밍, 그리고 매우 간단한 설정(instructor.from_openai(client)). Python 팀이라면 여기서 시작하세요.
**BAML**은 다른 접근 방식을 취합니다: custom DSL을 통한 스키마 우선. .baml 파일에서 스키마를 정의하고 Python, TypeScript, Ruby 등에 대한 클라이언트를 자동 생성합니다. SAP(스키마 정렬 파싱) 알고리즘은 지저분한 모델 출력을 우아하게 처리합니다. 크로스 언어 팀이나 LLM 레이어와 애플리케이션 레이어 간의 계약이 필요할 때 가장 좋습니다. Trade-off: 추가 빌드 단계와 학습해야 할 새로운 문법.
LangChain은 제공자 독립적인 구조화된 출력을 위해 .with_structured_output(schema)를 제공합니다. 이미 LangChain 생태계에 있다면 편리합니다. Trade-off: 무거운 의존성이며, 추상화가 필요한 제공자별 기능을 숨길 수 있습니다.
네이티브 API, response_format / output_config를 사용한 직접 호출은 제공자 SDK 외에는 제로 의존성이 필요합니다. 완전한 제어와 가시성을 얻습니다. 간단한 사용 사례나 최소한의 추상화를 선호하는 팀에 가장 적합합니다.
| 라이브러리 | 언어 | 제공자 | 자동 재시도 | 스트리밍 | GitHub 스타 | 학습 곡선 |
|---|---|---|---|---|---|---|
| Instructor | Python, TS | 15+ | 예 | 예 | 11K+ | 낮음 |
| BAML | Python, TS, Ruby, Go | All (DSL-agnostic) | 예 | 예 | 7K+ | 중간 |
| LangChain | Python, TS | 20+ | 부분 | 예 | 100K+ | 중간-높음 |
| 네이티브 API | Any | SDK당 1개 | 아니오 | 예 | N/A | 낮음 |
올바른 구조화된 출력 라이브러리 선택은 더 넓은 AI 스택 결정의 일부입니다. 전체 스택을 분석한 내용은 SaaS를 위한 최고의 AI 스택 가이드에서 확인할 수 있습니다.
Instructor, BAML, Mirascope 등의 심층 비교를 담은 LLM 구조화된 출력을 위한 최고의 라이브러리 [곧 공개]를 참조하세요.
결론: Python에는 Instructor, TypeScript에는 네이티브 API로 시작하세요. 크로스 언어 스키마 계약이 필요하면 BAML로 이동하세요. 구조화된 출력만을 위해 LangChain은 사용하지 마세요. 과잉입니다.
스키마 설계 모범 사례 (및 일반적인 실수)
스키마 설계는 출력 품질에 직접적인 영향을 미칩니다. 중요한 패턴과 정확도를 떨어뜨리는 실수들은 다음과 같습니다.
답변 앞에 추론 배치
Pydantic 섹션에서 다루었지만, 가장 영향력 있는 설계 결정이므로 반복합니다:
# Before: model guesses the answer, then rationalizes
class Bad(BaseModel):
answer: str
reasoning: str
# After: model thinks first, then commits
class Good(BaseModel):
reasoning: str = Field(description="Think step by step")
answer: strLLM은 왼쪽에서 오른쪽으로 생성합니다. 필드 순서는 프롬프트 순서입니다. 추론이 먼저이면 모델은 답변에 commit하기 전에 문제를 해결해야 합니다.
안티패턴 테이블
| 실수 | 문제점 | 해결책 |
|---|---|---|
| 답변 뒤에 추론 필드 배치 | 모델이 생각하기 전에 결정 | 추론을 답변 앞으로 이동 |
| 깊은 중첩 (4+ 레벨) | 높은 오류율, 느린 컴파일 | 2-3 레벨로 평탄화 |
| 필드 설명 없음 | 모델이 의도를 추측 | .describe() / Field(description=...) 추가 |
| null 처리 누락 | 모델이 필드를 채우기 위해 환각 발생 | Optional / .nullable() 사용 |
| 과도하게 큰 스키마 (50+ 필드) | 컴파일 타임아웃, 품질 저하 | 여러 호출로 분할 |
| 모호한 enum 옵션 | 모델이 잘못된 카테고리 선택 | 구체적이고 중복되지 않는 옵션 사용 |
Null을 명시적으로 처리
소스 텍스트에 데이터가 없을 수 있는 필드는 선택적으로 만드세요. 데이터가 없을 때 필수 필드를 강요하면 환각(hallucination)으로 이어집니다:
class PersonInfo(BaseModel):
name: str # Always present
email: Optional[str] = Field(None, description="Email if mentioned, null otherwise")
phone: Optional[str] = Field(None, description="Phone if mentioned, null otherwise")스키마를 집중적으로 유지
작업당 하나의 스키마. 하나의 거대한 스키마로 모든 것을 추출하려고 하지 마세요. 50개 이상의 필드가 필요하면 여러 추출 호출로 분할하세요. OpenAI의 Strict Mode에는 스키마 복잡성에 대한 실제 제한이 있으며, 작동하더라도 매우 큰 스키마는 출력 품질을 저하시킵니다.
결론: 추론 우선, 설명 필드, 명시적 nulls, 그리고 집중된 스키마. 이 네 가지를 올바르게 수행하면 구조화된 출력 정확도가 측정 가능하게 향상됩니다.
로컬 LLM과의 구조화된 출력
구조화된 출력을 위해 API 제공자가 필요하지 않습니다. 로컬 추론 엔진은 자체 하드웨어에서 실행되는 동일한 근본 메커니즘인 문법 기반 제약 디코딩을 통해 이를 지원합니다.
Ollama
로컬 구조화된 출력을 위한 가장 쉬운 경로입니다. Ollama는 format 파라미터를 통해 JSON 스키마를 허용합니다:
import ollama
from pydantic import BaseModel
class Country(BaseModel):
name: str
capital: str
languages: list[str]
response = ollama.chat(
model="llama3.2",
messages=[{"role": "user", "content": "Tell me about Japan."}],
format=Country.model_json_schema(),
)
import json
country = Country(**json.loads(response.message.content))Ollama는 내부적으로 제약 디코딩을 위해 XGrammar를 사용합니다. API 제공자와 동일한 보장: 100% 스키마 준수.
vLLM 및 SGLang
프로덕션 등급 로컬 추론을 위해, vLLM과 SGLang은 모두 guided_json 및 guided_regex 파라미터를 통해 구조화된 출력을 지원합니다. XGrammar는 기본 백엔드로, JSON 생성에서 거의 제로 오버헤드를 제공하며 대체 문법 엔진보다 최대 3.5배 빠릅니다.
Outlines
Outlines는 문법 기반 제약 생성을 개척한 오픈 소스 Python 라이브러리입니다. 모든 Hugging Face 모델과 함께 작동하며 JSON 스키마, 정규식 및 전체 문맥 자유 문법(CFG/EBNF) 제약을 지원합니다. 또한 vLLM 및 SGLang에 문법 백엔드 옵션으로 통합되어 있습니다.
API 제공자와의 주요 차이점: 로컬 구조화된 출력에는 스키마 하위 집합 제한이 없습니다. 문법을 완전히 제어합니다. 하지만 모델 품질은 더 다양합니다. 7B 파라미터 로컬 모델은 복잡한 추출 작업에서 GPT-4o나 Claude와 일치하지 않습니다. 스키마는 항상 유효하지만; 콘텐츠 품질은 모델에 따라 다릅니다.
결론: 개발에는 Ollama, 프로덕션에는 XGrammar와 함께 vLLM/SGLang. 로컬 구조화된 출력은 대부분의 사용 사례에 충분히 성숙했지만, 더 작은 모델은 스키마 내에서 더 낮은 품질의 콘텐츠를 생성한다는 점은 유의하세요.
FAQ
LLM에서 구조화된 출력이란 무엇인가?
구조화된 출력은 LLM의 응답이 미리 정의된 JSON 스키마를 준수하도록 보장하는 메커니즘입니다. 일반 텍스트나 심지어 JSON 모드와 달리, 구조화된 출력은 제약 디코딩을 사용하여 스키마의 모든 필드, 타입 및 제약 조건이 충족되도록 합니다 -- "대부분"이 아닌 100%의 시간 동안.
JSON 모드와 구조화된 출력의 차이점은 무엇인가?
JSON 모드는 구문적으로 유효한 JSON을 보장하지만 스키마는 강제하지 않습니다. 어떤 유효한 JSON 객체라도 받을 수 있습니다. 구조화된 출력(Strict Mode)은 제약 디코딩을 통해 전체 스키마 준수를 보장합니다. 프로덕션에는 Strict Mode를 사용하세요; JSON 모드는 사전에 스키마가 없을 때만 관련이 있습니다.
어떤 LLM 제공자가 네이티브로 구조화된 출력을 지원하는가?
OpenAI(2024년 8월 이후), Google Gemini(2024년, 2026년 확장), Anthropic(2025년 11월 베타, 2026년 초 GA), Cohere 및 xAI(Grok)는 모두 네이티브 구조화된 출력을 지원합니다. 로컬 측면에서는 Ollama, vLLM 및 SGLang이 문법 기반 제약 디코딩을 통해 이를 지원합니다.
제약 디코딩은 어떻게 스키마 준수를 보장하는가?
JSON 스키마는 유한 상태 기계(FSM)로 컴파일됩니다. 각 토큰 생성 단계에서 FSM을 통한 유효한 경로로 출력을 유지하는 토큰만 허용되며, 유효하지 않은 토큰의 로짓은 음의 무한대로 설정됩니다. 이는 유효하지 않은 토큰이 생성될 확률이 0임을 의미하며, 통계적 보장이 아닌 수학적 보장을 제공합니다.
구조화된 출력과 함수 호출 중 무엇을 사용해야 하는가?
특정 형태의 데이터를 원할 때 추출 및 분류에는 구조화된 출력을 사용하세요. 모델이 수행할 작업을 결정해야 할 때 에이전트 워크플로우에는 함수 호출을 사용하세요. 많은 프로덕션 애플리케이션은 둘 다 사용합니다. 데이터 추출에는 구조화된 출력, 오케스트레이션에는 함수 호출.
구조화된 출력을 스트리밍할 수 있는가?
예. OpenAI는 parse() 메서드로 스트리밍을 지원하며, Instructor는 필드별로 채워지는 Pydantic 모델을 스트리밍하기 위해 create_partial()을 제공합니다. 개별 스트리밍 청크는 개별적으로 스키마 유효하지 않으며, 필드는 점진적으로 채워진다는 점을 명심하세요.
Instructor 라이브러리는 무엇인가?
Instructor는 가장 인기 있는 구조화된 출력 라이브러리입니다(11K+ GitHub 스타, 월 3M+ 다운로드). Pydantic 기반 검증, 검증 피드백을 통한 자동 재시도 및 스트리밍 지원으로 제공자 SDK를 감쌉니다. OpenAI, Anthropic, Gemini, Cohere, Ollama 및 10개 이상의 다른 제공자와 함께 작동합니다.
구조화된 출력이 로컬 LLM에서 작동하는가?
예. Ollama는 JSON 스키마와 함께 format 파라미터를 통해 구조화된 출력을 지원합니다. vLLM과 SGLang은 guided_json 파라미터를 통해 이를 지원합니다. 세 곳 모두 제약 디코딩을 위해 XGrammar 또는 Outlines를 사용합니다. 스키마 준수 보장은 API 제공자와 동일합니다; 콘텐츠 품질은 모델에 따라 다릅니다.
일반적인 스키마 설계 실수는 무엇인가?
주요 실수: 추론 필드를 답변 필드 뒤에 배치(모델이 생각하기 전에 결정), 깊게 중첩된 스키마(4+ 레벨은 오류 증가), 필드 설명 누락(모델이 의도 추측), 선택적 데이터에 대한 null 처리 없음(환각 강제), 과도하게 큰 스키마(50+ 필드는 품질 저하).
구조화된 출력이 지연 시간을 추가하는가?
첫 요청에는 스키마 컴파일 오버헤드가 있으며, 일반적으로 FSM이 빌드되는 동안 50-200ms입니다. 동일한 스키마를 사용한 후속 요청은 캐시된 FSM을 사용하며 지연 시간이 거의 0에 가깝습니다. 대부분의 애플리케이션에서 이는 전체 모델 추론 시간에 비해 무시할 수 있습니다.
구조화된 출력을 이미지或多模态 입력과 함께 사용할 수 있는가?
예. 구조화된 출력은 입력이 아닌 응답 형식에 적용됩니다. 구조화된 출력 스키마와 함께 GPT-4o나 Gemini에 이미지를 보내면 이미지의 스키마 준수 분석을 받을 수 있습니다. 이는 영수증, 양식 또는 제품 이미지에서 구조화된 데이터를 추출하는 시각적 추출 워크플로우에 강력합니다.