---
title: 세분화된 도구 스트리밍
url: https://platform.claude.com/docs/ko/agents-and-tools/tool-use/fine-grained-tool-streaming
description: 지연 시간에 민감한 애플리케이션을 위해 서버 측 JSON 버퍼링 없이 도구 입력을 스트리밍합니다.
---

<Note>
  "zero data retention"(제로 데이터 보존), 즉 ZDR이 이 기능에 어떻게 적용되는지 알아보려면 [API 및 데이터 보존](https://platform.claude.com/docs/ko/manage-claude/api-and-data-retention)을 참조하세요.
</Note>

"Fine-grained tool streaming"(세분화된 도구 스트리밍)은 Claude가 도구의 입력을 생성하는 즉시 서버 측 버퍼링이나 JSON 검증 없이 클라이언트로 전달합니다. 버퍼링 단계를 건너뛰면 문서나 코드 블록과 같은 큰 매개변수의 첫 번째 조각이 도착하기까지의 시간이 줄어들며, 조각들은 표준 도구 사용과 동일한 [메시지 스트리밍](https://platform.claude.com/docs/ko/build-with-claude/streaming) 이벤트를 통해 도착합니다.

<Warning>
  API가 도구의 입력을 스트리밍하기 전에 버퍼링하거나 검증하지 않기 때문에, 부분적이거나 유효하지 않은 JSON을 받을 수 있습니다. [중지 이유](https://platform.claude.com/docs/ko/build-with-claude/handling-stop-reasons) `max_tokens`로 끝나는 응답은 매개변수를 중간에 잘라낼 수도 있습니다. 조각들을 누적하고, 파싱을 보호하며, 파싱할 수 없는 입력을 Claude에게 반환하는 방법은 [도구 응답에서 유효하지 않은 JSON 처리하기](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/fine-grained-tool-streaming#handling-invalid-json-in-tool-responses)를 참조하세요.
</Warning>

## 세분화된 도구 스트리밍 사용 방법

모든 모델은 Claude API, [Amazon Bedrock](https://platform.claude.com/docs/ko/build-with-claude/claude-in-amazon-bedrock), [Claude Platform on AWS](https://platform.claude.com/docs/ko/build-with-claude/claude-platform-on-aws), [Google Cloud](https://platform.claude.com/docs/ko/build-with-claude/claude-on-vertex-ai), [Microsoft Foundry](https://platform.claude.com/docs/ko/build-with-claude/claude-in-microsoft-foundry)에서 세분화된 도구 스트리밍을 지원합니다. 이를 사용하려면 세분화된 스트리밍을 활성화하려는 사용자 정의 도구에서 `eager_input_streaming`을 `true`로 설정하고, 요청에서 스트리밍을 활성화하세요.

`eager_input_streaming` 필드는 선택 사항입니다. 이를 `true`로 설정하면 해당 도구에 대해 세분화된 스트리밍이 켜지고, 생략하면 API가 각 매개변수 값을 스트리밍하기 전에 버퍼링하고 검증하는 표준 버퍼링 스트리밍이 적용됩니다. 예외는 레거시 `fine-grained-tool-streaming-2025-05-14` 베타 헤더를 여전히 보내는 요청으로, 이 경우 필드를 설정하지 않은 도구에 대해 세분화된 스트리밍이 켜집니다. 도구별 필드는 해당 헤더를 대체하며, 명시적으로 `false`를 지정하면 요청이 여전히 헤더를 보내더라도 해당 도구에 대해 버퍼링 스트리밍이 유지됩니다. 레거시 헤더는 [컴퓨터 사용](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/computer-use-tool) 또는 [브라우저 사용](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/browser-use-tool) 도구 세트 항목과 함께 사용할 수 없습니다. API는 둘 다 보내는 요청을 거부하므로, 헤더를 제거하고 필요한 사용자 정의 도구에 `eager_input_streaming`을 설정하세요. 필드 정의는 [도구 참조](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/tool-reference)를 참조하세요.

다음 예제는 `make_file` 도구에 대해 세분화된 스트리밍을 켜고 Claude에게 긴 시를 요청하여, 도구 입력이 스트리밍되는 것을 지켜볼 수 있을 만큼 충분히 크게 만듭니다:

<CodeGroup>
  ```bash cURL
  curl https://api.anthropic.com/v1/messages \
    -H "content-type: application/json" \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 65536,
      "tools": [
        {
          "name": "make_file",
          "description": "Write text to a file",
          "eager_input_streaming": true,
          "input_schema": {
            "type": "object",
            "properties": {
              "filename": {
                "type": "string",
                "description": "The filename to write text to"
              },
              "lines_of_text": {
                "type": "array",
                "description": "An array of lines of text to write to the file"
              }
            },
            "required": ["filename", "lines_of_text"]
          }
        }
      ],
      "messages": [
        {
          "role": "user",
          "content": "Can you write a long poem and make a file called poem.txt?"
        }
      ],
      "stream": true
    }'
  ```

  ```bash CLI
  ant messages create --stream --format jsonl <<'YAML' |
  model: claude-opus-5-5
  max_tokens: 65536
  tools:
    - name: make_file
      description: Write text to a file
      eager_input_streaming: true
      input_schema:
        type: object
        properties:
          filename:
            type: string
            description: The filename to write text to
          lines_of_text:
            type: array
            description: An array of lines of text to write to the file
        required:
          - filename
          - lines_of_text
  messages:
    - role: user
      content: Can you write a long poem and make a file called poem.txt?
  YAML
    jq -rj 'select(.delta.type == "input_json_delta") | .delta.partial_json'
  ```

  ```python Python
  client = anthropic.Anthropic()

  with client.messages.stream(
      max_tokens=65536,
      model="claude-opus-5-5",
      tools=[
          {
              "name": "make_file",
              "description": "Write text to a file",
              "eager_input_streaming": True,
              "input_schema": {
                  "type": "object",
                  "properties": {
                      "filename": {
                          "type": "string",
                          "description": "The filename to write text to",
                      },
                      "lines_of_text": {
                          "type": "array",
                          "description": "An array of lines of text to write to the file",
                      },
                  },
                  "required": ["filename", "lines_of_text"],
              },
          }
      ],
      messages=[
          {
              "role": "user",
              "content": "Can you write a long poem and make a file called poem.txt?",
          }
      ],
  ) as stream:
      for event in stream:
          if event.type == "input_json":
              print(event.partial_json, end="", flush=True)
      final_message = stream.get_final_message()

  print()
  for block in final_message.content:
      if block.type == "tool_use":
          print(f"Complete tool input: {block.input}")
  ```

  ```typescript TypeScript
  const client = new Anthropic();

  const stream = client.messages.stream({
    model: "claude-opus-5-5",
    max_tokens: 65536,
    tools: [
      {
        name: "make_file",
        description: "Write text to a file",
        eager_input_streaming: true,
        input_schema: {
          type: "object",
          properties: {
            filename: {
              type: "string",
              description: "The filename to write text to"
            },
            lines_of_text: {
              type: "array",
              description: "An array of lines of text to write to the file"
            }
          },
          required: ["filename", "lines_of_text"]
        }
      }
    ],
    messages: [
      {
        role: "user",
        content: "Can you write a long poem and make a file called poem.txt?"
      }
    ]
  });

  stream.on("inputJson", (partialJson) => {
    process.stdout.write(partialJson);
  });

  const message = await stream.finalMessage();
  console.log();
  for (const block of message.content) {
    if (block.type === "tool_use") {
      console.log("Complete tool input:", block.input);
    }
  }
  ```

  ```csharp C#
  AnthropicClient client = new();

  MessageCreateParams parameters = new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 65536,
      Tools =
      [
          new Tool
          {
              Name = "make_file",
              Description = "Write text to a file",
              EagerInputStreaming = true,
              InputSchema = new InputSchema
              {
                  Properties = new Dictionary<string, JsonElement>
                  {
                      ["filename"] = JsonSerializer.SerializeToElement(
                          new { type = "string", description = "The filename to write text to" }
                      ),
                      ["lines_of_text"] = JsonSerializer.SerializeToElement(
                          new { type = "array", description = "An array of lines of text to write to the file" }
                      ),
                  },
                  Required = ["filename", "lines_of_text"],
              },
          },
      ],
      Messages =
      [
          new()
          {
              Role = Role.User,
              Content = "Can you write a long poem and make a file called poem.txt?",
          },
      ],
  };

  // C# 예제는 입력을 직접 조립합니다: content block 인덱스 -> 누적된 JSON
  var toolInputs = new Dictionary<long, StringBuilder>();

  await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
  {
      if (
          streamEvent.TryPickContentBlockStart(out var start)
          && start.ContentBlock.TryPickToolUse(out _)
      )
      {
          toolInputs[start.Index] = new StringBuilder();
      }
      else if (
          streamEvent.TryPickContentBlockDelta(out var delta)
          && delta.Delta.TryPickInputJson(out var inputJson)
      )
      {
          Console.Write(inputJson.PartialJson);
          toolInputs[delta.Index].Append(inputJson.PartialJson);
      }
  }

  Console.WriteLine();
  foreach (var accumulatedInput in toolInputs.Values)
  {
      Console.WriteLine($"Complete tool input: {accumulatedInput}");
  }
  ```

  ```go Go
  client := anthropic.NewClient()

  makeFileTool := anthropic.ToolParam{
  	Name:                "make_file",
  	Description:         anthropic.String("Write text to a file"),
  	EagerInputStreaming: anthropic.Bool(true),
  	InputSchema: anthropic.ToolInputSchemaParam{
  		Properties: map[string]any{
  			"filename": map[string]any{
  				"type":        "string",
  				"description": "The filename to write text to",
  			},
  			"lines_of_text": map[string]any{
  				"type":        "array",
  				"description": "An array of lines of text to write to the file",
  			},
  		},
  		Required: []string{"filename", "lines_of_text"},
  	},
  }

  stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5_5,
  	MaxTokens: 65536,
  	Tools:     []anthropic.ToolUnionParam{{OfTool: &makeFileTool}},
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock(
  			"Can you write a long poem and make a file called poem.txt?",
  		)),
  	},
  })

  message := anthropic.Message{}
  for stream.Next() {
  	event := stream.Current()
  	if err := message.Accumulate(event); err != nil {
  		panic(err)
  	}
  	if delta, ok := event.AsAny().(anthropic.ContentBlockDeltaEvent); ok {
  		if inputJSON, ok := delta.Delta.AsAny().(anthropic.InputJSONDelta); ok {
  			fmt.Print(inputJSON.PartialJSON)
  		}
  	}
  }
  if err := stream.Err(); err != nil {
  	panic(err)
  }

  fmt.Println()
  for _, block := range message.Content {
  	if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
  		fmt.Printf("Complete tool input: %s\n", toolUse.Input)
  	}
  }
  ```

  ```java Java
  AnthropicClient client = AnthropicOkHttpClient.fromEnv();

  Tool makeFileTool = Tool.builder()
      .name("make_file")
      .description("Write text to a file")
      .eagerInputStreaming(true)
      .inputSchema(Tool.InputSchema.builder()
          .properties(Tool.InputSchema.Properties.builder()
              .putAdditionalProperty("filename", JsonValue.from(Map.of(
                  "type", "string",
                  "description", "The filename to write text to")))
              .putAdditionalProperty("lines_of_text", JsonValue.from(Map.of(
                  "type", "array",
                  "description", "An array of lines of text to write to the file")))
              .build())
          .addRequired("filename")
          .addRequired("lines_of_text")
          .build())
      .build();

  MessageCreateParams params = MessageCreateParams.builder()
      .model(Model.CLAUDE_OPUS_5_5)
      .maxTokens(65536L)
      .addTool(makeFileTool)
      .addUserMessage("Can you write a long poem and make a file called poem.txt?")
      .build();

  MessageAccumulator accumulator = MessageAccumulator.create();

  try (StreamResponse<RawMessageStreamEvent> streamResponse =
          client.messages().createStreaming(params)) {
      streamResponse.stream().forEach(event -> {
          accumulator.accumulate(event);
          if (event.isContentBlockDelta()) {
              var delta = event.asContentBlockDelta().delta();
              if (delta.isInputJson()) {
                  IO.print(delta.asInputJson().partialJson());
              }
          }
      });
  }

  IO.println("");
  accumulator.message().content().forEach(block ->
      block.toolUse().ifPresent(toolUse ->
          IO.println("Complete tool input: " + toolUse._input())));
  ```

  ```php PHP
  use Anthropic\Client;
  use Anthropic\Messages\InputJSONDelta;
  use Anthropic\Messages\Model;
  use Anthropic\Messages\RawContentBlockDeltaEvent;
  use Anthropic\Messages\RawContentBlockStartEvent;
  use Anthropic\Messages\ToolUseBlock;

  $client = new Client();

  $stream = $client->messages->createStream(
      maxTokens: 65536,
      model: Model::CLAUDE_OPUS_5_5,
      tools: [
          [
              'name' => 'make_file',
              'description' => 'Write text to a file',
              'eager_input_streaming' => true,
              'input_schema' => [
                  'type' => 'object',
                  'properties' => [
                      'filename' => [
                          'type' => 'string',
                          'description' => 'The filename to write text to',
                      ],
                      'lines_of_text' => [
                          'type' => 'array',
                          'description' => 'An array of lines of text to write to the file',
                      ],
                  ],
                  'required' => ['filename', 'lines_of_text'],
              ],
          ],
      ],
      messages: [
          [
              'role' => 'user',
              'content' => 'Can you write a long poem and make a file called poem.txt?',
          ],
      ],
  );

  // PHP 예제는 입력을 직접 조립합니다: index => 누적된 JSON 문자열
  $toolInputs = [];

  foreach ($stream as $event) {
      switch (true) {
          case $event instanceof RawContentBlockStartEvent:
              if ($event->contentBlock instanceof ToolUseBlock) {
                  $toolInputs[$event->index] = '';
              }
              break;
          case $event instanceof RawContentBlockDeltaEvent:
              if ($event->delta instanceof InputJSONDelta) {
                  echo $event->delta->partialJSON;
                  $toolInputs[$event->index] .= $event->delta->partialJSON;
              }
              break;
      }
  }

  echo "\n";
  foreach ($toolInputs as $toolInput) {
      echo "Complete tool input: {$toolInput}\n";
  }
  ```

  ```ruby Ruby
  client = Anthropic::Client.new

  stream = client.messages.stream(
    model: Anthropic::Models::Model::CLAUDE_OPUS_5_5,
    max_tokens: 65_536,
    tools: [
      {
        name: "make_file",
        description: "Write text to a file",
        eager_input_streaming: true,
        input_schema: {
          type: "object",
          properties: {
            filename: {
              type: "string",
              description: "The filename to write text to"
            },
            lines_of_text: {
              type: "array",
              description: "An array of lines of text to write to the file"
            }
          },
          required: ["filename", "lines_of_text"]
        }
      }
    ],
    messages: [
      {
        role: "user",
        content: "Can you write a long poem and make a file called poem.txt?"
      }
    ]
  )

  stream.each do |event|
    print event.partial_json if event.is_a?(Anthropic::Streaming::InputJsonEvent)
  end

  puts
  stream.accumulated_message.content.each do |block|
    puts "Complete tool input: #{block.input}" if block.type == :tool_use
  end
  ```
</CodeGroup>

모든 탭은 `make_file` 도구에 대해 세분화된 스트리밍을 켭니다. SDK 탭들은 각 입력 조각이 도착하는 순간 이를 출력한 다음, 스트림이 끝나면 누적된 전체 입력을 출력합니다. cURL 탭은 원시 이벤트 스트림을 보여주고, CLI 탭은 `jq`를 사용하여 조각만 출력합니다. 출력된 조각들이 합쳐져 전체 도구 입력이 되므로, Claude가 시를 쓰는 동안 시가 터미널을 채워 나갑니다:

```text wrap
{"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", "", "I.", "", "Beneath the vast and star-strewn sky,", "Where silver moonbeams softly lie,", ...
Complete tool input: {"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", ...]}
```

`eager_input_streaming`이 없으면 API는 각 매개변수 값을 스트리밍하기 전에 버퍼링하고 검증하므로, 큰 매개변수의 경우 Claude가 생성을 마칠 때까지 아무것도 출력되지 않습니다. 이를 사용하면 Claude가 매개변수를 시작하자마자 조각들이 도착하기 시작하며, 일반적으로 조각이 더 길고 단어 중간에서 끊기는 경우가 더 적습니다.

## 도구 입력 델타 누적하기

누적 규약은 표준 도구 사용 스트리밍과 동일하므로, 이 섹션은 `eager_input_streaming` 사용 여부와 관계없이 적용됩니다. 이벤트 형식은 메시지 스트리밍의 [입력 JSON 델타](https://platform.claude.com/docs/ko/build-with-claude/streaming#input-json-delta)를 참조하세요. 세분화된 도구 스트리밍은 결과에 대해 가정할 수 있는 것을 바꿉니다. 서버가 조각들을 검증하지 않고 스트리밍하므로, 누적된 문자열이 유효한 JSON이 아닐 수 있습니다.

`tool_use` 콘텐츠 블록이 스트리밍될 때, 초기 `content_block_start` 이벤트에는 `input: {}`(빈 객체)가 포함됩니다. 이것은 자리 표시자입니다. 실제 입력은 각각 `partial_json` 문자열 조각을 담은 일련의 `input_json_delta` 이벤트로 도착합니다. 전체 입력을 조립하려면 이 조각들을 이어 붙이고 블록이 닫힐 때 결과를 파싱하세요.

SDK가 누적기 헬퍼를 제공하는 경우(이전 예제의 Python, TypeScript, Go, Java, Ruby 탭처럼), 헬퍼가 이를 대신 처리합니다. SDK에 헬퍼가 없거나 입력 조립 방식을 완전히 제어하고 싶을 때는 수동 패턴을 사용하세요.

누적 규약:

1. `type: "tool_use"`인 `content_block_start`에서 빈 문자열을 초기화합니다: `input_json = ""`
2. `type: "input_json_delta"`인 각 `content_block_delta`에 대해 추가합니다: `input_json += event.delta.partial_json`
3. `content_block_stop`에서 누적된 문자열을 파싱합니다

다음 SDK 예제들처럼 파싱을 보호하세요. 응답은 매개변수 중간에 `max_tokens`에서 멈출 수도 있습니다. [중지 이유](https://platform.claude.com/docs/ko/build-with-claude/handling-stop-reasons)를 확인하고 더 높은 `max_tokens`로 요청을 재시도할지, 아니면 부분 입력을 복구할지 결정하세요.

초기 `input: {}`(객체)와 `partial_json`(문자열) 사이의 타입 불일치는 의도된 설계입니다. 빈 객체는 콘텐츠 배열에서 자리를 표시합니다. 델타 문자열들이 실제 값을 구성합니다.

<CodeGroup exclude="shell">
  <CodeGroupItem language="cURL">
    각 `tool_use` 블록의 입력 조각을 누적하려면 단일 셸 명령보다 더 많은 로직이 필요합니다. 이를 위해서는 [Claude SDK](https://platform.claude.com/docs/ko/cli-sdks-libraries/overview)를 사용하세요. 스트리밍되는 원시 조각을 보려면 [세분화된 도구 스트리밍 사용 방법](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/fine-grained-tool-streaming#how-to-use-fine-grained-tool-streaming)의 요청을 실행하세요.
  </CodeGroupItem>

  <CodeGroupItem language="CLI">
    각 `tool_use` 블록의 입력 조각을 누적하려면 단일 셸 명령보다 더 많은 로직이 필요합니다. 이를 위해서는 [Claude SDK](https://platform.claude.com/docs/ko/cli-sdks-libraries/overview)를 사용하세요. 스트리밍되는 원시 조각을 보려면 [세분화된 도구 스트리밍 사용 방법](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/fine-grained-tool-streaming#how-to-use-fine-grained-tool-streaming)의 요청을 실행하세요.
  </CodeGroupItem>

  ```python Python
  client = anthropic.Anthropic()

  tool_inputs: dict[int, str] = {}  # index -> accumulated JSON string

  with client.messages.stream(
      model="claude-opus-5-5",
      max_tokens=1024,
      tools=[
          {
              "name": "get_weather",
              "description": "Get current weather for a city",
              "eager_input_streaming": True,
              "input_schema": {
                  "type": "object",
                  "properties": {"city": {"type": "string"}},
                  "required": ["city"],
              },
          }
      ],
      messages=[{"role": "user", "content": "Weather in Paris?"}],
  ) as stream:
      for event in stream:
          match event.type:
              case "content_block_start" if event.content_block.type == "tool_use":
                  tool_inputs[event.index] = ""
              case "content_block_delta" if event.delta.type == "input_json_delta":
                  tool_inputs[event.index] += event.delta.partial_json
              case "content_block_stop" if event.index in tool_inputs:
                  raw_input = tool_inputs[event.index]
                  try:
                      parsed = json.loads(raw_input)
                  except json.JSONDecodeError:
                      # 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
                      # 이 페이지의 "도구 응답에서 잘못된 JSON 처리하기"를 참조하세요.
                      print(f"Invalid tool input: {raw_input}")
                  else:
                      print(f"Tool input: {parsed}")
  ```

  ```typescript TypeScript
  const client = new Anthropic();

  const toolInputs = new Map<number, string>();

  const stream = client.messages.stream({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    tools: [
      {
        name: "get_weather",
        description: "Get current weather for a city",
        eager_input_streaming: true,
        input_schema: {
          type: "object",
          properties: { city: { type: "string" } },
          required: ["city"]
        }
      }
    ],
    messages: [{ role: "user", content: "Weather in Paris?" }]
  });

  for await (const event of stream) {
    switch (event.type) {
      case "content_block_start":
        if (event.content_block.type === "tool_use") {
          toolInputs.set(event.index, "");
        }
        break;
      case "content_block_delta":
        if (event.delta.type === "input_json_delta") {
          toolInputs.set(
            event.index,
            (toolInputs.get(event.index) ?? "") + event.delta.partial_json
          );
        }
        break;
      case "content_block_stop":
        if (toolInputs.has(event.index)) {
          const rawInput = toolInputs.get(event.index)!;
          try {
            console.log("Tool input:", JSON.parse(rawInput));
          } catch {
            // 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
            // 이 페이지의 "Handling invalid JSON in tool responses"를 참조하세요.
            console.log("Invalid tool input:", rawInput);
          }
        }
        break;
    }
  }
  ```

  ```csharp C#
  AnthropicClient client = new();

  MessageCreateParams parameters = new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Tools =
      [
          new Tool
          {
              Name = "get_weather",
              Description = "Get current weather for a city",
              EagerInputStreaming = true,
              InputSchema = new InputSchema
              {
                  Properties = new Dictionary<string, JsonElement>
                  {
                      ["city"] = JsonSerializer.SerializeToElement(new { type = "string" }),
                  },
                  Required = ["city"],
              },
          },
      ],
      Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }],
  };

  // 블록 인덱스 -> 누적된 JSON 조각
  // 이 예제는 원시 스트림을 보여 주기 위해 델타를 수동으로 누적합니다.
  // SDK의 MessageContentAggregator를 사용하면 도구 입력을 자동으로 누적할 수도 있습니다.
  var toolInputs = new Dictionary<long, StringBuilder>();

  await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
  {
      if (
          streamEvent.TryPickContentBlockStart(out var start)
          && start.ContentBlock.TryPickToolUse(out _)
      )
      {
          toolInputs[start.Index] = new StringBuilder();
      }
      else if (
          streamEvent.TryPickContentBlockDelta(out var delta)
          && delta.Delta.TryPickInputJson(out var inputJson)
      )
      {
          toolInputs[delta.Index].Append(inputJson.PartialJson);
      }
      else if (
          streamEvent.TryPickContentBlockStop(out var stop)
          && toolInputs.TryGetValue(stop.Index, out var accumulated)
      )
      {
          try
          {
              using var parsed = JsonDocument.Parse(accumulated.ToString());
              Console.WriteLine($"Tool input: {parsed.RootElement}");
          }
          catch (JsonException)
          {
              // 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
              // 이 페이지의 "도구 응답에서 유효하지 않은 JSON 처리"를 참조하세요.
              Console.WriteLine($"Invalid tool input: {accumulated}");
          }
      }
  }
  ```

  ```go Go
  client := anthropic.NewClient()

  toolInputs := map[int64]string{} // content block index -> accumulated JSON

  stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5_5,
  	MaxTokens: 1024,
  	Tools: []anthropic.ToolUnionParam{{
  		OfTool: &anthropic.ToolParam{
  			Name:                "get_weather",
  			Description:         anthropic.String("Get current weather for a city"),
  			EagerInputStreaming: anthropic.Bool(true),
  			InputSchema: anthropic.ToolInputSchemaParam{
  				Properties: map[string]any{
  					"city": map[string]any{"type": "string"},
  				},
  				Required: []string{"city"},
  			},
  		},
  	}},
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock("Weather in Paris?")),
  	},
  })

  for stream.Next() {
  	switch event := stream.Current().AsAny().(type) {
  	case anthropic.ContentBlockStartEvent:
  		if _, ok := event.ContentBlock.AsAny().(anthropic.ToolUseBlock); ok {
  			toolInputs[event.Index] = ""
  		}
  	case anthropic.ContentBlockDeltaEvent:
  		if delta, ok := event.Delta.AsAny().(anthropic.InputJSONDelta); ok {
  			toolInputs[event.Index] += delta.PartialJSON
  		}
  	case anthropic.ContentBlockStopEvent:
  		if accumulated, ok := toolInputs[event.Index]; ok {
  			var parsed map[string]any
  			if err := json.Unmarshal([]byte(accumulated), &parsed); err != nil {
  				// 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
  				// 이 페이지의 "Handling invalid JSON in tool responses"를 참조하세요.
  				fmt.Println("Invalid tool input:", accumulated)
  			} else {
  				fmt.Println("Tool input:", parsed)
  			}
  		}
  	}
  }
  if err := stream.Err(); err != nil {
  	panic(err)
  }
  ```

  ```java Java
  AnthropicClient client = AnthropicOkHttpClient.fromEnv();
  ObjectMapper objectMapper = new ObjectMapper();

  Tool weatherTool = Tool.builder()
          .name("get_weather")
          .description("Get current weather for a city")
          .eagerInputStreaming(true)
          .inputSchema(Tool.InputSchema.builder()
                  .properties(Tool.InputSchema.Properties.builder()
                          .putAdditionalProperty("city", JsonValue.from(Map.of("type", "string")))
                          .build())
                  .addRequired("city")
                  .build())
          .build();

  MessageCreateParams createParams = MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024)
          .addTool(weatherTool)
          .addUserMessage("Weather in Paris?")
          .build();

  // 콘텐츠 블록 인덱스 -> 누적된 도구 입력 JSON
  Map<Long, StringBuilder> toolInputs = new HashMap<>();

  try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(createParams)) {
      var eventIterator = streamResponse.stream().iterator();
      while (eventIterator.hasNext()) {
          RawMessageStreamEvent event = eventIterator.next();
          switch (event.type().value()) {
              case CONTENT_BLOCK_START -> {
                  var blockStart = event.asContentBlockStart();
                  if (blockStart.contentBlock().isToolUse()) {
                      toolInputs.put(blockStart.index(), new StringBuilder());
                  }
              }
              case CONTENT_BLOCK_DELTA -> {
                  var blockDelta = event.asContentBlockDelta();
                  if (blockDelta.delta().isInputJson() && toolInputs.containsKey(blockDelta.index())) {
                      toolInputs.get(blockDelta.index()).append(blockDelta.delta().asInputJson().partialJson());
                  }
              }
              case CONTENT_BLOCK_STOP -> {
                  var blockStop = event.asContentBlockStop();
                  if (toolInputs.containsKey(blockStop.index())) {
                      String accumulated = toolInputs.get(blockStop.index()).toString();
                      try {
                          IO.println("Tool input: " + objectMapper.readTree(accumulated));
                      } catch (JsonProcessingException e) {
                          // 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
                          // 이 페이지의 "도구 응답의 잘못된 JSON 처리"를 참조하세요.
                          IO.println("Invalid tool input: " + accumulated);
                      }
                  }
              }
          }
      }
  }
  ```

  ```php PHP
  use Anthropic\Client;
  use Anthropic\Messages\InputJSONDelta;
  use Anthropic\Messages\Model;
  use Anthropic\Messages\RawContentBlockDeltaEvent;
  use Anthropic\Messages\RawContentBlockStartEvent;
  use Anthropic\Messages\RawContentBlockStopEvent;
  use Anthropic\Messages\ToolUseBlock;

  $client = new Client();

  // PHP SDK는 도구 입력용 스트림 누적기를 제공하지 않습니다.
  // 여기에 표시된 수동 패턴이 지원되는 방식입니다.
  $toolInputs = []; // index => accumulated JSON string

  $stream = $client->messages->createStream(
      maxTokens: 1024,
      model: Model::CLAUDE_OPUS_5_5,
      tools: [
          [
              'name' => 'get_weather',
              'description' => 'Get current weather for a city',
              'eager_input_streaming' => true,
              'input_schema' => [
                  'type' => 'object',
                  'properties' => ['city' => ['type' => 'string']],
                  'required' => ['city'],
              ],
          ],
      ],
      messages: [['role' => 'user', 'content' => 'Weather in Paris?']],
  );

  foreach ($stream as $event) {
      switch (true) {
          case $event instanceof RawContentBlockStartEvent:
              if ($event->contentBlock instanceof ToolUseBlock) {
                  $toolInputs[$event->index] = '';
              }
              break;
          case $event instanceof RawContentBlockDeltaEvent:
              if ($event->delta instanceof InputJSONDelta) {
                  $toolInputs[$event->index] .= $event->delta->partialJSON;
              }
              break;
          case $event instanceof RawContentBlockStopEvent:
              if (isset($toolInputs[$event->index])) {
                  $accumulated = $toolInputs[$event->index];
                  try {
                      $parsed = json_decode($accumulated, associative: true, flags: JSON_THROW_ON_ERROR);
                      echo "Tool input: " . json_encode($parsed) . "\n";
                  } catch (\JsonException $e) {
                      // 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
                      // 이 페이지의 "도구 응답에서 잘못된 JSON 처리"를 참조하세요.
                      echo "Invalid tool input: {$accumulated}\n";
                  }
              }
              break;
      }
  }
  ```

  ```ruby Ruby
  client = Anthropic::Client.new

  tool_inputs = {} # index -> accumulated JSON string

  stream = client.messages.stream_raw(
    model: Anthropic::Models::Model::CLAUDE_OPUS_5_5,
    max_tokens: 1024,
    tools: [
      {
        name: "get_weather",
        description: "Get current weather for a city",
        eager_input_streaming: true,
        input_schema: {
          type: "object",
          properties: {city: {type: "string"}},
          required: ["city"]
        }
      }
    ],
    messages: [{role: "user", content: "Weather in Paris?"}]
  )

  stream.each do |event|
    case event
    when Anthropic::Models::RawContentBlockStartEvent
      tool_inputs[event.index] = +"" if event.content_block.type == :tool_use
    when Anthropic::Models::RawContentBlockDeltaEvent
      if event.delta.is_a?(Anthropic::Models::InputJSONDelta)
        tool_inputs[event.index] << event.delta.partial_json
      end
    when Anthropic::Models::RawContentBlockStopEvent
      if tool_inputs.key?(event.index)
        accumulated = tool_inputs[event.index]
        begin
          parsed = JSON.parse(accumulated)
          puts "Tool input: #{parsed}"
        rescue JSON::ParserError
          # 누적된 문자열이 유효한 JSON이라는 보장은 없습니다.
          # 이 페이지의 "도구 응답에서 잘못된 JSON 처리하기"를 참조하세요.
          puts "Invalid tool input: #{accumulated}"
        end
      end
    end
  end
  ```
</CodeGroup>

<Tip>
  조각에 반응하는 것과 조각을 조립하는 것은 별개의 관심사입니다. 첫 번째 예제는 각 조각이 도착할 때마다 반응하면서도, 누적기 헬퍼를 사용하는 탭에서는 여전히 조립을 SDK에 맡깁니다. 누적기 헬퍼를 사용하지 않거나 조립을 완전히 제어하고 싶을 때 수동 패턴을 사용하세요.
</Tip>

## 도구 응답에서 유효하지 않은 JSON 처리하기

세분화된 도구 스트리밍을 사용하면 도구 호출에 대해 누적된 입력이 유효하지 않거나 불완전한 JSON일 수 있습니다. 그런 경우 도구를 실행할 수 없으므로, 대신 실패를 Claude에게 보고하세요. 도구 결과의 `content`가 반드시 JSON일 필요는 없지만, 원시 문자열을 단일 키 아래의 JSON 객체로 감싸면 유효하지 않은 JSON을 받았다는 사실이 Claude에게 명확해지고, 디버깅을 위해 원본 입력이 보존됩니다:

```json
{
  "INVALID_JSON": "<the unparseable input you received>"
}
```

문자열로 직렬화한 래퍼를 `is_error`가 `true`로 설정된 [도구 결과](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls#handling-errors-with-is-error) 콘텐츠 블록의 `content`로 반환하세요:

```json
{
  "type": "tool_result",
  "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
  "is_error": true,
  "content": "{\"INVALID_JSON\": \"<the unparseable input you received>\"}"
}
```

<Note>
  유효하지 않은 입력에 포함된 따옴표 및 기타 특수 문자가 올바르게 이스케이프되도록, 문자열을 이어 붙이는 대신 JSON 라이브러리로 래퍼를 만드세요.
</Note>

## 다음 단계

<CardGroup cols={2}>
  <Card title="컨텍스트 윈도우" icon="stack" href="https://platform.claude.com/docs/ko/build-with-claude/context-windows">
    컨텍스트 윈도우가 어떻게 작동하는지, 확장 사고와 도구 사용이 어떻게 계산에 포함되는지, 대화가 길어짐에 따라 컨텍스트를 어떻게 관리하는지 이해하세요.
  </Card>

  <Card title="메시지 스트리밍" icon="lightning" href="https://platform.claude.com/docs/ko/build-with-claude/streaming">
    텍스트, 도구 사용, 확장 사고 델타를 포함하여 서버 전송 이벤트로 Messages API 응답을 점진적으로 스트리밍하세요.
  </Card>

  <Card title="도구 호출 처리하기" icon="arrows-left-right" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls">
    tool\_use 블록을 파싱하고, tool\_result 응답을 형식화하며, is\_error로 오류를 처리하세요.
  </Card>

  <Card title="도구 참조" icon="book" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/tool-reference">
    Anthropic 제공 도구 디렉터리 및 선택적 도구 정의 속성에 대한 참조입니다.
  </Card>
</CardGroup>
