refusal 이해와 대응
안전 분류기가 거절하면 오류가 아니라 stop_reason: refusal이 담긴 200 응답이 온다. 분류기 카테고리, fallback 구성, reasoning_extraction 오탐을 피하는 법.
stop_reason: "refusal" 처리와 fallback 구성
거절이 일어나도 HTTP 오류가 아니라 정상 200 응답이 오기 때문에, 오류 핸들러만 있는
클라이언트는 거절을 알아채지 못하고 지나간다. 따라서 응답 처리 코드에 stop_reason
(응답이 왜 끝났는지 알려주는 API 필드)이 "refusal"인지 확인하는 분기를 직접
구현해야 한다. 어떤 분류기가 발동했는지는 stop_details.category 필드로 구분한다:
stop_details.category | 감시 영역 |
|---|---|
"cyber" | 공격적 사이버보안 기법 |
"bio" | 생물·생명과학 |
"reasoning_extraction" | 내부 추론 추출 시도 |
출력 생성 전에 거절된 요청은 과금되지 않는다.
거절된 요청을 다른 모델로 자동 재시도하는 fallback(폴백 — 실패 시 대신 응답할 예비
모델 구성)은 두 방식이 있다. 서버사이드는 요청 본문에 fallbacks 파라미터(베타)를
추가하면 같은 호출 안에서 예비 모델이 대신 응답하고, 클라이언트사이드는 SDK 미들웨어로
재시도를 구현한다. 소비자 서피스(claude.ai, Claude Code)에는 Opus 폴백이 기본
내장되어 있어 별도 설정 없이 작업이 이어진다:
// 서버사이드 fallback (베타) — refusal 시 같은 호출 안에서 Opus 4.8이 대신 응답
{
"model": "claude-fable-5",
"fallbacks": [{ "model": "claude-opus-4-8" }],
"messages": [{ "role": "user", "content": "..." }]
}DO
- stop_reason === "refusal" 분기를 클라이언트에 구현
- stop_details.category를 로깅해 오탐 패턴 파악
- 베타 fallbacks 또는 SDK 미들웨어로 자동 재시도 구성
DON'T
- refusal을 HTTP 오류로 가정하고 재시도 루프 — 200 응답이다
- 오탐이 반복되는 워크로드를 프롬프트 수정 없이 방치
출처:
공식 마이그레이션 가이드
추론 과정을 응답에 재현하라고 지시하지 마라
이전 모델에서 흔했던 "방금 추론 과정을 그대로 보여줘" 류의 reflection(반영) 패턴이
Fable 5에서는 reasoning_extraction 분류기를 자극한다. 이런 지시가 기존 스킬·시스템
프롬프트에 남아 있으면 정상 워크로드인데도 refusal과 Opus fallback이 반복된다.
마이그레이션 시 reflection 계열 프롬프트를 감사(audit)해 제거하는 것이 공식 권장이다.
추론 가시성이 정말 필요하다면, 전사를 요구하는 대신 공식 우회로를 쓴다:
thinking.display: "summarized"로 요약된 사고 블록을 받는다 (기본값은"omitted"— 사고 블록 미노출)- 진행 상황 노출이 목적이면 send_to_user 도구로 사용자 대면 메시지를 따로 흘린다
DO
- 기존 reflection 계열 스킬·프롬프트를 마이그레이션 시 감사
- 사고 가시성은 thinking.display=summarized로 해결
DON'T
- "방금 추론 과정을 그대로 옮겨 적어줘" 류의 지시
- 체인 오브 소트 전사를 요구하는 이전 모델용 템플릿 재사용