구조화 출력(JSON)으로 LLM 응답 받기
"JSON으로 답해 줘"라고 부탁하는 대신 받을 모양을 스키마로 정해 두면, 코드에서 바로 쓸 수 있는 응답이 돌아옵니다. 고객 문의 분류를 예로 정리했습니다.
15분2026.10 기준
이 글을 마치면
- 프롬프트로 JSON을 부탁할 때 어떤 문제가 생기고, 구조화 출력이 무엇을 막아 주는지 설명할 수 있습니다.
- Pydantic 모델로 스키마를 정하고 messages.parse로 검증된 결과를 받을 수 있습니다.
- 스키마가 보장하지 못하는 내용 오류와 예외 응답을 시험 표본과 stop_reason으로 걸러 낼 수 있습니다.
이런 분께
- LLM 답에서 값을 뽑아 DB나 화면에 넣어야 하는 개발자
- json.loads 오류와 빠진 필드 때문에 예외 처리가 늘어난 분
준비물
- Python과 anthropic SDK, pydantic 패키지
- 'LLM API 첫 호출하기' 글의 1~3단계를 마친 상태
단계별 따라하기
"JSON으로 답해 줘"만으로는 부족한 이유
프롬프트에 "JSON으로만 답해 줘"라고 적으면 대부분은 그렇게 답합니다. 하지만 수백, 수천 건을 처리하다 보면 가끔 모양이 어긋나고, 그 한 건이 프로그램을 멈춥니다.
구조화 출력은 응답을 내가 정한 JSON 스키마에 맞게만 생성하도록 제한하는 기능입니다. 문법이 틀린 JSON이나 빠진 필드, 다른 타입 때문에 다시 요청하는 일을 크게 줄여 줍니다.
프롬프트로만 JSON을 부탁할 때 생기는 일 (예시) 문제 이런 응답 코드에서는 앞뒤에 설명이 붙음 "분류 결과는 다음과 같습니다:" 뒤에 JSON json.loads에서 오류 코드 블록 표시가 붙음 ```json으로 감싼 JSON 그대로 파싱하면 오류 필드가 빠짐 category만 있고 summary가 없음 KeyError 타입이 다름 urgent에 불리언 대신 문자열 "네" 조건문이 엉뚱하게 동작 받을 모양을 스키마로 정하기
이 글에서는 고객 문의를 받아 분류(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: strLiteral["delivery", ...]- 정해 둔 값 중 하나만 허용합니다. JSON 스키마에서는 허용 값 목록(enum)이 됩니다.
urgent: bool- 참/거짓 값입니다. 문자열 "true"가 아니라 진짜 불리언으로 받습니다.
summary: str- 문자열입니다. 길이 제한 같은 조건은 아래 5단계를 참고하세요.
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인지 보세요. 문의 문장을 바꿔 여러 번 돌려도 이 모양은 그대로여야 합니다.
스키마가 막아 주지 못하는 것
구조화 출력은 응답의 모양을 지켜 줄 뿐 내용이 맞는지는 보장하지 않습니다. 분류가 팀의 기준과 다르거나 요약이 문의와 어긋날 수 있으니, 정답을 아는 문의 몇 건으로 먼저 시험하세요.
모양이 어긋나는 예외도 있습니다. 안전상 거절해 stop_reason이 refusal이거나 상한에 걸려 max_tokens로 끝난 응답은 스키마와 맞지 않을 수 있습니다. 결과를 쓰기 전에 stop_reason을 확인하고, 이런 응답은 다시 시도하거나 사람에게 넘기세요.
정답을 아는 문의 5건으로 시험한 결과 (예시) 문의 (요약) 기대 결과 일치 쿠폰 코드를 넣었는데 할인이 안 돼요 payment payment ○ 환불 신청했는데 언제 들어오나요? refund refund ○ 주소를 잘못 적었는데 바꿀 수 있나요? delivery delivery ○ 카드 결제가 두 번 됐어요. 하나 취소해 주세요 payment refund × 회원 탈퇴는 어디서 하나요? other other ○ 5건 4건 일치 (80%) 프롬프트 예시 문의 5건으로 시험해 보니 '카드 결제가 두 번 됐어요'를 refund로 분류했어. 우리 팀 기준으로는 결제 오류라 payment가 맞아. 네 가지 분류와 urgent의 기준을 시스템 프롬프트에 넣을 수 있게 짧게 정리해 줘. 애매한 경우를 어디로 보낼지도 적어 줘.
AI 답변 예시 시스템 프롬프트에 아래 기준을 넣어 보세요.
delivery: 배송 지연, 배송지 변경, 파손처럼 물건이 오는 과정의 문제
refund: 반품·환불 신청과 환불 일정 문의
payment: 결제 오류, 중복 결제, 쿠폰·할인 적용처럼 돈을 내는 과정의 문제. 중복 결제를 취소해 달라는 요청도 여기에 넣습니다.
other: 위 셋에 해당하지 않는 문의 (회원 정보, 이용 방법 등)
urgent: 고객이 날짜나 기한을 들어 서둘러 달라고 하면 true, 아니면 false
확인해 보기
- 기준을 넣은 뒤 같은 5건을 다시 돌려 모두 기대와 맞는지 확인하세요.
- 실제로 쓰기 전에는 지난 문의 수십 건으로 표본을 늘려 정확도를 다시 재 보는 편이 안전합니다.
실제로 쓰기 전에 알아 둘 것
구조화 출력은 응답 모양을 제한하기 위해 스키마를 문법(grammar)으로 바꿔 씁니다. 그래서 속도, 비용, 지원하는 스키마 기능에 몇 가지 특징이 있습니다.
운영 전에 알아 둘 특징 특징 내용 대처 첫 요청이 느림 새 스키마는 처음 쓸 때 문법을 만드느라 지연이 생김. 만든 문법은 마지막 사용 뒤 24시간 동안 캐시됨 배포 직후 한 번 미리 호출해 두기 입력 토큰이 조금 늘어남 출력 형식을 설명하는 시스템 프롬프트가 자동으로 더해짐 비용을 어림할 때 포함하기 지원하지 않는 제약 minimum, maxLength 같은 숫자·길이 제약은 API가 받지 않음. SDK 도우미는 이 조건을 필드 설명으로 옮기고 응답을 원래 조건으로 검사함 길이 제한은 프롬프트로 안내하고 코드에서도 확인 스키마를 바꾸면 캐시가 깨짐 output_config.format이 바뀌면 그 대화의 프롬프트 캐시가 무효화됨 스키마는 자주 바꾸지 않기 확인해 보기
- 새 스키마로 보낸 첫 요청이 유난히 느렸다면 문법을 만드는 시간 때문일 수 있습니다. 같은 스키마로 두 번째 요청을 보내 빨라지는지 보세요.
자주 하는 실수
- "JSON으로만 답해 줘"라는 프롬프트만 믿고 파싱 오류 처리를 빼먹기
- 모양이 맞으니 내용도 맞을 거라고 보고 시험 없이 바로 쓰기
- stop_reason을 확인하지 않아 거절되거나 잘린 응답까지 그대로 저장하기
참고 자료
- 구조화된 출력 — Claude Platform 문서 (Anthropic)
- Creating your first schema — JSON Schema
- Models — Pydantic Docs