도구 사용이란 무엇인가요?
기존 워크플로우는 프로젝트 관리 소프트웨어, 데이터베이스, 파일 등 다양한 기술에 의존합니다. Claude는 이러한 것들을 직접 확인할 수 없습니다. 대신 Claude는 외부 데이터와 작업에 접근할 수 있게 해주는 도구에 의존합니다.
도구란 무엇인가
간단히 말해, 도구는 여러분이 정의하고 Claude에게 노출하는 함수입니다. 도구가 무엇을 하는지, 어떤 입력을 받는지 설명하면, Claude가 언제 호출할지 결정합니다.
여기서 꼭 기억해야 할 핵심은 Claude가 도구를 실행하는 것이 아니라, 여러분의 코드가 실행한다는 점입니다. 흐름은 다음과 같습니다:
- Claude가 요청합니다 — 도구 호출을.
- 여러분의 코드가 실행합니다 — 해당 함수를.
- 결과가 Claude에게 다시 전달되고, Claude는 계속 진행합니다.
도구를 정의하는 방법
도구는 JSON 스키마로, 세 가지 부분으로 구성됩니다: 이름, 설명, 그리고 입력 스키마입니다. 이를 요청 본문에 tools 배열로 전달합니다.
설명은 Claude가 도구를 호출할지 여부를 결정하기 위해 읽는 부분입니다. 설명을 모호하게 작성하면 도구 사용 결과도 나빠집니다. 이것이 에이전트가 오작동하거나 사용 가능한 도구를 제대로 활용하지 못하는 가장 큰 원인입니다. 구체적으로 작성하세요.
도구 정의는 다음과 같은 모습입니다:
{
"name": "lookup_building_code",
"description": "Look up a specific building code section by its identifier. Returns the full text of that code section.",
"input_schema": {
"type": "object",
"properties": {
"section": {
"type": "string",
"description": "The building code section to look up"
}
},
"required": ["section"]
}
}그렇다면 이를 사용하면 어떤 일이 일어날까요? 에이전트에게 규정 준수 보고서를 보낸다고 가정해 봅시다. 첫 번째 턴에서 Claude는 stop_reason: "tool_use"와 함께 응답을 돌려줍니다 — 이것이 우리의 신호입니다. 해당 응답은 다음과 같은 모습입니다:

우리의 루프는 Claude가 요청한 매개변수로 lookup_building_code를 호출한 다음, 그 결과를 도구 결과로 다시 전달합니다 — 이는 도구 호출의 id와 연결된 tool_result 블록을 담은 사용자 메시지입니다:

그리고 Claude는 계속 진행합니다. 이 시점에서 우리는 Claude가 필요한 것을 얻을 때까지 도구를 계속 호출하고 결과를 반환할 수 있습니다.
여러 도구: Claude가 선택하게 하기
도구 하나만으로도 유용하지만, 흥미로운 부분은 Claude에게 여러 도구를 주고 어떤 순서로 어떤 도구를 선택하는지 지켜보는 것입니다.
이런 시나리오를 상상해 보세요: 덴버로 3일간 여행을 떠나기 위해 짐을 싸고 있고, 오늘의 날씨와 앞으로 며칠간의 예보를 모두 알고 싶습니다. 그래서 우리는 하나가 아니라 두 개의 도구를 선언합니다:
const tools = [
{
name: "get_weather",
description: "Get today's current weather for a city.",
input_schema: {
type: "object",
properties: {
city: { type: "string", description: "The city to check" }
},
required: ["city"]
}
},
{
name: "get_forecast",
description: "Get the weather forecast for the next few days for a city.",
input_schema: {
type: "object",
properties: {
city: { type: "string", description: "The city to check" }
},
required: ["city"]
}
}
];이 루프는 우리가 이미 본 에이전트 루프와 동일합니다. 유일하게 새로운 부분은 switch 문으로 도구 이름에 따라 분기하는 runTool 함수입니다 — 이 코드 블록이 바로 여러분의 코드가 실제로 실행되는 곳입니다:
function runTool(name, input) {
switch (name) {
case "get_weather":
return getWeather(input.city);
case "get_forecast":
return getForecast(input.city);
}
}
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
messages,
tools,
});
if (response.stop_reason !== "tool_use") {
// Claude가 완료됨 — 이것이 최종 답변입니다
break;
}
messages.push({ role: "assistant", content: response.content });
const toolResults = response.content
.filter((block) => block.type === "tool_use")
.map((block) => ({
type: "tool_result",
tool_use_id: block.id,
content: runTool(block.name, block.input),
}));
messages.push({ role: "user", content: toolResults });
}그리고 이것이 전체 패턴입니다. 세 번째 도구를 원하시나요? 배열에 추가하고, switch에 case를 추가하면 끝입니다.
이를 실행하면 Claude가 get_weather를 호출한 다음 get_forecast를 호출하는 것을 볼 수 있습니다 — 때로는 같은 턴에서, 때로는 차례로 호출합니다. 그런 다음 답변합니다: 겹겹이 입으세요, 오늘은 눈발이 날릴 것으로 예상되고, 주중에는 점점 따뜻해질 것입니다.
이제 Claude가 어떻게 선택했는지 주목해 보세요. Claude는 설명을 읽고, 여러분의 프롬프트를 "오늘의 날씨"와 "앞으로 며칠"에 매핑한 다음, 각각에 맞는 도구를 선택했습니다. 그래서 도구 설명이 정말 중요한 것입니다.
도구 러너: 반복 작업 건너뛰기
방금 작성한 코드에서 이미 두 가지 위험 신호를 발견하셨을 것입니다:
- 단순한 두 개의 조회 작업치고는 코드가 너무 많습니다.
- 실제 코드베이스에서는 가지고 있는 모든 함수마다 JSON 스키마를 직접 작성하고 싶지 않을 것입니다. 코드를 두 번 작성하는 것과 같습니다.
바로 이때 도구 러너가 등장합니다. 이는 Claude SDK에서 베타로 제공됩니다: TypeScript, Python, Ruby, C#, Go, Java, PHP. 각 도구를 한 번만 정의하면, 러너가 도구 사용/도구 결과 루프 전체를 내부적으로 처리합니다.
TypeScript에서 러너가 권장하는 헬퍼인 betaZodTool()은 데이터의 형태를 선언하고 검증하기 위한 TypeScript 라이브러리인 Zod로 작성된 짧은 스키마로 각 도구를 설명합니다. 이 헬퍼는 Zod 3.25.0 이상이 필요하며, 새로 설치하면 이 버전을 얻을 수 있습니다:
npm install zod여러분의 코드는 다음과 같이 줄어듭니다: 도구를 설명하고, 프롬프트를 보내고, 결과를 기다립니다. 다음은 도구 러너를 통해 연결된 동일한 두 도구 날씨 데모입니다:
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
// 직접 실행했던 동일한 두 조회를 각각 짧은 Zod 스키마로 감쌉니다
const weatherTool = betaZodTool({
name: "get_weather",
description: "Get today's current weather for a city.",
inputSchema: z.object({
city: z.string().describe("The city to check"),
}),
run: async ({ city }) => getWeather(city),
});
const forecastTool = betaZodTool({
name: "get_forecast",
description: "Get the weather forecast for the next few days for a city.",
inputSchema: z.object({
city: z.string().describe("The city to check"),
}),
run: async ({ city }) => getForecast(city),
});
const runner = client.beta.messages.toolRunner({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [
{
role: "user",
content:
"I'm packing for a three-day trip to Denver. What's the weather today and over the next few days?",
},
],
tools: [weatherTool, forecastTool],
});
// 도구 핑퐁이 모두 정리된 후 최종 메시지를 받기 위해 runner를 await합니다
const finalMessage = await runner;같은 시나리오지만, 코드는 훨씬 적습니다:
- while 루프도 없고, stop reason switch도 없고, 도구 결과를 메시지에 수동으로 다시 넣을 필요도 없습니다 — 러너가 이 모든 것을 처리합니다.
- 직접 작성한 JSON 스키마도 없고, 도구당 짧은 Zod 스키마만 있으면 되며, 이는 Claude가 보낸 입력도 검증합니다.
- 각
run함수는 방금 전 우리가 직접 실행했던 것과 동일한 조회 작업을 감쌉니다. - 러너를 await하면 모든 것이 완료된 후 최종 어시스턴트 메시지를 반환합니다.
실행해 보면 동일한 답을 얻을 수 있습니다.
실제 도구는 기존 코드를 감쌉니다
실제 상황에서는 여러분의 도구가 하드코딩된 날씨 데이터가 아닐 것입니다. 대신 이미 애플리케이션에 존재하는 실제 함수를 감싸게 될 것입니다.
규정 준수 검토 에이전트를 예로 들어보겠습니다: 이 에이전트의 도구는 코드베이스에 이미 존재하는 lookup_building_code와 search_building_code 함수를 얇게 감싼 래퍼입니다. 도구 러너를 사용하면 각 함수를 고유한 Zod 스키마를 가진 짧은 도구 정의로 감싸고, 에이전트는 작성하는 모든 발견 사항에서 구체적인 법규 조항을 인용합니다:

요약
- 도구는 Claude에게 여러분의 시스템에 대한 접근 권한을 부여합니다. 도구는 여러분이 정의하고 노출하는 함수이며, Claude가 언제 호출할지 결정하고, 여러분의 코드가 이를 실행합니다.
- 도구는 이름, 설명, 입력 스키마를 가진 JSON 스키마이며, 요청에서
tools배열로 전달됩니다. - 구체적인 설명을 작성하세요. 모호한 설명은 에이전트가 오작동하는 가장 큰 원인입니다.
stop_reason: "tool_use"는 도구를 실행하고 그 결과를 도구 결과로 다시 전달하라는 신호입니다.- 여러 도구를 사용할 때는 도구 이름에 따라 분기하세요. 도구를 추가한다는 것은 배열에 추가하고 case를 추가하는 것을 의미합니다.
- SDK의 도구 러너(Claude SDK 전반에서 베타로 제공됨)는 전체 루프를 처리하며, TypeScript에서는 각 도구가 Zod 스키마를 가진 짧은 정의입니다. 또는 루프를 직접 실행할 수도 있습니다.
- 여러분이 직접 실행하거나, 루프를 위임할 수 있습니다. 그 스펙트럼의 끝에는 관리형 에이전트가 있으며, 이는 에이전트 전체를 Anthropic에 위임합니다.