본문으로 건너뛰기

구조화 출력(JSON)으로 LLM 응답 받기

"JSON으로 답해 줘"라고 부탁하는 대신 받을 모양을 스키마로 정해 두면, 코드에서 바로 쓸 수 있는 응답이 돌아옵니다. 고객 문의 분류를 예로 정리했습니다.

실무개발자

15분2026.10 기준

이 글을 마치면

  • 프롬프트로 JSON을 부탁할 때 어떤 문제가 생기고, 구조화 출력이 무엇을 막아 주는지 설명할 수 있습니다.
  • Pydantic 모델로 스키마를 정하고 messages.parse로 검증된 결과를 받을 수 있습니다.
  • 스키마가 보장하지 못하는 내용 오류와 예외 응답을 시험 표본과 stop_reason으로 걸러 낼 수 있습니다.
비스듬히 놓인 줄글 카드가 이름표와 빈칸이 짝지어진 틀 카드를 지나, 중괄호 안에 값이 가지런히 채워지고 확인 표시가 붙은 카드로 이어지는 일러스트
일러스트: AI Ping

이런 분께

  • LLM 답에서 값을 뽑아 DB나 화면에 넣어야 하는 개발자
  • json.loads 오류와 빠진 필드 때문에 예외 처리가 늘어난 분

준비물

  • Python과 anthropic SDK, pydantic 패키지
  • 'LLM API 첫 호출하기' 글의 1~3단계를 마친 상태

단계별 따라하기

  1. "JSON으로 답해 줘"만으로는 부족한 이유

    프롬프트에 "JSON으로만 답해 줘"라고 적으면 대부분은 그렇게 답합니다. 하지만 수백, 수천 건을 처리하다 보면 가끔 모양이 어긋나고, 그 한 건이 프로그램을 멈춥니다.

    구조화 출력은 응답을 내가 정한 JSON 스키마에 맞게만 생성하도록 제한하는 기능입니다. 문법이 틀린 JSON이나 빠진 필드, 다른 타입 때문에 다시 요청하는 일을 크게 줄여 줍니다.

    프롬프트로만 JSON을 부탁할 때 생기는 일 (예시)
    문제이런 응답코드에서는
    앞뒤에 설명이 붙음"분류 결과는 다음과 같습니다:" 뒤에 JSONjson.loads에서 오류
    코드 블록 표시가 붙음```json으로 감싼 JSON그대로 파싱하면 오류
    필드가 빠짐category만 있고 summary가 없음KeyError
    타입이 다름urgent에 불리언 대신 문자열 "네"조건문이 엉뚱하게 동작
  2. 받을 모양을 스키마로 정하기

    이 글에서는 고객 문의를 받아 분류(category), 급한지 여부(urgent), 한 문장 요약(summary)을 돌려받는 예를 씁니다. 먼저 필드마다 타입과 허용 값을 정합니다.

    Python에서는 Pydantic 모델로 스키마를 정하는 방법이 가장 간단합니다. 스키마 파일이 따로 있다면 JSON 스키마를 그대로 써도 됩니다.

    고객 문의 분류에 쓸 필드 (예시)
    필드타입규칙
    category문자열delivery, refund, payment, other 중 하나
    urgent참/거짓고객이 기한을 들어 서둘러 달라고 하면 true
    summary문자열문의 내용을 한 문장으로
    프롬프트 예시
    고객 문의 분류 결과를 받을 Pydantic 모델을 만들어 줘.
    category는 delivery, refund, payment, other 중 하나, urgent는 참/거짓, summary는 문자열이야. 셋 다 꼭 있어야 해.
    AI 답변 예시

    기본값이 없는 필드는 모두 필수가 되므로 셋 다 따로 표시할 필요가 없습니다. SDK가 이 모델을 JSON 스키마로 바꿔 보내면서, 정하지 않은 필드가 끼어들지 않도록 additionalProperties: false도 붙여 줍니다.

    코드 예시
    from typing import Literal from pydantic import BaseModel class Inquiry(BaseModel): category: Literal["delivery", "refund", "payment", "other"] urgent: bool summary: str
    Literal["delivery", ...]
    정해 둔 값 중 하나만 허용합니다. JSON 스키마에서는 허용 값 목록(enum)이 됩니다.
    urgent: bool
    참/거짓 값입니다. 문자열 "true"가 아니라 진짜 불리언으로 받습니다.
    summary: str
    문자열입니다. 길이 제한 같은 조건은 아래 5단계를 참고하세요.
  3. messages.parse로 요청하고 결과 꺼내기

    anthropic SDK의 messages.parse에 모델 클래스를 넘기면, SDK가 스키마를 output_config.format으로 보내고 돌아온 응답을 검사한 뒤 객체로 돌려줍니다. 문자열을 직접 파싱할 필요가 없습니다.

    JSON 스키마를 직접 쓰는 경우에는 messages.create에 output_config={"format": {"type": "json_schema", "schema": ...}}를 넣고, 응답의 text 블록을 json.loads로 읽습니다.

    프롬프트 예시
    앞의 Inquiry 모델로 아래 고객 문의를 분류하는 코드를 써 줘. anthropic SDK의 messages.parse를 쓰고 모델은 claude-opus-5-5로 해 줘.
    문의: 주문한 지 일주일이 넘었는데 아직 ‘배송 준비 중’이에요. 이번 주 토요일 행사에 써야 해서 금요일까지는 꼭 받아야 합니다.
    AI 답변 예시

    Inquiry를 output_format으로 넘기면 검사를 통과한 결과가 parsed_output에 Inquiry 객체로 들어옵니다.

    실행 결과 예: delivery True 일주일 넘게 ‘배송 준비 중’인 주문을 금요일까지 받아야 한다는 문의

    코드 예시
    import anthropic # 2단계의 Inquiry 클래스를 이 자리에 둡니다 client = anthropic.Anthropic() text = "주문한 지 일주일이 넘었는데 아직 ‘배송 준비 중’이에요. 이번 주 토요일 행사에 써야 해서 금요일까지는 꼭 받아야 합니다." response = client.messages.parse( model="claude-opus-5-5", max_tokens=1024, messages=[{"role": "user", "content": f"다음 고객 문의를 분류해 줘: {text}"}], output_format=Inquiry, ) inquiry = response.parsed_output print(inquiry.category, inquiry.urgent, inquiry.summary)
    client.messages.parse(...)
    구조화 출력용 SDK 도우미입니다. 응답을 스키마로 검사한 뒤 돌려줍니다.
    output_format=Inquiry
    받을 모양입니다. SDK가 JSON 스키마로 바꿔 output_config.format으로 보냅니다.
    response.parsed_output
    검사를 통과한 Inquiry 객체입니다. inquiry.category처럼 속성으로 꺼내 씁니다.

    확인해 보기

    • category가 네 값 중 하나인지, urgent가 True나 False인지 보세요. 문의 문장을 바꿔 여러 번 돌려도 이 모양은 그대로여야 합니다.
  4. 스키마가 막아 주지 못하는 것

    구조화 출력은 응답의 모양을 지켜 줄 뿐 내용이 맞는지는 보장하지 않습니다. 분류가 팀의 기준과 다르거나 요약이 문의와 어긋날 수 있으니, 정답을 아는 문의 몇 건으로 먼저 시험하세요.

    모양이 어긋나는 예외도 있습니다. 안전상 거절해 stop_reason이 refusal이거나 상한에 걸려 max_tokens로 끝난 응답은 스키마와 맞지 않을 수 있습니다. 결과를 쓰기 전에 stop_reason을 확인하고, 이런 응답은 다시 시도하거나 사람에게 넘기세요.

    정답을 아는 문의 5건으로 시험한 결과 (예시)
    문의 (요약)기대결과일치
    쿠폰 코드를 넣었는데 할인이 안 돼요paymentpayment○
    환불 신청했는데 언제 들어오나요?refundrefund○
    주소를 잘못 적었는데 바꿀 수 있나요?deliverydelivery○
    카드 결제가 두 번 됐어요. 하나 취소해 주세요paymentrefund×
    회원 탈퇴는 어디서 하나요?otherother○
    5건4건 일치 (80%)
    프롬프트 예시
    문의 5건으로 시험해 보니 '카드 결제가 두 번 됐어요'를 refund로 분류했어. 우리 팀 기준으로는 결제 오류라 payment가 맞아.
    네 가지 분류와 urgent의 기준을 시스템 프롬프트에 넣을 수 있게 짧게 정리해 줘. 애매한 경우를 어디로 보낼지도 적어 줘.
    AI 답변 예시

    시스템 프롬프트에 아래 기준을 넣어 보세요.

    delivery: 배송 지연, 배송지 변경, 파손처럼 물건이 오는 과정의 문제

    refund: 반품·환불 신청과 환불 일정 문의

    payment: 결제 오류, 중복 결제, 쿠폰·할인 적용처럼 돈을 내는 과정의 문제. 중복 결제를 취소해 달라는 요청도 여기에 넣습니다.

    other: 위 셋에 해당하지 않는 문의 (회원 정보, 이용 방법 등)

    urgent: 고객이 날짜나 기한을 들어 서둘러 달라고 하면 true, 아니면 false

    확인해 보기

    • 기준을 넣은 뒤 같은 5건을 다시 돌려 모두 기대와 맞는지 확인하세요.
    • 실제로 쓰기 전에는 지난 문의 수십 건으로 표본을 늘려 정확도를 다시 재 보는 편이 안전합니다.
  5. 실제로 쓰기 전에 알아 둘 것

    구조화 출력은 응답 모양을 제한하기 위해 스키마를 문법(grammar)으로 바꿔 씁니다. 그래서 속도, 비용, 지원하는 스키마 기능에 몇 가지 특징이 있습니다.

    운영 전에 알아 둘 특징
    특징내용대처
    첫 요청이 느림새 스키마는 처음 쓸 때 문법을 만드느라 지연이 생김. 만든 문법은 마지막 사용 뒤 24시간 동안 캐시됨배포 직후 한 번 미리 호출해 두기
    입력 토큰이 조금 늘어남출력 형식을 설명하는 시스템 프롬프트가 자동으로 더해짐비용을 어림할 때 포함하기
    지원하지 않는 제약minimum, maxLength 같은 숫자·길이 제약은 API가 받지 않음. SDK 도우미는 이 조건을 필드 설명으로 옮기고 응답을 원래 조건으로 검사함길이 제한은 프롬프트로 안내하고 코드에서도 확인
    스키마를 바꾸면 캐시가 깨짐output_config.format이 바뀌면 그 대화의 프롬프트 캐시가 무효화됨스키마는 자주 바꾸지 않기

    확인해 보기

    • 새 스키마로 보낸 첫 요청이 유난히 느렸다면 문법을 만드는 시간 때문일 수 있습니다. 같은 스키마로 두 번째 요청을 보내 빨라지는지 보세요.

자주 하는 실수

  • "JSON으로만 답해 줘"라는 프롬프트만 믿고 파싱 오류 처리를 빼먹기
  • 모양이 맞으니 내용도 맞을 거라고 보고 시험 없이 바로 쓰기
  • stop_reason을 확인하지 않아 거절되거나 잘린 응답까지 그대로 저장하기

참고 자료

더 배워 보기