학습 자료

실행 루프 — 결과를 되먹인다


Article

13강 끝에서 확인한 게 있다. tool_use.input{"operation":"multiply","a":358,"b":47} 가 와도, Claude 가 358×47 을 계산해준 게 아니다. 저건 "이 인자로 calculator 를 불러달라"는 요청일 뿐이고, 실제로 358×47 을 계산하는 것도, 그 결과를 Claude 에게 다시 보여주는 것도 아직 우리 몫이었다. 오늘 그 되먹임 루프를 만든다.

대화가 실제로 오가는 순서

Claude 가 도구를 쓰는 대화는 한 번의 요청-응답이 아니라, 최소 두 번의 왕복으로 이뤄진다.

tool_result 를 되먹이는 순서
  1. ① user: 질문"358 곱하기 47은?"
  2. ② assistant: tool_usestop_reason이 tool_use. 아직 답이 아니다
  3. ③ 우리 코드: 실제 계산358 * 47 을 순수 JS 로 계산 — API 밖의 일
  4. ④ user: tool_result계산 결과를 다시 Claude 에게 보낸다
  5. ⑤ assistant: 최종 답stop_reason이 end_turn. 이제 진짜 답이다
③은 API 호출이 아니다. 우리 서버(또는 우리 스크립트)에서 그냥 자바스크립트 함수를 실행하는 것뿐이다.

여기서 중요한 건 ②와 ④가 같은 messages 배열에 계속 쌓인다는 점이다. Claude 에게는 대화가 끊긴 적이 없다. user, assistant, user, assistant 가 한 줄로 이어질 뿐이다.

실행 루프를 만든다

ai-lab-b 저장소에 lesson-14 폴더를 만들고 13강과 같은 방식으로 세팅한다. 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"],
    },
  },
];

function runCalculator(input: { operation: string; a: number; b: number }): number {
  const { operation, a, b } = input;
  switch (operation) {
    case "add":
      return a + b;
    case "subtract":
      return a - b;
    case "multiply":
      return a * b;
    case "divide":
      return a / b;
    default:
      throw new Error(`알 수 없는 연산: ${operation}`);
  }
}

runCalculator 는 순수 자바스크립트 함수다. Claude 를 부르지 않는다. 13강에서 확인한 tool_use.input 을 받아서, 그냥 계산만 한다.

도구 호출 결과를 되먹인다

이제 ask 함수를 만든다. 13강의 ask 와 다른 점은 하나다. stop_reasontool_use 인 동안 계속 반복한다.

async function ask(question: string) {
  console.log(`\n질문: "${question}"`);

  const messages: Anthropic.MessageParam[] = [{ role: "user", content: question }];

  let response = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 300,
    tools,
    messages,
  });

  console.log(`[1차 응답] stop_reason: ${response.stop_reason}`);

  while (response.stop_reason === "tool_use") {
    messages.push({ role: "assistant", content: response.content });

    const toolResults: Anthropic.ToolResultBlockParam[] = [];

    for (const block of response.content) {
      if (block.type === "tool_use" && block.name === "calculator") {
        const input = block.input as { operation: string; a: number; b: number };
        const result = runCalculator(input);
        console.log(`  [실행] calculator(${JSON.stringify(input)}) = ${result}`);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: String(result),
        });
      }
    }

    messages.push({ role: "user", content: toolResults });

    response = await client.messages.create({
      model: "claude-haiku-4-5",
      max_tokens: 300,
      tools,
      messages,
    });

    console.log(`[다음 응답] stop_reason: ${response.stop_reason}`);
  }

  for (const block of response.content) {
    if (block.type === "text") {
      console.log(`[최종 답] ${block.text}`);
    }
  }
}

await ask("358 곱하기 47은 얼마야?");
await ask("1000에서 237을 빼고, 그 결과를 3으로 나누면?");
while 루프 한 바퀴 안에서 하는 세 가지
  1. assistant 메시지를 쌓는다response.content 그대로 messages 에 push — 방금 온 tool_use 블록을 통째로 보관
  2. 도구를 실행한다runCalculator 로 실제 계산 → tool_use_id 와 짝지어 tool_result 준비
  3. 다시 messages.create 를 부른다tool_result 를 user 메시지로 넣고 재요청 — 답이 나올 때까지 반복

실제로 돌려본다

npx tsx --env-file=.env index.ts
터미널
  • 질문: "358 곱하기 47은 얼마야?"
  • [1차 응답] stop_reason: tool_use
  • [실행] calculator({"operation":"multiply","a":358,"b":47}) = 16826
  • [다음 응답] stop_reason: end_turn
  • [최종 답] 358 곱하기 47은 16,826입니다.
  • 질문: "1000에서 237을 빼고, 그 결과를 3으로 나누면?"
  • [1차 응답] stop_reason: tool_use
  • [실행] calculator({"operation":"subtract","a":1000,"b":237}) = 763
  • [다음 응답] stop_reason: tool_use
  • [실행] calculator({"operation":"divide","a":763,"b":3}) = 254.33333333333334
  • [다음 응답] stop_reason: end_turn
  • [최종 답] 답: 약 254.33
  • - 1000 - 237 = 763
  • - 763 ÷ 3 = 254.33333...

첫 질문은 루프가 딱 한 바퀴 돈다. 계산 한 번, tool_result 한 번, 바로 end_turn. 두 번째 질문이 흥미롭다. "1000에서 237을 빼고, 그 결과를 3으로 나누면" 은 계산이 두 단계다. Claude 는 이걸 한 번에 풀지 않고, 먼저 subtract 를 요청해 763 을 받은 뒤 그 763 을 다음 도구 호출(divide)의 입력으로 써서 다시 요청했다. 우리 while 루프가 두 번 돌았기 때문에 이게 자동으로 처리됐다.

13강 vs 14강 — 같은 tool_use, 다른 결말

지금까지 만든 것

my-ai-assistant/ (ai-lab-b, lesson-14)
  • my-ai-assistant
    • index.tstool_use → 실행 → tool_result 되먹임 루프
    • .env
    • package.json

정리하면

tool_use 는 대화의 끝이 아니라 중간이다. 우리 코드가 실행하고, tool_result 로 되먹이고, end_turn 이 올 때까지 반복해야 비로소 대화가 끝난다.

다시 짚어보기
  1. 01

    tool_use 응답을 messages 에 그대로 쌓았다

    assistant 의 response.content(tool_use 블록 포함)를 먼저 push 해야 tool_result 와 짝이 맞는다.

  2. 02

    tool_use.input 을 실제로 실행하고 tool_result 로 돌려줬다

    runCalculator 는 순수 JS 함수다. API 호출이 아니다. 결과는 tool_use_id 와 짝지어 user 메시지로 전송.

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

    stop_reason 이 end_turn 이 될 때까지 while 로 반복했다

    다단계 계산(뺄셈 후 나눗셈)은 루프가 두 번 돌아야 끝나는데, 이게 자동으로 처리됐다.

지금까지 도구는 계산기 하나, 그것도 우리 코드 안에서 끝나는 순수 함수였다. 다음 강의에서는 진짜 외부 API 를 부르는 도구를 만든다. npm 레지스트리에 실제로 접속해 패키지 정보를 가져오고, 패키지가 없을 때 에러를 어떻게 Claude 에게 되먹여야 하는지까지 다룬다.

실행 루프 — 결과를 되먹인다 — 디코드랩(DCODELAB)