프로그래매틱 도구 호출
프로그래매틱 도구 호출 (Programmatic tool calling)
프로그래매틱 도구 호출은 Claude가 코드 실행 컨테이너 안에서 직접 우리 도구를 호출하는 코드를 작성하게 해 줘요. 도구 호출마다 모델 왕복을 하지 않아도 되니까, 여러 도구를 쓰는 워크플로에서 지연 시간이 줄고 토큰 소모도 줄어들어요. Claude는 데이터가 모델의 컨텍스트 창에 도달하기 전에 필터링하거나 처리할 수 있어요. 이 기능은 allowed_callers 필드로 활성화하며, 코드 실행 도구 버전 code_execution_20260120 이상이 필요해요.
출처: 문서
본문
프로그래매틱 도구 호출은 Claude가 코드 실행 컨테이너 안에서 도구를 프로그래매틱하게 호출하는 코드를 작성하게 해 줘요. 그래서 각 도구 호출마다 모델 왕복을 하지 않아도 돼요. 이렇게 하면 여러 도구를 쓰는 워크플로의 지연 시간이 줄고, Claude가 데이터를 컨텍스트 창에 넣기 전에 필터링하거나 처리할 수 있어서 토큰 소모도 줄어들어요. BrowseComp과 DeepSearchQA 같은 에이전틱 검색 벤치마크(다단계 웹 연구와 복잡한 정보 검색을 테스트)에서 기본 검색 도구 위에 프로그래매틱 도구 호출을 더하면 성능이 평균 11% 향상되고 입력 토큰은 24% 줄었어요(동적 필터링으로 개선된 웹 검색 참고).
20명 직원의 예산 준수 여부를 확인하는 경우를 떠올려 보세요. 전통적인 방식은 20번의 개별 모델 왕복이 필요하고, 그 과정에서 수천 개의 비용 라인 항목을 컨텍스트로 끌어와야 해요. 프로그래매틱 도구 호출로는 단일 스크립트가 20번의 조회를 모두 실행하고 결과를 필터링해, 한도를 초과한 직원만 반환해요. 그렇게 Claude가 추론해야 할 범위가 수백 킬로바이트에서 몇 줄로 줄어들어요.
프로그래매틱 도구 호출에는 도구 버전 code_execution_20260120 이상의 코드 실행 도구가 필요해요.
빠른 시작 (Quick start)
Claude가 데이터베이스를 프로그래매틱하게 여러 번 조회하고 결과를 집계하는 예시예요. 도구 정의에 allowed_callers: ["code_execution_20260120"]를 추가하면 그 도구를 코드 실행 안에서 호출할 수 있게 돼요(allowed_callers 필드 참고):
ant messages create <<'YAML'
model: claude-opus-5-5
max_tokens: 4096
messages:
- role: user
content: >-
Query sales data for the West, East, and Central regions, then
tell me which region had the highest revenue
tools:
- type: code_execution_20260120
name: code_execution
- name: query_database
description: >-
Execute a SQL query against the sales database. Returns a list
of rows as JSON objects.
input_schema:
type: object
properties:
sql:
type: string
description: SQL query to execute
required:
- sql
allowed_callers:
- code_execution_20260120
YAML
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
messages=[
{
"role": "user",
"content": "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue",
}
],
tools=[
{"type": "code_execution_20260120", "name": "code_execution"},
{
"name": "query_database",
"description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
"input_schema": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SQL query to execute"}
},
"required": ["sql"],
},
"allowed_callers": ["code_execution_20260120"],
},
],
)
print(response)
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
messages: [
{
role: "user",
content:
"Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue"
}
],
tools: [
{
type: "code_execution_20260120",
name: "code_execution"
},
{
name: "query_database",
description:
"Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
input_schema: {
type: "object" as const,
properties: {
sql: {
type: "string",
description: "SQL query to execute"
}
},
required: ["sql"]
},
allowed_callers: ["code_execution_20260120"]
}
]
});
console.log(response);
AnthropicClient client = new();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
Messages = [
new() {
Role = Role.User,
Content = "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue"
}
],
Tools = [
new CodeExecutionTool20260120(),
new ToolUnion(new Tool()
{
Name = "query_database",
Description = "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["sql"] = JsonSerializer.SerializeToElement(new { type = "string", description = "SQL query to execute" }),
},
Required = ["sql"],
},
AllowedCallers = ["code_execution_20260120"]
}),
]
};
var message = await client.Messages.Create(parameters);
Console.WriteLine(message);
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue")),
},
Tools: []anthropic.ToolUnionParam{
{OfCodeExecutionTool20260120: &anthropic.CodeExecutionTool20260120Param{}},
{OfTool: &anthropic.ToolParam{
Name: "query_database",
Description: anthropic.String("Execute a SQL query against the sales database. Returns a list of rows as JSON objects."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"sql": map[string]any{
"type": "string",
"description": "SQL query to execute",
},
},
Required: []string{"sql"},
},
AllowedCallers: []string{"code_execution_20260120"},
}},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.RawJSON())
import com.anthropic.models.messages.CodeExecutionTool20260120;
// ...
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L)
.addUserMessage("Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue")
.addTool(CodeExecutionTool20260120.builder().build())
.addTool(Tool.builder()
.name("query_database")
.description("Execute a SQL query against the sales database. Returns a list of rows as JSON objects.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"sql", Map.of(
"type", "string",
"description", "SQL query to execute"
)
)))
.putAdditionalProperty("required", JsonValue.from(List.of("sql")))
.build())
.allowedCallers(List.of(Tool.AllowedCaller.of("code_execution_20260120")))
.build())
.build();
Message response = client.messages().create(params);
IO.println(response);
}
$client = new Client();
$message = $client->messages->create(
maxTokens: 4096,
messages: [
['role' => 'user', 'content' => 'Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue'],
],
model: 'claude-opus-5-5',
tools: [
[
'type' => 'code_execution_20260120',
'name' => 'code_execution',
],
[
'name' => 'query_database',
'description' => 'Execute a SQL query against the sales database. Returns a list of rows as JSON objects.',
'input_schema' => [
'type' => 'object',
'properties' => [
'sql' => [
'type' => 'string',
'description' => 'SQL query to execute',
],
],
'required' => ['sql'],
],
'allowed_callers' => ['code_execution_20260120'],
],
],
);
echo $message;
client = Anthropic::Client.new
message = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
messages: [
{
role: "user",
content: "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue"
}
],
tools: [
{
type: "code_execution_20260120",
name: "code_execution"
},
{
name: "query_database",
description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
input_schema: {
type: "object",
properties: {
sql: {
type: "string",
description: "SQL query to execute"
}
},
required: ["sql"]
},
allowed_callers: ["code_execution_20260120"]
}
]
)
puts message
응답은 stop_reason: "tool_use", container ID, 그리고 caller 필드가 호출한 코드 실행 실행을 식별하는 query_database용 tool_use 블록으로 끝나요. 예제 워크플로의 3단계처럼 결과를 반환하면 코드가 마저 끝낼 수 있어요.
프로그래매틱 도구 호출의 동작 방식 (How programmatic tool calling works)
도구를 코드 실행에서 호출 가능하도록 구성하고 Claude가 그 도구가 필요하다고 판단하면:
- Claude가 그 도구를 함수처럼 호출하는 Python 코드를 작성해요. 여러 도구 호출과 전·후 처리 로직을 포함할 수 있어요.
- Claude가 이 코드를 코드 실행으로 샌드박스 컨테이너에서 실행해요.
- 도구 함수가 호출되면 코드 실행이 멈추고 API가
tool_use블록을 반환해요. - 우리가 도구 결과를 제공하면 코드 실행이 계속돼요(중간 결과는 Claude의 컨텍스트 창에 로드되지 않아요).
- 모든 코드 실행이 끝나면 Claude가 최종 출력을 받고 작업을 이어가요.
이 접근 방식은 특히 다음에 유용해요:
- 대용량 데이터 처리: 도구 결과가 Claude의 컨텍스트에 도달하기 전에 필터링하거나 집계하기
- 다단계 워크플로: 도구 호출 사이에 Claude를 샘플링하지 않고 도구를 직렬 또는 루프로 호출해 토큰과 지연 시간 절약하기
- 조건부 로직: 중간 도구 결과에 기반해 결정 내리기
핵심 개념 (Core concepts)
allowed_callers 필드 (The allowed_callers field)
allowed_callers 필드는 어떤 컨텍스트가 도구를 호출할 수 있는지 지정해요:
{
"name": "query_database",
"description": "Execute a SQL query against the database",
"input_schema": {
// ...
},
"allowed_callers": ["code_execution_20260120"]
}
가능한 값:
["direct"]— Claude가 이 도구를 직접 호출하도록 안내함(생략 시 기본값)["code_execution_20260120"]— Claude가 이 도구를 코드 실행 안에서만 호출하도록 안내함["direct", "code_execution_20260120"]— Claude가 이 도구를 직접 또는 코드 실행 안에서 호출할 수 있음
"code_execution_20260120"과 "code_execution_20260521" 둘 다 allowed_callers에서 허용되고 서로 바꿔 쓸 수 있어요. 둘 중 어떤 코드 실행 도구 버전을 사용하는 요청이든 두 호출자 중 하나를 나열한 도구를 충족해요. 응답 블록은 요청이 선언한 버전과 무관하게 항상 호출자를 code_execution_20260120으로 표시해요.
응답의 caller 필드 (The caller field in responses)
모든 도구 사용 블록에는 어떻게 호출됐는지 나타내는 caller 필드가 포함돼요:
직접 호출(전통적인 도구 사용):
{
"type": "tool_use",
"id": "toolu_abc123",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": { "type": "direct" }
}
프로그래매틱 호출:
{
"type": "tool_use",
"id": "toolu_xyz789",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": {
"type": "code_execution_20260120",
"tool_id": "srvtoolu_abc123"
}
}
tool_id는 호출을 만든 코드 실행 server_tool_use 블록의 id예요. 그래서 각 프로그래매틱 tool_use를 만든 코드 실행 실행과 짝지을 수 있어요.
컨테이너 수명 주기 (Container lifecycle)
프로그래매틱 도구 호출은 코드 실행과 같은 컨테이너를 사용해요:
- 컨테이너 생성: 기존 컨테이너를 재사용하지 않는 한 요청마다 새 컨테이너가 생성돼요.
- 컨테이너 ID:
container필드에expires_at타임스탬프와 함께 응답으로 반환돼요. - 재사용: 다음 요청에 컨테이너 ID를 다시 전달해 상태를 유지하세요. 프로그래매틱 도구 호출이 결과를 기다리는 동안에는 컨테이너 ID가 선택이 아니라 필수예요. API는 컨테이너 ID 없이 요청을 거부해요.
- 만료:
expires_at이 컨테이너의 남은 수명을 알려 줘요. 유휴 컨테이너는 현재 약 5분 후에 회수되고, 어떤 컨테이너도 생성 후 30일 이상 재사용할 수 없어요.
예제 워크플로 (Example workflow)
완전한 프로그래매틱 도구 호출 흐름은 이렇게 동작해요:
1단계: 초기 요청 (Step 1: Initial request)
코드 실행과 프로그래매틱 호출을 허용하는 도구로 요청을 보내요. 프로그래매틱 호출을 활성화하려면 도구 정의에 allowed_callers 필드를 추가하세요.
요청 형태는 빠른 시작 예시와 동일해요. code_execution을 tools 목록에 포함하고, 코드에서 호출하려는 도구에 allowed_callers: ["code_execution_20260120"]를 추가한 뒤 사용자 메시지를 보내면 돼요. 이 워크플로의 남은 단계들은 사용자 메시지 "Query customer purchase history from the last quarter and identify our top 5 customers by revenue"를 사용해요.
2단계: 도구 호출이 포함된 API 응답 (Step 2: API response with tool call)
Claude가 우리 도구를 호출하는 코드를 작성해요. API가 멈추고 반환해요:
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll query the purchase history and analyze the results."
},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "code_execution",
"input": {
"code": "import json\n\nrows = json.loads(await query_database({'sql': '<sql>'}))\ntop_customers = sorted(rows, key=lambda x: x['revenue'], reverse=True)[:5]\nprint(f'Top 5 customers: {top_customers}')"
}
},
{
"type": "tool_use",
"id": "toolu_def456",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": {
"type": "code_execution_20260120",
"tool_id": "srvtoolu_abc123"
}
}
],
"container": {
"id": "container_xyz789",
"expires_at": "2026-01-20T14:30:00Z"
},
"stop_reason": "tool_use"
}
3단계: 도구 결과 제공 (Step 3: Provide tool result)
전체 대화 기록과 도구 결과를 보내요. 이 요청에서 세 가지가 중요해요:
- 결과를 담는 사용자 메시지는
tool_result블록만 포함할 수 있어요. 메시지 포맷 제한을 참고하세요. - 멈춘 응답의
containerID를 전달하세요. API는 대기 중인 프로그래매틱 도구 호출이 있는데 컨테이너 ID가 없는 이어짐 요청을 거부해요. - 원래 요청과 같은
tools배열을 보내세요. 멈춘 코드가 재개되려면 코드 실행 도구가 여전히 있어야 하고, 이 요청에서 보내는 도구들은 이 이후 턴에 Claude와 실행 중인 코드가 사용할 수 있는 정의예요.
ant messages create <<'YAML'
model: claude-opus-5-5
max_tokens: 4096
container: container_xyz789
messages:
- role: user
content: >-
Query customer purchase history from the last quarter and identify our
top 5 customers by revenue
- role: assistant
content:
- type: text
text: I'll query the purchase history and analyze the results.
- type: server_tool_use
id: srvtoolu_abc123
name: code_execution
input:
code: "..."
- type: tool_use
id: toolu_def456
name: query_database
input:
sql: "<sql>"
caller:
type: code_execution_20260120
tool_id: srvtoolu_abc123
- role: user
content:
- type: tool_result
tool_use_id: toolu_def456
content: >-
[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2",
"revenue": 38000}, ...]
# Same tools array as the original request
tools:
- type: code_execution_20260120
name: code_execution
- name: query_database
description: >-
Execute a SQL query against the sales database. Returns a list
of rows as JSON objects.
input_schema:
type: object
properties:
sql:
type: string
description: SQL query to execute
required:
- sql
allowed_callers:
- code_execution_20260120
YAML
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
container="container_xyz789", # Reuse the container
messages=[
{
"role": "user",
"content": "Query customer purchase history from the last quarter and identify our top 5 customers by revenue",
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll query the purchase history and analyze the results.",
},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "code_execution",
"input": {"code": "..."},
},
{
"type": "tool_use",
"id": "toolu_def456",
"name": "query_database",
"input": {"sql": "<sql>"},
"caller": {
"type": "code_execution_20260120",
"tool_id": "srvtoolu_abc123",
},
},
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_def456",
"content": '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]',
}
],
},
],
# Same tools array as the original request
tools=[
{"type": "code_execution_20260120", "name": "code_execution"},
{
"name": "query_database",
"description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
"input_schema": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SQL query to execute"}
},
"required": ["sql"],
},
"allowed_callers": ["code_execution_20260120"],
},
],
)
print(response)
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
container: "container_xyz789", // Reuse the container
messages: [
{
role: "user",
content:
"Query customer purchase history from the last quarter and identify our top 5 customers by revenue"
},
{
role: "assistant",
content: [
{ type: "text", text: "I'll query the purchase history and analyze the results." },
{
type: "server_tool_use",
id: "srvtoolu_abc123",
name: "code_execution",
input: { code: "..." }
},
{
type: "tool_use",
id: "toolu_def456",
name: "query_database",
input: { sql: "<sql>" },
caller: {
type: "code_execution_20260120",
tool_id: "srvtoolu_abc123"
}
}
]
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_def456",
content:
'[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]'
}
]
}
],
// Same tools array as the original request
tools: [
{
type: "code_execution_20260120",
name: "code_execution"
},
{
name: "query_database",
description:
"Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
input_schema: {
type: "object" as const,
properties: {
sql: {
type: "string",
description: "SQL query to execute"
}
},
required: ["sql"]
},
allowed_callers: ["code_execution_20260120"]
}
]
});
console.log(response);
AnthropicClient client = new();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
Container = "container_xyz789",
Messages =
[
new()
{
Role = Role.User,
Content = "Query customer purchase history from the last quarter and identify our top 5 customers by revenue"
},
new()
{
Role = Role.Assistant,
Content = new ContentBlock[]
{
new TextBlock { Text = "I'll query the purchase history and analyze the results." },
new ServerToolUseBlock
{
Id = "srvtoolu_abc123",
Name = "code_execution",
Input = new { code = "..." }
},
new ToolUseBlock
{
Id = "toolu_def456",
Name = "query_database",
Input = new { sql = "<sql>" },
Caller = new ToolCaller
{
Type = "code_execution_20260120",
ToolId = "srvtoolu_abc123"
}
}
}
},
new()
{
Role = Role.User,
Content = new ContentBlockParam[]
{
new ToolResultBlockParam
{
ToolUseID = "toolu_def456",
Content = "[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}, ...]"
}
}
}
],
// Same tools array as the original request
Tools = [
new CodeExecutionTool20260120(),
new ToolUnion(new Tool()
{
Name = "query_database",
Description = "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["sql"] = JsonSerializer.SerializeToElement(new { type = "string", description = "SQL query to execute" }),
},
Required = ["sql"],
},
AllowedCallers = ["code_execution_20260120"]
}),
]
};
var message = await client.Messages.Create(parameters);
Console.WriteLine(message);
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Container: anthropic.MessageCreateParamsContainerUnion{
OfString: anthropic.String("container_xyz789"),
},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Query customer purchase history from the last quarter and identify our top 5 customers by revenue")),
{
Role: anthropic.MessageParamRoleAssistant,
Content: []anthropic.ContentBlockParamUnion{
anthropic.NewTextBlock("I'll query the purchase history and analyze the results."),
{OfServerToolUse: &anthropic.ServerToolUseBlockParam{
ID: "srvtoolu_abc123",
Name: anthropic.ServerToolUseBlockParamNameCodeExecution,
Input: map[string]any{"code": "..."},
}},
{OfToolUse: &anthropic.ToolUseBlockParam{
ID: "toolu_def456",
Name: "query_database",
Input: map[string]any{"sql": "<sql>"},
Caller: anthropic.ServerToolUseBlockParamCallerUnion{
OfCodeExecution20260120: &anthropic.ServerToolCaller20260120Param{
ToolID: "srvtoolu_abc123",
},
},
}},
},
},
{
Role: anthropic.MessageParamRoleUser,
Content: []anthropic.ContentBlockParamUnion{
{OfToolResult: &anthropic.ToolResultBlockParam{
ToolUseID: "toolu_def456",
Content: []anthropic.ToolResultBlockParamContentUnion{
{OfText: &anthropic.TextBlockParam{
Text: `[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]`,
}},
},
}},
},
},
},
// Same tools array as the original request
Tools: []anthropic.ToolUnionParam{
{OfCodeExecutionTool20260120: &anthropic.CodeExecutionTool20260120Param{}},
{OfTool: &anthropic.ToolParam{
Name: "query_database",
Description: anthropic.String("Execute a SQL query against the sales database. Returns a list of rows as JSON objects."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"sql": map[string]any{
"type": "string",
"description": "SQL query to execute",
},
},
Required: []string{"sql"},
},
AllowedCallers: []string{"code_execution_20260120"},
}},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.RawJSON())
import com.anthropic.models.messages.CodeExecutionTool20260120;
// ...
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L)
.container("container_xyz789")
.addUserMessage("Query customer purchase history from the last quarter and identify our top 5 customers by revenue")
.addAssistantMessageOfBlockParams(List.of(
ContentBlockParam.ofText(
TextBlockParam.builder()
.text("I'll query the purchase history and analyze the results.")
.build()),
ContentBlockParam.ofServerToolUse(
ServerToolUseBlockParam.builder()
.id("srvtoolu_abc123")
.name("code_execution")
.input(JsonValue.from(Map.of("code", "...")))
.build()),
ContentBlockParam.ofToolUse(
ToolUseBlockParam.builder()
.id("toolu_def456")
.name("query_database")
.input(JsonValue.from(Map.of("sql", "<sql>")))
.codeExecution20260120Caller("srvtoolu_abc123")
.build())
))
.addUserMessageOfBlockParams(List.of(
ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId("toolu_def456")
.content("[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}, ...]")
.build())
))
// Same tools array as the original request
.addTool(CodeExecutionTool20260120.builder().build())
.addTool(Tool.builder()
.name("query_database")
.description("Execute a SQL query against the sales database. Returns a list of rows as JSON objects.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"sql", Map.of(
"type", "string",
"description", "SQL query to execute"
)
)))
.putAdditionalProperty("required", JsonValue.from(List.of("sql")))
.build())
.allowedCallers(List.of(Tool.AllowedCaller.of("code_execution_20260120")))
.build())
.build();
Message response = client.messages().create(params);
IO.println(response);
}
$client = new Client();
$message = $client->messages->create(
maxTokens: 4096,
messages: [
[
'role' => 'user',
'content' => 'Query customer purchase history from the last quarter and identify our top 5 customers by revenue',
],
[
'role' => 'assistant',
'content' => [
[
'type' => 'text',
'text' => "I'll query the purchase history and analyze the results.",
],
[
'type' => 'server_tool_use',
'id' => 'srvtoolu_abc123',
'name' => 'code_execution',
'input' => ['code' => '...'],
],
[
'type' => 'tool_use',
'id' => 'toolu_def456',
'name' => 'query_database',
'input' => ['sql' => '<sql>'],
'caller' => [
'type' => 'code_execution_20260120',
'tool_id' => 'srvtoolu_abc123',
],
],
],
],
[
'role' => 'user',
'content' => [
[
'type' => 'tool_result',
'tool_use_id' => 'toolu_def456',
'content' => '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]',
],
],
],
],
model: 'claude-opus-5-5',
container: 'container_xyz789',
// Same tools array as the original request
tools: [
[
'type' => 'code_execution_20260120',
'name' => 'code_execution',
],
[
'name' => 'query_database',
'description' => 'Execute a SQL query against the sales database. Returns a list of rows as JSON objects.',
'input_schema' => [
'type' => 'object',
'properties' => [
'sql' => [
'type' => 'string',
'description' => 'SQL query to execute',
],
],
'required' => ['sql'],
],
'allowed_callers' => ['code_execution_20260120'],
],
],
);
echo $message;
require "anthropic"
client = Anthropic::Client.new
message = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
container: "container_xyz789",
messages: [
{
role: "user",
content: "Query customer purchase history from the last quarter and identify our top 5 customers by revenue"
},
{
role: "assistant",
content: [
{
type: "text",
text: "I'll query the purchase history and analyze the results."
},
{
type: "server_tool_use",
id: "srvtoolu_abc123",
name: "code_execution",
input: { code: "..." }
},
{
type: "tool_use",
id: "toolu_def456",
name: "query_database",
input: { sql: "<sql>" },
caller: {
type: "code_execution_20260120",
tool_id: "srvtoolu_abc123"
}
}
]
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_def456",
content: '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]'
}
]
}
],
# Same tools array as the original request
tools: [
{
type: "code_execution_20260120",
name: "code_execution"
},
{
name: "query_database",
description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.",
input_schema: {
type: "object",
properties: {
sql: {
type: "string",
description: "SQL query to execute"
}
},
required: ["sql"]
},
allowed_callers: ["code_execution_20260120"]
}
]
)
puts message
4단계: 다음 도구 호출 또는 완료 (Step 4: Next tool call or completion)
코드는 멈춘 지점에서 이어받아 우리 결과를 처리해요. 각 이어짐 응답은 더 많은 프로그래매틱 tool_use 블록으로 다시 멈추거나, 코드 실행을 완료하고 Claude가 턴을 이어가게 해요(5단계). stop_reason과 각 tool_use 블록의 caller를 확인해 둘을 구분하세요. 우리를 위해 멈추는 응답은 stop_reason: "tool_use"와 caller가 코드 실행 버전을 가리키는 tool_use 블록을 가져요. 그 경우 대기 중인 모든 프로그래매틱 호출에 대한 tool_result를 하나의 사용자 메시지에 담아 3단계를 반복하면 돼요.
5단계: 최종 응답 (Step 5: Final response)
코드 실행이 완료되면 Claude가 최종 응답을 제공해요:
{
"content": [
{
"type": "code_execution_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {
"type": "code_execution_result",
"stdout": "Top 5 customers: [{'customer_id': 'C1', 'revenue': 45000}, {'customer_id': 'C2', 'revenue': 38000}, {'customer_id': 'C5', 'revenue': 32000}, {'customer_id': 'C8', 'revenue': 28500}, {'customer_id': 'C3', 'revenue': 24000}]",
"stderr": "",
"return_code": 0,
"content": []
}
},
{
"type": "text",
"text": "I've analyzed the purchase history from last quarter. Your top 5 customers generated $167,500 in total revenue, with Customer C1 leading at $45,000."
}
],
"stop_reason": "end_turn"
}
고급 패턴 (Advanced patterns)
루프가 있는 배치 처리 (Batch processing with loops)
Claude는 여러 항목을 효율적으로 처리하는 코드를 작성할 수 있어요:
regions = ["West", "East", "Central", "North", "South"]
results = {}
for region in regions:
rows = json.loads(await query_database({"sql": f"<sql for {region}>"}))
results[region] = sum(row["revenue"] for row in rows)
# Process results programmatically
top_region = max(results.items(), key=lambda x: x[1])
print(f"Top region: {top_region[0]} with ${top_region[1]:,} in revenue")
이 패턴은:
- 모델 왕복을 N번(지역마다 1번)에서 1번으로 줄여요.
- 큰 결과 집합을 Claude에게 돌아가기 전에 프로그래매틱하게 처리해요.
- 원시 데이터 대신 집계된 결론만 반환해서 토큰을 아껴요.
조기 종료 (Early termination)
Claude는 성공 기준이 충족되는 즉시 처리를 멈출 수 있어요:
endpoints = ["us-east", "eu-west", "apac"]
for endpoint in endpoints:
status = await check_health({"endpoint": endpoint})
if status == "healthy":
print(f"Found healthy endpoint: {endpoint}")
break # Stop early, don't check remaining
조건부 도구 선택 (Conditional tool selection)
path = "/tmp/example.txt"
file_info = json.loads(await get_file_info({"path": path}))
if file_info["size"] < 10000:
content = await read_full_file({"path": path})
else:
content = await read_file_summary({"path": path})
print(content)
데이터 필터링 (Data filtering)
server_id = "srv-01"
log_text = await fetch_logs({"server_id": server_id})
errors = [line for line in log_text.splitlines() if "ERROR" in line]
print(f"Found {len(errors)} errors")
for error in errors[-10:]: # Only return last 10 errors
print(error)
응답 형식 (Response format)
프로그래매틱 도구 호출 (Programmatic tool call)
코드 실행이 도구를 호출할 때:
{
"type": "tool_use",
"id": "toolu_abc123",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": {
"type": "code_execution_20260120",
"tool_id": "srvtoolu_xyz789"
}
}
도구 결과 처리 (Tool result handling)
우리 도구 결과는 실행 중인 코드로 다시 전달돼요:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "[{\"customer_id\": \"C1\", \"revenue\": 45000, \"orders\": 23}, {\"customer_id\": \"C2\", \"revenue\": 38000, \"orders\": 18}, ...]"
}
]
}
코드 실행 완료 (Code execution completion)
모든 도구 호출이 충족되고 코드가 완료되면:
{
"type": "code_execution_tool_result",
"tool_use_id": "srvtoolu_xyz789",
"content": {
"type": "code_execution_result",
"stdout": "Analysis complete. Top 5 customers identified from 847 total records.",
"stderr": "",
"return_code": 0,
"content": []
}
}
오류 처리 (Error handling)
흔한 오류 (Common errors)
| Error | Where it appears | Description | Solution |
|---|---|---|---|
invalid_tool_input |
error_code on the code_execution_tool_result error block in the response |
Invalid parameters were passed to the code execution tool | See the code execution tool errors |
invalid_request_error (on tool_choice) |
HTTP 400 error response | tool_choice names a tool whose allowed_callers does not include "direct" |
Either add "direct" to that tool's allowed_callers, or remove the tool from tool_choice and let Claude invoke it from code |
도구 호출 중 컨테이너 만료 (Container expiration during tool call)
도구 결과가 약 4분 안에 도착하지 않으면, 대기 중인 호출이 Claude의 실행 중 코드 안에서 TimeoutError를 발생시켜요. Claude는 stderr에서 그 오류를 보고 보통 호출을 재시도해요:
{
"type": "code_execution_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {
"type": "code_execution_result",
"stdout": "",
"stderr": "TimeoutError: Calling tool ['query_database'] timed out (no response after 270s).",
"return_code": 0,
"content": []
}
}
타임아웃을 막으려면:
- 응답의
expires_at필드를 모니터링하세요. - 도구 실행에 타임아웃을 구현하세요.
- 긴 작업을 더 작은 덩어리로 나누는 걸 고려하세요.
도구 실행 오류 (Tool execution errors)
도구가 오류를 반환하면:
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "Error: Query timeout - table lock exceeded 30 seconds"
}
Claude의 코드가 이 오류를 받고 적절히 처리할 수 있어요.
제약과 한계 (Constraints and limitations)
기능 비호환성 (Feature incompatibilities)
- 구조화 출력:
strict: true가 있는 도구는 프로그래매틱 호출과 함께 지원되지 않아요. - 도구 선택:
tool_choice로 특정 도구의 프로그래매틱 호출을 강제할 수 없어요. - 병렬 도구 사용: 프로그래매틱 호출에는
disable_parallel_tool_use: true가 지원되지 않아요.
입력 스키마 한계 (Input schema limitations)
input_schema에 재귀 $ref(자기 자신을 참조하는 스키마 같은 참조 순환)가 있는 커스텀 도구는 프로그래매틱 호출에 활성화할 수 없어요. 그런 도구의 allowed_callers에 코드 실행 도구 버전을 포함시키면 메시지에 Circular $ref detected가 담긴 400 invalid_request_error로 요청이 실패해요. 같은 스키마는 직접 도구 호출에는 허용돼요.
이를 해결하려면 다음 중 하나를 하세요:
allowed_callers를 생략하거나["direct"]로 설정해 도구를 직접 전용으로 유지하세요. 같은 요청의 다른 도구는 여전히 프로그래매틱 호출을 쓸 수 있어요.- 스키마에서 순환을 제거하세요. 예를 들어 재귀를 고정 깊이로 펴고 더 깊은 중첩은 가장 안쪽 수준의
description에 설명하거나, 재귀 속성을description이 기대 형태를 설명하는 평범한{"type": "object"}로 바꾸세요.
도구 제한 (Tool restrictions)
다음 도구는 프로그래매틱하게 호출할 수 없어요:
- MCP 커넥터가 제공하는 도구
- 컴퓨터 사용과 브라우저 사용 툴셋(
computer_toolset_20260801과browser_toolset_20260801), 이들은allowed_callers필드가"direct"만 허용해요.
메시지 포맷 제한 (Message formatting restrictions)
프로그래매틱 도구 호출에 응답할 때는 엄격한 포맷 요구 사항이 있어요:
도구 결과 전용 응답: 대기 중인 프로그래매틱 도구 호출이 결과를 기다리고 있으면, 응답 메시지는 tool_result 블록만 포함해야 해요. 도구 결과 뒤에도 텍스트 콘텐츠를 포함할 수 없어요.
프로그래매틱 도구 호출에 응답할 때 텍스트를 포함하면 안 됨(잘못됨):
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}]"
},
{ "type": "text", "text": "What should I do next?" }
]
}
프로그래매틱 도구 호출에 응답할 때는 도구 결과만(올바름):
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}]"
}
]
}
이 제한은 프로그래매틱(코드 실행) 도구 호출에 응답할 때만 적용돼요. 일반 클라이언트 쪽 도구 호출에서는 도구 결과 뒤에 텍스트 콘텐츠를 포함할 수 있어요.
텍스트 전용 도구 결과 콘텐츠: 프로그래매틱 호출에 답하는 각 tool_result의 content는 문자열이거나 text 블록이어야 해요. 이미지, 문서, 다른 콘텐츠 블록 타입은 거부돼요.
속도 제한 (Rate limits)
프로그래매틱 도구 호출은 일반 도구 호출과 같은 속도 제한을 받아요. 코드 실행에서의 각 도구 호출은 별도의 호출로 계산돼요.
사용 전에 도구 결과 검증하기 (Validate tool results before use)
프로그래매틱하게 호출될 사용자 정의 도구를 구현할 때:
- 도구 결과는 문자열로 반환돼요: 실행 환경이 처리할 수 있는 코드 스니펫이나 실행 가능한 명령을 포함한 어떤 콘텐츠도 담을 수 있어요.
- 외부 도구 결과 검증: 도구가 외부 소스의 데이터를 반환하거나 사용자 입력을 받는다면, 출력이 코드로 해석되거나 실행될 경우 코드 주입 위험을 인지하세요.
토큰 효율성 (Token efficiency)
프로그래매틱 도구 호출은 세 가지 방식으로 토큰 소비를 줄여요:
- 프로그래매틱 호출의 도구 결과는 Claude의 컨텍스트에 추가되지 않아요 — 최종 코드 출력만 추가돼요.
- 중간 처리는 코드에서 일어나요 — 필터링, 집계, 기타 변환이 모델 토큰을 소비하지 않아요.
- 하나의 코드 실행에 여러 도구 호출 — 별도의 모델 턴에 비해 오버헤드를 줄여요.
예를 들어 도구 10개를 직접 호출하면 프로그래매틱하게 호출하고 요약을 반환하는 것보다 약 10배의 토큰을 써요.
Anthropic의 프로덕션 Claude 모델에 대한 내부 평가에서:
- 75개 도구 프로젝트 관리 에이전트 벤치마크에서 프로그래매틱 도구 호출을 켜면 청구되는 입력 토큰이 작업 정확도 변화 없이 약 38% 줄었어요.
- τ²-bench(항공, 소매, 통신 도메인)에서는 각 턴이 하나 또는 두 개의 순차 도구 호출을 하기 때문에, 프로그래매틱 도구 호출은 점수를 바꾸지 않았고 비용이 약 8% 늘었어요. 순차 단일 호출 워크플로는 이점이 없어요.
- 프로덕션 API 트래픽에서
tools배열에 10~49개 도구 정의가 있는 요청은 프로그래매틱 도구 호출을 켜면 보통 20%에서 40%의 토큰 절감을 봐요.
실제 절감은 워크로드 형태에 따라 달라져요. 프로그래매틱 호출을 쓸 때를 참고하세요.
사용량과 가격 (Usage and pricing)
프로그래매틱 도구 호출은 코드 실행과 같은 가격을 사용해요. 자세한 내용은 코드 실행 가격을 보세요.
모범 사례 (Best practices)
도구 설계 (Tool design)
- 자세한 출력 설명 제공: Claude는 코드에서 도구 결과를 역직렬화하므로, 형식(JSON 구조와 필드 타입)을 문서화하세요.
- 구조화된 데이터 반환: JSON이나 다른 머신 읽기 가능한 형식이 프로그래매틱 처리에 가장 좋아요.
- 응답을 간결하게 유지: 처리 오버헤드를 최소화하려면 필요한 데이터만 반환하세요.
프로그래매틱 호출을 쓸 때 (When to use programmatic calling)
프로그래매틱 도구 호출은 작은 고정 오버헤드(컨테이너 시작, 스크립트 생성)를 도구 결과 토큰과 모델 왕복의 큰 절감으로 바꿔 줘요. 이 거래가 가치가 있는지는 워크로드 형태에 달려 있어요.
강하게 어울리는 경우:
- 많은 항목에 걸친 팬아웃이나 병렬 작업(예: 50개 엔드포인트 확인, 20개 레코드 조회)
- Claude의 컨텍스트에 도달하기 전에 필터링·집계·요약할 수 있는 큰 도구 결과
- 반복적 쿼리와 결과 필터링이 워크플로를 지배하는 에이전틱 검색과 검색
약하게 어울리는 경우:
- 각 호출이 이전 결과에 대한 Claude의 추론에 의존하는 엄격한 순차 워크플로. 그 경우 스크립트가 모델 왕복을 건너뛸 수 없기 때문이에요.
- 응답이 작은 소수의 도구 호출. 특히 대화 첫 턴에서는 컨테이너와 스크립트 오버헤드가 절감보다 클 수 있어요.
- 호출 사이에 즉각적인 사용자 피드백이 필요한 도구
확신이 없다면, 널리 켜기 전에 대표 트래픽 샘플에서 allowed_callers 유무에 따른 청구 입력 토큰을 측정하세요.
성능 최적화 (Performance optimization)
- 여러 관련 요청을 할 때 컨테이너를 재사용해 상태를 유지하세요.
- 가능하면 유사한 작업을 하나의 코드 실행으로 배치하세요.
문제 해결 (Troubleshooting)
흔한 문제 (Common issues)
tool_choice 설정 시 invalid_request_error
tool_choice는allowed_callers에"direct"가 없는 도구를 가리킬 수 없어요. 그 도구의allowed_callers에"direct"를 추가하거나,tool_choice에서 그 도구를 제거하고 Claude가 코드에서 호출하게 하세요.
컨테이너 만료
- 멈춘 응답의
expires_at타임스탬프보다 훨씬 전에 각 프로그래매틱 도구 호출에 응답하세요. Claude의 코드는 약 4분 후에 결과를 기다리는 것을 멈추고, 유휴 컨테이너는 현재 약 5분 후에 회수돼요. - 더 빠른 도구 실행을 구현하는 걸 고려하세요.
도구 결과가 제대로 파싱되지 않음
- 도구가 Claude가 역직렬화할 수 있는 문자열 데이터를 반환하는지 확인하세요.
- 도구 설명에 명확한 출력 형식 문서를 제공하세요.
디버깅 팁 (Debugging tips)
- 모든 도구 호출과 결과를 로깅해 흐름을 추적하세요.
caller필드를 확인해 프로그래매틱 호출을 확인하세요.- 컨테이너 ID를 모니터링해 올바른 재사용을 보장하세요.
- 도구를 독립적으로 테스트한 뒤 프로그래매틱 호출을 활성화하세요.
프로그래매틱 도구 호출이 잘 동작하는 이유 (Why programmatic tool calling works)
Claude는 대량의 코드로 훈련되므로, 도구를 호출 가능한 Python 함수로 제시하면 그 강점을 활용할 수 있어요:
- 도구 구성: 체이닝 호출, 루프, 조건문이 일련의 모델 왕복 대신 평범한 Python 제어 흐름이 돼요.
- 결과 처리: Claude의 코드가 큰 도구 출력을 필터링·집계하거나 파일에 쓰고, 최종 출력만 컨텍스트 창에 들어와요.
- 지연 시간: 하나의 코드 실행 안에서는 도구 호출 사이에 모델을 다시 샘플링하지 않아요.
대안 구현 (Alternative implementations)
프로그래매틱 도구 호출은 일반화 가능한 패턴이라 우리 인프라에서도 구현할 수 있어요. 접근 방식들을 비교하면:
클라이언트 쪽 직접 실행 (Client-side direct execution)
Claude에게 코드 실행 도구를 제공하고 그 환경에서 어떤 함수를 사용할 수 있는지 설명하세요. Claude가 코드로 도구를 호출하면 우리 애플리케이션이 그 함수들이 정의된 곳에서 로컬로 실행해요.
장점:
- 애플리케이션을 크게 재구성할 필요 없음
- 환경과 지침을 완전히 제어
단점:
- 샌드박스 밖에서 신뢰할 수 없는 코드를 실행함
- 도구 호출이 코드 주입의 경로가 될 수 있음
이런 경우에 사용: 애플리케이션이 임의 코드를 안전하게 실행할 수 있고, 가장 작은 구현을 원하며, Anthropic의 관리형 제공이 필요에 맞지 않을 때.
자체 관리 샌드박스 실행 (Self-managed sandboxed execution)
Claude의 관점에서는 같은 접근 방식이지만, 코드가 네트워크 이그레스 금지 같은 보안 제한이 있는 샌드박스 컨테이너에서 실행돼요. 도구가 외부 리소스를 필요로 하면 샌드박스 밖에서 도구 호출을 실행하는 프로토콜이 필요해요.
장점:
- 우리 인프라에서 안전한 프로그래매틱 도구 호출
- 실행 환경을 완전히 제어
단점:
- 구축하고 유지하기 복잡함
- 인프라와 프로세스 간 통신 둘 다 관리해야 함
이런 경우에 사용: 보안이 중요하고 Anthropic의 관리형 솔루션이 요구 사항에 맞지 않을 때.
Anthropic 관리형 실행 (Anthropic-managed execution)
Anthropic의 프로그래매틱 도구 호출은 Claude에 맞게 튜닝된 주관적 Python 환경을 가진 샌드박스 실행의 관리형 버전이에요. Anthropic이 컨테이너 관리, 코드 실행, 안전한 도구 호출 통신을 처리해요.
장점:
- 기본적으로 안전하고 보안이 보장됨
- 도구 정의 하나로 활성화되고, 실행할 인프라가 없음
- Claude에 최적화된 환경과 지침
Claude API, AWS의 Claude Platform, 또는 Microsoft Foundry를 사용한다면 Anthropic의 관리형 솔루션을 고려하세요. Microsoft Foundry에서는 프로그래매틱 도구 호출에 Hosted on Anthropic 배포가 필요해요.
데이터 보존 (Data retention)
프로그래매틱 도구 호출은 코드 실행 인프라 위에 구축되고 같은 샌드박스 컨테이너를 사용해요. 실행 아티팩트와 출력을 포함한 컨테이너 데이터는 최대 30일 동안 보존돼요.
모든 기능의 ZDR 자격은 API와 데이터 보존을 참고하세요.