구조화 출력 강제하기
모델 응답을 파싱 가능한 형태(대개 JSON)로 받아내야 할 때. 어떤 방법을 고를지, 스키마에서 뭐가 막히는지, 파싱이 깨질 때 뭘 하는지.
무엇을 언제 쓰나
| 방법 | 강제 수준 | 쓸 때 | 안 쓸 때 |
|---|---|---|---|
JSON outputs (output_config.format) |
하드 (constrained decoding) | 응답 전체가 스키마여야 함. 데이터 추출, 최종 JSON 응답 | 자유 산문이 섞여야 함 |
Strict tool use (strict: true) |
하드 (constrained decoding) | 툴 호출의 파라미터를 정확히 강제. 신뢰성 있는 tool call | 응답 자체를 구조화하고 싶음 → JSON outputs |
XML 태그 (<result>...) |
소프트 (스티어링만) | 약한 구조 + 자유 산문 혼합, 여러 필드 구분 | 기계 파싱 신뢰성 필요 → 위 둘 |
- 세 방법은 배타적이 아니다. agentic 워크플로우에서 JSON outputs + strict tool use 를 한 요청에 병용할 수 있다 (verified 2026-07): "파라미터가 보장된 툴 호출"과 "구조화된 최종 응답"을 동시에.
- XML 태그는 constrained decoding 이 아니다. 모델이 태그를 빠뜨릴 수 있으니 파서는 관용적으로 짜라.
- 예전에 assistant prefill(
{"role":"assistant","content":"{"})로 JSON 을 강제하던 패턴은 현행 Claude 에서 400 에러다 (§7). structured outputs / tool calling / XML 로 대체하라.
Anthropic — JSON outputs
응답 전체를 스키마에 맞춘다. 동작 원리는 grammar-constrained sampling(constrained decoding): 샘플링 시점에 문법으로 제약되어 스키마 위반이 원천 불가능하다. JSON.parse 에러가 나지 않고 재시도가 필요 없다.
from anthropic import Anthropic
client = Anthropic()
schema = {"type": "object",
"properties": {"name": {"type": "string"},
"priority": {"type": "string", "enum": ["low", "high"]}},
"required": ["name", "priority"],
"additionalProperties": False} # 모든 object 에 필수
resp = client.messages.create(model="claude-opus-5", max_tokens=1024,
output_config={"format": {"type": "json_schema", "schema": schema}},
messages=[{"role": "user", "content": "회의록에서 액션아이템 뽑아줘: ..."}])
- 모델 지원: Claude 4.5 and later + Mythos Preview (verified 2026-07). Vertex AI 가 지원 폭이 가장 넓다.
- 구버전 top-level
output_format파라미터와 베타 헤더structured-outputs-2025-11-13은output_config.format로 이동했다. 구 문법도 전환기 동안은 동작하지만 새 코드는output_config를 써라.
Pydantic / Zod 연동
파싱까지 SDK 가 해준다.
from pydantic import BaseModel
class ActionItem(BaseModel):
name: str
priority: str
resp = client.messages.parse(model="claude-opus-5", max_tokens=1024,
output_format=ActionItem, messages=[...])
item = resp.parsed_output # ActionItem 인스턴스
TypeScript 는 zodOutputFormat(schema) 를 output_config.format 에 넣고 resp.parsed_output 을 받는다.
Anthropic — 스키마 제약 (400 유발 지점)
모든 object 에 additionalProperties: false 와 required 가 반드시 있어야 한다. 아래 미지원 기능을 넣으면 400 이다.
| 지원 | 미지원 (400) | |
|---|---|---|
| 타입 | object/array/string/integer/number/boolean/null | — |
| enum | 스칼라(문자열·숫자·bool·null) | enum 안 복합 타입 |
| 조합 | anyOf, allOf, const |
allOf + $ref 조합 |
| 참조 | 내부 $ref/$def/definitions |
external $ref(http://…) |
| 재귀 | — | 재귀 스키마 전면 미지원 |
| 수치 제약 | — | minimum/maximum/multipleOf |
| 문자열 제약 | string formats(date, email, uri, uuid …) | minLength/maxLength |
| 배열 제약 | minItems 0 또는 1 |
그 외 minItems 값, maxItems |
- 재귀 스키마가 필요하면 (트리·중첩 댓글 등) Anthropic JSON outputs 로는 못 한다. 깊이를 고정한 평탄화 스키마로 바꾸거나 XML/후처리로 우회하라.
- 값 범위·길이·정규식은 스키마가 강제하지 못한다 → 앱단에서 검증하라 (Pydantic validator 등).
Anthropic — Strict tool use
툴의 이름과 입력 파라미터를 스키마에 강제한다. strict 없이는 모델이 enum 자리에 임의 문자열을, 정수 자리에 "2" 를 넣는 등 타입 불일치가 생길 수 있다.
tools=[{"name": "create_ticket", "description": "Create a support ticket.",
"strict": True, # top-level 필드 (tool_choice 아님)
"input_schema": {"type": "object",
"properties": {"title": {"type": "string"},
"severity": {"type": "string", "enum": ["p1", "p2"]}},
"required": ["title", "severity"], "additionalProperties": False}}]
스키마 제약은 JSON outputs 와 동일하다.
함정 (gotcha)
- max_tokens 절단: constrained decoding 은 "유효한 JSON"을 보장하지만 "완결된 JSON"을 보장하지 않는다.
max_tokens에 걸리면 중간에서 잘려 파싱 불가다. 응답의stop_reason == "max_tokens"을 반드시 가드하고, 넉넉히 잡거나 스트리밍하라 (§7: 비스트리밍 ~16000, 128K 출력은 스트리밍 필수). - 거절(refusal):
stop_reason == "refusal"이면 스키마에 맞는 본문이 안 온다. 이때만stop_details가 채워지고 다른 경우엔null이니 접근 전에 가드하라. - 문법 컴파일 지연 & 24시간 캐시: 첫 요청은 문법 컴파일로 지연이 붙는다. 컴파일된 문법은 마지막 사용 기준 약 24시간 캐시된다 (재확인 필요 — 정확 TTL 미검증). 캐시는 스키마 구조나 (병용 시) 툴 셋을 바꾸면 무효화되지만, 툴/필드의 name·description 만 바꾸면 유지된다.
- prompt cache 무효화:
output_config.format을 바꾸면 그 스레드의 prompt cache 가 깨진다. 형식을 자주 토글하지 마라. → prompt-caching - format-via-example 안티패턴: "이렇게 생긴 JSON 줘" 하고 예시만 던지면 구조가 다른 입력에서 파서가 깨진다. 예시 대신 스키마를 명시하고 structured outputs 를 써라.
다른 프로바이더 (비교용)
아래 OpenAI 값은 aggregator 출처 [MED]. openai.com 공식 페이지가 확인 안 됨.
- OpenAI:
response_format에{type:"json_schema", ..., strict:true}. 모든 필드가 required (optional 은anyOf에null유니온으로 우회), 모든 object 에additionalProperties:false, 중첩 최대 5단계 (재확인 필요), root 를anyOf로 두는 것 불가.pattern·길이·수치·format은 강제 안 함. - Anthropic 은 재귀를 전면 미지원인데 OpenAI 는 제한적 재귀(CFG)가 된다는 서술이 있으나 엇갈려서 미확정 (재확인 필요). 상세 대조는 openai-api-parity.
참고
- prompting-cheatsheet — 오버트리거 완화, 긍정 지시 등 프롬프팅 기본기
- anti-patterns — few-shot pollution, instruction stacking 등 실패 패턴 카탈로그
- anthropic-messages-api —
output_config,stop_reason, 스트리밍 상세 - set-up-tool-use — 툴 정의와 tool call 루프
- prompt-caching — format 변경이 캐시를 깨는 이유
출처: Anthropic 사실은 first-party [HIGH]·규격 §7 기준. OpenAI 비교는 aggregator [MED]. 원본 → 2026-07-25-structured-output-antipatterns