구조화 출력 신뢰성 확보
스키마를 강제했으니 이제 응답은 항상 유효한 JSON이라고 믿고 싶지만, 그 믿음에는 구멍이 있다. 제약 디코딩은 모델이 정상적으로 끝까지 응답을 생성하는 한에서만 스키마를 보장한다. 모델이 안전상의 이유로 응답을 거부하거나, 토큰 한도에 걸려 중간에 잘리면 결과는 스키마를 만족하지 않는다. 이 절은 "스키마를 썼는데도 깨지는" 경우를 다룬다. CCA-F 시험의 신뢰성 영역에서 이 지점은 단골 출제 대상이며, 핵심은 stop_reason을 먼저 확인한 뒤에야 응답 본문을 파싱하라는 원칙이다.
스키마를 만족해도 깨지는 두 경우
첫째는 거부다. 응답의 stop_reason이 refusal이면, 모델이 안전상의 판단으로 작업을 거부한 것이고 이때 출력은 스키마를 따르지 않을 수 있다. 본문을 곧바로 json.loads에 넘기면 파싱 예외가 난다. 따라서 응답을 읽기 전에 stop_reason을 먼저 검사하고, refusal이면 별도 경로로 처리해야 한다.
둘째는 토큰 한도 초과다. stop_reason이 max_tokens이면 모델이 max_tokens 한도에 걸려 응답을 다 만들기 전에 멈춘 것이다. 잘린 JSON은 닫히지 않은 괄호로 끝나 파싱이 불가능하다. 계층적 스키마는 출력이 길어지기 쉬우므로 이 경우가 특히 잦다. 대처는 max_tokens를 충분히 높이는 것이다. 시험에서 "구조화 출력을 켰는데 JSON이 중간에 잘렸다"는 증상의 정답은 스키마를 고치는 선택지가 아니라 max_tokens를 늘리는 선택지다.
# /examples/15/reliability_check.pyimport anthropicimport jsonclient = anthropic.Anthropic()response = client.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=[{"role": "user", "content": "..."}], output_config={ "format": { "type": "json_schema", "schema": { "type": "object", "properties": {"summary": {"type": "string"}}, "required": ["summary"], "additionalProperties": False } } },)if response.stop_reason == "refusal": handle_refusal(response.stop_details)elif response.stop_reason == "max_tokens": raise RuntimeError("응답이 잘림. max_tokens를 높여 재시도")else: text = next(b.text for b in response.content if b.type == "text") data = json.loads(text) print(data["summary"])
이 분기 순서가 정답의 형태다. stop_reason을 먼저 검사하고, 정상 종료(end_turn)일 때만 본문을 파싱한다. 거부는 stop_details에 사유 범주가 담겨 오므로 그것으로 후속 처리를 분기할 수 있다.
스키마 컴파일과 24시간 캐시
성능 면에서 알아 둘 동작이 하나 있다. 새 스키마를 처음 사용하면 그 스키마를 디코딩 제약으로 변환하는 일회성 컴파일 비용이 발생해 첫 요청의 지연이 늘어난다. 이후에는 같은 스키마가 24시간 동안 캐시되어 후속 요청에는 그 비용이 들지 않는다. 시험에서 "구조화 출력을 처음 켰더니 첫 요청만 느렸다"는 증상이 나오면, 이는 결함이 아니라 스키마 컴파일이라는 정상 동작이며 동일 스키마 재사용으로 해결된다는 점이 정답이다. 실무에서는 스키마를 자주 바꾸지 않고 고정해 캐시 효과를 누리는 것이 바람직하다.
무엇과 함께 쓸 수 없고 무엇과 함께 쓰는가
구조화 출력에는 호환성 경계가 있다. 인용(Citations) 기능과는 함께 쓸 수 없으며, 두 기능을 같이 요청하면 400 오류가 난다. 또한 마지막 어시스턴트 턴 프리필과도 호환되지 않는다. 본디 claude-opus-4-8 같은 최신 모델에서는 프리필 자체가 거부되므로, 형식을 강제하려고 프리필을 시도하는 선택지는 이중으로 틀린 함정이다. 형식 강제의 올바른 도구는 프리필이 아니라 output_config.format이다.
반대로 잘 조합되는 기능도 분명하다. 구조화 출력은 배치 API, 스트리밍, 토큰 카운팅, 그리고 적응형 사고(thinking)와 함께 동작한다. 모델이 thinking으로 추론한 뒤 최종 응답을 스키마에 맞춰 내보내는 조합은 자연스럽다. 시험에서 "구조화 출력과 적응형 사고를 함께 쓸 수 있는가"라는 물음의 답은 가능하다는 쪽이다. 다만 이 모델군에서 사고 설정은 어디까지나 adaptive만 쓰며, 폐기된 budget_tokens나 temperature를 함께 넣는 선택지는 별개의 함정으로 등장한다.
정리
- 제약 디코딩은 모델이 끝까지 정상 생성하는 한에서만 스키마를 보장한다. 거부와 토큰 한도 초과에서는 스키마가 깨질 수 있다.
- stop_reason을 먼저 검사하고, refusal과 max_tokens를 분기 처리한 뒤 정상 종료일 때만 본문을 파싱한다. 잘린 JSON의 정답은 스키마 수정이 아니라 max_tokens 증가다.
- 새 스키마는 일회성 컴파일로 첫 요청이 느리고 이후 24시간 캐시된다. 첫 요청만 느린 것은 결함이 아니라 정상 동작이며, 스키마 고정으로 캐시 효과를 누린다.
- 구조화 출력은 인용 기능 및 프리필과 호환되지 않는다(인용은 400). 형식 강제는 프리필이 아니라 output_config.format으로 한다.
- 구조화 출력은 배치, 스트리밍, 토큰 카운팅, 적응형 사고와 함께 동작한다. 사고는 adaptive만 쓰며 budget_tokens나 temperature를 더하는 선택지는 함정이다.