← All guides

Claude API JSON 구조화 출력 완전 가이드: Tool Use vs 프롬프트 방식

Claude API에서 안정적인 JSON 응답 받기 — Tool Use 스키마 정의, Pydantic 검증, 파싱 에러 처리, 실패율 0% 달성 패턴. Python 코드 예제 포함.

Claude API JSON 구조화 출력 완전 가이드: Tool Use vs 프롬프트 방식

Claude API에서 JSON을 안정적으로 받는 방법은 두 가지다: (1) Tool Use로 스키마를 강제하거나, (2) 시스템 프롬프트로 JSON 전용 응답을 유도하는 것이다. Tool Use 방식은 스키마 위반 자체가 불가능하므로 프로덕션에서 권장된다. 프롬프트 방식은 코드 수정 없이 빠르게 시험할 때 유용하다. 두 방법 모두 Pydantic 검증과 에러 처리를 조합하면 실패율 0%에 가까운 파이프라인을 구축할 수 있다.


방법 1: Tool Use로 JSON 강제 (권장)

Tool Use는 Claude가 반드시 지정된 JSON 스키마 형태로 응답하도록 강제한다. 마크다운 코드 블록, 부가 설명, 필드명 오류가 원천 차단된다. tool_choice{"type": "tool", "name": "..."} 으로 고정하면 Claude는 해당 도구만 호출한다.

import anthropic
import json

client = anthropic.Anthropic()

tools = [{
    "name": "extract_info",
    "description": "정보를 구조화된 형식으로 추출합니다",
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string", "description": "이름"},
            "email": {"type": "string", "description": "이메일"},
            "summary": {"type": "string", "description": "요약"}
        },
        "required": ["name", "email", "summary"]
    }
}]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "extract_info"},
    messages=[{"role": "user", "content": "다음 텍스트에서 정보를 추출하세요: ..."}]
)

# Tool input은 이미 dict — json.loads() 불필요
for block in response.content:
    if block.type == "tool_use":
        data = block.input  # 바로 사용 가능
        print(data)

핵심 포인트: block.input은 이미 파이썬 딕셔너리다. json.loads()를 따로 호출할 필요가 없다. 응답이 tool_use 타입 블록이 아니라면 API 호출 자체가 실패한 것이다.


방법 2: 프롬프트 엔지니어링으로 JSON 유도

빠른 프로토타이핑이나 도구 스키마를 정의하기 어려운 복잡한 케이스에는 시스템 프롬프트로 JSON을 유도할 수 있다. 단, 이 방식은 Claude 모델 버전이나 입력에 따라 마크다운 래핑, 불필요한 텍스트 앞/뒤 삽입이 발생할 수 있다.

import anthropic
import json
import re

client = anthropic.Anthropic()

system_prompt = """당신은 데이터 추출 전문가입니다.

규칙:
1. 반드시 유효한 JSON만 응답하세요.
2. 마크다운 코드 블록(```json ... ```)을 사용하지 마세요.
3. JSON 앞뒤에 어떤 설명도 추가하지 마세요.

응답 형식 예시:
{"name": "홍길동", "email": "hong@example.com", "summary": "개발자"}
"""

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    system=system_prompt,
    messages=[{
        "role": "user",
        "content": "다음 텍스트에서 정보를 추출하세요: 홍길동, hong@example.com, 5년 경력 백엔드 개발자"
    }]
)

raw_text = response.content[0].text

# 마크다운 코드 블록 제거 (안전 처리)
cleaned = re.sub(r"```(?:json)?\s*|\s*```", "", raw_text).strip()
data = json.loads(cleaned)
print(data)

프롬프트 방식의 주의점: json.loads() 호출 전 항상 마크다운 래퍼를 제거하는 정규식을 거쳐야 한다. 그렇지 않으면 JSONDecodeError가 빈번하게 발생한다.


두 방법 비교

항목 Tool Use (권장) 프롬프트 방식
신뢰성 매우 높음 (스키마 강제) 중간 (프롬프트 의존)
구현 복잡도 중간 (스키마 정의 필요) 낮음 (빠른 시작)
파싱 작업 없음 (block.input이 dict) 필요 (래퍼 제거 + json.loads)
스키마 검증 API 레벨에서 자동 별도 Pydantic 필요
권장 사용 케이스 프로덕션, 자동화 파이프라인 프로토타입, 탐색 단계
비용 동일 (도구 호출 추가 토큰 소폭 증가) 동일

프로덕션 환경에서는 항상 Tool Use를 사용하라. 프롬프트 방식은 스키마를 확정하기 전 빠른 실험 단계에서만 쓰는 것이 좋다.


Pydantic으로 응답 검증

Tool Use로도 input_schema에 정의되지 않은 비즈니스 로직 규칙(예: 이메일 형식, 숫자 범위)은 API가 검증하지 않는다. Pydantic을 함께 쓰면 타입 안전성과 런타임 검증을 동시에 확보할 수 있다.

from pydantic import BaseModel, EmailStr, validator
import anthropic

class ExtractedInfo(BaseModel):
    name: str
    email: EmailStr          # 이메일 형식 자동 검증
    summary: str
    experience_years: int

    @validator("experience_years")
    def check_experience(cls, v):
        if v < 0 or v > 50:
            raise ValueError(f"경력 연수 범위 오류: {v}")
        return v

client = anthropic.Anthropic()

tools = [{
    "name": "extract_resume",
    "description": "이력서에서 핵심 정보를 추출합니다",
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "summary": {"type": "string"},
            "experience_years": {"type": "integer"}
        },
        "required": ["name", "email", "summary", "experience_years"]
    }
}]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "extract_resume"},
    messages=[{"role": "user", "content": "이력서 텍스트: ..."}]
)

for block in response.content:
    if block.type == "tool_use":
        # Pydantic으로 검증 — 실패 시 ValidationError 발생
        info = ExtractedInfo(**block.input)
        print(info.model_dump())

Pydantic의 ValidationError는 어떤 필드가 왜 잘못됐는지 명확한 메시지를 제공한다. 이를 로깅에 활용하면 스키마를 반복 개선하는 데 유용하다.


파싱 에러 처리 패턴

에러 처리 없는 JSON 파이프라인은 프로덕션에서 반드시 실패한다. 아래는 재시도 로직과 폴백을 포함한 방어적 패턴이다.

import anthropic
import json
import re
import logging
from pydantic import BaseModel, ValidationError
from typing import Optional

logger = logging.getLogger(__name__)

def safe_extract(text: str, model_class: type[BaseModel], max_retries: int = 2) -> Optional[BaseModel]:
    """
    Tool Use 기반 추출 + Pydantic 검증 + 재시도 로직
    """
    client = anthropic.Anthropic()

    # Tool Use 스키마를 Pydantic 모델에서 자동 생성 가능 (또는 수동 정의)
    tools = [{
        "name": "extract_data",
        "description": "텍스트에서 구조화 데이터를 추출합니다",
        "input_schema": model_class.model_json_schema()
    }]

    for attempt in range(max_retries + 1):
        try:
            response = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=1024,
                tools=tools,
                tool_choice={"type": "tool", "name": "extract_data"},
                messages=[{"role": "user", "content": f"다음에서 정보를 추출하세요:\n{text}"}]
            )

            for block in response.content:
                if block.type == "tool_use":
                    return model_class(**block.input)  # Pydantic 검증

        except ValidationError as e:
            logger.warning(f"시도 {attempt + 1}: Pydantic 검증 실패 — {e}")
            if attempt == max_retries:
                logger.error("최대 재시도 횟수 초과. None 반환.")
                return None

        except Exception as e:
            logger.error(f"API 호출 오류: {e}")
            return None

    return None

에러 처리의 3원칙:

  1. ValidationError 전용 재시도 — API 에러(anthropic.APIError)와 스키마 불일치(ValidationError)를 분리해서 처리한다.
  2. 재시도 횟수 제한 — 무한 루프 방지를 위해 max_retries를 2-3으로 제한한다.
  3. None 반환 vs 예외 발생 — 파이프라인 중단이 필요 없는 배치 작업에서는 None을 반환하고, 실시간 서비스에서는 예외를 상위로 전파한다.

에러 처리에 대한 더 자세한 내용은 Claude API 에러 처리 가이드를 참고하라.


실전 예제: 이력서 파싱, 제품 분류, 감성 분석

예제 1: 이력서 파싱

이력서 텍스트에서 구조화 데이터를 추출하는 가장 일반적인 use case다.

tools = [{
    "name": "parse_resume",
    "description": "이력서를 파싱합니다",
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "phone": {"type": "string"},
            "skills": {
                "type": "array",
                "items": {"type": "string"}
            },
            "experience_years": {"type": "integer"},
            "latest_job_title": {"type": "string"}
        },
        "required": ["name", "email", "skills", "experience_years"]
    }
}]

resume_text = """
홍길동
hong@example.com | 010-1234-5678
시니어 백엔드 엔지니어 (8년 경력)
기술 스택: Python, FastAPI, PostgreSQL, Redis, Docker
"""

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=512,
    tools=tools,
    tool_choice={"type": "tool", "name": "parse_resume"},
    messages=[{"role": "user", "content": resume_text}]
)

for block in response.content:
    if block.type == "tool_use":
        print(block.input)
# 출력: {"name": "홍길동", "email": "hong@example.com", "skills": ["Python", "FastAPI", ...], "experience_years": 8, ...}

예제 2: 제품 분류

이커머스에서 상품 설명을 카테고리와 태그로 자동 분류한다.

tools = [{
    "name": "classify_product",
    "description": "제품을 카테고리와 태그로 분류합니다",
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {
                "type": "string",
                "enum": ["전자제품", "의류", "식품", "도서", "스포츠", "기타"]
            },
            "subcategory": {"type": "string"},
            "tags": {"type": "array", "items": {"type": "string"}},
            "confidence": {"type": "number", "minimum": 0, "maximum": 1}
        },
        "required": ["category", "tags", "confidence"]
    }
}]

예제 3: 감성 분석

고객 리뷰나 소셜 미디어 텍스트의 감성을 구조화된 형식으로 분석한다.

tools = [{
    "name": "analyze_sentiment",
    "description": "텍스트의 감성을 분석합니다",
    "input_schema": {
        "type": "object",
        "properties": {
            "sentiment": {
                "type": "string",
                "enum": ["매우_긍정", "긍정", "중립", "부정", "매우_부정"]
            },
            "score": {"type": "number", "minimum": -1, "maximum": 1},
            "key_phrases": {"type": "array", "items": {"type": "string"}},
            "emotion": {
                "type": "string",
                "enum": ["기쁨", "만족", "중립", "불만", "분노", "슬픔"]
            }
        },
        "required": ["sentiment", "score", "key_phrases"]
    }
}]

세 예제 모두 tool_choice로 특정 도구를 강제 호출하는 패턴이 동일하다. 스키마의 enum을 활용하면 출력값을 미리 정의된 목록으로 제한할 수 있어 후속 처리가 간단해진다.

Claude API 구조화 출력의 영어 심화 가이드는 Claude API Structured Output을 참고하라. API 전반에 대한 입문 내용은 Claude API 한국어 입문을 확인하라.


Agent SDK Cookbook으로 더 빠르게 구축하기

구조화 출력, Tool Use, 멀티턴 대화, 에이전트 루프를 바로 쓸 수 있는 30개 이상의 프로덕션 레시피가 담긴 가이드북.

→ Claude Agent SDK Cookbook 구매하기 ($49)


Frequently Asked Questions

Tool Use 방식과 프롬프트 방식 중 어느 것을 써야 하나요?

프로덕션 환경에서는 항상 Tool Use를 사용하라. tool_choice로 특정 도구를 강제하면 Claude는 반드시 해당 스키마에 맞는 JSON을 반환하며, 응답 파싱 코드가 단순해지고 마크다운 래퍼나 부가 텍스트로 인한 파싱 오류가 원천 차단된다. 프롬프트 방식은 스키마를 아직 확정하지 않은 프로토타입 단계에서만 임시로 활용하라.

tool_choiceauto로 설정하면 어떻게 되나요?

tool_choice: auto는 Claude가 도구를 호출할지 말지를 스스로 판단한다. 이 경우 Claude가 도구를 호출하지 않고 텍스트로만 응답할 수 있어 JSON 강제 효과가 없다. JSON을 반드시 받아야 할 때는 {"type": "tool", "name": "도구명"}으로 명시해야 한다.

Pydantic 검증이 실패할 때 어떻게 처리해야 하나요?

ValidationError를 잡아서 로깅한 뒤, 동일한 입력으로 최대 2-3회 재시도하는 것이 표준 패턴이다. 재시도 시 시스템 프롬프트에 실패한 필드와 기대값을 명시적으로 추가하면 성공률이 높아진다. 재시도 후에도 실패하면 해당 레코드를 별도 큐에 넣어 수동 검토하는 폴백을 마련하라.

JSON 스키마에서 enum을 쓰면 어떤 이점이 있나요?

enum 제약은 Claude가 스키마에 정의된 값 목록 중에서만 선택하도록 강제한다. 예를 들어 감성 분석에서 "positive", "neutral", "negative" 외 값이 절대 나오지 않도록 보장할 수 있다. 이를 통해 후속 if-else 분기나 데이터베이스 enum 컬럼 검증 로직을 크게 단순화할 수 있다.

Tool Use 사용 시 토큰 비용이 더 드나요?

도구 정의 스키마 자체가 입력 토큰에 포함되므로 소폭 증가한다. 일반적으로 스키마 크기에 따라 요청당 100-500 토큰 정도 추가된다. 프롬프트 캐싱(cache_control: ephemeral)을 도구 정의에 적용하면 반복 호출 시 이 추가 비용의 90%를 절감할 수 있다. 비용 절감 전략은 Claude API 비용 절감 가이드를 참고하라.

중첩된 JSON 스키마(nested object)도 지원되나요?

지원된다. input_schema에서 "type": "object" 안에 "properties"를 중첩하면 된다. 배열 안에 객체를 넣는 것도 가능하다("type": "array", "items": {"type": "object", ...}). 다만 중첩이 깊어질수록 스키마 토큰이 늘어나므로, 3단계 이상의 중첩은 평탄화를 고려하라.

AI Disclosure: Written with Claude Code.

도구와 자료