---
title: 스트리밍 거부 처리하기
url: https://platform.claude.com/docs/ko/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals
description: 스트리밍 응답에서 거부 중단 사유를 감지하고 처리하며, 거부된 요청을 폴백 모델에서 재시도합니다.
---

Claude 4 모델부터, Claude API의 "streaming"(스트리밍) 응답은 스트리밍 분류기가 잠재적인 정책 위반을 처리하기 위해 개입할 때 **`stop_reason`: `"refusal"`**&#xC744; 반환합니다. 이 안전 기능은 실시간 스트리밍 중 콘텐츠 규정 준수를 유지하는 데 도움이 됩니다.

<Tip>
  이 페이지는 스트리밍 응답에서 거부가 어떻게 나타나는지를 다룹니다. 모든 `stop_reason` 값과 그 처리 방법은 [중단 사유와 폴백](https://platform.claude.com/docs/ko/build-with-claude/handling-stop-reasons)을 참조하세요. 거부된 요청을 다른 Claude 모델에서 재시도하려면 [거부와 폴백](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback)을 참조하세요.
</Tip>

## API 응답 형식

스트리밍 분류기가 Anthropic의 정책을 위반하는 콘텐츠를 감지하면, API는 다음 응답을 반환합니다:

```json
{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Hello.."
    }
  ],
  "stop_reason": "refusal",
  "stop_details": {
    "type": "refusal",
    "category": "cyber",
    "explanation": "This request was declined because it could enable cyber harm."
  }
}
```

이벤트 스트림에서 `stop_details`는 `stop_reason`과 함께 `message_delta` 이벤트에 도착합니다.

<Note>
  스트리밍 분류기의 `refusal` 응답에는 `category`와 사용자에게 표시할 수 있는 사람이 읽을 수 있는 `explanation`이 담긴 `stop_details` 객체가 포함됩니다. 전체 응답 형태와 사용 가능한 카테고리는 [거부와 폴백](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback#refusal-response)을 참조하세요.

  거부 시 `stop_details` 객체는 항상 존재하지만, 예를 들어 거부가 명명된 카테고리에 매핑되지 않는 경우처럼 `category` 및 `explanation` 필드는 `null`일 수 있습니다. `category`와 `explanation`이 채워져 있다고 가정하지 말고 `stop_reason` 또는 `stop_details.type`을 기준으로 분기하세요. 그리고 이 값들이 `null`일 때는 자체적인 사용자 대상 메시지를 제공하세요.
</Note>

## 거부 후 컨텍스트 재설정

**`stop_reason`: `refusal`**&#xC744; 받으면, 계속하기 전에 반드시 대화 컨텍스트를 재설정해야 합니다. 거부를 유발한 턴을 제거하거나 다시 표현할 수 있으며, 대화 기록을 완전히 지울 수도 있습니다. 재설정 없이 계속하려고 하면 거부가 계속 발생합니다.

<Note>
  응답이 거부된 경우에도 사용량 지표는 여전히 응답에 제공됩니다. 거부된 요청에 요금이 청구되는지 여부는 거부가 발생한 시점과 해당 카테고리에 따라 달라집니다. [거부에 대한 요금 부과 방식](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback#how-refusals-are-billed)을 참조하세요.
</Note>

<Tip>
  컨텍스트 재설정이 복구하는 유일한 방법은 아닙니다. 거부된 요청을 다른 Claude 모델에서 재시도할 수도 있으며, [거부와 폴백](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback) 페이지에서 서버 측 폴백, SDK 미들웨어 또는 수동 재시도로 이를 설정하는 방법을 보여줍니다.
</Tip>

## 구현 가이드

애플리케이션에서 스트리밍 거부를 감지하고 처리하는 방법은 다음과 같습니다:

<CodeGroup>
  ```bash cURL
  response=$(curl -N https://api.anthropic.com/v1/messages \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -d '{
      "model": "claude-opus-5-5",
      "messages": [{"role": "user", "content": "Hello"}],
      "max_tokens": 1024,
      "stream": true
    }')

  if echo "$response" | jq -R -e 'select(startswith("data: "))
      | sub("^data: "; "") | fromjson
      | select(.delta.stop_reason == "refusal")' >/dev/null; then
    echo "Response refused - resetting conversation context"
    # 여기에서 대화 상태를 초기화하세요
  fi
  ```

  ```bash CLI
  response=$(ant messages create --stream --format jsonl \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: Hello}')

  if echo "$response" | jq -e 'select(.delta.stop_reason == "refusal")' >/dev/null; then
    echo "Response refused - resetting conversation context"
    # 여기에서 대화 상태를 초기화하세요
  fi
  ```

  ```python Python
  client = anthropic.Anthropic()
  messages = []


  def reset_conversation():
      """Reset conversation context after refusal"""
      global messages
      messages = []
      print("Conversation reset due to refusal")


  try:
      with client.messages.stream(
          max_tokens=1024,
          messages=messages + [{"role": "user", "content": "Hello"}],
          model="claude-opus-5-5",
      ) as stream:
          for event in stream:
              # 메시지 델타에서 거부 여부를 확인합니다
              if event.type == "message_delta":
                  if event.delta.stop_reason == "refusal":
                      reset_conversation()
                      break
  except Exception as e:
      print(f"Error: {e}")
  ```

  ```typescript TypeScript
  const client = new Anthropic();
  let messages: Anthropic.MessageParam[] = [];

  function resetConversation() {
    // 거부 후 대화 컨텍스트를 초기화합니다
    messages = [];
    console.log("Conversation reset due to refusal");
  }

  try {
    const stream = await client.messages.stream({
      messages: [...messages, { role: "user", content: "Hello" }],
      model: "claude-opus-5-5",
      max_tokens: 1024
    });

    for await (const event of stream) {
      // 메시지 델타에서 거부 여부를 확인합니다
      if (event.type === "message_delta" && event.delta.stop_reason === "refusal") {
        resetConversation();
        break;
      }
    }
  } catch (error) {
    console.error("Error:", error);
  }
  ```

  ```csharp C#
  List<Message> messages = new();
  AnthropicClient client = new();

  var parameters = new MessageCreateParams
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello" }]
  };

  try
  {
      await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
      {
          if (
              streamEvent.TryPickDelta(out var deltaEvent)
              && deltaEvent.Delta.StopReason == StopReason.Refusal
          )
          {
              ResetConversation();
              break;
          }
      }
  }
  catch (Exception e)
  {
      Console.WriteLine($"Error: {e.Message}");
  }

  void ResetConversation()
  {
      messages.Clear();
      Console.WriteLine("Conversation reset due to refusal");
  }
  ```

  ```go Go
  var messages []anthropic.MessageParam

  func resetConversation() {
  	messages = []anthropic.MessageParam{}
  	fmt.Println("Conversation reset due to refusal")
  }
  // ...
  	client := anthropic.NewClient()

  	stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
  		Model:     anthropic.ModelClaudeOpus5_5,
  		MaxTokens: 1024,
  		Messages: []anthropic.MessageParam{
  			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")),
  		},
  	})

  streamLoop:
  	for stream.Next() {
  		event := stream.Current()
  		switch eventVariant := event.AsAny().(type) {
  		case anthropic.MessageDeltaEvent:
  			if eventVariant.Delta.StopReason == anthropic.StopReasonRefusal {
  				resetConversation()
  				break streamLoop
  			}
  		}
  	}

  	if err := stream.Err(); err != nil {
  		log.Fatal(err)
  	}
  ```

  ```java Java
  import com.anthropic.core.http.StreamResponse;
  import com.anthropic.models.messages.RawMessageStreamEvent;
  import com.anthropic.models.messages.StopReason;
  // ...

  List<MessageParam> messages = new ArrayList<>();

  void main() {
      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      MessageCreateParams params = MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024L)
          .addUserMessage("Hello")
          .build();

      try (StreamResponse<RawMessageStreamEvent> stream = client.messages().createStreaming(params)) {
          stream.stream().forEach(event -> {
              event.messageDelta().ifPresent(deltaEvent -> {
                  deltaEvent.delta().stopReason().ifPresent(stopReason -> {
                      if (stopReason.equals(StopReason.REFUSAL)) {
                          resetConversation();
                      }
                  });
              });
          });
      } catch (Exception e) {
          System.err.println("Error: " + e.getMessage());
      }
  }

  void resetConversation() {
      messages.clear();
      IO.println("Conversation reset due to refusal");
  }
  ```

  ```php PHP
  $client = new Client();
  $messages = [];

  function resetConversation(&$messages) {
      $messages = [];
      echo "Conversation reset due to refusal\n";
  }

  try {
      $stream = $client->messages->createStream(
          maxTokens: 1024,
          messages: [
              ['role' => 'user', 'content' => 'Hello']
          ],
          model: 'claude-opus-5-5',
      );

      foreach ($stream as $event) {
          if ($event->type === 'message_delta' && $event->delta->stopReason === 'refusal') {
              resetConversation($messages);
              break;
          }
      }
  } catch (\Exception $e) {
      echo "Error: " . $e->getMessage() . "\n";
  }
  ```

  ```ruby Ruby
  client = Anthropic::Client.new
  messages = []

  def reset_conversation(messages)
    messages.clear
    puts "Conversation reset due to refusal"
  end

  begin
    stream = client.messages.stream(
      model: :"claude-opus-5-5",
      max_tokens: 1024,
      messages: [{ role: "user", content: "Hello" }]
    )

    stream.each do |event|
      if event.type == :message_delta && event.delta.stop_reason == :refusal
        reset_conversation(messages)
        break
      end
    end
  rescue => e
    puts "Error: #{e.message}"
  end
  ```
</CodeGroup>

## 현재 거부 유형

API는 현재 세 가지 방식으로 거부를 처리합니다:

| 거부 유형           | 응답 형식                        | 발생 시점                 |
| --------------- | ---------------------------- | --------------------- |
| 스트리밍 분류기 거부     | **`stop_reason`: `refusal`** | 스트리밍 중 콘텐츠가 정책을 위반할 때 |
| API 입력 및 저작권 검증 | 400 오류 코드                    | 입력이 검증 검사를 통과하지 못할 때  |
| 모델 생성 거부        | 표준 텍스트 응답                    | 모델 자체가 거부할 때          |

## 모범 사례

* **거부 모니터링:** 오류 처리에 **`stop_reason`: `refusal`** 검사를 포함하세요
* **자동 재설정:** 거부가 감지되면 자동으로 컨텍스트를 재설정하도록 구현하세요
* **다른 모델로 폴백:** 거부된 요청이 사용자에게 거부로 표시되는 대신 다른 Claude 모델에서 재시도되도록 [서버 측 폴백 또는 SDK 미들웨어](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback)를 구성하세요. Claude Haiku 5.5에는 서버 측 폴백이 없으므로 클라이언트에서 재시도를 설정하세요
* **수동 재시도 시 폴백 크레딧 사용:** 재시도를 직접 구현하는 경우, 재시도 시 프롬프트 캐시 비용을 두 번 지불하지 않도록 거부의 [폴백 크레딧](https://platform.claude.com/docs/ko/build-with-claude/fallback-credit) 토큰을 전달하세요. Claude Haiku 5.5 거부에는 폴백 크레딧이 포함되지 않습니다
* **맞춤 메시지 제공:** 거부 발생 시 더 나은 UX를 위해 사용자 친화적인 메시지를 만드세요
* **거부 패턴 추적:** 거부 빈도를 모니터링하여 프롬프트의 잠재적 문제를 식별하세요

## 마이그레이션 참고 사항

이 기능이 처음 출시되었을 때 거부 처리를 구축했거나 기존 통합에 추가하는 경우, 다음 사항을 확인하세요:

* **거부는 오류가 아니라 응답입니다.** 거부는 `stop_reason`: `"refusal"`이 포함된 성공적인 HTTP 200 응답으로 도착하므로, 오류율만을 기반으로 구축된 모니터링에서는 드러나지 않습니다. 거부를 별도의 신호로 추적하세요.
* **거부에는 구조화된 세부 정보가 포함됩니다.** 모든 모델에서 거부에는 거절의 원인이 된 정책 카테고리를 식별하는 `stop_details` 객체도 포함됩니다. 전체 응답 형태는 [거부와 폴백](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback#refusal-response)을 참조하세요.
* **다른 모델에서 재시도하세요.** 거부된 요청을 동일한 모델에 다시 보내면 대개 또 다른 거부가 발생합니다. 컨텍스트를 재설정하는 것에 그치지 말고 [서버 측 폴백, SDK 미들웨어 또는 수동 재시도](https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback)를 사용하여 폴백 모델에서 재시도하고, 재시도를 직접 구현하는 경우 [폴백 크레딧](https://platform.claude.com/docs/ko/build-with-claude/fallback-credit)을 사용하세요. Claude Haiku 5.5에는 서버 측 폴백이 없고 그 거부에는 폴백 크레딧이 포함되지 않으므로 클라이언트에서 재시도를 설정하세요.
* **배치 결과에서 거부를 확인하세요.** [Message Batch](https://platform.claude.com/docs/ko/build-with-claude/batch-processing)에서 거부된 요청은 오류 결과가 아니라 `stop_reason`: `"refusal"`이 포함된 성공 결과로 반환됩니다.
* **`stop_reason`을 중심으로 처리를 일원화하세요.** API는 계속해서 `stop_reason`: `"refusal"`을 중심으로 거부 처리를 통합하고 있으므로, 모델별 동작이 아닌 중단 사유를 기준으로 분기하세요.

## 다음 단계

<CardGroup cols={2}>
  <Card title="거부와 폴백" icon="arrows-clockwise" href="https://platform.claude.com/docs/ko/build-with-claude/refusals-and-fallback">
    거부된 요청을 서버 측 또는 클라이언트에서 다른 Claude 모델로 재시도합니다.
  </Card>

  <Card title="중단 사유와 폴백" icon="code" href="https://platform.claude.com/docs/ko/build-with-claude/handling-stop-reasons">
    모든 `stop_reason` 값과 그 처리 방법.
  </Card>

  <Card title="메시지 스트리밍" icon="lightning" href="https://platform.claude.com/docs/ko/build-with-claude/streaming">
    응답을 스트리밍하고 `message_delta` 이벤트가 도착하는 대로 `stop_reason`을 읽습니다.
  </Card>

  <Card title="다국어 지원" icon="text-aa" href="https://platform.claude.com/docs/ko/build-with-claude/multilingual-support">
    Claude의 교차 언어 기능으로 다양한 언어의 사용자에게 서비스를 제공합니다.
  </Card>
</CardGroup>
