LLM 구조화 출력: JSON 스키마와 Zod로 응답 형식을 보장하고 검증하기
Claude 구조화 출력 설정, 지원되지 않는 스키마 기능, 거절·토큰 한도 처리
핵심 요약
- Claude 구조화 출력은 output_config.format으로 스키마에 맞는 JSON 응답을 제약된 디코딩으로 보장한다.
- 숫자·문자열 길이 제약, 재귀 스키마, 외부 $ref는 지원되지 않으므로 수신 후 검증으로 처리한다.
- 거절과 max_tokens 중단 시에는 스키마가 깨질 수 있으므로 stop_reason을 먼저 확인한다.
- 형식 보장과 내용 정확성은 별개이므로 원문 대조와 평가 세트로 내용을 검증한다.
LLM으로 이메일에서 고객 정보를 뽑거나, 문의 내용을 분류하거나, 문서를 요약해 DB에 넣으려면 응답이 정해진 JSON 형태여야 한다. 프롬프트에 "JSON으로만 답해"라고 적는 방식은 가끔 앞뒤에 설명 문장이 붙거나 필드가 빠져 파싱 오류를 낸다. Claude의 구조화 출력(Structured outputs) 문서는 제약된 디코딩으로 스키마에 맞는 응답을 보장한다고 설명한다. 이 글은 설정 방법과 함께, 보장이 깨지는 예외 상황과 애플리케이션 쪽 검증까지 정리한다. 환각을 줄이는 검증 방법은 LLM 환각 줄이기 글에서 다뤘다.
두 가지 기능: JSON 출력과 엄격한 도구 사용
| 기능 | 설정 | 보장하는 것 |
|---|---|---|
| JSON 출력 | output_config.format에 type: "json_schema"와 스키마 | 모델의 최종 응답이 스키마에 맞는 JSON |
| 엄격한 도구 사용 | 도구 정의에 strict: true | 도구 이름과 입력값이 input_schema에 맞음 |
두 기능은 한 요청에서 함께 쓸 수 있다. 문서는 예전 output_format 매개변수가 deprecated되었다고 밝히므로 새 코드는 output_config.format을 쓴다. 지원 모델 목록은 문서에서 확인한다.
1단계: Zod 스키마로 요청하기(TypeScript)
import Anthropic from '@anthropic-ai/sdk'
import { z } from 'zod'
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod'
const Ticket = z.object({
category: z.enum(['billing', 'bug', 'account', 'other']),
urgency: z.enum(['low', 'medium', 'high']),
summary: z.string(),
customer_email: z.string(),
})
const client = new Anthropic()
const response = await client.messages.parse({
model: 'claude-sonnet-5-5',
max_tokens: 1024,
messages: [{ role: 'user', content: '다음 문의를 분류해 줘:\n' + inquiryText }],
output_config: { format: zodOutputFormat(Ticket) },
})
const ticket = response.parsed_output // 스키마로 파싱·검증된 객체SDK의 parse 헬퍼는 응답을 스키마로 파싱한 결과를 parsed_output으로 돌려준다. Python에서는 Pydantic 모델을 같은 방식으로 넘길 수 있다.
2단계: 지원되지 않는 스키마 기능 피하기
제약된 디코딩은 모든 JSON Schema 기능을 지원하지 않는다. 문서가 밝힌 주요 제한은 다음과 같다.
| 지원하지 않음 | 대안 |
|---|---|
| 재귀 스키마 | 깊이를 고정한 구조로 펼침 |
숫자 제약(minimum, maximum, multipleOf) | 응답 수신 후 애플리케이션에서 검증 |
문자열 길이 제약(minLength, maxLength) | 수신 후 검증, 필요 시 잘라내기 |
배열 minItems 0·1 외의 제약 | 수신 후 개수 검사 |
외부 $ref | 내부 $defs로 옮김 |
additionalProperties를 false 외 값으로 | 객체 필드를 명시 |
반면 기본 타입, enum, const, anyOf, allOf, 내부 $ref, date-time·email·uuid 같은 문자열 형식은 지원된다. Zod 스키마에 .min(), .max()를 걸어 두었다면 그 제약은 디코딩 단계가 아니라 응답 이후 검증에서 처리된다고 보고 설계한다.
3단계: 보장이 깨지는 세 가지 경우 처리하기
- 거절(
stop_reason: "refusal"): 안전상의 이유로 거절하면 상태 코드는 200이고 토큰 비용도 청구되지만, 거절 메시지가 우선하므로 출력이 스키마와 맞지 않을 수 있다. - 토큰 한도(
stop_reason: "max_tokens"): 응답이 잘려 불완전한 JSON이 될 수 있다. 문서는max_tokens를 늘려 다시 요청하라고 안내한다. - 열거형 대소문자: 문자열
enum·const값의 대소문자는 보장되지 않아, 공백 뒤 단어의 첫 글자가 다르게 나올 수 있다. 열거값은 대소문자를 무시하고 비교한다.
if (response.stop_reason === 'refusal') {
return { ok: false, reason: 'refused' } // 사람 검토 대기열로
}
if (response.stop_reason === 'max_tokens') {
return { ok: false, reason: 'truncated' } // max_tokens를 늘려 재시도
}
const parsed = Ticket.safeParse(response.parsed_output)
if (!parsed.success) return { ok: false, reason: 'schema' }4단계: 형식이 맞아도 내용은 따로 검증한다
구조화 출력은 형식을 보장할 뿐 내용의 정확성을 보장하지 않는다. 고객 이메일 필드에 원문에 없는 주소가 들어갈 수도 있다. 다음 검증을 애플리케이션에 둔다.
- 추출한 값이 원문에 실제로 존재하는지 문자열 대조로 확인한다(이메일, 주문 번호 등).
- 숫자·길이 제약은 Zod의
safeParse로 다시 검사한다. - 분류 결과의 분포를 모니터링해 특정 범주로 쏠리는 변화를 감지한다.
- 평가용 샘플 세트를 만들어 프롬프트·모델 변경 때마다 정확도를 비교한다.
배포 전 평가 항목은 LLM 배포 전 검증 체크리스트 글을, 운영 지표는 LLMOps 모니터링 글을 참고한다.
초보자가 자주 실수하는 포인트
- 프롬프트에 "JSON으로만 답해"라고만 적고 파싱 실패를 재시도로 해결하려는 경우
- 스키마에 minLength·maximum을 넣고 모델이 지킬 것이라고 가정하는 경우
- stop_reason을 확인하지 않고 parsed_output을 바로 사용하는 경우
- 열거값을 대소문자 구분 비교해 정상 응답을 오류로 처리하는 경우
체크리스트
- output_config.format을 사용하고 deprecated된 output_format을 쓰지 않는가
- 스키마에 지원되지 않는 제약이 없는가
- refusal과 max_tokens를 별도로 처리하는가
- 응답 후 Zod safeParse로 2차 검증하는가
- 추출 값이 원문에 존재하는지 대조하는가
자주 묻는 질문
구조화 출력을 쓰면 재시도 로직이 필요 없나요?
스키마 위반으로 인한 재시도는 줄어들지만, 거절·토큰 한도·네트워크 오류에 대한 처리는 여전히 필요하다.
도구 호출 입력도 보장되나요?
도구 정의에 strict: true를 주는 엄격한 도구 사용 기능이 도구 이름과 입력값의 스키마 준수를 보장한다.
Python에서도 같은 기능을 쓸 수 있나요?
문서는 Python SDK에서 Pydantic 모델을 넘겨 parsed_output을 받는 예시를 제공한다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.