byteforce

CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API

도구 사용

도구 결과 보내기

Sending tool results

이제 마지막 두 단계입니다. Claude가 보낸 tool_use 블록을 받아 실제 함수를 실행하고, 그 결과를 tool_result 블록에 담아 다시 보냅니다. 이 블록은 role:user 메시지 안에 들어가며, tool_use_id로 어떤 요청에 대한 답인지 짝지어 줍니다.

전체 내레이션영상 나레이션 한국어 번역 (전체)

Stephen Grider · Anthropic 기술 스태프

이제 4단계입니다 — Claude가 실행해 달라고 요청한 도구 함수를 실제로 돌립니다. 지난 단계에서 우리는 tool_use 블록이 든 응답을 Claude에게서 받았습니다. 노트북에서 그 response 변수를 출력해 보면, content 리스트의 두 번째 블록이 바로 tool_use 블록입니다. 그래서 그것에 접근하려면 response.content[1]을 씁니다.

그 안에는 input 필드가 있습니다. 이건 Claude가 get_current_datetime 함수에 넘기라고 요청하는 인자(딕셔너리)입니다. 그 딕셔너리를 꺼내려면 .input을 이어 붙입니다. (여기서 타입 에러가 보일 수 있는데, 이 영상에서는 일단 무시합니다. 곧 한꺼번에 고칩니다.)

그런데 우리 get_current_datetime 함수는 딕셔너리를 받지 않습니다 — date_format이라는 키워드 인자를 받죠. 그래서 그 딕셔너리를 키워드 인자들로 펼쳐 함수에 적용합니다. get_current_datetime(**response.content[1].input)처럼요. 실행하면 실제 현재 시각이 나옵니다. 타입 에러만 빼면 꽤 쉽게 끝났습니다.

이제 5단계 — Claude에 후속 요청을 보냅니다. 이 요청에는 전체 대화 기록이 들어갑니다. 원래의 user 메시지, 방금 처리한 tool_use 블록이 든 assistant 메시지, 그리고 맨 끝에 새로 덧붙이는 또 하나의 user 메시지입니다. 이 user 메시지에는 지금까지 못 봤던 새로운 종류의 블록 — tool_result 블록 — 이 들어갑니다.

tool_result 블록은 user 메시지 안에 들어가고, 도구를 실행한 결과를 담습니다. 방금 호출한 도구 함수가 돌려준 것을 그대로 Claude에 다시 먹이는 셈입니다. 이 블록에는 몇 가지 키가 있는데, 무슨 일을 하는지 정확히 이해하는 게 중요합니다.

첫 번째이자 가장 까다로운 것이 tool_use_id입니다. 예를 들어 calculator라는 도구를 만들었다고 합시다. 사용자가 “10 더하기 10은? 그리고 30 더하기 30은?”이라고 물으면, Claude는 calculator를 두 번 부르고 싶어 할 수 있습니다. 그러면 assistant 메시지에 tool_use 블록이 두 개 담겨 옵니다 — 하나는 10+10, 하나는 30+30.

우리는 calculator를 두 번 실행하고, 후속 요청의 user 메시지에 tool_result 블록을 두 개 보냅니다. 이때 Claude는 어떤 결과가 어떤 요청에 속하는지 알아야 합니다. 순서에만 의존하지 않으려고 ID를 씁니다. 원래 tool_useid가 있는데(예: AB3, PO9), 후속 요청의 tool_result마다 그 tool_use_id를 맞춰 줍니다 — AB3↔20, PO9↔60 식으로요. 그게 tool_use_id의 역할입니다 — 요청과 결과를 짝지어 줍니다.

나머지 키도 알아 둡니다. content는 도구 함수에서 나온 출력입니다. 숫자·딕셔너리·리스트가 나와도 보통 평범한 JSON 문자열로 바꿔 넣습니다. 그리고 선택적으로 is_error 필드를 넣을 수 있습니다 — 도구 실행 중 뭔가 잘못되면 True로, 기본값은 항상 False입니다.

이제 노트북으로 가서 메시지 리스트에 이 새 user 메시지를 덧붙입니다. roleuser, content는 블록 하나짜리 리스트입니다. 그 블록은 typetool_result, tool_use_id는 위 tool_use 블록의 id와 같아야 하므로 response.content[1].id로 가져옵니다. content에는 함수 호출 결과(result 변수에 담아 둔 값)를, 그리고 에러가 없었으니 is_errorFalse를 넣습니다(기본값이라 꼭 필요하진 않지만요).

메시지 리스트를 출력해 보면 이제 전체 대화 기록이 보입니다 — 원래 user 요청, 도구를 쓰라는 Claude의 요청, 그리고 방금 덧붙인 tool_result 블록이 든 user 메시지. 마지막으로 이 리스트를 Claude에 다시 보냅니다. client.messages.create를 모델·max_tokens·메시지 리스트와 함께 부르는데, 도구를 쓸 일이 없어 보여도 원래 도구 스키마를 반드시 함께 보내야 합니다. tool_usetool_result 블록이 그 도구를 가리키고 있기 때문입니다.

실행하면 Claude의 최종 응답이 나옵니다 — “현재 시각은 15:04입니다” 같은 텍스트 블록 하나죠. 성공적인 도구 호출입니다. 전체 과정을 한 번 더 정리하면: 도구 함수와 스키마를 쓰고, 모든 요청에 스키마를 포함하고, Claude가 텍스트+tool_use 블록으로 답하면, 우리 서버에서 함수를 실행하고, 전체 대화 기록 + 스키마 + tool_result 블록이 든 user 메시지로 후속 요청을 보내면, Claude가 그 결과를 활용해 최종 텍스트 답을 돌려줍니다.

이 장에서 배우는 것What you'll learn

약 9분
1

4단계 — tool_use 블록의 .input을 함수에 **로 펼쳐 실행

2

5단계 — 전체 대화 기록 끝에 tool_result 블록이 든 user 메시지를 덧붙임

3

tool_use_id는 요청과 결과를 짝짓는다 — 순서가 아니라 ID로 매칭

4

content는 함수 출력(보통 JSON 문자열), is_error는 실패 시 True

5

후속 요청에도 도구 스키마를 반드시 포함 — 블록들이 그 도구를 가리키기 때문

6

Claude가 결과를 활용해 최종 텍스트 답을 돌려준다(end_turn)

먼저 짚고 갈 용어
tool_result 블록
도구 실행 결과를 담아 Claude에 돌려보내는 블록. 반드시 role:user 메시지 안에 들어간다.
tool_use_id
어떤 tool_use 요청에 대한 결과인지 짝짓는 ID. 원래 블록의 .id와 일치시켜야 한다.
content (블록 내)
도구 함수가 돌려준 출력. 숫자·딕셔너리여도 보통 평범한 JSON 문자열로 바꿔 넣는다.
is_error
도구 실행 실패 여부. 잘못되면 True, 기본값은 False(생략 가능).

마지막 두 단계 — 실행하고, 되먹이기

Run the tool, feed it back

도구 사용은 다섯 단계입니다. 앞 레슨에서 1~3단계(함수·스키마 작성, 스키마와 함께 호출)를 했고, Claude가 tool_use 블록으로 답했습니다. 이제 4단계(도구 실행)5단계(결과를 되먹이고 재호출)를 마무리합니다.

도구 사용 5단계 · 칩을 눌러 흐름을 따라가기
우리 서버
Claude

이 레슨은 4단계와 5단계를 다룹니다 — 도구를 실제로 실행하고, 그 결과를 tool_result 블록으로 되먹입니다. 도구 스키마는 매 요청마다 함께 보낸다는 점을 기억하세요.

4단계 — 도구 함수 실행

Run the tool function

응답의 두 번째 블록이 tool_use 블록입니다. response.content[1]로 접근하고, .input으로 Claude가 넘기라는 인자(딕셔너리)를 꺼냅니다.

노트북 · tool_use 블록 살펴보기
response.content[1]
# ToolUseBlock(id="toolu_01B8bi7q7qzvwP8zSM8Hp3BD",
#   input={"date_format": "%H:%M:%S"},
#   name="get_current_datetime", type="tool_use")

response.content[1].input
# {"date_format": "%H:%M:%S"}  ← Claude가 넘기라는 인자

우리 함수는 딕셔너리가 아니라 date_format 키워드 인자를 받습니다. 그래서 그 딕셔너리를 **로 펼쳐 함수에 넘깁니다.

노트북 · 도구 실행
# 4단계 — Claude가 요청한 도구 함수를 실제로 실행
# get_current_datetime은 딕셔너리가 아니라 date_format 키워드 인자를 받으므로
# input 딕셔너리를 ** 로 펼쳐 키워드 인자로 넘긴다
result = get_current_datetime(**response.content[1].input)

result
# "15:04:22"  ← 실제 현재 시각
타입 에러는 잠깐 무시

.input·.id를 이어 붙일 때 타입 에러가 보일 수 있습니다. 강의에서는 일단 넘어가고 뒤에서 한꺼번에 고칩니다 — 동작에는 문제가 없습니다.

5단계 — tool_result 블록 만들기

Build the tool_result block

전체 대화 기록 끝에 새 user 메시지를 덧붙입니다. 그 안에는 처음 보는 tool_result 블록이 들어갑니다 — 도구 실행 결과를 담아 Claude에 되먹이는 블록입니다. 키를 눌러 각 필드의 역할을 확인하세요.

// messages 리스트 끝에 덧붙이는 user 메시지 · 키를 눌러 설명 보기
"role":"user"
"type":"tool_result"
"tool_use_id":response.content[1].id
"content":result
"is_error":False
노트북 · tool_result 블록 덧붙이기
# 5단계 — 전체 대화 기록 끝에 tool_result 블록이 든 user 메시지를 덧붙인다
messages.append({
    "role": "user",
    "content": [
        {
            "type": "tool_result",
            "tool_use_id": response.content[1].id,   # 요청한 tool_use의 id와 일치
            "content": result,                       # 함수 출력
            "is_error": False,                      # 실패 시 True (기본 False)
        }
    ]
})
tool_use_id가 핵심

한 번에 도구를 여러 개 부르면 tool_use 블록이 여러 개 옵니다. Claude는 순서가 아니라 id로 결과를 짝짓습니다 — 그래서 각 tool_resulttool_use_id를 원래 요청의 .id와 맞춰야 합니다.

content에는 함수 출력을(숫자·딕셔너리여도 보통 JSON 문자열로), is_error에는 실패 여부를 넣습니다(기본 False).

후속 요청 → 최종 답, 그리고 정리

Follow-up & recap

이제 이 메시지 리스트를 Claude에 다시 보냅니다. 도구를 더 쓸 일이 없어 보여도 원래 도구 스키마를 반드시 포함합니다 — tool_use·tool_result 블록이 그 도구를 가리키기 때문입니다.

노트북 · 후속 요청(스키마 포함)
# 후속 요청 — 도구를 안 써도 원래 스키마를 반드시 포함해야 한다
response = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    tools=[get_current_datetime_schema],   # 블록들이 이 도구를 가리키므로 필수
)
출력 · 최종 응답
Message(
  id="msg_01AQjmLxwL9BhXaE9bWfuFi4",
  content=[TextBlock(text="The current time is 15:04:22.", type="text")],
  role="assistant", stop_reason="end_turn", type="message"
)
# 텍스트 블록 하나뿐 — 도구 호출 완료, 최종 답
전체 과정 한눈에
  • 도구 함수와 스키마를 쓰고, 모든 요청에 스키마를 포함한다.
  • Claude가 텍스트 + tool_use 블록으로 답하면, 우리 서버에서 함수를 실행한다.
  • 전체 대화 기록 + 스키마 + tool_result 블록(role:user)으로 후속 요청을 보낸다.
  • Claude가 결과를 활용해 최종 텍스트 답을 돌려준다(stop_reason="end_turn").

Q1tool_result 블록은 어떤 메시지 안에 들어가나요?

Q2tool_use_id의 역할은?

Q3도구를 더 쓰지 않을 후속 요청인데도 tools를 보내는 이유는?


LAB · 실습 콘솔TOOL RESULT

도구 결과 되먹이기

tool_use 블록을 실제로 실행해 tool_result로 되먹이고, 결과를 순서가 아니라 tool_use_id로 짝짓습니다.

index.js
// 도구 결과 되먹이기 — Claude가 tool_use 블록으로 도구 실행을 요청하면,
// 우리 서버에서 함수를 돌리고 그 결과를 tool_result 블록에 담아 되돌려준다.
// tool_result 는 role:user 메시지 안에 들어가며, tool_use_id 로
// "어떤 요청에 대한 답인지"를 짝짓는다 — 순서가 아니라 ID로 매칭한다.

// 도구 함수 — 로컬에서 실제로 실행된다.
function calculator(input) {
  return input.a + input.b;
}
const TOOLS = { calculator: calculator };

// Claude가 돌려준 assistant 메시지. content 는 블록의 배열이고,
// 텍스트 블록 뒤에 tool_use 블록이 온다. 여러 개일 수 있다.
// ── 여기서부터 직접 고쳐 보세요 ──
// 숫자를 바꾸거나 tool_use 블록을 더 넣어 보세요. id 하나를 일부러 틀리게
// 두면(예: "PO9" → "XXX") 아래 매칭에서 어떻게 잡히는지 볼 수 있습니다.
const assistantContent = [
  { type: "text", text: "두 계산을 도구로 처리하겠습니다." },
  { type: "tool_use", id: "AB3", name: "calculator", input: { a: 10, b: 10 } },
  { type: "tool_use", id: "PO9", name: "calculator", input: { a: 30, b: 30 } },
];

// 4단계 — 각 tool_use 블록의 input 을 함수에 넘겨 실제로 실행한다.
// 5단계 — 결과를 tool_result 블록으로 만든다. tool_use_id 는 원래 블록의 id.
function runTools(content) {
  return content
    .filter(function (b) { return b.type === "tool_use"; })
    .map(function (block) {
      const fn = TOOLS[block.name];
      let result, isError = false;
      try {
        if (!fn) throw new Error("알 수 없는 도구: " + block.name);
        result = String(fn(block.input));       // content 는 보통 JSON 문자열로
      } catch (e) {
        result = e.message; isError = true;      // 실패 시 is_error: true
      }
      return { type: "tool_result", tool_use_id: block.id, content: result, is_error: isError };
    });
}

const toolResults = runTools(assistantContent);

// 후속 요청에 보낼 user 메시지 — tool_result 블록들을 담는다.
const followupUserMessage = { role: "user", content: toolResults };

console.log("── 실행 결과 (tool_result 블록) ──");
toolResults.forEach(function (r) {
  console.log("  tool_use_id " + r.tool_use_id + " → " + r.content +
    (r.is_error ? "  (is_error: true)" : ""));
});

// ID 매칭 vs 순서 매칭 — 결과 순서를 뒤집어도 ID로는 정확히 짝지어진다.
console.log("");
console.log("── ID로 짝짓기 (결과 순서를 뒤집어도 정확) ──");
const shuffled = toolResults.slice().reverse();
assistantContent
  .filter(function (b) { return b.type === "tool_use"; })
  .forEach(function (use) {
    const byId = shuffled.find(function (r) { return r.tool_use_id === use.id; });
    const byOrderIdx = assistantContent
      .filter(function (b) { return b.type === "tool_use"; })
      .indexOf(use);
    const byOrder = shuffled[byOrderIdx];
    console.log("  요청 " + use.id + " (" + use.input.a + "+" + use.input.b + ")");
    console.log("    · ID 매칭   → " + (byId ? byId.content : "짝을 찾지 못함(id 불일치)"));
    console.log("    · 순서 매칭  → " + byOrder.content +
      (byOrder.tool_use_id !== use.id ? "  ← 엉뚱한 결과에 붙음" : ""));
  });

console.log("");
console.log("── Claude에 다시 보낼 대화 기록 ──");
console.log("  1. user      — 원래 질문");
console.log("  2. assistant — 텍스트 + tool_use 블록 " +
  assistantContent.filter(function (b) { return b.type === "tool_use"; }).length + "개");
console.log("  3. user      — tool_result 블록 " + followupUserMessage.content.length + "개");
console.log("후속 요청에도 원래 도구 스키마(tools)를 반드시 함께 보냅니다 — 블록들이 그 도구를 가리키기 때문입니다.");
MEMBER SESSION REQUIRED · REGISTRATION IS FREE

여기부터는 등록한 분에게 열립니다.

전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.

등록하고 이어서 읽기

이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.