도구 및 멀티턴 워크플로에서의 thinking
도구 및 멀티턴 워크플로에서의 thinking (Thinking in tool and multi-turn workflows)
이 페이지는 thinking을 켠 상태에서 완전한 2턴 도구 사용 왕복(round trip)을 단계별로 보여줘요: Claude가 생각하고, 도구 호출을 요청하고, 결과를 받고, 답을 끝내며, 매 단계에서 thinking 블록을 올바르게 처리해요. 전체 규칙은 Thinking 페이지의 도구 사용과 thinking 및 Thinking 블록 보존하기에 있고, 이 페이지는 그 규칙을 실행 가능한 코드로 보여줘요.
출처: 문서
본문
이 페이지는 thinking을 켠 상태에서 완전한 2턴 도구 사용 왕복(round trip)을 단계별로 보여줘요: Claude가 생각하고, 도구 호출을 요청하고, 결과를 받고, 답을 끝내며, 매 단계에서 thinking 블록을 올바르게 처리해요. 전체 규칙은 Thinking 페이지의 도구 사용과 thinking 및 Thinking 블록 보존하기에 있고, 이 페이지는 그 규칙을 실행 가능한 코드로 보여줘요.
이 워크스루가 적용하는 규칙
각 링크는 Thinking 페이지의 전체 진술로 이어져요:
- 수동 모드에서 도구 선택을
auto나none으로 제한: 도구 사용을 강제하는tool_choice옵션은 수동 확장 thinking(thinking: {type: "enabled"})에서 오류를 반환해요. 적응형 thinking은 강제 도구 사용을 지원해요. - 어시스턴트 턴당 하나의 thinking 구성 유지: 도구 사용 루프는 하나의 어시스턴트 턴이므로, 구성은 턴 사이에서만 바꾸세요.
- thinking 블록을 완전하고 수정 없이 다시 전달: 도구 결과를 반환할 때 어시스턴트 메시지의 thinking 블록이 함께 돌아와야 해요.
- 받은 어시스턴트 메시지를 정확히 그대로 에코: 메시지를 다시 만들거나
redacted_thinking블록을 걸러내면 400 오류가 나요.
샘플은 적응형 thinking을 씁니다. 확장 thinking만 지원하는 모델에서는 thinking: {type: "enabled", budget_tokens: N}을 대신하세요. 왕복 규칙은 동일해요.
2턴 도구 사용 왕복 워크스루
예시는 get_weather 도구를 정의하고, Claude가 생각하고 도구 호출을 요청하게 한 뒤, thinking 블록을 포함해 받은 그대로의 어시스턴트 턴과 함께 도구 결과를 반환해요.
<CodeGroup>
```bash CLI
ant messages create --transform content <<'YAML'
model: claude-opus-4-8
max_tokens: 16000
thinking:
type: adaptive
tools:
- name: get_weather
description: Get current weather for a location
input_schema:
type: object
properties:
location:
type: string
description: City name
required:
- location
messages:
- role: user
content: "What's the weather in Paris?"
YAML
```
```python Python
client = anthropic.Anthropic()
weather_tool = {
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string", "description": "City name"}},
"required": ["location"],
},
}
# First request - Claude responds with thinking and tool request
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
thinking={"type": "adaptive"},
tools=[weather_tool],
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)
print(response)
```
```typescript TypeScript
const client = new Anthropic();
const weatherTool: Anthropic.Tool = {
name: "get_weather",
description: "Get current weather for a location",
input_schema: {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}
};
// First request - Claude responds with thinking and tool request
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
thinking: {
type: "adaptive"
},
tools: [weatherTool],
messages: [{ role: "user", content: "What's the weather in Paris?" }]
});
console.log(response);
```
```csharp C#
AnthropicClient client = new();
var weatherTool = new ToolUnion(new Tool()
{
Name = "get_weather",
Description = "Get current weather for a location",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }),
},
Required = ["location"],
},
});
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus4_8,
MaxTokens = 16000,
Thinking = new ThinkingConfigAdaptive(),
Tools = [weatherTool],
Messages = [new() { Role = Role.User, Content = "What's the weather in Paris?" }]
};
var message = await client.Messages.Create(parameters);
Console.WriteLine(message);
```
```go Go
client := anthropic.NewClient()
weatherTool := anthropic.ToolUnionParam{
OfTool: &anthropic.ToolParam{
Name: "get_weather",
Description: anthropic.String("Get current weather for a location"),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name",
},
},
Required: []string{"location"},
},
},
}
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 16000,
Thinking: anthropic.ThinkingConfigParamUnion{
OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{},
},
Tools: []anthropic.ToolUnionParam{weatherTool},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in Paris?")),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response)
```
```java Java
import com.anthropic.models.messages.ThinkingConfigAdaptive;
// ...
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(16000L)
.thinking(ThinkingConfigAdaptive.builder().build())
.addTool(Tool.builder()
.name("get_weather")
.description("Get current weather for a location")
.inputSchema(Tool.InputSchema.builder()
.properties(JsonValue.from(Map.of(
"location", Map.of("type", "string", "description", "City name")
)))
.required(List.of("location"))
.build())
.build())
.addUserMessage("What's the weather in Paris?")
.build();
Message response = client.messages().create(params);
IO.println(response);
```
```php PHP
$client = new Client();
$weatherTool = [
'name' => 'get_weather',
'description' => 'Get current weather for a location',
'input_schema' => [
'type' => 'object',
'properties' => [
'location' => ['type' => 'string', 'description' => 'City name']
],
'required' => ['location']
]
];
$message = $client->messages->create(
maxTokens: 16000,
messages: [
['role' => 'user', 'content' => "What's the weather in Paris?"]
],
model: 'claude-opus-4-8',
thinking: ['type' => 'adaptive'],
tools: [$weatherTool],
);
echo $message;
```
```ruby Ruby
client = Anthropic::Client.new
weather_tool = {
name: "get_weather",
description: "Get current weather for a location",
input_schema: {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}
}
message = client.messages.create(
model: "claude-opus-4-8",
max_tokens: 16000,
thinking: {
type: "adaptive"
},
tools: [weather_tool],
messages: [
{ role: "user", content: "What's the weather in Paris?" }
]
)
puts message
```
</CodeGroup>
<Note>
이 출력 같은 thinking 텍스트를 보려면 요청에 `display: "summarized"`를 추가하세요. claude-opus-4-8을 포함해 display가 omitted로 기본 설정된 모델에서는 그렇지 않으면 `thinking` 필드가 빈 문자열로 돌아오고 `signature`만 채워져요. 어느 쪽이든 content 배열을 그대로 다시 에코하세요. [Thinking 표시 제어](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display)를 보세요.
</Note>
```json Output
{
"content": [
{
"type": "thinking",
"thinking": "The user wants to know the current weather in Paris. I have access to a function `get_weather`...",
"signature": "BDaL4VrbR2Oj0hO4XpJxT28J5T...."
},
{
"type": "text",
"text": "I can help you get the current weather information for Paris. Let me check that for you"
},
{
"type": "tool_use",
"id": "toolu_01CswdEQBMshySk6Y9DFKrfq",
"name": "get_weather",
"input": {
"location": "Paris"
}
}
]
}
```
각 샘플은 자급자족 스크립트예요: 첫 요청을 반복하고, 방금 받은 응답을 사용해 즉시 후속 요청을 보내요.
<CodeGroup>
```bash CLI
# First turn: write the assistant content array (thinking and tool_use
# blocks, signatures intact) to a file. Routing model-generated text
# through a file keeps it out of shell-expansion position later.
ant messages create --transform content --format jsonl \
> assistant_content.json <<'YAML'
model: claude-opus-4-8
max_tokens: 16000
thinking:
type: adaptive
tools:
- name: get_weather
description: Get current weather for a location
input_schema:
type: object
properties:
location:
type: string
description: City name
required: [location]
messages:
- role: user
content: What's the weather in Paris?
YAML
# Second turn: jq fills the two null placeholders from the captured file,
# so the blocks return verbatim as the assistant message. The thinking
# block MUST accompany the tool_use block. The quoted delimiter keeps the
# shell from expanding anything in the body.
jq --slurpfile blocks assistant_content.json '
.messages[1].content = $blocks[0] |
.messages[2].content[0].tool_use_id =
($blocks[0][] | select(.type == "tool_use") | .id)
' <<'JSON' | ant messages create
{
"model": "claude-opus-4-8",
"max_tokens": 16000,
"thinking": {"type": "adaptive"},
"tools": [{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"]
}
}],
"messages": [
{"role": "user", "content": "What's the weather in Paris?"},
{"role": "assistant", "content": null},
{"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": null,
"content": "Current temperature: 88°F"
}]}
]
}
JSON
```
```python Python
client = anthropic.Anthropic()
weather_tool = {
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string", "description": "City name"}},
"required": ["location"],
},
}
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
thinking={"type": "adaptive"},
tools=[weather_tool],
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)
# Extract the tool use block to get its ID for the tool result
tool_use_block = next(block for block in response.content if block.type == "tool_use")
# Call your actual weather API, here is where your actual API call would go
# Let's pretend this is what we get back
weather_data = {"temperature": 88}
# Second request - Include the assistant turn and the tool result
continuation = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
thinking={"type": "adaptive"},
tools=[weather_tool],
messages=[
{"role": "user", "content": "What's the weather in Paris?"},
# Echo the assistant content exactly as received. When a thinking
# block is present, it must accompany the tool_use block.
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": f"Current temperature: {weather_data['temperature']}°F",
}
],
},
],
)
print(continuation)
```
```typescript TypeScript
const client = new Anthropic();
const weatherTool: Anthropic.Tool = {
name: "get_weather",
description: "Get current weather for a location",
input_schema: {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}
};
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
thinking: {
type: "adaptive"
},
tools: [weatherTool],
messages: [{ role: "user", content: "What's the weather in Paris?" }]
});
// Extract the tool use block to get its ID for the tool result
const toolUseBlock = response.content.find(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
);
// Call your actual weather API, here is where your actual API call would go
// Let's pretend this is what we get back
const weatherData = { temperature: 88 };
if (toolUseBlock) {
// Second request - Include the assistant turn and the tool result
const continuation = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
thinking: {
type: "adaptive"
},
tools: [weatherTool],
messages: [
{ role: "user", content: "What's the weather in Paris?" },
// Echo the assistant content exactly as received. When a thinking
// block is present, it must accompany the tool_use block.
{ role: "assistant", content: response.content },
{
role: "user",
content: [
{
type: "tool_result" as const,
tool_use_id: toolUseBlock.id,
content: `Current temperature: ${weatherData.temperature}°F`
}
]
}
]
});
console.log(continuation);
}
```
```csharp C#
AnthropicClient client = new();
var weatherTool = new ToolUnion(new Tool()
{
Name = "get_weather",
Description = "Get current weather for a location",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }),
},
Required = ["location"],
},
});
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus4_8,
MaxTokens = 16000,
Thinking = new ThinkingConfigAdaptive(),
Tools = [weatherTool],
Messages = [
new() { Role = Role.User, Content = "What's the weather in Paris?" }
]
};
var response = await client.Messages.Create(parameters);
// Extract the tool_use block to get its ID for the tool result
ToolUseBlock? toolUseBlock = null;
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var toolUse))
{
toolUseBlock = toolUse;
break;
}
}
var weatherData = new { temperature = 88 };
// Build continuation with tool result
var continuationParams = new MessageCreateParams
{
Model = Model.ClaudeOpus4_8,
MaxTokens = 16000,
Thinking = new ThinkingConfigAdaptive(),
Tools = [weatherTool],
Messages = [
new() { Role = Role.User, Content = "What's the weather in Paris?" },
// response.Content includes the thinking blocks; passing them back is required
new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() },
new() { Role = Role.User, Content = new MessageParamContent(new List<ContentBlockParam>
{
new ContentBlockParam(new ToolResultBlockParam()
{
ToolUseID = toolUseBlock?.ID ?? "",
Content = $"Current temperature: {weatherData.temperature}°F"
})
})}
]
};
var continuation = await client.Messages.Create(continuationParams);
Console.WriteLine(continuation);
```
```go Go
client := anthropic.NewClient()
weatherTool := anthropic.ToolUnionParam{
OfTool: &anthropic.ToolParam{
Name: "get_weather",
Description: anthropic.String("Get current weather for a location"),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name",
},
},
Required: []string{"location"},
},
},
}
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 16000,
Thinking: anthropic.ThinkingConfigParamUnion{
OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{},
},
Tools: []anthropic.ToolUnionParam{weatherTool},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in Paris?")),
},
})
if err != nil {
log.Fatal(err)
}
var toolUseBlock anthropic.ToolUseBlock
for _, block := range response.Content {
if v, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
toolUseBlock = v
break
}
}
weatherData := map[string]int{"temperature": 88}
continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 16000,
Thinking: anthropic.ThinkingConfigParamUnion{
OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{},
},
Tools: []anthropic.ToolUnionParam{weatherTool},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in Paris?")),
response.ToParam(),
anthropic.NewUserMessage(
anthropic.NewToolResultBlock(toolUseBlock.ID, fmt.Sprintf("Current temperature: %d°F", weatherData["temperature"]), false),
),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(continuation)
```
```java Java
import com.anthropic.models.messages.ThinkingConfigAdaptive;
// ...
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool weatherTool = Tool.builder()
.name("get_weather")
.description("Get current weather for a location")
.inputSchema(Tool.InputSchema.builder()
.properties(JsonValue.from(Map.of(
"location", Map.of("type", "string", "description", "City name")
)))
.required(List.of("location"))
.build())
.build();
MessageCreateParams initialParams = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(16000L)
.thinking(ThinkingConfigAdaptive.builder().build())
.addTool(weatherTool)
.addUserMessage("What's the weather in Paris?")
.build();
Message response = client.messages().create(initialParams);
ToolUseBlock toolUseBlock = null;
for (var block : response.content()) {
if (block.toolUse().isPresent()) {
toolUseBlock = block.toolUse().get();
break;
}
}
int temperature = 88;
// Second request: echo the assistant turn as received, then the tool result
MessageCreateParams continuationParams = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(16000L)
.thinking(ThinkingConfigAdaptive.builder().build())
.addTool(weatherTool)
.addUserMessage("What's the weather in Paris?")
.addMessage(response)
.addUserMessageOfBlockParams(List.of(
ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId(toolUseBlock.id())
.content("Current temperature: " + temperature + "°F")
.build()
)
))
.build();
Message continuation = client.messages().create(continuationParams);
IO.println(continuation);
}
```
```php PHP
$client = new Client();
$weatherTool = [
'name' => 'get_weather',
'description' => 'Get current weather for a location',
'input_schema' => [
'type' => 'object',
'properties' => [
'location' => [
'type' => 'string',
'description' => 'City name'
]
],
'required' => ['location']
]
];
$response = $client->messages->create(
maxTokens: 16000,
messages: [
['role' => 'user', 'content' => "What's the weather in Paris?"]
],
model: 'claude-opus-4-8',
thinking: ['type' => 'adaptive'],
tools: [$weatherTool],
);
$toolUseBlock = null;
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
$toolUseBlock = $block;
break;
}
}
$weatherData = ['temperature' => 88];
$continuation = $client->messages->create(
maxTokens: 16000,
messages: [
['role' => 'user', 'content' => "What's the weather in Paris?"],
['role' => 'assistant', 'content' => $response->content],
['role' => 'user', 'content' => [
[
'type' => 'tool_result',
'tool_use_id' => $toolUseBlock->id,
'content' => "Current temperature: {$weatherData['temperature']}°F"
]
]]
],
model: 'claude-opus-4-8',
thinking: ['type' => 'adaptive'],
tools: [$weatherTool],
);
echo $continuation;
```
```ruby Ruby
client = Anthropic::Client.new
weather_tool = {
name: "get_weather",
description: "Get current weather for a location",
input_schema: {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}
}
response = client.messages.create(
model: "claude-opus-4-8",
max_tokens: 16000,
thinking: {
type: "adaptive"
},
tools: [weather_tool],
messages: [
{ role: "user", content: "What's the weather in Paris?" }
]
)
tool_use_block = response.content.find { |block| block.type == :tool_use }
raise "No tool_use block found" unless tool_use_block
weather_data = { temperature: 88 }
continuation = client.messages.create(
model: "claude-opus-4-8",
max_tokens: 16000,
thinking: {
type: "adaptive"
},
tools: [weather_tool],
messages: [
{ role: "user", content: "What's the weather in Paris?" },
{ role: "assistant", content: response.content },
{ role: "user", content: [
{
type: "tool_result",
tool_use_id: tool_use_block.id,
content: "Current temperature: #{weather_data[:temperature]}°F"
}
] }
]
)
puts continuation
```
</CodeGroup>
```json Output
{
"content": [
{
"type": "text",
"text": "Currently in Paris, the temperature is 88°F (31°C)"
}
]
}
```
인터리브된 thinking이 흐름을 바꾸는 방식
인터리브된 thinking은 Claude가 도구 호출 사이에 생각하게 해서, 각 도구 결과에 대해 행동하기 전에 추론하게 해요. 개념과 모델별 가용성은 Thinking 페이지의 인터리브된 thinking에서 다뤄요. 인터리빙은 thinking 블록이 나타나는 위치를 바꾸지, 도구 호출이 이어질 수 있는지 여부는 바꾸지 않아요. 다음 비교는 두 도구 워크플로에서 인터리브된 thinking이 무엇을 바꾸는지 보여줘요:
```text
User: "What's the total revenue if we sold 150 units at $50 each,
and how does this compare to our average monthly revenue?"
Response 1: [thinking] "I need to calculate 150 * $50, then check the database..."
[tool_use: calculator] { "expression": "150 * 50" }
↓ tool result: "7500"
Response 2: [tool_use: database_query] { "query": "SELECT AVG(revenue)..." }
↑ no thinking block
↓ tool result: "5200"
Response 3: [text] "The total revenue is $7,500, which is 44% above your
average monthly revenue of $5,200."
↑ no thinking block
```
```text
User: "What's the total revenue if we sold 150 units at $50 each,
and how does this compare to our average monthly revenue?"
Response 1: [thinking] "I need to calculate 150 * $50 first..."
[tool_use: calculator] { "expression": "150 * 50" }
↓ tool result: "7500"
Response 2: [thinking] "Got $7,500. Now I should query the database to compare..."
[tool_use: database_query] { "query": "SELECT AVG(revenue)..." }
↑ thinking after receiving calculator result
↓ tool result: "5200"
Response 3: [thinking] "$7,500 vs $5,200 average - that's a 44% increase..."
[text] "The total revenue is $7,500, which is 44% above your
average monthly revenue of $5,200."
↑ thinking before final answer
```