tool schema 정의 — 첫 도구(계산기)
Article
12강에서 get_current_time 도구를 만들었지만, 인자가 하나도 없었다. 진짜 도구는 대부분 인자를 받는다. "무엇을 얼마나" 계산할지, "어디를" 검색할지. 그 인자를 Claude 가 정확히 뽑아내게 하려면 input_schema 를 촘촘히 써야 한다. 오늘은 사칙연산 계산기를 만들며 그 감각을 잡는다.
계산기 도구를 정의한다
ai-lab-b 저장소에 lesson-13 폴더를 만들고 12강과 같은 방식으로 세팅한다. index.ts 를 만든다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "calculator",
description:
"사칙연산(덧셈·뺄셈·곱셈·나눗셈)을 정확히 계산한다. 암산으로 틀리기 쉬운 큰 수나 소수 계산이 필요할 때 반드시 이 도구를 쓴다.",
input_schema: {
type: "object",
properties: {
operation: {
type: "string",
enum: ["add", "subtract", "multiply", "divide"],
description: "수행할 연산. add=덧셈, subtract=뺄셈(a-b), multiply=곱셈, divide=나눗셈(a/b)",
},
a: { type: "number", description: "첫 번째 피연산자" },
b: { type: "number", description: "두 번째 피연산자" },
},
required: ["operation", "a", "b"],
},
},
];
input_schema 는 그냥 JSON 스키마다. 10강에서 JSON 출력을 다룰 때 본 것과 같은 문법이다. 눈여겨볼 세 가지가 있다.
- enumoperation 이 add/subtract/multiply/divide 넷 중 하나로만 오게 강제한다
- 필드별 descriptionsubtract 가 a-b 인지 b-a 인지처럼, 이름만으론 모호한 걸 못박는다
- requiredoperation·a·b 세 필드가 다 없으면 tool_use 자체가 불완전해진다
Anthropic.Tool 하나에 이제 함수 하나를 물어보는 게 아니라, "이 함수는 이런 모양의 입력을 받는다"는 계약서를 준 셈이다.
실제로 여러 번 물어본다
같은 도구를 두고 질문 네 개를 던진다. 계산이 필요한 것 셋, 필요 없는 것 하나다.
async function ask(question: string) {
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 300,
tools,
messages: [{ role: "user", content: question }],
});
console.log(`\n질문: "${question}"`);
console.log(`stop_reason: ${response.stop_reason}`);
for (const block of response.content) {
if (block.type === "text") {
console.log(`[text] ${block.text}`);
} else if (block.type === "tool_use") {
console.log(`[tool_use] name=${block.name} input=${JSON.stringify(block.input)}`);
}
}
}
await ask("358 곱하기 47은 얼마야?");
await ask("1000에서 237을 빼면 얼마 남아?");
await ask("17을 3으로 나누면?");
await ask("너는 이름이 뭐야?");
npx tsx --env-file=.env index.ts
- 질문: "358 곱하기 47은 얼마야?"
- stop_reason: tool_use
- [tool_use] name=calculator input={"operation":"multiply","a":358,"b":47}
- 질문: "1000에서 237을 빼면 얼마 남아?"
- stop_reason: tool_use
- [tool_use] name=calculator input={"operation":"subtract","a":1000,"b":237}
- 질문: "17을 3으로 나누면?"
- stop_reason: tool_use
- [tool_use] name=calculator input={"operation":"divide","a":17,"b":3}
- 질문: "너는 이름이 뭐야?"
- stop_reason: end_turn
- [text] 안녕하세요! 저는 Claude라고 합니다. Anthropic에서 만든 AI 어시스턴트입니다.
- 저는 다양한 질문에 답변하고, 정보를 제공하고, 문제를 해결하는 것을 도와드릴 수 있습니다. 또한 계산기 도구를 사용하여 수학 연산(덧셈, 뺄셈, 곱셈, 나눗셈)도 수행할 수 있습니다.
세 번 다 operation 이 우리 enum 값 중 하나로, a·b 도 정확한 숫자로 왔다. "1000에서 237을 빼면" 이 subtract, a: 1000, b: 237 로 왔고, 뺄셈 순서까지 description 에 적어둔 대로 지켜졌다. 마지막 질문은 계산이 필요 없으니 도구를 부르지 않고 그냥 답했다.
description·enum 을 빼면 어떻게 되나
이번엔 일부러 부실하게 정의한 도구로 같은 질문을 던져본다. loose-schema.ts 를 만든다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// description 도 없고, operation 도 자유 문자열이다. Claude 가 뭘 넣을지 우리가 통제할 수 없다.
const tools: Anthropic.Tool[] = [
{
name: "calc",
input_schema: {
type: "object",
properties: {
operation: { type: "string" },
a: { type: "number" },
b: { type: "number" },
},
},
},
];
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 300,
tools,
messages: [{ role: "user", content: "358 곱하기 47은 얼마야?" }],
});
console.log(`stop_reason: ${response.stop_reason}`);
for (const block of response.content) {
if (block.type === "tool_use") {
console.log(`[tool_use] name=${block.name} input=${JSON.stringify(block.input)}`);
} else if (block.type === "text") {
console.log(`[text] ${block.text}`);
}
}
npx tsx --env-file=.env loose-schema.ts
세 번 반복 실행한 실제 결과다.
- $ npx tsx --env-file=.env loose-schema.ts
- stop_reason: tool_use
- [text] 358을 47로 곱하는 계산을 해드리겠습니다.
- [tool_use] name=calc input={"a":358,"b":47,"operation":"multiply"}
- $ npx tsx --env-file=.env loose-schema.ts
- stop_reason: tool_use
- [tool_use] name=calc input={"a":358,"b":47,"operation":"multiply"}
- $ npx tsx --env-file=.env loose-schema.ts
- stop_reason: tool_use
- [text] 358을 47로 곱하겠습니다.
- [tool_use] name=calc input={"a":358,"b":47,"operation":"multiply"}
이번엔 세 번 다 "multiply" 로 정확히 왔다. haiku-4-5 가 워낙 명확한 질문이라 스키마 없이도 알아서 맞춘 것이다. 이 결과만 보면 enum 이 굳이 필요 없어 보일 수 있다.
아직 계산은 안 끝났다
tool_use.input 에 {"operation":"multiply","a":358,"b":47} 가 왔다고, Claude 가 358×47 을 계산해서 준 게 아니다. 저건 여전히 "이 인자로 calculator 를 불러달라"는 요청일 뿐이고, 진짜 358×47 을 계산해서 Claude 에게 돌려주는 건 우리 코드의 몫이다.
지금까지 만든 것
- my-ai-assistant
- index.ts
- loose-schema.ts
- .env
- package.json
정리하면
input_schema 는 Claude 가 무슨 값을 만들어낼 수 있는지의 경계선이다. enum·description·required 를 촘촘히 쓸수록 그 경계가 좁아지고, 좁아질수록 우리 코드가 믿고 처리할 수 있는 입력이 된다.
- 01
enum·description·required 를 갖춘 calculator 도구를 정의했다
operation 은 4개 값으로, a·b 는 숫자로, 셋 다 필수로 못박았다.
- 02
실제 질문 네 개로 tool_use.input 을 확인했다
덧셈 계열 질문 셋은 모두 정확한 operation·a·b 로 왔고, 계산이 필요 없는 질문은 텍스트로 답했다.
npx tsx --env-file=.env index.ts - 03
스키마를 부실하게 만들어 대조군을 돌렸다
이번엔 우연히 맞았지만, enum 이 없으면 그 정확도는 스키마 보장 없이 순전히 운에 달려 있다는 걸 확인했다.
npx tsx --env-file=.env loose-schema.ts
다음 강의에서는 이 tool_use.input 을 실제로 실행하고, 그 결과를 Claude 에게 다시 보내는 실행 루프를 만든다. messages 배열에 tool_result 를 추가해 대화를 이어가면, Claude 가 계산 결과를 받아 문장으로 마무리 짓는 것까지 직접 본다.