학습 자료

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 출력을 다룰 때 본 것과 같은 문법이다. 눈여겨볼 세 가지가 있다.

input_schema 를 촘촘히 쓰는 세 가지 장치
  1. enumoperation 이 add/subtract/multiply/divide 넷 중 하나로만 오게 강제한다
  2. 필드별 descriptionsubtract 가 a-b 인지 b-a 인지처럼, 이름만으론 모호한 걸 못박는다
  3. requiredoperation·a·b 세 필드가 다 없으면 tool_use 자체가 불완전해진다
12강의 get_current_time 은 인자가 없어 이 셋이 필요 없었다. 인자가 생기는 순간부터 스키마 품질이 곧 정확도다.

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

세 번 반복 실행한 실제 결과다.

터미널 (3회 반복 실행)
  • $ 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 이 굳이 필요 없어 보일 수 있다.

calculator (enum O) vs calc (enum X)

아직 계산은 안 끝났다

tool_use.input{"operation":"multiply","a":358,"b":47} 가 왔다고, Claude 가 358×47 을 계산해서 준 게 아니다. 저건 여전히 "이 인자로 calculator 를 불러달라"는 요청일 뿐이고, 진짜 358×47 을 계산해서 Claude 에게 돌려주는 건 우리 코드의 몫이다.

지금까지 만든 것

my-ai-assistant/ (ai-lab-b, lesson-13)
  • my-ai-assistant
    • index.tsenum·description·required 를 갖춘 calculator 도구
    • loose-schema.ts일부러 부실하게 정의한 대조군
    • .env
    • package.json

정리하면

input_schema 는 Claude 가 무슨 값을 만들어낼 수 있는지의 경계선이다. enum·description·required 를 촘촘히 쓸수록 그 경계가 좁아지고, 좁아질수록 우리 코드가 믿고 처리할 수 있는 입력이 된다.

다시 짚어보기
  1. 01

    enum·description·required 를 갖춘 calculator 도구를 정의했다

    operation 은 4개 값으로, a·b 는 숫자로, 셋 다 필수로 못박았다.

  2. 02

    실제 질문 네 개로 tool_use.input 을 확인했다

    덧셈 계열 질문 셋은 모두 정확한 operation·a·b 로 왔고, 계산이 필요 없는 질문은 텍스트로 답했다.

    npx tsx --env-file=.env index.ts
  3. 03

    스키마를 부실하게 만들어 대조군을 돌렸다

    이번엔 우연히 맞았지만, enum 이 없으면 그 정확도는 스키마 보장 없이 순전히 운에 달려 있다는 걸 확인했다.

    npx tsx --env-file=.env loose-schema.ts

다음 강의에서는 이 tool_use.input 을 실제로 실행하고, 그 결과를 Claude 에게 다시 보내는 실행 루프를 만든다. messages 배열에 tool_result 를 추가해 대화를 이어가면, Claude 가 계산 결과를 받아 문장으로 마무리 짓는 것까지 직접 본다.

tool schema 정의 — 첫 도구(계산기) — 디코드랩(DCODELAB)