실행 루프 — 결과를 되먹인다
Article
13강 끝에서 확인한 게 있다. tool_use.input 에 {"operation":"multiply","a":358,"b":47} 가 와도, Claude 가 358×47 을 계산해준 게 아니다. 저건 "이 인자로 calculator 를 불러달라"는 요청일 뿐이고, 실제로 358×47 을 계산하는 것도, 그 결과를 Claude 에게 다시 보여주는 것도 아직 우리 몫이었다. 오늘 그 되먹임 루프를 만든다.
대화가 실제로 오가는 순서
Claude 가 도구를 쓰는 대화는 한 번의 요청-응답이 아니라, 최소 두 번의 왕복으로 이뤄진다.
- ① user: 질문"358 곱하기 47은?"
- ② assistant: tool_usestop_reason이 tool_use. 아직 답이 아니다
- ③ 우리 코드: 실제 계산358 * 47 을 순수 JS 로 계산 — API 밖의 일
- ④ user: tool_result계산 결과를 다시 Claude 에게 보낸다
- ⑤ assistant: 최종 답stop_reason이 end_turn. 이제 진짜 답이다
여기서 중요한 건 ②와 ④가 같은 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_reason 이 tool_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으로 나누면?");
- assistant 메시지를 쌓는다response.content 그대로 messages 에 push — 방금 온 tool_use 블록을 통째로 보관
- 도구를 실행한다runCalculator 로 실제 계산 → tool_use_id 와 짝지어 tool_result 준비
- 다시 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 루프가 두 번 돌았기 때문에 이게 자동으로 처리됐다.
지금까지 만든 것
- my-ai-assistant
- index.ts
- .env
- package.json
정리하면
tool_use 는 대화의 끝이 아니라 중간이다. 우리 코드가 실행하고, tool_result 로 되먹이고, end_turn 이 올 때까지 반복해야 비로소 대화가 끝난다.
- 01
tool_use 응답을 messages 에 그대로 쌓았다
assistant 의 response.content(tool_use 블록 포함)를 먼저 push 해야 tool_result 와 짝이 맞는다.
- 02
tool_use.input 을 실제로 실행하고 tool_result 로 돌려줬다
runCalculator 는 순수 JS 함수다. API 호출이 아니다. 결과는 tool_use_id 와 짝지어 user 메시지로 전송.
npx tsx --env-file=.env index.ts - 03
stop_reason 이 end_turn 이 될 때까지 while 로 반복했다
다단계 계산(뺄셈 후 나눗셈)은 루프가 두 번 돌아야 끝나는데, 이게 자동으로 처리됐다.
지금까지 도구는 계산기 하나, 그것도 우리 코드 안에서 끝나는 순수 함수였다. 다음 강의에서는 진짜 외부 API 를 부르는 도구를 만든다. npm 레지스트리에 실제로 접속해 패키지 정보를 가져오고, 패키지가 없을 때 에러를 어떻게 Claude 에게 되먹여야 하는지까지 다룬다.