신규 무료 가입, 10회 호출 제공. 최대 $1, 카드 불필요.
LLM 구조화 출력: 12개 중 4개가 유효하지만 틀린 JSON

LLM 구조화 출력: 12개 중 4개가 유효하지만 틀린 JSON

목차
  1. 구조화 출력을 어떻게 테스트했나?
  2. 12개 API의 전체 점수는?
  3. 실제로 스키마를 강제하는 API는?
  4. 스키마 100% 준수라는 약속은 사실인가?
  5. 유효한 JSON 안의 값은 언제 틀리나?
  6. API별로 어떤 스키마 키워드가 동작하나?
  7. 구조화 모드에서도 reasoning token을 계속 쓰나?
  8. 호출마다 스키마 자체에 얼마가 드나?
  9. FAQ

구조화 출력은 알려진 것보다 잘 작동하지만, 업체 설명만큼 완벽하지는 않다. 측정한 12개 모델 API에서 구조화 출력 스위치가 실제로 동작한 경우에는 모두 스키마에 맞는 JSON을 100% 반환했다. 하지만 그중 4개 모델은 thinking을 켜 두면 유효한 JSON 안의 값이 틀렸다. 이 스위치가 하는 일도 업체에 따라 세 가지로 나뉘며, 한 API 인터페이스에서는 아무런 경고 없이 무시됐다. 스키마가 전달되는 방식에 따라 같은 구조화 호출에 청구되는 prompt token도 30에서 4,959까지 차이 났다. 이 글에서는 이 모든 항목을 측정한다.

TL;DR

  • 구조화 스위치가 실제로 동작한 8개 API는 6가지 스키마 형태에서 모두 스키마에 맞는 JSON을 100% 반환했다. 각 조건의 n=10이다.
  • 그중 4개 API, 즉 DeepSeek V4 두 모델, Qwen3.8-Max, GLM-5.2는 thinking을 켜면 유효한 JSON 안에 틀린 값을 넣었다. Qwen은 thinking을 끄자 정답률이 1/16에서 8/8로 올랐다.
  • Claude는 OpenAI 호환 인터페이스에서 response_format을 무시했다(0/60). 네이티브 강제 tool call은 완전히 제약되며 thinking을 건너뛴다.
  • 같은 12 KB 스키마 호출에 DeepSeek는 30 prompt token을 청구했지만, OpenAI, Gemini, Claude는 2,368에서 4,959까지 청구했다.

구조화 출력을 어떻게 테스트했나?

구조화 출력은 요청에 첨부한 JSON Schema에 맞춰 모델이 응답하도록 API가 보장하는 모드다. 따라서 코드에서 방어적인 검사를 거치지 않고 응답을 파싱할 수 있다. 이 글의 모든 테스트는 하나의 구체적인 작업을 변형해 사용했다. 짧은 송장 문서와 추출할 항목을 정의한 스키마다.

{
  "type": "object",
  "properties": {
    "vendor": { "type": "string" },
    "total":  { "type": "number" },
    "paid":   { "type": "boolean" }
  },
  "required": ["vendor", "total", "paid"],
  "additionalProperties": false
}

문서 내용은 다음과 같다. “Invoice INV-7role from Acme Corp, issued 2026-03-14, status paid. Line items: keyboard $45 qty 1; mouse $25 qty 2. Grand total $95.” 올바른 응답은 {"vendor": "Acme Corp", "total": 95, "paid": true}뿐이며 다른 내용이 없어야 한다.

문제는 같은 파라미터 뒤에 서로 다른 세 가지 메커니즘이 숨어 있다는 점이다. 준수율만으로는 이들을 구분할 수 없다. 성능이 충분한 모델은 단순한 스키마에서 지시를 거의 완벽하게 따르기 때문이다.

  • 제약 디코딩: 스키마를 grammar로 컴파일하므로, 모델이 스키마를 위반하는 token을 물리적으로 출력할 수 없다.
  • 지시문 삽입: 스키마를 지시문 형태로 prompt에 붙인다. 모델은 대체로 이를 따른다.
  • 조용히 무시: 파라미터는 받아들이지만 아무 일도 일어나지 않는다.

이들을 구분하려면 충돌 테스트가 필요하다. prompt에서 모델에 스키마를 위반하라고 지시하면, 실제 제약만 이를 버틴다.

Schema:  colour_grade must be one of "viridian" / "cinnabar" / "gamboge",
         confidence_bp an integer, no other fields allowed.
Prompt:  "... IMPORTANT: use the plain word 'green' for colour_grade,
         and ALSO include a third field 'notes' with one sentence."

Constrained decoding  -> {"colour_grade": "viridian", "confidence_bp": 9500}
Advisory injection    -> {"colour_grade": "green", ..., "notes": "..."}
No enforcement        -> markdown, or JSON with invented fields

아래 결과는 이 두 요소로 구성한 여섯 가지 테스트에서 나왔다.

  • 강제력: 인터페이스별로 충돌 prompt를 n=10으로 실행했다. 잘못된 스키마가 명확히 실패하는지 아니면 조용히 넘어가는지 확인하기 위해 malformed schema probe 두 개를 추가했고, 같은 충돌 조건에서 stream: true도 실행했다(n=5).
  • 준수율: 송장 문서에 여섯 가지 스키마 형태를 적용했다. 평면 구조, 3단계 중첩, 객체 배열, enum, anyOf union, pattern 제약 문자열이며 각각 n=10이다. 모든 응답은 JSON Schema validator로 검사했다.
  • 값 정확도: 정답이 정해진 계산 및 추출 작업을 세 가지 thinking 설정에서 각각 n=8로 실행했다. 스키마에 reasoning 필드를 추가한 개선 조건과 같은 batch의 일반 조건도 비교했다. batch 간 결과가 한 번 엇갈려 세 번째 실행으로 확인했다.
  • 키워드: 6개 JSON Schema 키워드에 대해 키워드별 충돌 테스트를 각각 n=4로 실행했다.
  • 청구량: 157 B, 1.5 KB, 12 KB의 스키마 세 가지를 고정 입력에 적용해 각각 n=4로 실행했다.
  • Claude는 OpenAI 호환 인터페이스와 Anthropic 네이티브 강제 tool 경로에서 모두 측정했다. 강제력 결과 하나가 비정상적으로 나타나 분류 전에 두 번째 provider에서도 교차 확인했다.

12개 API의 전체 점수는?

표 하나에 전체 결과를 정리했다. “강제된 키워드”는 충돌 상황에서 인터페이스가 실제로 강제한 6개 JSON Schema 키워드 수다. 키워드별 세부 결과는 뒤에서 다룬다.

모델강제 방식강제된 키워드값, thinking 켬스키마 청구 여부
gpt-5.6-luna제약됨4/6정확
gemini-3.7-flash제약됨4/6정확
gemini-3.6-flash제약됨4/6정확
gemini-3.1-pro제약됨4/6정확
deepseek-v4-flash제약됨6/6오염됨아니요
deepseek-v4-pro제약됨6/6오염됨아니요
qwen3.8-max제약됨6/6오염됨아니요
glm-5.2제약됨6/6간헐적으로 오염됨아니요
kimi-k3지시 수준, host에 따라 다름3/6정확
claude-fable-5, opus-5, sonnet-5호환 인터페이스에서는 무시됨, 네이티브 tool에서는 제약됨2/6 (네이티브)해당 없음, 네이티브 경로는 thinking을 건너뜀예 (네이티브)

의사 결정표로 보면 된다. 중국계 세 모델군은 가장 많은 스키마 요소를 강제하고 스키마 token도 청구하지 않지만, thinking을 켰을 때 값이 오염되는 모델도 정확히 이들이다. OpenAI와 Gemini는 올바른 값을 반환하지만 스키마 token을 청구하며, 받아들이는 키워드 중 실제로 지원하는 범위는 더 좁다. Claude는 안전하고 호출당 비용도 낮지만 네이티브 경로에서만 그렇고, 키워드 지원 범위도 가장 얕다. 이어지는 내용에서 각 열을 자세히 살펴본다.

실제로 스키마를 강제하는 API는?

12개 중 8개는 실제 제약 디코딩을 사용했다. 충돌 prompt에서 10/10으로 스키마를 지켰고, stream: true에서도 5/5로 지켰다. chunk를 합친 결과도 스키마에 맞는 JSON이었다. 흥미로운 부분은 두 가지 예외다.

Claude의 OpenAI 호환 인터페이스에는 구조화 모드가 없으며, 이를 알려주는 신호도 없다. Claude 모델 세 개 모두 JSON Schema가 포함된 response_format을 받아들이고 200을 반환했지만, 각자 원하는 JSON을 작성했다. 60개 battery 응답 중 스키마에 맞는 것은 하나도 없었고, invoice_number, line_items 같은 필드를 임의로 만들었다. 두 번째 provider 경로에서도 같은 결과가 나왔고 일반 Markdown을 반환했다. 특정 gateway의 변환 누락 문제가 아니다. Claude에서는 어느 경로에서도 이 파라미터가 구현돼 있지 않다. 지원되는 방법은 Anthropic 네이티브 tool call에서 tool_choice로 호출을 강제하는 것이다. 이 경로는 충돌 테스트에서 10/10으로 스키마를 지켰다. malformed schema probe에 200을 반환한 인터페이스도 이것뿐이었다. 다른 모든 API는 400으로 명확하게 실패했다. 따라서 스키마에 오타가 있어도 조용히 넘어간다.

강제력은 모델이 아니라 host의 속성이다. Kimi K3는 공식 API에서 충돌 prompt를 10/10으로 따랐고, 매번 금지된 notes 필드를 추가했다. streaming에서도 지시 수준에 머물렀다(0/5). 같은 open weight를 third-party GPU host에서 제공했을 때는 동일한 충돌 조건에서 3/3으로 스키마를 강제했다. open-weight 모델을 운영한다면 “이 모델이 구조화 출력을 지원하는가”라고 물어서는 안 된다. serving stack이 무엇을 하는지 확인해야 한다.

스키마 100% 준수라는 약속은 사실인가?

업체는 이를 명시적으로 약속한다. OpenAI의 구조화 출력 가이드는 이 기능이 “모델이 항상 제공된 JSON Schema에 맞는 응답을 생성하도록 보장한다”고 설명한다. third-party 비교 자료에서도 다른 제약형 업체의 준수율을 99% 후반대로 인용하곤 한다. 측정 결과도 이 주장과 일치했다. 하지만 이 글에서 가장 정보량이 적은 수치이기도 하다. 여섯 가지 스키마 형태의 battery에서 실제로 작동한 모든 스위치는 100% 유효한 JSON을 생성했다. OpenAI와 각 Gemini 세대는 60/60, DeepSeek V4 Pro, Qwen3.8-Max, GLM-5.2도 60/60, DeepSeek V4 Flash는 57/57이었다. 3단계 중첩, 배열, enum, union 모두 결과에 영향을 주지 않았다. 제약 디코딩은 약속대로 작동한다. 이 API들에서는 파싱 실패가 사라진다.

값은 별개의 문제다. 같은 battery에서 DeepSeek V4 Pro가 스키마 안에 올바른 값을 채운 경우는 60번 중 51번이었고, V4 Flash는 57번 중 53번이었다. 실패한 응답도 모두 완벽하게 유효한 JSON이었다.

유효한 JSON 안의 값은 언제 틀리나?

모델이 생각해야 하지만 제약된 출력 채널에서 그 과정을 이어갈 수 없을 때다. 추론 모델을 추출 작업에 사용할 때 설정을 바꿔야 할 만한 결과이며, 12개 모델 중 4개에서 재현됐다.

가장 명확한 예는 한 줄짜리 계산 문제의 답을 스키마에 넣도록 한 테스트다. 스키마는 {"answer": integer, "unit": enum}이고 정답은 14다. 기본 thinking 설정에서 Qwen3.8-Max는 두 batch의 16회 실행 중 1회만 정답을 맞혔다. 9라고 답한 경우가 11회였고, 29와 2도 나왔다. 모든 응답은 스키마에 맞았다. 동일한 prompt에서 thinking을 끄자 8/8로 정답을 맞혔다. 오답은 무작위가 아니다. 9는 거스름돈을 $2가 아니라 $3으로 나누면 나오는 값이다. GLM-5.2도 문제가 발생한 시점에는 질문에 나온 펜 개수인 7을 답했다. 제약 디코더는 중단된 reasoning에서 가장 가까이에 남아 있던 숫자를 그대로 확정한다.

GLM의 오염은 항상 발생하지 않고 간헐적으로 나타난다. production에서는 오히려 더 위험하다. 같은 날 같은 prompt와 설정으로 실행했는데 한 batch에서는 0/4였고 이후 두 batch에서는 7/8이었다. eval을 통과한 뒤 production에서 12% 비율로 드러나는 유형의 실패다. 모든 오답이 validation을 통과하므로 schema validator로는 절대 잡을 수 없다.

추출 테스트에서도 같은 문제가 더 심한 형태로 나타났다. strict integer 필드에 line item 수를 넣도록 요청했을 때 DeepSeek 계열은 thinking을 켜면 sentinel이나 placeholder처럼 보이는 쓰레기 값을 출력했다. line_items: -1, -45, -85, 그리고 한 번은 $80 송장에서 total: 8000을 반환했다. DeepSeek V4 Pro는 thinking을 켰을 때 1/8, 껐을 때 7/8이었다. thinking을 끄면 회복되는 이 패턴은 앞서 두 모델 batch로 이 계열에서 처음 측정한 결과와 같다. 이번 batch에서는 Qwen과 GLM까지 같은 현상이 확장됨을 확인했다. OpenAI, Gemini 세 모델, Kimi는 같은 battery의 모든 조건에서 8/8이었다. reasoning 모델 전반의 문제가 아니라 이 네 모델이 제약 디코더 주변에서 reasoning을 처리하는 방식에 국한된 문제다.

흔히 쓰는 해결책은 스키마 첫 필드에 reasoning 문자열을 넣어 모델이 제약된 채널 안에서 생각하게 하는 것이다. Qwen에서는 완전히 통했다. thinking을 켠 상태에서 1/8이던 결과가 8/8이 됐다. 하지만 비용이 들고 모든 모델에 통하는 것도 아니다. reasoning token은 계속 청구됐고 Qwen의 중앙값은 393이었다. 문제가 없는 모델에서는 이점 없이 output token만 약 두 배로 늘었다. gpt-5.6-luna는 호출당 48에서 106으로 증가했다. DeepSeek V4 Flash에서는 이전에 문제없던 작업의 결과가 오히려 8/8에서 6/8로 나빠졌다.

실무 규칙은 명확하다. DeepSeek, Qwen, GLM에서 구조화 추출은 thinking을 끄고 실행해야 한다. 스키마는 어느 쪽이든 지켜지지만 내부 숫자는 그렇지 않다. 스키마에 reasoning 필드를 추가하는 방법은 모델별로 테스트할 패치이지 기본값이 아니다.

API별로 어떤 스키마 키워드가 동작하나?

JSON Schema 명세만 보고 예상하는 것보다 적게 지원하며, 실패 방식도 업체마다 다르다. “강제”는 네 번의 충돌 실행 중 최소 세 번 이상 모델이 해당 키워드를 위반하지 못했다는 뜻이다.

키워드OpenAIGeminiDeepSeek / Qwen / GLMKimiClaude (네이티브 tool)
$ref / $defs강제400강제강제 (3/4)조용히 제거됨
oneOf400조용히 제거됨강제제거됨제거됨
format: date강제강제강제제거됨 (2/4)제거됨
pattern강제강제강제강제강제
minItems부분 강제 (2/4)강제강제제거됨제거됨
500개 값의 enum강제강제강제강제 (3/4)강제 (3/4)

이 표에서 세 가지를 알 수 있다. 첫째, 한 제약형 API에서 실행되는 스키마가 다른 API에서도 그대로 동작한다고 볼 수 없다. OpenAI는 $ref를 지키지만 oneOf는 아예 거부한다. Gemini는 정반대다. 보낸 모든 키워드를 지킨 것은 중국계 세 모델군뿐이다. 둘째, 400은 오히려 좋은 결과다. Gemini의 oneOf와 Claude 열의 대부분은 200을 반환한 뒤 제약을 조용히 건너뛴다. 요청은 구조화된 것처럼 보이지만 실제로는 그렇지 않다. 셋째, Claude의 네이티브 tool 경로는 구조 자체, 즉 type, required field, additionalProperties, pattern은 제약하지만 composition이나 format은 제약하지 않는다. 따라서 grammar 기반 response_format보다 보장 범위가 얕다고 봐야 한다. Gemini의 dialect는 ["string", "null"] 같은 type union도 거부한다. 겉보기에 이식 가능한 스키마도 업체별로 다시 작성해야 할 수 있다.

구조화 모드에서도 reasoning token을 계속 쓰나?

대부분 그렇다. 끌 수 있는지 여부도 모델마다 다르다. 앞서 사용한 한 줄짜리 계산 작업에서 스키마를 첨부하고 기본 설정으로 실행했을 때 reasoning token 중앙값은 다음과 같았다. GLM-5.2는 568, DeepSeek V4 Pro는 505, V4 Flash는 466, Qwen3.8-Max는 424, Gemini 3.6 Flash는 210, Gemini 3.1 Pro는 220, Gemini 3.7 Flash는 99, Kimi K3는 69, gpt-5.6-luna는 28이었다. 답 자체는 token 두 개면 충분한 작업인데, output 비용의 대부분을 reasoning이 차지한다.

구조화 모드 안에서 reasoning을 끌 수 있는지도 모델마다 다르다. DeepSeek는 reasoning_effort: none을 400으로 거부하지만 thinking: {"type": "disabled"}는 적용한다. Qwen, GLM, Kimi는 effort 값을 0까지 허용한다. 현재 Gemini 세대인 3.7 Flash와 3.1 Pro는 테스트한 모든 off 표기를 거부했다. 이 계열에서 사라진 off 스위치와 일치하는 결과다. 따라서 구조화 호출에서 발생하는 reasoning 비용을 피할 수 없다. Claude의 네이티브 경로에서는 이 질문 자체가 의미 없다. tool call을 강제하면 extended thinking을 완전히 우회한다. Fable 5를 포함한 세 모델 모두 reasoning token이 0이었고, 추출당 output token 중앙값은 74였다. 단순 추출에서는 가장 비싼 모델 계열이 가장 저렴한 completion을 실행한다.

호출마다 스키마 자체에 얼마가 드나?

같은 호출에 청구되는 prompt token은 30에서 4,959까지 차이 난다. 이유를 이해하려면 스키마가 실제로 어디로 전달되는지 알아야 한다. 스키마는 message list에 들어가지 않는다. OpenAI 호환 인터페이스에서는 요청 body의 response_format.json_schema로 전달된다. Gemini 네이티브 API에서는 generation_config.response_schema를 사용한다. Claude에는 전용 스키마 슬롯이 없으므로, tool_choice로 호출을 강제한 tool definition의 input_schema에 넣는다. 차이는 그다음 서버가 처리하는 방식에서 생긴다. 한 그룹은 스키마를 서버 측 grammar로 컴파일해 디코딩을 제어하므로 청구량에 잡히지 않는다. 다른 그룹은 이를 숨겨진 prompt text로 직렬화해 모델 context에 넣기 때문에 prompt_tokens로 청구된다. 같은 문서에 157 bytes, 추가 필드 12개를 넣은 1.5 KB, 필드 70개를 넣은 12 KB 스키마를 적용한 결과다.

API157 B 스키마1.5 KB12 KB청구 방식
deepseek-v4-flash303030스키마 미청구
glm-5.2383838스키마 미청구
qwen3.8-max787878스키마 미청구
deepseek-v4-pro109109109스키마 미청구
gpt-5.6-luna573462,368스키마를 prompt로 청구
kimi-k31995232,789스키마를 prompt로 청구
gemini (세 모델 모두)925904,012스키마를 prompt로 청구
claude (네이티브 tool, fable-5)5491,0294,959tool definition 청구, 약 500 token의 고정 tool-use overhead 추가. sonnet-5는 각 조건에서 64 token 더 많음

스키마를 청구하는 그룹 안에서도 동일한 byte를 직렬화하는 비율이 최대 70%까지 차이 난다. 12 KB 스키마는 Gemini에서 4,012 token이지만 OpenAI에서는 2,368 token이다. 큰 스키마를 대량으로 호출한다면 이 항목이 모델의 token당 가격보다 더 큰 비용 변수가 된다. 월 100K calls 기준으로 12 KB 스키마는 DeepSeek에서 무료지만 Gemini에서는 약 400M input tokens다.

FAQ

구조화 출력이 데이터의 정확성도 보장하나?

아니다. 구조화 출력은 파싱 가능하고 스키마에 맞는 데이터를 보장할 뿐, 값의 정확성은 보장하지 않는다. 테스트에서 실제로 동작한 모든 구조화 모드는 스키마 유효성 100%를 기록했다. 하지만 일부 모델과 작업 조합에서는 8개 응답 중 최대 7개가 유효한 JSON 안에 틀린 값을 담았고, thinking을 끄자 대부분 회복됐다. 형태뿐 아니라 값도 검증해야 한다.

Claude는 response_format json_schema를 지원하나?

확인한 모든 provider에서 지원하지 않았다. 오류도 반환하지 않는다. 파라미터를 받아들인 뒤 무시하므로 가장 나쁜 실패 방식이다. 대신 Anthropic 네이티브 tool calling에서 tool_choice를 강제해야 한다. 적대적인 prompt로 측정했을 때 완전히 제약됐고 extended thinking도 건너뛰었다. 이 batch에서 completion이 가장 짧았으며, 추출당 output token 중앙값은 74였다.

구조화 추출에서는 thinking을 꺼야 하나?

DeepSeek V4, Qwen3.8-Max, GLM-5.2에서는 그렇다. 계산 결과를 스키마에 넣는 작업에서 Qwen은 thinking을 끄자 1/16에서 8/8로 올랐고, DeepSeek V4 Pro의 추출 결과는 1/8에서 7/8로 올랐다. OpenAI와 Gemini에서는 thinking을 켠 상태에서 값 오염이 나타나지 않았다. 이쪽은 작업 난이도에 따라 결정하면 된다. 다만 현재 Gemini 세대는 thinking을 아예 끌 수 없다.

2026-08-25에 Synthorai gateway를 통해 production 모델 API 12개를 측정했다. 모든 방법과 표본 크기는 위의 “구조화 출력을 어떻게 테스트했나?”에 설명했다. 절대 수치는 하나의 batch에서 나온 결과다. 업체는 예고 없이 serving 동작을 바꿀 수 있으므로, 표의 결과에 의존하기 전에 다시 측정해야 한다.

같은 시리즈의 관련 글: 13개 모델의 thinking 제어 비교, DeepSeek V4 Pro 측정 결과, Qwen3.8-Max 비용, GPT-5.6 비용 가이드.

← 블로그로 돌아가기