병렬 도구 사용

병렬 도구 사용 (Parallel tool use)

기본적으로 Claude는 한 응답에서 여러 도구를 호출할 수 있어요. 이 페이지는 그 호출들을 어떻게 실행할지, 병렬성을 계속 유지하도록 메시지 기록을 어떻게 포맷할지, 그리고 필요할 때 병렬 도구 사용을 어떻게 끌지 다뤄요. 단일 호출 흐름은 도구 호출 처리하기를 참고하세요.

출처: 문서

본문

기본적으로 Claude는 한 응답에서 여러 도구를 호출할 수 있어요. 이 페이지는 그 호출들을 어떻게 실행할지, 병렬성이 계속 동작하도록 메시지 기록을 어떻게 포맷할지, 그리고 필요할 때 병렬 도구 사용을 어떻게 비활성화할지 다뤄요. 단일 호출 흐름은 도구 호출 처리하기를 참고하세요.

실행 의미론 (Execution semantics)

Claude가 도구를 호출하면 응답의 stop_reasontool_use가 되고, 한 번의 어시스턴트 턴에 여러 tool_use 블록이 담길 수 있어요. 그 호출들을 어떻게 실행할지는 우리 몫이에요. API는 실행 순서를 강제하지 않아요. 호출들을 동시에(Promise.all, asyncio.gather) 실행하거나, 나타난 순서대로 순차 실행하거나, 도구에 맞는 어떤 조합으로도 실행할 수 있어요.

도구가 무엇을 하는지에 따라 전략을 고르세요. 독립적이고 읽기 전용 작업은 보통 병렬로 실행해도 안전해서 지연 시간을 줄일 수 있어요. 부작용이 있거나, 공유 상태를 쓰거나, 순서 요구 사항이 있는 도구는 순차로 실행하는 게 나을 수 있어요.

어떤 전략을 쓰든 각 tool_use 블록에 대해 tool_result 하나를 모두 다음 사용자 메시지에 모아서 반환하세요. tool_use_id로 각 결과를 호출에 짝지어 주고, 모든 tool_result 블록을 그 메시지에서 텍스트 콘텐츠보다 앞에 두세요. 전체 포맷 규칙은 도구 호출 처리하기를 보세요. 특정 호출을 실행하지 않기로 했다면(예: 배치를 순차 실행했는데 앞선 호출이 실패한 경우), 그 호출에 대해 is_error: true와 짧은 설명이 담긴 tool_result를 여전히 반환해야 해요.

{
  "type": "tool_result",
  "tool_use_id": "toolu_02",
  "is_error": true,
  "content": "Not executed: the preceding write_file call failed."
}

컴퓨터 사용 도구브라우저 사용 도구는 더 엄격해요. Claude가 한 턴에 이들의 멤버 도구 호출을 여러 개 반환하면(배치 액션), 나타난 순서대로 순차 실행하고 첫 실패에서 멈춰야 해요. 각 도구는 건너뛴 호출에 대해 반환할 정확한 텍스트를 정의해요.

병렬 도구 호출 테스트하기 (Test parallel tool calls)

**대부분 애플리케이션에는 Tool Runner를 쓰세요:** SDK [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner)는 여러 도구 호출이 있는 응답을 처리하고 결과를 대신 포맷해 주니, 직접 작성할 필요가 없어요. 커스텀 배칭, 순서, 오류 처리처럼 호출 실행 방식을 직접 제어해야 할 때 이 페이지의 수동 패턴을 쓰세요.

다음 스크립트는 병렬 도구 호출을 유발해야 하는 요청을 보내고, 응답에 그게 들어 있는지 확인하며, 병렬성이 계속 동작하도록 도구 결과를 포맷해요. 환경에 ANTHROPIC_API_KEY를 설정하고 실행하세요:

```bash cURL # This end-to-end test flow doesn't translate well to a one-off shell command. # See the SDK tabs for the full flow. The underlying HTTP request is a standard # tool use request with multiple tools defined. ```
# This end-to-end test flow doesn't translate well to a one-off shell command.
# See the SDK tabs for the full flow.
client = Anthropic()

# Define tools
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA",
                }
            },
            "required": ["location"],
        },
    },
    {
        "name": "get_time",
        "description": "Get the current time in a given timezone",
        "input_schema": {
            "type": "object",
            "properties": {
                "timezone": {
                    "type": "string",
                    "description": "The timezone, e.g. America/New_York",
                }
            },
            "required": ["timezone"],
        },
    },
]

# Test conversation with parallel tool calls
messages = [
    {
        "role": "user",
        "content": "What's the weather in SF and NYC, and what time is it there?",
    }
]

# Make initial request
print("Requesting parallel tool calls...")
response = client.messages.create(
    model="claude-opus-5-5", max_tokens=1024, messages=messages, tools=tools
)

# Check for parallel tool calls
tool_uses = [block for block in response.content if block.type == "tool_use"]
print(f"\n✓ Claude made {len(tool_uses)} tool calls")

if len(tool_uses) > 1:
    print("✓ Parallel tool calls detected!")
    for tool in tool_uses:
        print(f"  - {tool.name}: {tool.input}")
else:
    print("✗ No parallel tool calls detected")

# Simulate tool execution and format results correctly
tool_results = []
for tool_use in tool_uses:
    if tool_use.name == "get_weather":
        if "San Francisco" in str(tool_use.input):
            result = "San Francisco: 68°F, partly cloudy"
        else:
            result = "New York: 45°F, clear skies"
    else:  # get_time
        if "Los_Angeles" in str(tool_use.input):
            result = "2:30 PM PST"
        else:
            result = "5:30 PM EST"

    tool_results.append(
        {"type": "tool_result", "tool_use_id": tool_use.id, "content": result}
    )

# Continue conversation with tool results
messages.extend(
    [
        {"role": "assistant", "content": response.content},
        {"role": "user", "content": tool_results},  # All results in one message!
    ]
)

# Get final response
print("\nGetting final response...")
final_response = client.messages.create(
    model="claude-opus-5-5", max_tokens=1024, messages=messages, tools=tools
)

final_text = next(
    block.text for block in final_response.content if block.type == "text"
)
print(f"\nClaude's response:\n{final_text}")

# Verify formatting
print("\n--- Verification ---")
print(f"✓ Tool results sent in single user message: {len(tool_results)} results")
print("✓ No text before tool results in content array")
print("✓ Conversation formatted correctly for future parallel tool use")
const client = new Anthropic();

// Define tools
const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description: "Get the current weather in a given location",
    input_schema: {
      type: "object" as const,
      properties: {
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        }
      },
      required: ["location"]
    }
  },
  {
    name: "get_time",
    description: "Get the current time in a given timezone",
    input_schema: {
      type: "object" as const,
      properties: {
        timezone: {
          type: "string",
          description: "The timezone, e.g. America/New_York"
        }
      },
      required: ["timezone"]
    }
  }
];

// Make initial request
console.log("Requesting parallel tool calls...");
const response = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [
    {
      role: "user",
      content: "What's the weather in SF and NYC, and what time is it there?"
    }
  ],
  tools: tools
});

// Check for parallel tool calls
const toolUses = response.content.filter((block) => block.type === "tool_use");
console.log(`\n✓ Claude made ${toolUses.length} tool calls`);

if (toolUses.length > 1) {
  console.log("✓ Parallel tool calls detected!");
  for (const tool of toolUses) {
    if (tool.type === "tool_use") {
      console.log(`  - ${tool.name}: ${JSON.stringify(tool.input)}`);
    }
  }
} else {
  console.log("✗ No parallel tool calls detected");
}

// Simulate tool execution and format results correctly
const toolResults: Anthropic.ToolResultBlockParam[] = toolUses
  .filter((block): block is Anthropic.ToolUseBlock => block.type === "tool_use")
  .map((toolUse) => {
    const input = toolUse.input as Record<string, string>;
    let result: string;
    if (toolUse.name === "get_weather") {
      result = input.location?.includes("San Francisco")
        ? "San Francisco: 68F, partly cloudy"
        : "New York: 45F, clear skies";
    } else {
      result = input.timezone?.includes("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST";
    }

    return {
      type: "tool_result" as const,
      tool_use_id: toolUse.id,
      content: result
    };
  });

// Get final response with correct formatting
console.log("\nGetting final response...");
const finalResponse = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [
    {
      role: "user",
      content: "What's the weather in SF and NYC, and what time is it there?"
    },
    { role: "assistant", content: response.content },
    { role: "user", content: toolResults }
  ],
  tools: tools
});

for (const block of finalResponse.content) {
  if (block.type === "text") {
    console.log(`\nClaude's response:\n${block.text}`);
  }
}

// Verify formatting
console.log("\n--- Verification ---");
console.log(`✓ Tool results sent in single user message: ${toolResults.length} results`);
console.log("✓ No text before tool results in content array");
console.log("✓ Conversation formatted correctly for future parallel tool use");
AnthropicClient client = new();

var tools = new List<ToolUnion>
{
    new ToolUnion(new Tool()
    {
        Name = "get_weather",
        Description = "Get the current weather in a given location",
        InputSchema = new InputSchema()
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }),
            },
            Required = ["location"],
        },
    }),
    new ToolUnion(new Tool()
    {
        Name = "get_time",
        Description = "Get the current time in a given timezone",
        InputSchema = new InputSchema()
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["timezone"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The timezone, e.g. America/New_York" }),
            },
            Required = ["timezone"],
        },
    }),
};

Console.WriteLine("Requesting parallel tool calls...");
var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Messages = [new() { Role = Role.User, Content = "What's the weather in SF and NYC, and what time is it there?" }],
    Tools = tools
};

var response = await client.Messages.Create(parameters);

var toolUses = new List<ToolUseBlock>();
foreach (var block in response.Content)
{
    if (block.TryPickToolUse(out var toolUse))
    {
        toolUses.Add(toolUse);
    }
}
Console.WriteLine($"\n✓ Claude made {toolUses.Count} tool calls");

if (toolUses.Count > 1)
{
    Console.WriteLine("✓ Parallel tool calls detected!");
    foreach (var tool in toolUses)
    {
        Console.WriteLine($"  - {tool.Name}: {JsonSerializer.Serialize(tool.Input)}");
    }
}
else
{
    Console.WriteLine("✗ No parallel tool calls detected");
}

var toolResults = new List<ContentBlockParam>();
foreach (var toolUse in toolUses)
{
    string result;
    if (toolUse.Name == "get_weather")
    {
        result = JsonSerializer.Serialize(toolUse.Input).Contains("San Francisco")
            ? "San Francisco: 68°F, partly cloudy"
            : "New York: 45°F, clear skies";
    }
    else
    {
        result = JsonSerializer.Serialize(toolUse.Input).Contains("Los_Angeles")
            ? "2:30 PM PST"
            : "5:30 PM EST";
    }

    toolResults.Add(new ContentBlockParam(new ToolResultBlockParam()
    {
        ToolUseID = toolUse.ID,
        Content = result,
    }));
}

Console.WriteLine("\nGetting final response...");
var finalParameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Messages = [
        new() { Role = Role.User, Content = "What's the weather in SF and NYC, and what time is it there?" },
        new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() },
        new() { Role = Role.User, Content = new MessageParamContent(toolResults) }
    ],
    Tools = tools
};

var finalResponse = await client.Messages.Create(finalParameters);
var text = finalResponse.Content.Select(b => b.Value).OfType<TextBlock>().FirstOrDefault();
Console.WriteLine($"\nClaude's response:\n{text?.Text}");

Console.WriteLine("\n--- Verification ---");
Console.WriteLine($"✓ Tool results sent in single user message: {toolResults.Count} results");
Console.WriteLine("✓ No text before tool results in content array");
Console.WriteLine("✓ Conversation formatted correctly for future parallel tool use");
client := anthropic.NewClient()

tools := []anthropic.ToolUnionParam{
	{OfTool: &anthropic.ToolParam{
		Name:        "get_weather",
		Description: anthropic.String("Get the current weather in a given location"),
		InputSchema: anthropic.ToolInputSchemaParam{
			Properties: map[string]any{
				"location": map[string]any{
					"type":        "string",
					"description": "The city and state, e.g. San Francisco, CA",
				},
			},
			Required: []string{"location"},
		},
	}},
	{OfTool: &anthropic.ToolParam{
		Name:        "get_time",
		Description: anthropic.String("Get the current time in a given timezone"),
		InputSchema: anthropic.ToolInputSchemaParam{
			Properties: map[string]any{
				"timezone": map[string]any{
					"type":        "string",
					"description": "The timezone, e.g. America/New_York",
				},
			},
			Required: []string{"timezone"},
		},
	}},
}

fmt.Println("Requesting parallel tool calls...")
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in SF and NYC, and what time is it there?")),
	},
	Tools: tools,
})
if err != nil {
	log.Fatal(err)
}

// Find tool use blocks using type switch
type toolUseInfo struct {
	ID    string
	Name  string
	Input json.RawMessage
}
var toolUses []toolUseInfo
for _, block := range response.Content {
	switch variant := block.AsAny().(type) {
	case anthropic.ToolUseBlock:
		toolUses = append(toolUses, toolUseInfo{
			ID:    variant.ID,
			Name:  variant.Name,
			Input: variant.Input,
		})
	}
}

fmt.Printf("\n✓ Claude made %d tool calls\n", len(toolUses))

if len(toolUses) > 1 {
	fmt.Println("✓ Parallel tool calls detected!")
	for _, tool := range toolUses {
		fmt.Printf("  - %s: %s\n", tool.Name, string(tool.Input))
	}
} else {
	fmt.Println("✗ No parallel tool calls detected")
}

// Build tool results
var toolResults []anthropic.ContentBlockParamUnion
for _, toolUse := range toolUses {
	var result string
	inputStr := string(toolUse.Input)

	if toolUse.Name == "get_weather" {
		if strings.Contains(inputStr, "San Francisco") {
			result = "San Francisco: 68°F, partly cloudy"
		} else {
			result = "New York: 45°F, clear skies"
		}
	} else {
		if strings.Contains(inputStr, "Los_Angeles") {
			result = "2:30 PM PST"
		} else {
			result = "5:30 PM EST"
		}
	}

	toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, result, false))
}

fmt.Println("\nGetting final response...")
finalResponse, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in SF and NYC, and what time is it there?")),
		response.ToParam(),
		anthropic.NewUserMessage(toolResults...),
	},
	Tools: tools,
})
if err != nil {
	log.Fatal(err)
}

var finalText string
for _, block := range finalResponse.Content {
	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
		finalText = textBlock.Text
		break
	}
}

fmt.Printf("\nClaude's response:\n%s\n", finalText)

fmt.Println("\n--- Verification ---")
fmt.Printf("✓ Tool results sent in single user message: %d results\n", len(toolResults))
fmt.Println("✓ No text before tool results in content array")
fmt.Println("✓ Conversation formatted correctly for future parallel tool use")
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

Tool weatherTool = Tool.builder()
    .name("get_weather")
    .description("Get the current weather in a given location")
    .inputSchema(InputSchema.builder()
        .properties(JsonValue.from(Map.of(
            "location", Map.of(
                "type", "string",
                "description", "The city and state, e.g. San Francisco, CA"
            )
        )))
        .putAdditionalProperty("required", JsonValue.from(List.of("location")))
        .build())
    .build();

Tool timeTool = Tool.builder()
    .name("get_time")
    .description("Get the current time in a given timezone")
    .inputSchema(InputSchema.builder()
        .properties(JsonValue.from(Map.of(
            "timezone", Map.of(
                "type", "string",
                "description", "The timezone, e.g. America/New_York"
            )
        )))
        .putAdditionalProperty("required", JsonValue.from(List.of("timezone")))
        .build())
    .build();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024L)
    .addTool(weatherTool)
    .addTool(timeTool)
    .addUserMessage("What's the weather in SF and NYC, and what time is it there?")
    .build();

IO.println("Requesting parallel tool calls...");
Message response = client.messages().create(params);

List<ToolUseBlock> toolUses = new ArrayList<>();
for (ContentBlock block : response.content()) {
    if (block.toolUse().isPresent()) {
        toolUses.add(block.toolUse().get());
    }
}

IO.println("\n✓ Claude made " + toolUses.size() + " tool calls");

if (toolUses.size() > 1) {
    IO.println("✓ Parallel tool calls detected!");
    for (ToolUseBlock tool : toolUses) {
        IO.println("  - " + tool.name() + ": " + tool._input());
    }
} else {
    IO.println("✗ No parallel tool calls detected");
}

List<ContentBlockParam> toolResults = new ArrayList<>();
for (ToolUseBlock toolUse : toolUses) {
    String result;
    if (toolUse.name().equals("get_weather")) {
        String location = toolUse._input().toString();
        result = location.contains("San Francisco")
            ? "San Francisco: 68°F, partly cloudy"
            : "New York: 45°F, clear skies";
    } else {
        String timezone = toolUse._input().toString();
        result = timezone.contains("Los_Angeles")
            ? "2:30 PM PST"
            : "5:30 PM EST";
    }
    toolResults.add(ContentBlockParam.ofToolResult(
        ToolResultBlockParam.builder()
            .toolUseId(toolUse.id())
            .content(result)
            .build()
    ));
}

IO.println("\nGetting final response...");
MessageCreateParams finalParams = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024L)
    .addTool(weatherTool)
    .addTool(timeTool)
    .addUserMessage("What's the weather in SF and NYC, and what time is it there?")
    .addMessage(response)
    .addUserMessageOfBlockParams(toolResults)
    .build();

Message finalResponse = client.messages().create(finalParams);
finalResponse.content().stream()
    .flatMap(block -> block.text().stream())
    .forEach(textBlock -> IO.println("\nClaude's response:\n" + textBlock.text()));

IO.println("\n--- Verification ---");
IO.println("✓ Tool results sent in single user message: " + toolResults.size() + " results");
IO.println("✓ No text before tool results in content array");
IO.println("✓ Conversation formatted correctly for future parallel tool use");
$client = new Client();

$tools = [
    [
        'name' => 'get_weather',
        'description' => 'Get the current weather in a given location',
        'input_schema' => [
            'type' => 'object',
            'properties' => [
                'location' => [
                    'type' => 'string',
                    'description' => 'The city and state, e.g. San Francisco, CA'
                ]
            ],
            'required' => ['location']
        ]
    ],
    [
        'name' => 'get_time',
        'description' => 'Get the current time in a given timezone',
        'input_schema' => [
            'type' => 'object',
            'properties' => [
                'timezone' => [
                    'type' => 'string',
                    'description' => 'The timezone, e.g. America/New_York'
                ]
            ],
            'required' => ['timezone']
        ]
    ]
];

echo "Requesting parallel tool calls...\n";
$response = $client->messages->create(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => "What's the weather in SF and NYC, and what time is it there?"]
    ],
    model: 'claude-opus-5-5',
    tools: $tools,
);

$toolUses = array_filter($response->content, fn($block) => $block->type === 'tool_use');
echo "\n✓ Claude made " . count($toolUses) . " tool calls\n";

if (count($toolUses) > 1) {
    echo "✓ Parallel tool calls detected!\n";
    foreach ($toolUses as $tool) {
        echo "  - {$tool->name}: " . json_encode($tool->input) . "\n";
    }
} else {
    echo "✗ No parallel tool calls detected\n";
}

$toolResults = [];
foreach ($toolUses as $toolUse) {
    if ($toolUse->name === 'get_weather') {
        $result = str_contains(json_encode($toolUse->input), 'San Francisco')
            ? 'San Francisco: 68°F, partly cloudy'
            : 'New York: 45°F, clear skies';
    } else {
        $result = str_contains(json_encode($toolUse->input), 'Los_Angeles')
            ? '2:30 PM PST'
            : '5:30 PM EST';
    }

    $toolResults[] = [
        'type' => 'tool_result',
        'tool_use_id' => $toolUse->id,
        'content' => $result
    ];
}

echo "\nGetting final response...\n";
$finalResponse = $client->messages->create(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => "What's the weather in SF and NYC, and what time is it there?"],
        ['role' => 'assistant', 'content' => $response->content],
        ['role' => 'user', 'content' => $toolResults]
    ],
    model: 'claude-opus-5-5',
    tools: $tools,
);

$textBlock = array_find($finalResponse->content, static fn ($block): bool => $block->type === 'text');
echo "\nClaude's response:\n{$textBlock->text}\n";

echo "\n--- Verification ---\n";
echo "✓ Tool results sent in single user message: " . count($toolResults) . " results\n";
echo "✓ No text before tool results in content array\n";
echo "✓ Conversation formatted correctly for future parallel tool use\n";
client = Anthropic::Client.new

tools = [
  {
    name: "get_weather",
    description: "Get the current weather in a given location",
    input_schema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        }
      },
      required: ["location"]
    }
  },
  {
    name: "get_time",
    description: "Get the current time in a given timezone",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "The timezone, e.g. America/New_York"
        }
      },
      required: ["timezone"]
    }
  }
]

puts "Requesting parallel tool calls..."
response = client.messages.create(
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [
    { role: "user", content: "What's the weather in SF and NYC, and what time is it there?" }
  ],
  tools: tools
)

tool_uses = response.content.select { |block| block.type == :tool_use }
puts "\n✓ Claude made #{tool_uses.length} tool calls"

if tool_uses.length > 1
  puts "✓ Parallel tool calls detected!"
  tool_uses.each do |tool|
    puts "  - #{tool.name}: #{tool.input}"
  end
else
  puts "✗ No parallel tool calls detected"
end

tool_results = tool_uses.map do |tool_use|
  result = if tool_use.name == "get_weather"
    location = tool_use.input[:location].to_s
    location.include?("San Francisco") ? "San Francisco: 68°F, partly cloudy" : "New York: 45°F, clear skies"
  else
    timezone = tool_use.input[:timezone].to_s
    timezone.include?("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST"
  end

  {
    type: "tool_result",
    tool_use_id: tool_use.id,
    content: result
  }
end

puts "\nGetting final response..."
final_response = client.messages.create(
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [
    { role: "user", content: "What's the weather in SF and NYC, and what time is it there?" },
    { role: "assistant", content: response.content },
    { role: "user", content: tool_results }
  ],
  tools: tools
)

final_text = final_response.content.find { |block| block.type == :text }
puts "\nClaude's response:\n#{final_text.text}"

puts "\n--- Verification ---"
puts "✓ Tool results sent in single user message: #{tool_results.length} results"
puts "✓ No text before tool results in content array"
puts "✓ Conversation formatted correctly for future parallel tool use"

마지막의 요약 줄들은 병렬성이 동작하도록 하는 두 가지 포맷 규칙을 다시 강조해요. 모든 도구 결과가 단일 사용자 메시지로 반환되고, 그 메시지에서 도구 결과보다 앞에 텍스트 콘텐츠가 없어야 한다는 것이죠.

병렬 도구 사용 극대화하기 (Maximizing parallel tool use)

Claude 4 이상 모델은 요청이 여러 도구의 이점을 누릴 수 있을 때 기본적으로 병렬 도구 호출을 해요. 모든 모델에서 `병렬 도구 호출의 가능성은 타겟 프롬프팅으로 높일 수 있어요:

Claude 4 이상 모델에는 시스템 프롬프트에 이것을 추가하세요:
```text wrap
For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
```

더 강력한 병렬 도구 사용이 필요하면(기본으로 충분하지 않을 때 권장) 이것을 쓰세요:

```text wrap
<use_parallel_tool_calls>
For maximum efficiency, whenever you perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. Prioritize calling tools in parallel whenever possible. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. When running multiple read-only commands like `ls` or `list_dir`, always run all of the commands in parallel. Err on the side of maximizing parallel tool calls rather than running too many tools sequentially.
</use_parallel_tool_calls>
```
특정 사용자 메시지 안에서도 병렬 도구 사용을 장려할 수 있어요:
```text wrap
Instead of:
"What's the weather in Paris? Also check London."

Use:
"Check the weather in Paris and London simultaneously."

Or be explicit:
"Please use parallel tool calls to get the weather for Paris, London, and Tokyo at the same time."
```
**긴 에이전트 루프에서의 Claude Fable 5.1**

Claude Fable 5.1은 이전 모델보다 병렬 도구 호출을 덜 할 수 있어요. 특히 다음 읽기가 암시만 되는 긴 에이전트 루프에서 두드러져요(커스텀 코딩 에이전트, bash·텍스트 편집기 하니스, 컴퓨터 사용). 표준 함수 호출은 영향받지 않아요. 추가할 배칭 지침과 위치는 에이전트 루프에서 독립 도구 호출 배치하기를 보세요.

병렬 도구 사용 비활성화하기 (Disable parallel tool use)

병렬 도구 사용은 기본적으로 켜져 있어요. 끄려면 tool_choice 객체 안에 disable_parallel_tool_use: true를 설정하세요. 최상위 요청 매개변수가 아니에요. 그 효과는 tool_choice 타입에 따라 달라져요.

도구 호출 최대 1개 (At most one tool call)

tool_choice 타입이 auto(기본)일 때 disable_parallel_tool_use: true를 설정하면 Claude는 응답당 최대 하나의 도구만 호출해요. Claude는 도구를 호출하지 않고 일반 텍스트로 답할 수도 있어요. 강조된 줄이 표준 도구 사용 요청과 유일하게 다른 부분이에요:

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "tools": [{ "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } }], "tool_choice": {"type": "auto", "disable_parallel_tool_use": true}, "messages": [ {"role": "user", "content": "What is the weather in San Francisco and New York?"} ] }' ```
ant messages create <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
tools:
  - name: get_weather
    description: Get the current weather in a given location
    input_schema:
      type: object
      properties:
        location:
          type: string
          description: The city and state, e.g. San Francisco, CA
      required: [location]
tool_choice:
  type: auto
  disable_parallel_tool_use: true
messages:
  - role: user
    content: What is the weather in San Francisco and New York?
YAML
client = Anthropic()

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Get the current weather in a given location",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    }
                },
                "required": ["location"],
            },
        }
    ],
    tool_choice={"type": "auto", "disable_parallel_tool_use": True},
    messages=[
        {
            "role": "user",
            "content": "What is the weather in San Francisco and New York?",
        }
    ],
)
print(response.content)
const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather in a given location",
      input_schema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "The city and state, e.g. San Francisco, CA"
          }
        },
        required: ["location"]
      }
    }
  ],
  tool_choice: { type: "auto", disable_parallel_tool_use: true },
  messages: [{ role: "user", content: "What is the weather in San Francisco and New York?" }]
});
console.log(response.content);
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Tools = [
        new ToolUnion(new Tool()
        {
            Name = "get_weather",
            Description = "Get the current weather in a given location",
            InputSchema = new InputSchema()
            {
                Properties = new Dictionary<string, JsonElement>
                {
                    ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }),
                },
                Required = ["location"],
            },
        }),
    ],
    ToolChoice = new ToolChoiceAuto { DisableParallelToolUse = true },
    Messages = [new() { Role = Role.User, Content = "What is the weather in San Francisco and New York?" }]
};

var response = await client.Messages.Create(parameters);
Console.WriteLine(response);
client := anthropic.NewClient()

response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Tools: []anthropic.ToolUnionParam{
		{OfTool: &anthropic.ToolParam{
			Name:        "get_weather",
			Description: anthropic.String("Get the current weather in a given location"),
			InputSchema: anthropic.ToolInputSchemaParam{
				Properties: map[string]any{
					"location": map[string]any{
						"type":        "string",
						"description": "The city and state, e.g. San Francisco, CA",
					},
				},
				Required: []string{"location"},
			},
		}},
	},
	ToolChoice: anthropic.ToolChoiceUnionParam{
		OfAuto: &anthropic.ToolChoiceAutoParam{
			DisableParallelToolUse: anthropic.Bool(true),
		},
	},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco and New York?")),
	},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(response.Content)
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

InputSchema schema = InputSchema.builder()
    .properties(
        JsonValue.from(
            Map.of(
                "location", Map.of(
                    "type", "string",
                    "description", "The city and state, e.g. San Francisco, CA"
                )
            )
        )
    )
    .putAdditionalProperty("required", JsonValue.from(List.of("location")))
    .build();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024L)
    .addTool(
        Tool.builder()
            .name("get_weather")
            .description("Get the current weather in a given location")
            .inputSchema(schema)
            .build()
    )
    .toolChoice(ToolChoiceAuto.builder().disableParallelToolUse(true).build())
    .addUserMessage("What is the weather in San Francisco and New York?")
    .build();

Message response = client.messages().create(params);
IO.println(response.content());
$client = new Client();

$response = $client->messages->create(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'What is the weather in San Francisco and New York?']
    ],
    model: 'claude-opus-5-5',
    toolChoice: ['type' => 'auto', 'disableParallelToolUse' => true],
    tools: [
        [
            'name' => 'get_weather',
            'description' => 'Get the current weather in a given location',
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'The city and state, e.g. San Francisco, CA'
                    ]
                ],
                'required' => ['location']
            ]
        ]
    ],
);

echo $response;
client = Anthropic::Client.new

response = client.messages.create(
  model: "claude-opus-5-5",
  max_tokens: 1024,
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather in a given location",
      input_schema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "The city and state, e.g. San Francisco, CA"
          }
        },
        required: ["location"]
      }
    }
  ],
  tool_choice: { type: "auto", disable_parallel_tool_use: true },
  messages: [
    { role: "user", content: "What is the weather in San Francisco and New York?" }
  ]
)
puts response.content

정확히 도구 하나 호출 (Exactly one tool call)

tool_choice 타입이 anytool일 때 disable_parallel_tool_use: true를 설정하면 Claude는 정확히 하나의 도구만 호출해요. Claude Opus 5.5, Claude Fable 5.1, Claude Mythos 5.1은 이런 tool_choice 타입을 지원하지 않아요(도구 사용 강제하기 참고). 다음 예시는 any를 써요. 같은 필드가 tool에서도 동작해요:

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [{ "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } }], "tool_choice": {"type": "any", "disable_parallel_tool_use": true}, "messages": [ {"role": "user", "content": "What is the weather in San Francisco and New York?"} ] }' ```
ant messages create <<'YAML'
model: claude-opus-5
max_tokens: 1024
tools:
  - name: get_weather
    description: Get the current weather in a given location
    input_schema:
      type: object
      properties:
        location:
          type: string
          description: The city and state, e.g. San Francisco, CA
      required: [location]
tool_choice:
  type: any
  disable_parallel_tool_use: true
messages:
  - role: user
    content: What is the weather in San Francisco and New York?
YAML
client = Anthropic()

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Get the current weather in a given location",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    }
                },
                "required": ["location"],
            },
        }
    ],
    tool_choice={"type": "any", "disable_parallel_tool_use": True},
    messages=[
        {
            "role": "user",
            "content": "What is the weather in San Francisco and New York?",
        }
    ],
)
print(response.content)
const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather in a given location",
      input_schema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "The city and state, e.g. San Francisco, CA"
          }
        },
        required: ["location"]
      }
    }
  ],
  tool_choice: { type: "any", disable_parallel_tool_use: true },
  messages: [{ role: "user", content: "What is the weather in San Francisco and New York?" }]
});
console.log(response.content);
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5,
    MaxTokens = 1024,
    Tools = [
        new ToolUnion(new Tool()
        {
            Name = "get_weather",
            Description = "Get the current weather in a given location",
            InputSchema = new InputSchema()
            {
                Properties = new Dictionary<string, JsonElement>
                {
                    ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }),
                },
                Required = ["location"],
            },
        }),
    ],
    ToolChoice = new ToolChoiceAny { DisableParallelToolUse = true },
    Messages = [new() { Role = Role.User, Content = "What is the weather in San Francisco and New York?" }]
};

var response = await client.Messages.Create(parameters);
Console.WriteLine(response);
client := anthropic.NewClient()

response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5,
	MaxTokens: 1024,
	Tools: []anthropic.ToolUnionParam{
		{OfTool: &anthropic.ToolParam{
			Name:        "get_weather",
			Description: anthropic.String("Get the current weather in a given location"),
			InputSchema: anthropic.ToolInputSchemaParam{
				Properties: map[string]any{
					"location": map[string]any{
						"type":        "string",
						"description": "The city and state, e.g. San Francisco, CA",
					},
				},
				Required: []string{"location"},
			},
		}},
	},
	ToolChoice: anthropic.ToolChoiceUnionParam{
		OfAny: &anthropic.ToolChoiceAnyParam{
			DisableParallelToolUse: anthropic.Bool(true),
		},
	},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco and New York?")),
	},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(response.Content)
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

InputSchema schema = InputSchema.builder()
    .properties(
        JsonValue.from(
            Map.of(
                "location", Map.of(
                    "type", "string",
                    "description", "The city and state, e.g. San Francisco, CA"
                )
            )
        )
    )
    .putAdditionalProperty("required", JsonValue.from(List.of("location")))
    .build();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5)
    .maxTokens(1024L)
    .addTool(
        Tool.builder()
            .name("get_weather")
            .description("Get the current weather in a given location")
            .inputSchema(schema)
            .build()
    )
    .toolChoice(ToolChoiceAny.builder().disableParallelToolUse(true).build())
    .addUserMessage("What is the weather in San Francisco and New York?")
    .build();

Message response = client.messages().create(params);
IO.println(response.content());
$client = new Client();

$response = $client->messages->create(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'What is the weather in San Francisco and New York?']
    ],
    model: 'claude-opus-5',
    toolChoice: ['type' => 'any', 'disableParallelToolUse' => true],
    tools: [
        [
            'name' => 'get_weather',
            'description' => 'Get the current weather in a given location',
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'The city and state, e.g. San Francisco, CA'
                    ]
                ],
                'required' => ['location']
            ]
        ]
    ],
);

echo $response;
client = Anthropic::Client.new

response = client.messages.create(
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather in a given location",
      input_schema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "The city and state, e.g. San Francisco, CA"
          }
        },
        required: ["location"]
      }
    }
  ],
  tool_choice: { type: "any", disable_parallel_tool_use: true },
  messages: [
    { role: "user", content: "What is the weather in San Francisco and New York?" }
  ]
)
puts response.content

문제 해결 (Troubleshooting)

Claude가 예상했는데 병렬 도구 호출을 하지 않으면, 이런 흔한 문제들을 확인해 보세요:

1. 잘못된 도구 결과 포맷

가장 흔한 문제는 대화 기록에서 도구 결과를 잘못 포맷하는 거예요. 이러면 Claude가 "배워서" 병렬 호출을 피하게 돼요.

병렬 도구 사용에 특히 중요한 것:

  • 틀림: 각 도구 결과에 대해 별도의 사용자 메시지
  • 맞음: 모든 도구 결과를 단일 사용자 메시지에 함께
// Wrong: separate user messages reduce parallel tool use
[
  {"role": "assistant", "content": [tool_use_1, tool_use_2]},
  {"role": "user", "content": [tool_result_1]},
  {"role": "user", "content": [tool_result_2]}  // Separate message
]

// Correct: one user message with all results maintains parallel tool use
[
  {"role": "assistant", "content": [tool_use_1, tool_use_2]},
  {"role": "user", "content": [tool_result_1, tool_result_2]}  // Single message
]

다른 포맷 규칙은 도구 호출 처리하기를 보세요.

2. 약한 프롬프팅

기본 프롬프팅으로는 충분하지 않을 수 있어요. 병렬 도구 사용 극대화하기의 더 강한 시스템 프롬프트를 사용하세요.

3. 병렬 도구 사용량 측정하기

병렬 도구 호출이 동작하는지 확인하려면:

```bash cURL # Measuring parallel tool use is client-side analysis of responses you've already # collected, so it doesn't translate to a one-off shell command. See the SDK tabs. ```
# Measuring parallel tool use is client-side analysis of responses you've already
# collected, so it doesn't translate to a one-off shell command. See the SDK tabs.
messages = []  # Message objects returned by client.messages.create across your run

tool_call_messages = [
    msg for msg in messages if any(block.type == "tool_use" for block in msg.content)
]
total_tool_calls = sum(
    len([block for block in msg.content if block.type == "tool_use"])
    for msg in tool_call_messages
)
avg_tools_per_message = (
    total_tool_calls / len(tool_call_messages) if tool_call_messages else 0.0
)
print(f"Average tools per message: {avg_tools_per_message}")
# Should be > 1.0 if parallel calls are working
const messages: Anthropic.Message[] = []; // Message objects returned by client.messages.create across your run

const toolCallMessages = messages.filter((message) =>
  message.content.some((block) => block.type === "tool_use")
);
const totalToolCalls = toolCallMessages.reduce(
  (sum, message) => sum + message.content.filter((block) => block.type === "tool_use").length,
  0
);
const avgToolsPerMessage =
  toolCallMessages.length > 0 ? totalToolCalls / toolCallMessages.length : 0;
console.log(`Average tools per message: ${avgToolsPerMessage}`);
// Should be > 1.0 if parallel calls are working
List<Message> messages = []; // Message objects returned by client.Messages.Create across your run

var toolCallMessages = messages
    .Where(message => message.Content.Any(block => block.TryPickToolUse(out _)))
    .ToList();
var totalToolCalls = toolCallMessages
    .Sum(message => message.Content.Count(block => block.TryPickToolUse(out _)));
var avgToolsPerMessage = toolCallMessages.Count > 0 ? (double)totalToolCalls / toolCallMessages.Count : 0.0;
Console.WriteLine($"Average tools per message: {avgToolsPerMessage}");
// Should be > 1.0 if parallel calls are working
var messages []anthropic.Message // Message values returned by client.Messages.New across your run

toolCallMessageCount := 0
totalToolCalls := 0
for _, message := range messages {
	callsInMessage := 0
	for _, block := range message.Content {
		if block.Type == "tool_use" {
			callsInMessage++
		}
	}
	if callsInMessage > 0 {
		toolCallMessageCount++
		totalToolCalls += callsInMessage
	}
}

avgToolsPerMessage := 0.0
if toolCallMessageCount > 0 {
	avgToolsPerMessage = float64(totalToolCalls) / float64(toolCallMessageCount)
}
fmt.Println("Average tools per message:", avgToolsPerMessage)
// Should be > 1.0 if parallel calls are working
List<Message> messages = List.of(); // Message objects returned by client.messages().create() across your run

List<Message> toolCallMessages = messages.stream()
    .filter(message -> message.content().stream().anyMatch(ContentBlock::isToolUse))
    .toList();
long totalToolCalls = toolCallMessages.stream()
    .mapToLong(message -> message.content().stream().filter(ContentBlock::isToolUse).count())
    .sum();
double avgToolsPerMessage = toolCallMessages.isEmpty() ? 0.0 : (double) totalToolCalls / toolCallMessages.size();
IO.println("Average tools per message: " + avgToolsPerMessage);
// Should be > 1.0 if parallel calls are working
// $messages: Message objects returned by $client->messages->create() across your run
$messages = [];

$toolCallMessages = array_values(array_filter(
    $messages,
    fn ($message) => count(array_filter($message->content, fn ($block) => $block->type === 'tool_use')) > 0
));
$totalToolCalls = array_sum(array_map(
    fn ($message) => count(array_filter($message->content, fn ($block) => $block->type === 'tool_use')),
    $toolCallMessages
));
$avgToolsPerMessage = count($toolCallMessages) > 0 ? $totalToolCalls / count($toolCallMessages) : 0.0;
echo "Average tools per message: {$avgToolsPerMessage}\n";
// Should be > 1.0 if parallel calls are working
messages = [] # Message objects returned by client.messages.create across your run

tool_call_messages = messages.select { |message| message.content.any? { |block| block.type == :tool_use } }
total_tool_calls = tool_call_messages.sum { |message| message.content.count { |block| block.type == :tool_use } }
avg_tools_per_message = tool_call_messages.empty? ? 0.0 : total_tool_calls.to_f / tool_call_messages.size
puts "Average tools per message: #{avg_tools_per_message}"
# Should be > 1.0 if parallel calls are working

4. 배치의 호출들이 서로 의존하는 것처럼 보인다

실행 순서는 우리 선택이에요. 도구에 순서 의존성이 있다면 배치를 순차 실행하고 첫 실패에서 멈추는 게 유효한 전략이에요(컴퓨터 사용브라우저 사용 도구에는 필수예요). 실행하지 않은 호출에는 is_error: true를 반환하세요. 병렬로 실행했는데 호출이 선행 조건이 완료되지 않아 실패한다면, 자연스러운 오류 메시지와 함께 is_error: true를 반환하세요. Claude는 다음 턴에 그 호출을 다시 발행해요. 의존하는 호출이 함께 나타나는 걸 줄이려면 시스템 프롬프트에 "Only batch tool calls that are independent of each other."를 추가하세요.

더 알아보기 (Learn more)