구조화 출력: JSON, 스키마 검증, 제약 디코딩
LLM은 문자열을 반환합니다. 애플리케이션은 JSON이 필요합니다. 이 간극은 모델 환각(Hallucination)보다 더 많은 프로덕션 시스템을 망가뜨려 왔습니다. 구조화 출력(Structured Outputs)은 자연어와 타입이 있는 데이터 사이를 잇는 다리입니다. 제대로 만들면 LLM은 신뢰할 수 있는 API가 됩니다. 잘못 만들면 새벽 3시에 정규식(Regex)으로 자유 텍스트를 파싱하는 처지가 됩니다.
유형: Build
언어: Python
선수 지식: Phase 10, Lessons 01-05 (LLMs from Scratch)
예상 시간: 약 90분
관련: Phase 5 · 20 (구조화 출력과 제약 디코딩(Structured Outputs & Constrained Decoding))은 디코더 수준 이론(FSM/CFG logit processors, Outlines, XGrammar)을 다룹니다. 이 lesson은 프로덕션 SDK 표면인 OpenAI response_format, Anthropic 도구 사용(Tool Use), Instructor에 집중합니다. API 아래에서 무슨 일이 일어나는지 이해하고 싶다면 Phase 5 · 20을 먼저 읽으세요.
학습 목표
- OpenAI와 Anthropic API 파라미터를 사용해 JSON 모드(JSON Mode)와 스키마 제약 출력(Schema-Constrained Outputs)을 구현합니다.
- 잘못된 LLM 출력을 거부하고 오류 피드백으로 재시도하는 Pydantic 검증 계층(Validation Layer)을 만듭니다.
- 제약 디코딩(Constrained Decoding)이 후처리 없이 토큰 수준에서 유효한 JSON을 강제하는 원리를 설명합니다.
- 비구조 텍스트를 타입이 있는 데이터 구조로 안정적으로 변환하는 견고한 추출 프롬프트를 설계합니다.
문제
LLM에 "이 텍스트에서 상품명, 가격, 재고 여부를 추출해줘"라고 요청합니다. 모델은 이렇게 응답합니다.
상품은 Sony WH-1000XM5 헤드폰이며, 가격은 $348.00이고 현재 재고가 있습니다.
이 답은 완전히 맞는 문장입니다. 하지만 애플리케이션 입장에서는 전혀 쓸모가 없습니다. 재고 시스템에는 {"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true} 같은 형태가 필요합니다. 즉, 특정 키, 특정 타입, 특정 값 제약을 가진 JSON 객체가 필요합니다. 자연어 문장은 필요하지 않습니다.
순진한 해결책은 프롬프트에 "JSON으로 응답해"라는 한 줄을 추가하는 것입니다. 이 방법은 90% 정도는 작동합니다. 나머지 10%에서는 모델이 JSON을 마크다운(Markdown) 코드 펜스로 감싸거나, "Here's the JSON:" 같은 서문을 붙이거나, 괄호를 일찍 닫아 문법적으로 잘못된 JSON을 생성합니다. JSON 파서가 터지고 파이프라인이 깨집니다. 그래서 예외 처리(try/except)와 재시도 루프를 덧붙입니다. 그런데 재시도 때마다 데이터가 조금씩 달라지는 경우가 생깁니다. 결국 파싱 문제 위에 일관성 문제까지 얹히게 됩니다.
이것은 프롬프트 엔지니어링(Prompt Engineering) 문제가 아니라 디코딩(Decoding) 문제입니다. 모델은 왼쪽에서 오른쪽으로 토큰을 생성합니다. 각 위치에서 10만 개 이상의 어휘 중 가장 그럴듯한 다음 토큰을 고릅니다. 그런데 그중 대부분은 그 위치에서 잘못된 JSON을 만들어 냅니다. 모델이 방금 {"price":까지 출력했다면 다음 토큰은 숫자, 문자열을 위한 따옴표, null, true, false, 또는 음수 기호 중 하나여야 합니다. 그 외의 토큰은 잘못된 JSON을 만들어 냅니다. 제약이 없는 상태라면 모델은 영어 단어처럼 그럴듯하지만 문법적으로는 치명적인 토큰을 골라 버릴 수 있습니다.
개념
구조화 출력 스펙트럼
구조화 출력 제어에는 네 단계가 있습니다. 뒤로 갈수록 더 신뢰할 수 있습니다.
graph LR
subgraph Spectrum["구조화 출력 스펙트럼"]
direction LR
A["프롬프트 기반\n'JSON을 반환하세요'\n약 90% 유효"] --> B["JSON 모드\n유효한 JSON 보장\n스키마 보장 없음"]
B --> C["스키마 모드\nJSON + 스키마 일치\n준수 보장"]
C --> D["제약 디코딩\n토큰 수준 강제\n100% 준수"]
end
style A fill:#1a1a2e,stroke:#ff6b6b,color:#fff
style B fill:#1a1a2e,stroke:#ffa500,color:#fff
style C fill:#1a1a2e,stroke:#51cf66,color:#fff
style D fill:#1a1a2e,stroke:#0f3460,color:#fff
프롬프트 기반(Prompt-Based), 예: "유효한 JSON으로 응답하세요". 별도의 강제가 없습니다. 모델은 보통 따르지만 가끔 따르지 않습니다. 신뢰도는 약 90%입니다. 대표적인 실패 모드는 마크다운 코드 펜스로 감싸 버리기, 서문 텍스트 덧붙이기, 출력 잘림, 잘못된 구조입니다.
JSON 모드(JSON Mode): API가 출력이 유효한 JSON임을 보장합니다. OpenAI의 response_format: { type: "json_object" }가 이 모드를 켭니다. 출력은 파싱 오류 없이 읽힙니다. 다만 기대한 스키마와 일치한다는 보장은 없습니다. 추가 키, 잘못된 타입, 빠진 필드가 나올 수 있습니다.
스키마 모드(Schema Mode): API에 JSON Schema를 전달하면 출력이 그 스키마와 일치하도록 보장합니다. 2026년 기준 주요 제공자는 모두 이를 네이티브로 지원합니다. OpenAI는 response_format: { type: "json_schema", json_schema: {...} } 또는 tool_choice="required"를 사용하고, Anthropic은 input_schema가 있는 도구 사용(Tool Use)을 사용하며, Gemini는 response_schema와 response_mime_type: "application/json"을 사용합니다. 출력은 지정한 키, 타입, 제약을 정확하게 따릅니다.
제약 디코딩(Constrained Decoding): 생성 중 각 토큰 위치에서 잘못된 출력으로 이어질 토큰을 모두 마스킹합니다. 스키마가 숫자를 요구하는데 모델이 문자를 내보내려 하면 그 토큰의 확률을 0으로 만듭니다. 그 결과 모델은 유효한 출력으로 이어지는 토큰만 생성할 수 있습니다. OpenAI 구조화 출력 모드와 Outlines, Guidance 같은 라이브러리가 내부적으로 사용하는 방식이 바로 이 제약 디코딩입니다.
JSON Schema: 계약(Contract) 언어
JSON Schema는 모델이나 검증 계층에 "출력이 어떤 모양이어야 하는가"를 알려주는 방법입니다. 모든 주요 구조화 출력 시스템이 이를 공통 언어로 사용합니다.
{
"type": "object",
"properties": {
"product": { "type": "string" },
"price": { "type": "number", "minimum": 0 },
"in_stock": { "type": "boolean" },
"categories": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["product", "price", "in_stock"]
}
이 스키마는 출력이 객체여야 하며, 문자열 product, 0 이상의 숫자 price, 불리언 in_stock, 그리고 선택적인 문자열 배열 categories를 가져야 한다고 명시합니다. 이 형태와 맞지 않는 출력은 거부됩니다.
스키마는 까다로운 경우를 다음과 같이 풀어 줍니다. 중첩 객체(Nested Object), 타입이 있는 항목으로 구성된 배열, 열거형(Enum, 문자열을 특정 값 집합으로 제한), 패턴 매칭(문자열에 대한 정규식), 조합자(oneOf, anyOf, allOf를 통한 다형 출력)가 그 예입니다.
Pydantic 패턴
Python에서는 JSON Schema를 손으로 작성하지 않습니다. 보통은 Pydantic 모델을 정의해 두면 스키마를 자동으로 생성해 줍니다.
from pydantic import BaseModel
class Product(BaseModel):
product: str
price: float
in_stock: bool
categories: list[str] = []
이 코드는 위와 같은 JSON Schema를 생성합니다. Instructor 라이브러리와 OpenAI SDK는 Pydantic 모델을 그대로 받습니다. 모델 클래스를 넘기면 검증된 인스턴스를 돌려받는 식입니다. LLM 출력이 스키마와 맞지 않으면 Instructor가 자동으로 재시도합니다.
같은 문제를 푸는 또 다른 인터페이스입니다. 모델이 JSON을 직접 생성하게 하는 대신, 타입이 있는 파라미터를 가진 도구(Tool) 또는 함수(Function)를 미리 정의해 둡니다. 그러면 모델은 구조화된 인수를 가진 함수 호출을 출력합니다. OpenAI는 이를 함수 호출(Function Calling)이라고 부르고, Anthropic은 도구 사용(Tool Use)이라고 부릅니다. 이름은 다르지만 결과는 같습니다. 결국 우리가 얻는 것은 구조화된 데이터입니다.
graph TD
subgraph ToolUse["도구 사용(Tool Use) 흐름"]
U["사용자: 이 리뷰 텍스트에서\n상품 정보를 추출"] --> M["모델이 입력을 처리"]
M --> TC["도구 호출(Tool Call):\nextract_product(\n product='Sony WH-1000XM5',\n price=348.00,\n in_stock=true\n)"]
TC --> V["함수 스키마에 대해\n검증"]
V --> R["구조화 결과:\n{product, price, in_stock}"]
end
style U fill:#1a1a2e,stroke:#0f3460,color:#fff
style TC fill:#1a1a2e,stroke:#e94560,color:#fff
style V fill:#1a1a2e,stroke:#ffa500,color:#fff
style R fill:#1a1a2e,stroke:#51cf66,color:#fff
도구 사용은 모델이 파라미터만 채우면 되는 상황이 아니라, "여러 함수 중 어떤 것을 호출해야 하는지"까지 골라야 할 때 특히 유용합니다. 서로 다른 추출 스키마가 10개 있고 입력에 따라 모델이 올바른 것을 골라야 한다면, 도구 사용은 스키마 선택과 구조화 출력을 한 번에 제공합니다.
흔한 실패 모드
스키마 강제가 있어도 구조화 출력은 미묘한 형태로 실패할 수 있습니다.
환각된 값(Hallucinated Values): 출력은 스키마와 맞지만 데이터 자체가 지어낸 값인 경우입니다. 텍스트에는 $348라고 되어 있는데 모델이 {"price": 299.99}를 만듭니다. 스키마 검증은 이를 잡지 못합니다. 타입은 맞고 값이 틀렸기 때문입니다.
열거형 혼동(Enum Confusion): 필드를 ["in_stock", "out_of_stock", "preorder"]로 제한해 두었습니다. 그런데 모델이 "available"이라고 출력해 버립니다. 의미적으로는 맞지만 허용 집합에는 없는 값입니다. 잘 만든 제약 디코딩은 이런 경우를 막아 줍니다. 프롬프트 기반 접근으로는 막을 수 없습니다.
중첩 객체 깊이(Nested Object Depth): 4단계 이상으로 깊게 중첩된 스키마일수록 더 많은 오류가 발생합니다. 중첩이 한 단계씩 깊어질 때마다 모델이 구조를 놓칠 수 있는 지점이 한 군데씩 늘어납니다.
배열 길이(Array Length): 모델이 배열 항목을 너무 많이 또는 너무 적게 만들어 낼 수 있습니다. 스키마는 minItems와 maxItems를 지원하지만, 모든 제공자가 이를 디코딩 수준에서 강제하지는 않습니다.
선택 필드 누락(Optional Field Omission): 기술적으로는 선택(Optional) 사항이지만, 실제 사용 사례에서는 중요한 필드를 모델이 생략해 버리는 경우입니다. 데이터가 가끔 비어 있을 수 있더라도 그 필드를 필수(Required)로 두고, 모델이 null을 명시적으로 만들어 내도록 강제하는 편이 좋습니다.
직접 만들기
Step 1: JSON Schema 검증기
Python 객체가 JSON Schema와 일치하는지 검사하는 검증기를 처음부터 만들어 봅니다. 이 코드는 출력 쪽에서 스키마 준수 여부를 확인할 때 실제로 돌아가는 로직에 해당합니다.
import json
def validate_schema(data, schema):
errors = []
_validate(data, schema, "", errors)
return errors
def _validate(data, schema, path, errors):
schema_type = schema.get("type")
if schema_type == "object":
if not isinstance(data, dict):
errors.append(f"{path}: object가 필요하지만 {type(data).__name__}입니다")
return
for key in schema.get("required", []):
if key not in data:
errors.append(f"{path}.{key}: 필수 필드가 없습니다")
properties = schema.get("properties", {})
for key, value in data.items():
if key in properties:
_validate(value, properties[key], f"{path}.{key}", errors)
elif schema_type == "array":
if not isinstance(data, list):
errors.append(f"{path}: array가 필요하지만 {type(data).__name__}입니다")
return
min_items = schema.get("minItems", 0)
max_items = schema.get("maxItems", float("inf"))
if len(data) < min_items:
errors.append(f"{path}: array 항목 수 {len(data)}가 최소 {min_items}보다 작습니다")
if len(data) > max_items:
errors.append(f"{path}: array 항목 수 {len(data)}가 최대 {max_items}보다 큽니다")
items_schema = schema.get("items", {})
for i, item in enumerate(data):
_validate(item, items_schema, f"{path}[{i}]", errors)
elif schema_type == "string":
if not isinstance(data, str):
errors.append(f"{path}: string이 필요하지만 {type(data).__name__}입니다")
return
enum_values = schema.get("enum")
if enum_values and data not in enum_values:
errors.append(f"{path}: '{data}'는 허용 값 {enum_values}에 없습니다")
elif schema_type == "number":
if not isinstance(data, (int, float)):
errors.append(f"{path}: number가 필요하지만 {type(data).__name__}입니다")
return
minimum = schema.get("minimum")
maximum = schema.get("maximum")
if minimum is not None and data < minimum:
errors.append(f"{path}: {data}는 minimum {minimum}보다 작습니다")
if maximum is not None and data > maximum:
errors.append(f"{path}: {data}는 maximum {maximum}보다 큽니다")
elif schema_type == "boolean":
if not isinstance(data, bool):
errors.append(f"{path}: boolean이 필요하지만 {type(data).__name__}입니다")
elif schema_type == "integer":
if not isinstance(data, int) or isinstance(data, bool):
errors.append(f"{path}: integer가 필요하지만 {type(data).__name__}입니다")
Step 2: Pydantic 스타일 모델에서 스키마 생성
이번에는 최소한의 클래스-스키마(class-to-schema) 변환기를 만들어 봅니다. Python 클래스를 정의하면 그에 맞는 JSON Schema가 자동으로 생성되도록 하는 것이 목표입니다.
class SchemaField:
def __init__(self, field_type, required=True, default=None, enum=None, minimum=None, maximum=None):
self.field_type = field_type
self.required = required
self.default = default
self.enum = enum
self.minimum = minimum
self.maximum = maximum
def python_type_to_schema(field):
type_map = {
str: "string",
int: "integer",
float: "number",
bool: "boolean",
}
schema = {}
if field.field_type in type_map:
schema["type"] = type_map[field.field_type]
elif field.field_type == list:
schema["type"] = "array"
schema["items"] = {"type": "string"}
elif isinstance(field.field_type, dict):
schema = field.field_type
if field.enum:
schema["enum"] = field.enum
if field.minimum is not None:
schema["minimum"] = field.minimum
if field.maximum is not None:
schema["maximum"] = field.maximum
return schema
def model_to_schema(name, fields):
properties = {}
required = []
for field_name, field in fields.items():
properties[field_name] = python_type_to_schema(field)
if field.required:
required.append(field_name)
return {
"type": "object",
"properties": properties,
"required": required,
}
Step 3: 제약 토큰 필터
이제 제약 디코딩을 시뮬레이션해 봅니다. 부분 JSON 문자열과 스키마가 주어졌을 때 현재 위치에서 어떤 토큰 범주가 유효한지 판단하는 함수를 작성합니다.
def next_valid_tokens(partial_json, schema):
stripped = partial_json.strip()
if not stripped:
return ["{"]
try:
json.loads(stripped)
return ["<EOS>"]
except json.JSONDecodeError:
pass
last_char = stripped[-1] if stripped else ""
if last_char == "{":
return ['"', "}"]
elif last_char == '"':
if stripped.endswith('":'):
return ['"', "0-9", "true", "false", "null", "[", "{"]
return ["a-z", '"']
elif last_char == ":":
return [" ", '"', "0-9", "true", "false", "null", "[", "{"]
elif last_char == ",":
return [" ", '"', "{", "["]
elif last_char in "0123456789":
return ["0-9", ".", ",", "}", "]"]
elif last_char == "}":
return [",", "}", "]", "<EOS>"]
elif last_char == "]":
return [",", "}", "<EOS>"]
elif last_char == "[":
return ['"', "0-9", "true", "false", "null", "{", "[", "]"]
else:
return ["any"]
def demonstrate_constrained_decoding():
partial_states = [
'',
'{',
'{"product"',
'{"product":',
'{"product": "Sony"',
'{"product": "Sony",',
'{"product": "Sony", "price":',
'{"product": "Sony", "price": 348',
'{"product": "Sony", "price": 348}',
]
print(f"{'부분 JSON':<45} {'다음 유효 토큰'}")
print("-" * 80)
for state in partial_states:
valid = next_valid_tokens(state, {})
display = state if state else "(빈 문자열)"
print(f"{display:<45} {valid}")
Step 4: 추출 파이프라인
마지막으로 지금까지 만든 조각들을 모아 추출 파이프라인을 구성합니다. 스키마를 정의하고, LLM이 구조화 출력을 생성한다고 가정해 시뮬레이션하고, 출력을 검증하고, 실패 시 재시도까지 처리합니다.
def simulate_llm_extraction(text, schema, attempt=0):
if "headphones" in text.lower() or "sony" in text.lower():
if attempt == 0:
return '{"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true, "categories": ["audio", "headphones"]}'
return '{"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true}'
if "laptop" in text.lower():
return '{"product": "MacBook Pro 16", "price": 2499.00, "in_stock": false, "categories": ["computers"]}'
return '{"product": "Unknown", "price": 0, "in_stock": false}'
def extract_with_retry(text, schema, max_retries=3):
for attempt in range(max_retries):
raw = simulate_llm_extraction(text, schema, attempt)
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f" 시도 {attempt + 1}: JSON 파싱 오류 -- {e}")
continue
errors = validate_schema(data, schema)
if not errors:
return data
print(f" 시도 {attempt + 1}: 스키마 검증 오류 -- {errors}")
return None
product_schema = {
"type": "object",
"properties": {
"product": {"type": "string"},
"price": {"type": "number", "minimum": 0},
"in_stock": {"type": "boolean"},
"categories": {"type": "array", "items": {"type": "string"}},
},
"required": ["product", "price", "in_stock"],
}
Step 5: 전체 파이프라인 실행
def run_demo():
print("=" * 60)
print(" 구조화 출력 파이프라인 데모")
print("=" * 60)
print("\n--- 스키마 정의 ---")
product_fields = {
"product": SchemaField(str),
"price": SchemaField(float, minimum=0),
"in_stock": SchemaField(bool),
"categories": SchemaField(list, required=False),
}
generated_schema = model_to_schema("Product", product_fields)
print(json.dumps(generated_schema, indent=2))
print("\n--- 스키마 검증 ---")
test_cases = [
({"product": "Test", "price": 10.0, "in_stock": True}, "유효한 객체"),
({"product": "Test", "price": -5.0, "in_stock": True}, "음수 가격"),
({"product": "Test", "in_stock": True}, "가격 누락"),
({"product": "Test", "price": "ten", "in_stock": True}, "가격이 문자열"),
("not an object", "객체 대신 문자열"),
]
for data, label in test_cases:
errors = validate_schema(data, product_schema)
status = "통과" if not errors else f"실패: {errors}"
print(f" {label}: {status}")
print("\n--- 제약 디코딩 시뮬레이션 ---")
demonstrate_constrained_decoding()
print("\n--- 추출 파이프라인 ---")
texts = [
"The Sony WH-1000XM5 headphones are priced at $348 and currently available.",
"The new MacBook Pro 16-inch laptop costs $2499 but is sold out.",
"This is a random sentence with no product info.",
]
for text in texts:
print(f"\n 입력: {text[:60]}...")
result = extract_with_retry(text, product_schema)
if result:
print(f" 출력: {json.dumps(result)}")
else:
print(f" 출력: 재시도 후 실패")
사용해보기
OpenAI 구조화 출력
OpenAI의 구조화 출력 모드는 내부적으로 제약 디코딩을 사용합니다. 모델이 생성하는 모든 토큰은 Pydantic 스키마와 일치하는 출력을 만들도록 보장됩니다. 별도의 재시도도, 추가 검증도 필요하지 않습니다. 제약 자체가 디코딩 과정 안에 녹아 있는 셈입니다.
Anthropic은 도구 사용(Tool Use)을 통해 구조화 출력을 만들어 냅니다. 모델은 input_schema와 일치하는 구조화 인수를 가진 도구 호출(Tool Call)을 생성합니다. 결과는 같지만 API 표면(Surface)이 다른 셈입니다.
Instructor 라이브러리
Instructor는 어떤 LLM 클라이언트(Client)든 감싸서 검증된 Pydantic 인스턴스를 반환하도록 만들어 줍니다. 첫 시도가 검증에 실패하면 오류를 컨텍스트로 다시 모델에 보내 출력을 고치게 합니다. OpenAI뿐 아니라 다른 어떤 제공자에서도 동일한 방식으로 작동합니다.
산출물 만들기
이 lesson은 outputs/prompt-structured-extractor.md를 만들어 냅니다. 이는 스키마 정의가 주어졌을 때 임의의 텍스트에서 구조화 데이터를 추출하는, 재사용 가능한 프롬프트 템플릿입니다. JSON Schema와 비구조 텍스트를 넣으면 검증된 JSON을 반환합니다.
또한 outputs/skill-structured-outputs.md도 함께 만듭니다. 이는 제공자, 신뢰성 요구사항, 스키마 복잡도에 따라 올바른 구조화 출력 전략을 고를 수 있도록 도와주는 의사결정 프레임워크입니다.
연습문제
-
(쉬움) 스키마 검증기를 확장해 oneOf를 지원하도록 만드세요. 데이터는 여러 스키마 중 정확히 하나와 일치해야 합니다. 이는 다형(Polymorphic) 출력, 예를 들어 서로 다른 모양을 가진 Product 또는 Service 객체를 처리할 때 필요합니다.
-
(중간) 두 스키마를 비교해 호환성을 깨는 변경(Breaking Change; 제거된 필수 필드, 변경된 타입)과 호환성을 유지하는 변경(Non-Breaking Change; 추가된 선택 필드, 완화된 제약)을 식별하는 "스키마 디프(Schema Diff)" 도구를 만드세요. 프로덕션에서 추출 스키마를 버전 관리할 때 꼭 필요합니다.
-
(중간) 좀 더 현실적인 제약 디코딩 시뮬레이터를 구현하세요. JSON Schema와 100개 토큰으로 이루어진 어휘(문자, 숫자, 구두점, 키워드)가 주어지면 생성 과정을 단계별로 진행하면서 각 위치의 잘못된 토큰을 마스킹합니다. 각 단계에서 전체 어휘 중 몇 퍼센트가 유효한지 측정해 보세요.
-
(어려움) 추출 평가 스위트를 만드세요. 손으로 라벨링한 JSON 출력이 함께 있는 상품 설명 50개를 준비합니다. 추출 파이프라인을 50개 모두에 실행하고 완전 일치(Exact Match), 필드 단위 정확도(Field-Level Accuracy), 타입 준수(Type Compliance)를 측정하세요. 그리고 어떤 필드가 가장 추출하기 어려운지 찾아냅니다.
-
(어려움) 추출 파이프라인에 신뢰도 점수(Confidence Scores)를 추가하세요. 각 추출 필드에 대해 모델이 얼마나 확신하고 있는지 추정합니다. 토큰 확률을 활용하거나, 같은 입력으로 추출을 3회 반복해 일관성을 측정하는 방식으로 구할 수 있습니다. 신뢰도가 낮은 필드는 사람이 검토하도록 넘깁니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| JSON 모드(JSON Mode) | "JSON을 반환한다" | 문법적으로 유효한 JSON 출력을 보장하는 API 플래그(flag). 특정 스키마는 강제하지 않습니다. |
| 구조화 출력(Structured Output) | "타입이 있는 JSON" | 특정 JSON Schema와 일치하며 올바른 키, 타입, 제약을 가진 출력 |
| 제약 디코딩(Constrained Decoding) | "가이드된 생성" | 각 토큰 위치에서 잘못된 출력으로 이어질 토큰을 마스킹해 100% 스키마 준수를 보장하는 방식 |
| JSON Schema | "JSON 템플릿" | JSON 데이터의 구조, 타입, 제약을 선언적으로 기술하는 언어. OpenAPI, JSON Forms 등에서 사용됩니다. |
| Pydantic | "Python dataclasses+" | 타입 검증이 있는 데이터 모델을 정의하는 Python 라이브러리. FastAPI와 Instructor가 JSON Schema 생성에 사용합니다. |
| 함수 호출(Function Calling) | "도구 사용(Tool Use)과 같은 것" | 자유 텍스트 대신 이름과 타입이 있는 인수를 가진 구조화 함수 호출을 LLM이 출력하는 방식. OpenAI와 Anthropic 모두 지원합니다. |
| Instructor | "LLM용 Pydantic" | LLM 클라이언트(Client)를 감싸 검증된 Pydantic 인스턴스를 반환하고, 검증 실패 시 자동 재시도하는 Python 라이브러리 |
| 토큰 마스킹(Token Masking) | "어휘 필터링" | 생성 중 특정 토큰의 확률을 0으로 만들어 모델이 그 토큰을 만들 수 없게 하는 방식 |
| 스키마 준수(Schema Compliance) | "모양이 맞다" | 출력에 필수(Required) 필드가 모두 있고, 타입이 정확하며, 값이 제약 안에 있고, 허용되지 않은 추가 필드가 없는 상태 |
| 재시도 루프(Retry Loop) | "될 때까지 다시 시도" | 검증 오류를 모델에 다시 보내 출력을 고치게 하는 방식. Instructor가 설정 가능한 최대 횟수까지 자동으로 수행합니다. |
더 읽을거리