Skip to main content
Summary

지난 장에서는 프롬프트를 어떻게 쓰면 모델의 답변을 더 원하는 방향으로 이끌 수 있는지 살펴보았다. 그런데 실제 서비스를 만들기 시작하면 곧 이런 생각이 든다.

'답변이 친절한 것은 좋은데, 이걸 코드가 어떻게 믿고 처리하지?'

사용자에게 보여 주는 챗봇 답변이라면 자연스러운 문장이 중요하다. 하지만 애플리케이션 코드와 연결되는 순간에는 이야기가 달라진다. 주문 정보를 추출해야 한다면 주문 번호가 있어야 하고, 리뷰를 분석해야 한다면 감정 점수는 숫자여야 하며, 회의록에서 할 일을 뽑는다면 담당자와 마감일이 정해진 위치에 있어야 한다.

서비스에서는 멋진 문장보다 파싱 가능한 응답이 더 중요할 때가 많다.

구조화 출력은 바로 이 문제를 해결하기 위한 방법이다. LLM에게 자유롭게 말하게 두는 대신, 응답을 미리 정한 데이터 구조에 맞춰 내보내도록 만드는 것이다.

구조화 출력

자연어 응답은 사람에게 좋고, 코드에는 불친절하다

LLM은 기본적으로 문장을 생성하는 모델이다. 그래서 아무 제약 없이 요청하면 이런 식으로 답할 수 있다.

요약하자면, 이 글은 재택근무 도입 이후 생산성이 좋아졌다는 내용입니다.

핵심 포인트:
- 출퇴근 시간이 줄었습니다.
- 집중 시간이 늘었습니다.
- 팀 간 소통 방식은 아직 개선이 필요합니다.

할 일은 다음과 같습니다. 인사팀은 다음 주까지 설문을 준비하고, 개발팀은 협업 도구를 점검하면 좋겠습니다.

사람이 읽기에는 충분히 괜찮다. 하지만 코드 입장에서는 곤란하다. 제목은 어디까지인가? 핵심 포인트는 배열인가, 그냥 문자열인가? '다음 주까지'는 날짜인가, 자연어인가? 인사팀과 개발팀의 할 일은 어떤 필드로 저장해야 하는가?

개발자는 결국 정규식과 문자열 분리 코드를 붙이기 시작한다. 하지만 LLM 응답은 매번 조금씩 달라진다. 어느 날은 마크다운 코드 블록이 들어가고, 어느 날은 설명 문장이 앞에 붙고, 어느 날은 필드 이름이 바뀐다.

문제는 단순히 귀찮다는 데서 끝나지 않는다. 서비스 코드에서는 다음과 같은 오류가 실제 장애로 이어질 수 있다.

  • JSON처럼 보이지만 마지막 중괄호가 빠져 파싱에 실패한다.

  • 응답 앞뒤에 ```json 코드 블록이 섞여 그대로는 읽을 수 없다.

  • 필수 필드인 userId, summary, score 중 하나가 빠진다.

  • 숫자여야 할 confidence가 '높음' 같은 문자열로 온다.

  • enum으로 정한 positive, neutral, negative 대신 '긍정적'이라는 값이 온다.

프롬프트에 '반드시 JSON으로 답하세요'라고 적는 것만으로는 부족하다. 그것은 부탁에 가깝다. 서비스에는 부탁이 아니라 계약이 필요하다.

구조화 출력에서 스키마는 이 계약서 역할을 한다. 모델에게 '대충 이런 식으로 답해 줘'가 아니라 '이 필드 이름, 이 타입, 이 필수 여부에 맞춰 응답해'라고 명시하는 것이다.

JSON 모드와 Structured Outputs는 다르다

LLM API에서 구조화된 응답을 얻는 가장 익숙한 형식은 JSON이다. JSON은 객체, 배열, 문자열, 숫자, 불리언, null을 표현할 수 있고, 거의 모든 프로그래밍 언어에서 쉽게 파싱할 수 있다.

초기에는 보통 프롬프트로 이런 요청을 했다.

다음 글을 요약하고 JSON으로만 답하세요.
필드: title, bullets, confidence
설명은 붙이지 마세요.

이 방식은 간단하지만 안정적이지 않다. 그래서 여러 제공자는 'JSON 모드'를 제공한다. JSON 모드는 모델이 유효한 JSON을 생성하도록 돕는다. OpenAI 문서에서도 JSON 모드는 유효한 JSON 생성을 보장하는 데 초점이 있고, Structured Outputs는 지정한 스키마에 맞게 응답을 생성하는 데 초점이 있다고 설명한다.1

둘은 비슷해 보이지만 중요한 차이가 있다.

JSON 모드는 이렇게 말하는 것에 가깝다.

응답은 JSON 문법에 맞아야 한다.

Structured Outputs는 이렇게 말하는 것에 가깝다.

응답은 JSON 문법에 맞아야 하고, title은 문자열, bullets는 문자열 배열, confidence는 숫자여야 하며, 추가 필드는 허용하지 않는다.

예를 들어 JSON 모드에서는 다음 응답이 모두 '유효한 JSON'일 수 있다.

{
  'summary': '문서 요약입니다.'
}
{
  'title': '문서 요약',
  'bullet_points': ['핵심 1', '핵심 2'],
  'confidence': '높음'
}

첫 번째는 필드가 부족하고, 두 번째는 필드명이 다르며 confidence 타입도 다르다. JSON 문법만 맞는다고 해서 애플리케이션이 원하는 데이터라는 뜻은 아니다.

반면 스키마 기반 구조화 출력에서는 응답의 모양을 더 엄격하게 정한다. 이때 자주 쓰는 표준이 JSON Schema다. JSON Schema는 JSON 데이터가 어떤 필드를 가져야 하는지, 각 필드의 타입은 무엇인지, 어떤 값이 필수인지 설명하는 청사진이다.2

응답 형식은 애플리케이션 계약이다

구조화 출력을 설계할 때는 먼저 '모델이 무슨 말을 해야 하는가'보다 '우리 코드가 어떤 데이터를 필요로 하는가'를 생각해야 한다.

예를 들어 문서 요약 기능을 만든다고 해 보자. 화면에는 제목, 핵심 bullet, 후속 작업, 신뢰도를 보여 주고 싶다. 그렇다면 LLM 응답은 이런 구조를 갖는 편이 좋다.

summary_schema = {
    'type': 'object',
    'properties': {
        'title': {
            'type': 'string',
            'description': '문서 내용을 한 문장으로 요약한 제목'
        },
        'bullets': {
            'type': 'array',
            'items': {'type': 'string'},
            'description': '핵심 내용을 짧은 문장 목록으로 정리'
        },
        'actionItems': {
            'type': 'array',
            'items': {
                'type': 'object',
                'properties': {
                    'task': {'type': 'string'},
                    'owner': {'type': 'string'},
                    'dueDate': {'type': 'string'},
                    'priority': {
                        'type': 'string',
                        'enum': ['low', 'medium', 'high']
                    }
                },
                'required': ['task', 'owner', 'dueDate', 'priority'],
                'additionalProperties': False
            }
        },
        'confidence': {
            'type': 'number',
            'description': '요약 결과에 대한 모델의 확신 정도. 0에서 1 사이 값'
        }
    },
    'required': ['title', 'bullets', 'actionItems', 'confidence'],
    'additionalProperties': False
}

이 스키마에는 몇 가지 중요한 요소가 들어 있다.

type은 값의 종류를 정한다. title은 문자열이고, bullets는 배열이며, actionItems는 객체 배열이다. required는 반드시 포함되어야 하는 필드를 정한다. enum은 허용되는 값의 후보를 제한한다. priority가 low, medium, high 중 하나로만 오게 만드는 식이다. additionalProperties: False는 약속하지 않은 필드가 끼어드는 것을 막는다.

이 스키마를 사용하면 모델 응답은 대략 다음과 같은 형태가 된다.

{
  'title': '재택근무 실험 결과와 후속 개선 과제',
  'bullets': [
    '출퇴근 시간이 줄어 개인 집중 시간이 늘었다.',
    '대부분의 팀에서 업무 만족도가 상승했다.',
    '비동기 소통 규칙은 아직 명확히 정리되지 않았다.'
  ],
  'actionItems': [
    {
      'task': '전사 재택근무 만족도 설문 작성',
      'owner': '인사팀',
      'dueDate': '2026-05-20',
      'priority': 'high'
    },
    {
      'task': '협업 도구 사용 가이드 업데이트',
      'owner': '개발팀',
      'dueDate': '2026-05-24',
      'priority': 'medium'
    }
  ],
  'confidence': 0.82
}

이제 코드는 훨씬 편해진다. data['title']을 제목 영역에 넣고, data['bullets']를 반복 렌더링하고, data['actionItems']를 할 일 목록 DB에 저장하면 된다. 자연어 문장을 억지로 쪼갤 필요가 없다.

OpenAI API에서 스키마로 응답 받기

다음은 OpenAI 스타일의 Chat Completions API에서 JSON Schema 기반 응답 형식을 지정하는 예시다. 실제 프로젝트에서는 사용하는 SDK 버전과 모델에 따라 세부 문법이 달라질 수 있으므로, 현재 사용하는 제공자의 최신 문서를 함께 확인해야 한다.

from openai import OpenAI
import json

client = OpenAI()

summary_schema = {
    'type': 'object',
    'properties': {
        'title': {'type': 'string'},
        'bullets': {
            'type': 'array',
            'items': {'type': 'string'}
        },
        'actionItems': {
            'type': 'array',
            'items': {
                'type': 'object',
                'properties': {
                    'task': {'type': 'string'},
                    'owner': {'type': 'string'},
                    'dueDate': {'type': 'string'},
                    'priority': {
                        'type': 'string',
                        'enum': ['low', 'medium', 'high']
                    }
                },
                'required': ['task', 'owner', 'dueDate', 'priority'],
                'additionalProperties': False
            }
        },
        'confidence': {'type': 'number'}
    },
    'required': ['title', 'bullets', 'actionItems', 'confidence'],
    'additionalProperties': False
}

response = client.chat.completions.create(
    model='gpt-4o-mini',
    messages=[
        {
            'role': 'system',
            'content': '문서를 요약하고, 반드시 주어진 스키마에 맞는 데이터로 답하세요.'
        },
        {
            'role': 'user',
            'content': '''
재택근무 실험 결과, 출퇴근 시간이 줄어든 직원들은 집중 시간이 늘었다고 답했다.
다만 신규 입사자 온보딩과 팀 간 비동기 소통 규칙은 아직 개선이 필요하다.
인사팀은 다음 주까지 만족도 설문을 준비하고, 개발팀은 협업 도구 사용 가이드를 정리하기로 했다.
'''
        }
    ],
    response_format={
        'type': 'json_schema',
        'json_schema': {
            'name': 'summary_result',
            'strict': True,
            'schema': summary_schema
        }
    }
)

raw = response.choices[0].message.content
data = json.loads(raw)

print(data['title'])
print(data['actionItems'][0]['priority'])

중요한 점은 스키마가 프롬프트 바깥의 API 파라미터로 들어간다는 것이다. 프롬프트로만 형식을 요청하는 방식보다 안정적으로 코드와 연결할 수 있다. 물론 스키마를 넣었다고 해서 값의 사실성까지 자동으로 보장되는 것은 아니다. confidence가 숫자로 오는 것은 보장할 수 있어도, 그 숫자가 정말 정확한 신뢰도인지는 별도의 평가와 검증이 필요하다.

Pydantic으로 한 번 더 검증하기

구조화 출력을 쓴다고 해서 백엔드 검증을 생략하면 안 된다. LLM은 애플리케이션의 바깥에서 들어오는 입력과 비슷하게 취급해야 한다. 신뢰하되, 검증해야 한다.

Python에서는 Pydantic을 자주 사용한다. Pydantic은 데이터 모델을 클래스로 정의하고, 들어온 값이 타입과 조건에 맞는지 검사해 주는 도구다.

from typing import Literal
from pydantic import BaseModel, Field, ValidationError, ConfigDict

class ActionItem(BaseModel):
    model_config = ConfigDict(extra='forbid')

    task: str = Field(min_length=1)
    owner: str = Field(min_length=1)
    dueDate: str = Field(min_length=1)
    priority: Literal['low', 'medium', 'high']

class SummaryResult(BaseModel):
    model_config = ConfigDict(extra='forbid')

    title: str = Field(min_length=1)
    bullets: list[str] = Field(min_length=1)
    actionItems: list[ActionItem]
    confidence: float = Field(ge=0.0, le=1.0)

try:
    result = SummaryResult.model_validate(data)
except ValidationError as error:
    print('검증 실패:', error)
    raise

print(result.title)
print(result.actionItems[0].task)

여기에는 스키마보다 더 애플리케이션에 가까운 규칙이 들어간다. title은 빈 문자열이면 안 되고, bullets는 최소 한 개 이상이어야 하며, confidence는 0과 1 사이여야 한다. 또한 extra='forbid'를 통해 정의하지 않은 필드가 들어오는 것을 막는다.

구조화 출력은 모델이 처음부터 올바른 모양으로 답하도록 돕는다. Pydantic 검증은 그 결과를 우리 시스템 경계에서 다시 확인한다. 둘은 경쟁 관계가 아니라 서로를 보완하는 관계다.

검증 실패는 예외 상황이 아니라 정상 경로다

프로덕션에서는 '이론적으로는 실패하지 않아야 한다'는 말이 별 도움이 되지 않는다. 네트워크가 끊길 수 있고, 모델이 안전상의 이유로 거부할 수 있고, 최대 토큰 길이에 걸려 응답이 중간에 끊길 수 있다. 스키마는 맞지만 값이 비어 있거나 품질이 낮을 수도 있다.

따라서 구조화 출력을 사용할 때는 실패 경로를 처음부터 설계해야 한다.

import json
from pydantic import ValidationError

class StructuredOutputError(Exception):
    pass

def summarize_with_retry(client, text: str, max_retries: int = 2) -> SummaryResult:
    messages = [
        {
            'role': 'system',
            'content': '문서를 요약하고 정해진 스키마에 맞게 답하세요.'
        },
        {
            'role': 'user',
            'content': text
        }
    ]

    last_error = None

    for attempt in range(max_retries + 1):
        if last_error is not None:
            messages.append({
                'role': 'user',
                'content': f'이전 응답은 검증에 실패했습니다. 오류: {last_error}. 스키마와 타입을 지켜 다시 답하세요.'
            })

        response = client.chat.completions.create(
            model='gpt-4o-mini',
            messages=messages,
            response_format={
                'type': 'json_schema',
                'json_schema': {
                    'name': 'summary_result',
                    'strict': True,
                    'schema': summary_schema
                }
            }
        )

        raw = response.choices[0].message.content

        try:
            data = json.loads(raw)
            return SummaryResult.model_validate(data)
        except (json.JSONDecodeError, ValidationError) as error:
            last_error = str(error)

    raise StructuredOutputError(f'구조화 출력 생성 실패: {last_error}')

이 예시는 일부러 단순하게 썼다. 실제 서비스에서는 여기에 로깅, 사용자별 요청 ID, 모델명, 프롬프트 버전, 원본 응답 일부, 재시도 횟수 등을 함께 남긴다. 그래야 나중에 어떤 스키마에서 실패가 잦은지, 특정 문서 유형에서 오류가 나는지 확인할 수 있다.

검증 실패 후에는 선택지가 있다. 한두 번 재시도할 수 있고, 기본값을 넣어 일부 기능만 제공할 수도 있다. 사용자에게 '현재 요약을 생성하지 못했습니다'라는 실패 응답을 줄 수도 있다. 중요한 것은 실패를 숨기지 않는 것이다. 잘못된 데이터를 조용히 DB에 저장하는 것보다 명시적으로 실패하는 편이 훨씬 안전하다.

구조화 출력은 도구 호출의 언어이기도 하다

구조화 출력은 단순히 예쁜 JSON을 얻기 위한 기능이 아니다. 다음 장에서 자세히 볼 Function Calling과 Tool Use의 기반이기도 하다.

Function Calling은 모델이 외부 시스템과 연결될 수 있도록 함수 이름과 인자를 생성하는 방식이다. OpenAI 문서에서는 Function Calling을 모델이 애플리케이션이 제공하는 데이터와 동작에 연결되는 방법으로 설명한다.3

예를 들어 사용자가 이렇게 말한다고 해 보자.

내일 오후 3시에 민수와 회의 잡아 줘.

모델이 자유롭게 답한다면 이렇게 말할 수 있다.

좋습니다. 내일 오후 3시에 민수님과 회의를 잡아 드릴게요.

하지만 캘린더 API를 호출해야 하는 서비스라면 이런 문장만으로는 부족하다. 실제로 필요한 것은 함수 인자다.

{
  'attendee': '민수',
  'date': '2026-05-13',
  'time': '15:00',
  'durationMinutes': 30,
  'title': '회의'
}

즉, 모델은 사람에게 말하는 대신 함수에 넘길 구조화 데이터를 만들어야 한다. 검색, 데이터베이스 조회, 항공권 예약, 결제, 이메일 발송 같은 워크플로에서는 이 차이가 매우 중요하다.

도구 호출에서 스키마는 '이 함수는 어떤 인자를 받을 수 있는가'를 정의한다. 모델은 사용자의 자연어 요청을 읽고, 어떤 도구를 쓸지 고른 다음, 그 도구의 스키마에 맞는 인자를 생성한다. 물론 실제 함수 실행은 모델이 아니라 우리 애플리케이션 코드가 담당한다. 모델은 '이렇게 호출하면 좋겠다'는 구조화된 요청을 만드는 역할을 한다.

이 관점을 이해하면 RAG와 에이전트도 더 선명해진다. RAG에서 모델은 검색 질의를 구조화할 수 있고, 에이전트에서는 다음 행동을 JSON 형태로 선택할 수 있다. 결국 LLM 애플리케이션은 자연어 생성기라기보다, 자연어와 소프트웨어 시스템 사이에서 구조화된 의도를 만들어 내는 계층이 된다.

제공자마다 방식이 다르다

구조화 출력은 이제 여러 LLM 제공자가 지원하는 중요한 기능이 되었다. 하지만 이름과 세부 방식은 조금씩 다르다.

OpenAI는 JSON 모드, JSON Schema 기반 Structured Outputs, Function Calling을 제공한다. Gemini 계열은 response schema 방식으로 JSON 응답 구조를 지정할 수 있다. Anthropic 계열은 Tool Use의 입력 스키마를 활용해 구조화된 인자를 받는 식으로 설계하는 경우가 많다. LiteLLM 문서는 여러 제공자의 response_format과 json_schema 지원 여부를 추상화해서 다루며, 모델별 지원 여부를 확인하는 기능도 제공한다.4

자체 모델이나 오픈소스 모델을 운영할 때도 구조화 출력은 중요하다. vLLM은 structured_outputs 기능으로 choice, regex, json, grammar 같은 제약된 출력 방식을 지원하며, OpenAI 호환 서버에서도 관련 파라미터를 사용할 수 있다.5

따라서 실전 코드에서는 특정 제공자의 API 문법을 비즈니스 로직 깊숙이 섞어 두지 않는 편이 좋다. 핵심은 '우리 서비스의 데이터 계약'을 별도로 두는 것이다.

예를 들어 요약 기능이라면 다음 세 층을 분리한다.

  • 도메인 모델: SummaryResult, ActionItem처럼 우리 서비스가 실제로 사용하는 데이터 구조

  • 제공자 어댑터: OpenAI, Gemini, Anthropic, vLLM 등 각 API 호출 방식

  • 검증 계층: Pydantic, Zod, JSON Schema validator처럼 최종 응답을 검사하는 코드

이렇게 나누면 나중에 모델을 바꾸더라도 서비스 전체를 갈아엎지 않아도 된다. OpenAI에서 Gemini로 바꾸거나, 클라우드 API에서 자체 호스팅 모델로 바꿔도 SummaryResult라는 계약은 유지된다. 바뀌는 것은 어댑터뿐이다.

스키마를 잘 설계하는 법

구조화 출력의 품질은 스키마 설계에 크게 좌우된다. 너무 느슨한 스키마는 코드에 충분한 안정성을 주지 못하고, 너무 복잡한 스키마는 모델이 값을 채우기 어렵게 만든다.

처음에는 작고 명확한 스키마로 시작하는 편이 좋다. 필드 이름은 코드가 이해하기 쉬우면서도 모델에게 의미가 분명해야 한다. a, b, data1 같은 이름보다 title, summary, actionItems가 낫다. 각 필드에는 가능하면 짧은 description을 달아 모델이 어떤 값을 넣어야 하는지 알려 준다.

날짜나 상태값처럼 후속 코드에서 중요하게 쓰는 값은 문자열을 자유롭게 받기보다 형식을 정하는 편이 안전하다. 예를 들어 priority는 low, medium, high 중 하나로 제한하고, status는 pending, in_progress, done 중 하나로 제한한다. 날짜는 프로젝트에서 정한 규칙에 따라 YYYY-MM-DD 문자열로 받도록 지시한다. 다만 제공자에 따라 JSON Schema의 모든 키워드를 지원하지 않을 수 있으므로, 실제 API가 지원하는 범위 안에서 설계해야 한다.

또 하나의 요령은 '모델이 판단할 값'과 '시스템이 계산할 값'을 구분하는 것이다. 예를 들어 totalPrice는 모델이 계산하게 할 수도 있지만, 주문 항목과 수량, 단가가 있다면 백엔드 코드가 계산하는 편이 낫다. 모델에게 맡겨야 할 것은 자연어 이해와 분류이고, 정확한 산술이나 권한 검사는 애플리케이션 코드가 담당하는 편이 안전하다.

구조는 맞아도 내용은 틀릴 수 있다

구조화 출력은 강력하지만 마법은 아니다. 스키마는 그릇의 모양을 정한다. 그릇 안에 담긴 내용이 사실인지, 최신 정보인지, 비즈니스 규칙에 맞는지는 별도의 문제다.

예를 들어 모델이 다음과 같은 응답을 했다고 해 보자.

{
  'title': '계약 갱신 일정',
  'bullets': ['계약은 7월에 종료된다.'],
  'actionItems': [
    {
      'task': '계약 갱신 메일 발송',
      'owner': '법무팀',
      'dueDate': '2026-06-01',
      'priority': 'high'
    }
  ],
  'confidence': 0.91
}

스키마만 보면 완벽하다. 하지만 원문 계약서의 종료일이 실제로는 8월이라면 내용은 틀렸다. priority가 high로 온 것도 비즈니스 정책상 맞지 않을 수 있다. confidence가 0.91이라고 해서 정말 믿을 만하다는 뜻도 아니다. 모델이 스스로 추정한 값일 뿐이다.

그래서 중요한 필드는 후속 검증을 붙여야 한다. 계약 날짜는 원문에서 인용 위치를 함께 받거나, 규칙 기반 파서로 다시 확인할 수 있다. 금액은 백엔드에서 재계산할 수 있다. 고객 등급이나 결제 가능 여부는 데이터베이스에서 조회해야 한다.

구조화 출력의 목적은 LLM을 완전히 믿기 위한 것이 아니다. LLM의 출력을 소프트웨어가 검사하고 다룰 수 있는 형태로 바꾸는 것이다.

작은 실습: 리뷰 분석 결과를 구조화하기

마지막으로 아주 작은 기능을 설계해 보자. 사용자의 상품 리뷰를 분석해서 화면에 표시하고, 나중에 통계에도 활용하고 싶다. 이때 필요한 응답 구조를 먼저 정한다.

review_schema = {
    'type': 'object',
    'properties': {
        'sentiment': {
            'type': 'string',
            'enum': ['positive', 'neutral', 'negative']
        },
        'score': {
            'type': 'number'
        },
        'summary': {
            'type': 'string'
        },
        'keyPhrases': {
            'type': 'array',
            'items': {'type': 'string'}
        },
        'needsHumanReview': {
            'type': 'boolean'
        }
    },
    'required': ['sentiment', 'score', 'summary', 'keyPhrases', 'needsHumanReview'],
    'additionalProperties': False
}

이 스키마의 장점은 후속 코드가 명확해진다는 것이다. sentiment는 차트 집계에 쓰고, score는 정렬에 쓰며, keyPhrases는 태그처럼 보여 줄 수 있다. needsHumanReview가 true면 운영자 검수 큐로 보낼 수 있다.

프롬프트는 길 필요가 없다.

다음 상품 리뷰를 분석하세요.
불만, 환불 요구, 욕설, 안전 문제 등이 있으면 needsHumanReview를 true로 설정하세요.

그리고 실제 응답은 다음처럼 코드가 바로 읽을 수 있는 데이터가 된다.

{
  'sentiment': 'negative',
  'score': 0.22,
  'summary': '배송은 빨랐지만 제품이 파손되어 도착했고 고객은 환불을 원한다.',
  'keyPhrases': ['빠른 배송', '제품 파손', '환불 요청'],
  'needsHumanReview': true
}

이제 LLM 응답은 단순한 문장이 아니다. 서비스의 다음 동작을 결정하는 데이터가 되었다.

다음 장으로 가기 전에

구조화 출력은 LLM을 제품에 넣기 위한 중요한 전환점이다. 프롬프트만 잘 쓰는 단계에서는 모델이 좋은 답변을 하도록 설득한다. 구조화 출력을 쓰는 단계에서는 모델의 답변을 애플리케이션이 처리할 수 있는 계약으로 바꾼다.

이 장에서 기억할 핵심은 단순하다.

LLM의 자유 형식 자연어 응답은 사람에게는 편하지만 코드에는 불안정하다. JSON 모드는 유효한 JSON을 얻는 데 도움이 되지만, 필드명과 타입과 필수 여부까지 충분히 보장하려면 스키마 기반 구조화 출력이 필요하다. 그리고 프로덕션에서는 모델의 구조화 출력만 믿지 말고, Pydantic 같은 검증 계층과 재시도, 로깅, 실패 응답 경로를 함께 설계해야 한다.

다음 장에서는 이 구조화 출력이 한 단계 더 나아가 외부 도구 호출로 연결되는 과정을 살펴본다. 모델이 단지 JSON을 반환하는 것이 아니라, 검색 함수나 데이터베이스 조회 함수, 예약 API를 호출하기 위한 인자를 생성하기 시작하면 LLM 애플리케이션은 완전히 다른 모습이 된다.

참고

1OpenAI Structured Outputs

2How JSON Schema works for Structured Outputs and Tool Integration

3OpenAI Function Calling

4LiteLLM Structured Outputs JSON Mode

5vLLM Structured Outputs

책 목록

처음 만나는 LLM: 원리부터 Mini GPT 구현, RAG 서비스 개발까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기