구조화된 모델 출력

구조화된 모델 출력 (Structured Outputs)

앱끼리 데이터를 주고받는 데 JSON만큼 널리 쓰이는 형식도 없죠. Structured Outputs는 모델이 항상 내가 제공한 JSON Schema에 맞는 응답을 내도록 보장하는 기능이에요. 모델이 필수 키를 빼먹거나 유효하지 않은 enum 값을 지어내는 일을 걱정하지 않아도 돼요.

출처: Structured model outputs - OpenAI Docs

왜 유용한가

Structured Outputs를 쓰면 다음 세 가지가 달라져요.

  1. 안정적인 타입 안전성: 형식이 잘못된 응답을 검증하거나 재시도할 필요가 없다
  2. 명시적 거부(safety refusal): 안전상의 모델 거부를 프로그램에서 감지할 수 있다
  3. 더 단순한 프롬프트: 일관된 형식을 위해 강한 어조의 프롬프트가 필요 없다

REST API에서 JSON Schema를 직접 지원할 뿐만 아니라, Python·JavaScript 라이브러리는 각각 pydantic.BaseModelz.object로 객체 스키마를 정의하게 해줘요. Ruby SDK는 Sorbet T::Struct 스키마를 지원하고요.

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()


class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]


response = client.responses.parse(
    model="gpt-6-astra",
    input=[
        {"role": "system", "content": "Extract the event information."},
        {
            "role": "user",
            "content": "Alice and Bob are going to a science fair on Friday.",
        },
    ],
    text_format=CalendarEvent,
)

event = response.output_parsed

함수 호출 vs text.format

Structured Outputs는 API에서 두 가지 형태로 제공돼요.

  1. 함수 호출(function calling)을 쓸 때
  2. json_schema 응답 형식을 쓸 때

함수 호출은 모델과 애플리케이션 기능을 이어주는 앱을 만들 때 적합해요. 예를 들어 모델에 데이터베이스를 조회하는 함수를 열어주면 사용자의 주문을 돕는 어시스턴트를 만들 수 있어요. 반대로 response_format을 통한 Structured Outputs는 모델이 사용자에게 응답할 때 구조를 강제하고 싶을 때 좋아요. 수학 튜터 앱에서 답변의 각 부분을 서로 다르게 UI로 그리려고 특정 JSON Schema를 요구하는 경우가 그렇죠.

Structured Outputs vs JSON mode

Structured Outputs는 기존 JSON mode의 발전형이에요. 둘 다 유효한 JSON을 만들지만, 스키마 준수를 보장하는 건 Structured Outputs뿐이에요. 가능하면 항상 JSON mode 대신 Structured Outputs를 쓰는 걸 권해요.

구분 Structured Outputs JSON Mode
유효한 JSON 출력
스키마 준수 예 (지원 스키마) 아니요
호환 모델 gpt-4o-mini, gpt-4o-2024-08-06 이후 gpt-3.5-turbo, gpt-4-*, gpt-4o-*, 호환 GPT-5 모델
활성화 text: { format: { type: "json_schema", "strict": true, "schema": ... } } text: { format: { type: "json_object" } }

Structured Outputs는 GPT-4o부터 시작하는 최신 대규모 언어 모델에서 지원돼요. 새 프로젝트라면 gpt-6-astra로 시작하세요.

스키마 정의하기

스키마를 만들 때 모델 생성 품질을 높이려면 다음을 지켜요.

  • 키 이름을 명확하고 직관적으로
  • 중요한 키에 명확한 title과 description 작성
  • 사용 사례에 가장 잘 맞는 구조를 찾기 위해 evals 만들고 활용

같은 스키마로 첫 요청을 보내면 API가 스키마를 처리하느라 추가 지연이 생겨요. 하지만 이후 같은 스키마 요청에는 추가 지연이 없어요.

지원 스키마

Structured Outputs는 JSON Schema 언어의 일부만 지원해요.

지원 타입: String, Number, Boolean, Integer, Object, Array, Enum, anyOf

String 속성: pattern(정규식), format(date-time, time, date, duration, email, hostname, ipv4, ipv6, uuid)

Number 속성: multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum

Array 속성: minItems, maxItems

반드시 지켜야 할 규칙

  • 루트 객체는 anyOf가 아니어야 하고 객체여야 해요. Zod에서 흔히 쓰는 discriminated union은 최상위에 anyOf를 만들기 때문에, 루트에 쓰면 동작하지 않아요.
  • 모든 필드는 required여야 해요. 선택적 파라미터가 필요하면 null을 포함한 유니언 타입으로 흉내 내요.
  • 객체에는 항상 additionalProperties: false를 설정해야 해요. Structured Outputs는 지정된 키·값만 생성하므로 이 값을 설정해야 기능을 켤 수 있어요.
  • 중첩 깊이와 크기 제한: 전체 객체 속성은 최대 5000개, 중첩은 최대 10단계.
  • 문자열 크기 제한: 모든 속성 이름·정의 이름·enum 값·const 값의 전체 문자열 길이는 120,000자를 넘길 수 없어요.
  • enum 크기 제한: 전체 enum 값은 최대 1000개. 단일 enum 속성에서 250개가 넘으면 값 전체 문자열 길이가 15,000자 이하여야 해요.
  • 키 순서: 출력은 스키마 키 순서대로 생성돼요.
  • 아직 미지원 키워드: allOf, not, dependentRequired, dependentSchemas, if, then, else. fine-tuned 모델에서는 minLength, maxLength, pattern, format, minimum, maximum, multipleOf, patternProperties, minItems, maxItems도 추가로 미지원이에요.

strict: true로 켰는데 미지원 JSON Schema를 호출하면 에러가 나요.

엣지 케이스 처리

모델이 제공한 JSON Schema와 일치하지 않는 응답을 낼 수도 있어요. 안전상 이유로 거부(refusal)하거나, max token 한계에 도달해 응답이 불완전한 경우가 그 예예요. 응답의 statusincomplete이고 incomplete_details.reasonmax_output_tokens인지 확인하거나, 메시지 콘텐츠 타입이 refusal인지 검사해서 처리해요.

더 알아보기