주요 LLM 제공업체(OpenAI, Gemini, Anthropic, Mistral, AWS Bedrock) 간 구조화 출력 비교

조금 다른 API에는 특별한 접근 방식이 필요합니다.

Page content

다음은 인기 있는 LLM 제공업체들 간 구조화된 출력 (신뢰할 수 있는 JSON을 반환받는 것) 지원 상황의 나란한 비교표이며, 최소한의 Python 예제 코드도 함께 제공합니다.

colorised bars standing

이미 Ollama에 호스팅된 LLM에서 구조화된 출력을 요청하는 방법에 대해 살펴본 바 있습니다. JSON이 네트워크를 통해 반환된 후, 실전에 적용 가능한 Python 기반 LLM 구조화된 출력 검증에서는 서비스 내 파싱, Pydantic 체크, 재시도(retry), 테스트 과정들을 다루고 있습니다. 이번 글에서는 다른 제공업체로부터 동일한 결과를 요청하는 방법을 검토해 보겠습니다.

핵심 요약 (TL;DR)

제공업체 네이티브 “JSON 모드” JSON 스키마 강제 적용 일반적인 설정 옵션 비고
OpenAI 예 예 (1급 지원) response_format={"type":"json_schema", ...} Responses API 또는 Chat Completions를 통해 동작; 함수 호출(function calling)도 가능합니다.
Google Gemini 예 예 (1급 지원) response_schema= + response_mime_type="application/json" 스키마가 설정된 경우 엄격하게 검증된 JSON을 반환합니다.
Anthropic (Claude) 간접적 예 (Tool Use를 통한 JSON 스키마) tools=[{input_schema=...}] + tool_choice 모델이 스키마로 정의된 도구를 “호출”하도록 강제합니다. 스키마 형식의 인자가 반환됩니다.
Mistral 예 부분적 (JSON만 지원; 서버 측 스키마 없음) response_format={"type":"json_object"} JSON 형식을 보장하지만, 클라이언트 측에서 스키마에 맞춰 검증해야 합니다.
AWS Bedrock (플랫폼) 모델에 따라 다름 예 (Tool/Converse 스키마를 통해) toolConfig.tools[].toolSpec.inputSchema Bedrock의 Converse API는 JSON 스키마에 맞춰 도구 입력을 검증합니다.

LLM 구조화된 출력 - 일반 정보

LLM 구조화된 출력이란 대규모 언어 모델(LLMs)이 자유 형식의 텍스트를 생성하는 것이 아니라, 사전에 정의된 특정 형식 또는 구조를 엄격하게 준수하여 응답을 생성하는 능력을 말합니다. 이러한 구조화된 출력은 JSON, XML, 테이블, 템플릿 등의 형태를 취할 수 있어, 데이터를 기계가 읽을 수 있는 형태로 만들어 일관성을 유지하고 소프트웨어가 다양한 애플리케이션에서 쉽게 파싱하여 사용할 수 있게 합니다.

구조화된 출력은 일반적으로 끝이 없는 자연어 텍스트를 생성하는 기존 LLM 출력과 다릅니다. 대신 구조화된 출력은 JSON 객체처럼 정의된 키와 값 유형을 가진 스키마나 형식, 또는 출력의 특정 클래스(예: 다중 선택지 답변, 감정 클래스, 데이터베이스 행 형식)를 강제합니다. 이 접근 방식은 신뢰성을 높이고 오류와 환각(hallucination)을 줄이며, 데이터베이스, API, 워크플로우 등의 시스템으로의 통합을 단순화합니다.

LLM에서 구조화된 출력을 생성하는 과정에는 주로 다음과 같은 기법이 포함됩니다:

  • 모델이 원하는 형식으로 출력을 생성하도록 안내하는 상세한 프롬프트 지시사항을 지정합니다.
  • Python에서 Pydantic 같은 검증 및 파싱 도구를 사용하여 출력이 스키마와 일치하는지 확인합니다.
  • 때로는 문법 또는 유한 상태 머신(finite state machines)에 기반한 디코딩 제약 조건을 적용하여 형식에 대한 토큰 수준에서의 준수 여부를 보장합니다.

구조화된 LLM 출력의 이점에는 다음이 포함됩니다:

  • 기계 가독성 향상 및 통합 용이성.
  • 변동성 및 오류 감소.
  • 일관된 데이터 형식을 필요로 하는 작업의 예측 가능성 및 검증 가능성 향상.

그 외 과제로는 효과적인 스키마 설계, 복잡한 중첩 데이터 처리, 자유 형식 텍스트 생성에 비해 추론 능력에 잠재적 한계가 있을 수 있습니다.

전반적으로, 구조화된 출력을 통해 LLM은 인간이 읽기 쉬운 텍스트만이 아니라 정확하고 형식화된 데이터를 요구하는 애플리케이션에서 더욱 유용해집니다.

구조화된 출력 Python 예제

아래 모든 스니펫은 JSON으로 이벤트 정보를 추출합니다: {title, date, location}. 필요에 따라 키나 모델을 자유롭게 교체하십시오.

1) OpenAI — JSON 스키마 (엄격)

from openai import OpenAI
import json

client = OpenAI()

schema = {
    "name": "Event",
    "schema": {
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "date":  {"type": "string", "description": "YYYY-MM-DD"},
            "location": {"type": "string"}
        },
        "required": ["title", "date", "location"],
        "additionalProperties": False
    },
    "strict": True
}

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": "Extract the event from: 'PyData Sydney is on 2025-11-03 at Darling Harbour.'"}
    ],
    response_format={"type": "json_schema", "json_schema": schema},
)

data = json.loads(resp.choices[0].message.content)
print(data)

OpenAI의 Structured Outputs 기능은 서버 측에서 이 스키마를 강제합니다.


2) Google Gemini — response schema + JSON MIME

import google.generativeai as genai
from google.genai import types

# API 키로 설정
# genai.configure(api_key="your-api-key")

schema = types.Schema(
    type=types.Type.OBJECT,
    properties={
        "title": types.Schema(type=types.Type.STRING),
        "date": types.Schema(type=types.Type.STRING),
        "location": types.Schema(type=types.Type.STRING),
    },
    required=["title", "date", "location"],
    additional_properties=False,
)

resp = genai.generate_content(
    model="gemini-2.0-flash",
    contents="Extract the event from: 'PyData Sydney is on 2025-11-03 at Darling Harbour.'",
    generation_config=genai.GenerationConfig(
        response_mime_type="application/json",
        response_schema=schema,
    ),
)

print(resp.text)  # 이미 스키마에 맞는 유효한 JSON

Gemini는 response_schema에 부합하는 엄격한 JSON을 반환합니다.


3) Anthropic (Claude) — JSON 스키마를 이용한 Tool Use

from anthropic import Anthropic
import json

client = Anthropic()

tool = {
    "name": "extract_event",
    "description": "Return event details.",
    "input_schema": {
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "date": {"type": "string"},
            "location": {"type": "string"}
        },
        "required": ["title", "date", "location"],
        "additionalProperties": False
    }
}

msg = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=256,
    tools=[tool],
    tool_choice={"type": "tool", "name": "extract_event"},  # 스키마 강제
    messages=[{"role": "user", "content":
        "Extract the event from: 'PyData Sydney is on 2025-11-03 at Darling Harbour.'"}],
)

# Claude는 도구를 "호출"합니다. 인자(스키마와 일치)를 가져옵니다.
tool_use = next(b for b in msg.content if b.type == "tool_use")
print(json.dumps(tool_use.input, indent=2))

Claude에는 일반적인 “JSON 모드” 토글 스위치가 없습니다. 대신, **input_schema**를 가진 Tool Use를 통해 검증된, 스키마 형태의 인자를 얻을 수 있습니다(그 사용도 강제 가능).


4) Mistral — JSON 모드 (클라이언트 측 검증)

from mistralai import Mistral
import json

client = Mistral()

resp = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role":"user","content":
        "Return JSON with keys title, date (YYYY-MM-DD), location for: "
        "'PyData Sydney is on 2025-11-03 at Darling Harbour.'"}],
    response_format={"type": "json_object"}  # 유효한 JSON 보장
)

data = json.loads(resp.choices[0].message.content)
print(data)
# 팁: `data`를 로컬에서 Pydantic/JSON 스키마에 대해 검증하십시오.

Mistral의 json_object는 JSON 형식을 강제합니다(사용자의 정확한 스키마가 아님) — 클라이언트 측에서 검증하십시오.


5) AWS Bedrock — Converse API Tool 스키마 (모델 비종속적)

import boto3, json

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0"

tools = [{
    "toolSpec": {
        "name": "extract_event",
        "inputSchema": {
            "json": {
                "type": "object",
                "properties": {
                    "title": {"type": "string"},
                    "date": {"type": "string"},
                    "location": {"type": "string"}
                },
                "required": ["title","date","location"],
                "additionalProperties": False
            }
        }
    }
}]

resp = bedrock.converse(
    modelId=model_id,
    toolConfig={"tools": tools},
    toolChoice={"tool": {"name": "extract_event"}},  # 스키마 강제
    messages=[{"role":"user","content":[{"text":
        "Extract the event from: 'PyData Sydney is on 2025-11-03 at Darling Harbour.'"}]}],
)

# toolUse 내용 추출
tool_use = next(
    c["toolUse"] for c in resp["output"]["message"]["content"] if "toolUse" in c
)
print(json.dumps(tool_use["input"], indent=2))

Bedrock은 JSON 스키마에 대해 도구 입력을 검증하며, 많은 호스팅 모델(예: Claude)은 Converse를 통해 이를 지원합니다.


실무 가이드 및 검증

  • 가장 강력한 서버 측 보장을 원할 경우: OpenAI 구조화된 출력 또는 Gemini 응답 스키마를 사용하세요.
  • 이미 Claude/Bedrock을 사용 중이라면: JSON 스키마를 가진 Tool을 정의하고 그 사용을 강제하십시오. 도구의 **인자(arguments)**를 유형 지정된 객체로 읽으세요.
  • Mistral을 사용 중이라면: json_object를 활성화하고 로컬에서(예: Pydantic 사용) 검증하십시오.

검증 패턴 (모두에게 적용 가능)

from pydantic import BaseModel, ValidationError

class Event(BaseModel):
    title: str
    date: str
    location: str

try:
    event = Event.model_validate(data)  # 어떤 제공업체이든 `data`
except ValidationError as e:
    # e.errors()를 사용하여 처리 / 재시도 / 모델에게 수정 요청
    print(e)

유용한 링크

구독하기

시스템, 인프라, AI 엔지니어링에 관한 새 글을 받아보세요.