이 글은 Modelflare 블로그의 영어 기사를 번역 및 재구성한 것입니다.
Responses API와 Chat Completions에서의 엄격한(strict) JSON Schema 출력을 필드 단위로 설명합니다. 검증 계층, 실패 시 처리 방법, 루트 호환성 테스트까지 다룹니다.
Structured Outputs를 사용하면 모델에게 단순히 'JSON으로 반환해 달라'고 요청하는 것이 아니라, 정의된 스키마에 맞춰 JSON을 애플리케이션 차원에서 요구할 수 있습니다. OpenAI 호환 API에서는 동일한 스키마를 Responses API와 Chat Completions 모두에서 사용할 수 있지만, 리퀘스트 필드는 다릅니다. 또한, 선택한 모델과 라우트(route)에서 상호 운용성이 있는지 검증이 필요합니다.
운영 환경(production environment)에서 안전하게 사용하는 패턴은 3단계 구성입니다. 모델이 지원한다면 strict한 스키마 출력을 사용하고, 반환된 JSON을 애플리케이션에서 파싱 및 검증한 후, 그 위에 비즈니스 규칙을 값에 적용합니다. 스키마 준수 덕분에 많은 포맷상의 실패는 사라지지만, 모델의 답변이 사실적으로 또는 의미적으로 올바르다는 것까지 보장하지는 않습니다.
Structured Outputs와 JSON Mode는 별개입니다
모델에게 JSON 형식 출력을 요청하는 방법은 일반적으로 3가지가 있습니다.
| 방법 | 유효한 JSON을 얻을 수 있는가 | 스키마를 강제하는가 | 주요 용도 |
|---|
| 프롬프트만 사용 | 보장되지 않음 | 안 함 | 파싱 실패가 허용되는 프로토타입 |
| JSON Mode | 지원하며, 생성이 완료되면 얻을 수 있음 | 안 함 | 형태는 애플리케이션 측에서 검증하는 유연한 JSON |
strict: true의 Structured Outputs | | | |
| 얻을 수 있음 (생성 완료와 거부 처리 전제) | 함 (지원되는 JSON Schema의 서브셋 범위 내) | 타입이 지정된 정보 추출 또는 애플리케이션 워크플로우 | |
OpenAI의 Structured Outputs 가이드에서는 선택한 모델과 엔드포인트가 지원하는 경우, JSON Mode 대신 Structured Outputs를 사용하는 것을 권장합니다. 해당 가이드에서는 다음 2가지 역할도 구분하고 있습니다.
- 모델이 예측 가능한 데이터 객체로 사용자에게 답변해야 하는 경우: 구조화된 응답 형식(structured response format)을 사용한다.
- 모델이 애플리케이션에 액션을 요구해야 하는 경우: Function Calling을 사용한다.
본 기사에서는 전자를 다룹니다. 툴 루프(tool loop)에서도 엄격한 인자 스키마(strict argument schema)를 재사용할 수 있지만, Call ID나 결과 메시지는 다른 계약(contract)을 따릅니다.
스키마는 엔드포인트를 선택하기 전에 하나 정의합니다
지원 문의 분류(triage)를 예로 들어보겠습니다. 애플리케이션에는 다음 4가지 필드가 필요합니다.
category:
billing, technical, account 중 하나 -
priority: 1부터 3까지의 정수 -
requires_human: 부울 값 -
summary: 지원 대기열(support queue)용 짧은 설명
JSON Schema는 다음과 같습니다.
{
"type": "object",
"properties": {
...
모든 속성(property)을 필수(required)로 지정하고, additionalProperties를 false로 설정하면 하위 코드(downstream code)에게 안정적인 객체를 얻을 수 있습니다. 하지만 JSON Schema의 모든 키워드가 어디서나 사용 가능한 것은 아닙니다. OpenAI가 지원하는 것은 문서화된 서브셋이며, 다른 OpenAI 호환 프로바이더는 다른 서브셋을 지원하거나 아예 strict한 스키마 모드 자체가 없을 수도 있습니다.
Responses API에서 스키마를 전송하기
Responses API에서는 응답의 스키마를 text.format 아래에 배치합니다. 다음 예제에서는 Modelflare의 API 키를 환경 변수에 설정하고 있지만, 모델에게는 이용하는 API 키로 현재 사용 가능한 것 중 Structured Outputs 동작을 검증한 것을 지정해 주십시오.
export MODELFLARE_API_KEY="<YOUR_API_KEY>"
export MODEL_ID="<MODEL_WITH_VERIFIED_STRUCTURED_OUTPUT_SUPPORT>"
curl -sS https://modelflare.dev/v1/responses
...
Responses API의 응답에서는 생성된 콘텐츠가 타입이 지정된 출력 아이템(typed output item)으로 반환됩니다. SDK를 사용하면 집계된 출력 텍스트나 파싱된 출력 등 편리한 헬퍼 기능이 제공되지만, HTTP 클라이언트를 사용하여 처리할 경우에는 구조화 객체(structured object)가 최상위 필드에 있다고 가정해서는 안 됩니다. 완료된 응답 객체를 읽은 후, 그 안에 있는 텍스트 출력을 찾아 JSON으로 파싱해야 합니다.
Chat Completions에서 동일한 스키마 전송하기
Chat Completions에서는 응답 스키마를 response_format.json_schema 아래에 위치시킵니다. 스키마 본체는 변하지 않으며, 달라지는 것은 래퍼(wrapper) 부분만입니다.
curl -sS https://modelflare.dev/v1/chat/completions \
-H "Authorization: Bearer $MODELFLARE_API_KEY" \
-H "Content-Type: application/json" \
...
Chat Completions에서는 JSON 텍스트가 보통 choices[0].message.content에 반환됩니다. 이 문자열을 타입이 지정된 객체로 취급하는 것이 아니라, 그 내용을 파싱해야 합니다.
두 래퍼의 차이점을 요약하면 다음과 같습니다:
| 용도 | Responses API | Chat Completions |
|---|
| 스키마 컨테이너 | text.format | response_format.json_schema |
| 포맷 유형 | text.format.type | response_format.type |
| 스키마 이름 | text.format.name | response_format.json_schema.name |
| 스키마 본체 | text.format.schema | response_format.json_schema.schema |
| strict 플래그 | text.format.strict | response_format.json_schema.strict |
| 결과 위치 | 타입이 지정된 출력 아이템/출력 텍스트 헬퍼 | choices[0].message.content |
Modelflare의 OpenAI 호환 레이어에는 지원 대상인 OpenAI/Codex 라우트에서 이 두 래퍼를 상호 변환하는 로직이 명시적으로 구현되어 있습니다. 하지만, 이 변환을 통해 Structured Outputs를 지원하지 않는 상위 모델(upstream model)이 지원하게 되는 것은 아닙니다. Chat Completions의 패스스루(Pass-through)로 제공되는 다른 모델군에서는 상위 프로바이더의 요청 계약 자체가 판단 기준이 됩니다.
구조와 의미는 분리하여 검증하기
모델이 다음과 같은 결과를 반환했다고 가정해 봅시다:
{
"category": "billing",
"priority": 2,
...
}
이 객체는 스키마를 충족하지만, 애플리케이션은 여전히 두 가지 종류의 검사를 별도로 수행해야 합니다.
구조 검증 (Structure Validation)
JSON Schema 유효성 검사기(validator)나 타입이 지정된 SDK 헬퍼를 사용하여 다음 사항을 확인합니다:
- 모든 필수 속성이 포함되어 있는지
- 예상치 못한 속성이 포함되어 있지 않은지
- 각 값이 요구되는 타입인지
category가 허용된 enum 값인지
priority가 허용 범위 내에 있는지
프로바이더가 엄격한 준수를 약속하더라도, 애플리케이션 측에서 검증을 수행하면 미지원 라우트(route), 통합 오류, 잘려나간 콘텐츠, 미래의 계약 변경으로부터 시스템의 다른 부분을 보호할 수 있습니다.
의미 및 비즈니스 규칙 검증 (Meaning and Business Rule Validation)
스키마만으로는 고객이 실제로 이중 청구되었는지, priority 2가 올바른지, 환급에 사람의 승인이 필요한지를 판단할 수 없습니다. 이러한 판단에는 원본 데이터와 애플리케이션 정책이 필요합니다.
영향도가 큰 워크플로우에서는 모델이 반환하는 객체를 '제안된 분류'로 취급해야 합니다. 신뢰할 수 있는 기록과 대조하고, 권한이나 금액 상한은 결정론적(deterministic) 코드로 강제하며, 안전한 감사용 참조를 남겨야 합니다. 구조적으로 유효한 모델 출력이라고 해서 승인(authorization), 청구(billing), 보안 경계를 우회해서는 안 됩니다.
불완전하거나 예외적인 출력 처리
프로덕션 환경의 파서는 정상적인 JSON.parse 호출 이상의 것이 필요합니다.
거부 (refusal)
모델은 안전상의 이유로 요청을 거부할 수 있습니다. OpenAI의 Structured Outputs 계약에서는, 거부를 일반 스키마 출력과 구별할 수 있도록 되어 있습니다. JSON 객체를 찾기 전에 응답의 상태와 거부 표현(representation)을 확인하고, 거부를 스키마 파싱 오류로 보고하지 않도록 주의해야 합니다.
잘림 및 출력 제한 (Truncation and Output Limits)
객체가 완성되기 전에 생성이 중단된 경우, 누락된 끝부분을 스키마로 채울 수 없습니다. 엔드포인트의 완료 상태와 중지 이유를 확인하십시오. 출력 제한은 예상되는 최대 유효 객체에 충분한 값으로 설정하되, 스키마와 요청하는 내용의 크기는 작게 유지해야 합니다.
지원되지 않는 스키마 키워드
strict 출력 구현은 JSON Schema의 하위 집합(subset)만 지원하는 경우가 일반적입니다. 먼저 객체(object), 배열(array), 기본형(primitive type), enum, 필수 필드(required field), 추가 속성(additional property)의 명시적인 처리부터 시작해야 합니다. 참조(references), 재귀적 구조(recursive structure), 복잡한 유니온(complex union), 고급 검증 키워드를 사용하기 전에는 제공업체의 최신 문서를 확인하십시오.
새로운 스키마의 초기 지연 시간 (Initial Latency for New Schemas)
제공업체에 따라 새로운 스키마를 전처리하여 캐시합니다. 따라서, 새로운 스키마로 보내는 첫 번째 요청은 그 이후의 요청보다 시간이 더 걸릴 수 있습니다. 요청마다 고유한 스키마를 생성하기보다는 안정적인 버전 관리된 스키마를 재사용하고, 최초 측정값은 정상 상태의 지연 시간 비교와 분리하여 처리해야 합니다.
모델 또는 라우트 비호환성 (Model or Route Incompatibility)
OpenAI 호환 엔드포인트라 할지라도, 일반적인 Chat Completions는 수락하면서 json_schema를 거부하거나, strict을 무시하거나, 이러한 필드를 지원하지 않는 모델로 그대로 전달하는 경우가 있습니다. JSON을 포함한 200 응답이 반환되더라도, strict 준수의 증거가 될 수는 없습니다. 잘못된 입력이나 엣지 케이스(edge case)의 입력으로 테스트하여 얻은 객체를 검증해야 합니다.
프로덕션 배포 전 호환성 테스트 수행하기
활성화할 예정인 모델과 라우트별로 작은 테스트 매트릭스를 준비합니다.
| 테스트 | 기록할 증거 (Evidence) |
|---|
| 필수 항목만 포함하는 최소 객체 | 상태, 모델, 라우트, 파싱된 객체, 검증 결과 |
| ... | |
| 스키마, 프롬프트(prompt), 리전(region), 스트리밍 모드(streaming mode), 출력 제한이 다른 조건에서 모델을 비교하고, 그 결과를 오직 모델의 품질에만 돌리는 것을 피해야 합니다. 제공업체의 동작이나 모델 제공 상황은 변할 수 있으므로, 테스트를 수행한 날짜도 정확하게 기록해 두어야 합니다. | |
워크플로우를 테스트한 후 엔드포인트 선택하기
애플리케이션이 이미 타입 지정 출력 아이템(typed output item), Responses API의 스트리밍 이벤트, 또는 더 광범위한 Responses API의 툴 워크플로우를 사용하고 있다면, Responses API를 사용해야 합니다. 메시지 기반의 안정적인 통합이 있고, 선택한 라우트가 response_format 계약을 지원한다면, Chat Completions를 사용해야 합니다. 보다 넓은 관점에서의 판단에 대해서는 Responses API vs Chat Completions에서 설명했습니다.
어떤 엔드포인트를 선택하든 다음 절차를 지켜야 합니다.
- JSON Schema의 정보 출처를 버전 관리되는 단일 위치에 통합합니다.
- 의미를 변경하지 않고, 해당 스키마를 각 엔드포인트의 래퍼(wrapper)로 변환합니다.
- 사용하기 전에 완료된 응답을 검증합니다.
- 구조 검증 후에 결정적인 비즈니스 규칙을 적용합니다.
- 파싱(parse), 거부, 자르기(truncate), 호환성 실패를 각각 분리하여 모니터링합니다.
클라이언트의 초기 마이그레이션은 OpenAI-Compatible API Guide부터 시작하세요. 이벤트 파싱이나 중간에 끊긴 스트림(stream)에 대해서는 AI API Streaming Guide를 참조하세요. 그 후에, API 키가 실제로 사용하는 경로(route)에서 Structured Outputs의 동작을 검증합시다.
Structured Outputs는 모델 응답을 신뢰할 수 있는 애플리케이션 인터페이스로 제한하는 경우에 가장 유용합니다. 이는 사실 확인(fact-checking), 권한 부여(authorization), 과금 로직을 대체할 수는 없습니다. 그리고 "OpenAI 호환"이라는 것은 Base URL에서 추론할 수 있는 보장이 아니라, 기능별로 테스트해야 하는 주장입니다.
필자는 AI API 게이트웨이 Modelflare 개발팀입니다. 여러 프로바이더를 OpenAI 호환 API로 다루고 싶다면 https://modelflare.dev도 확인해 주세요.
토론 (Discussion)