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_use에 id가 있는데(예: AB3, PO9), 후속 요청의 tool_result마다 그 tool_use_id를 맞춰 줍니다 — AB3↔20, PO9↔60 식으로요. 그게 tool_use_id의 역할입니다 — 요청과 결과를 짝지어 줍니다.
나머지 키도 알아 둡니다. content는 도구 함수에서 나온 출력입니다. 숫자·딕셔너리·리스트가 나와도 보통 평범한 JSON 문자열로 바꿔 넣습니다. 그리고 선택적으로 is_error 필드를 넣을 수 있습니다 — 도구 실행 중 뭔가 잘못되면 True로, 기본값은 항상 False입니다.
이제 노트북으로 가서 메시지 리스트에 이 새 user 메시지를 덧붙입니다. role은 user, content는 블록 하나짜리 리스트입니다. 그 블록은 type이 tool_result, tool_use_id는 위 tool_use 블록의 id와 같아야 하므로 response.content[1].id로 가져옵니다. content에는 함수 호출 결과(result 변수에 담아 둔 값)를, 그리고 에러가 없었으니 is_error는 False를 넣습니다(기본값이라 꼭 필요하진 않지만요).
메시지 리스트를 출력해 보면 이제 전체 대화 기록이 보입니다 — 원래 user 요청, 도구를 쓰라는 Claude의 요청, 그리고 방금 덧붙인 tool_result 블록이 든 user 메시지. 마지막으로 이 리스트를 Claude에 다시 보냅니다. client.messages.create를 모델·max_tokens·메시지 리스트와 함께 부르는데, 도구를 쓸 일이 없어 보여도 원래 도구 스키마를 반드시 함께 보내야 합니다. tool_use와 tool_result 블록이 그 도구를 가리키고 있기 때문입니다.
실행하면 Claude의 최종 응답이 나옵니다 — “현재 시각은 15:04입니다” 같은 텍스트 블록 하나죠. 성공적인 도구 호출입니다. 전체 과정을 한 번 더 정리하면: 도구 함수와 스키마를 쓰고, 모든 요청에 스키마를 포함하고, Claude가 텍스트+tool_use 블록으로 답하면, 우리 서버에서 함수를 실행하고, 전체 대화 기록 + 스키마 + tool_result 블록이 든 user 메시지로 후속 요청을 보내면, Claude가 그 결과를 활용해 최종 텍스트 답을 돌려줍니다.
이 장에서 배우는 것What you'll learn
약 9분4단계 — tool_use 블록의 .input을 함수에 **로 펼쳐 실행
5단계 — 전체 대화 기록 끝에 tool_result 블록이 든 user 메시지를 덧붙임
tool_use_id는 요청과 결과를 짝짓는다 — 순서가 아니라 ID로 매칭
content는 함수 출력(보통 JSON 문자열), is_error는 실패 시 True
후속 요청에도 도구 스키마를 반드시 포함 — 블록들이 그 도구를 가리키기 때문
Claude가 결과를 활용해 최종 텍스트 답을 돌려준다(end_turn)
role:user 메시지 안에 들어간다.tool_use 요청에 대한 결과인지 짝짓는 ID. 원래 블록의 .id와 일치시켜야 한다.True, 기본값은 False(생략 가능).도구 사용은 다섯 단계입니다. 앞 레슨에서 1~3단계(함수·스키마 작성, 스키마와 함께 호출)를 했고, Claude가 tool_use 블록으로 답했습니다. 이제 4단계(도구 실행)와 5단계(결과를 되먹이고 재호출)를 마무리합니다.
이 레슨은 4단계와 5단계를 다룹니다 — 도구를 실제로 실행하고, 그 결과를 tool_result 블록으로 되먹입니다. 도구 스키마는 매 요청마다 함께 보낸다는 점을 기억하세요.
응답의 두 번째 블록이 tool_use 블록입니다. response.content[1]로 접근하고, .input으로 Claude가 넘기라는 인자(딕셔너리)를 꺼냅니다.
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를 이어 붙일 때 타입 에러가 보일 수 있습니다. 강의에서는 일단 넘어가고 뒤에서 한꺼번에 고칩니다 — 동작에는 문제가 없습니다.
전체 대화 기록 끝에 새 user 메시지를 덧붙입니다. 그 안에는 처음 보는 tool_result 블록이 들어갑니다 — 도구 실행 결과를 담아 Claude에 되먹이는 블록입니다. 키를 눌러 각 필드의 역할을 확인하세요.
# 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 블록이 여러 개 옵니다. Claude는 순서가 아니라 id로 결과를 짝짓습니다 — 그래서 각 tool_result의 tool_use_id를 원래 요청의 .id와 맞춰야 합니다.
content에는 함수 출력을(숫자·딕셔너리여도 보통 JSON 문자열로), is_error에는 실패 여부를 넣습니다(기본 False).
이제 이 메시지 리스트를 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"
)
# 텍스트 블록 하나뿐 — 도구 호출 완료, 최종 답tool_use 블록으로 답하면, 우리 서버에서 함수를 실행한다.tool_result 블록(role:user)으로 후속 요청을 보낸다.stop_reason="end_turn").Q1tool_result 블록은 어떤 메시지 안에 들어가나요?
도구 실행 결과는 우리가 Claude에게 “돌려주는” 입력이므로, 항상 user 메시지 안의 tool_result 블록으로 보냅니다.
Q2tool_use_id의 역할은?
도구를 여러 번 부르면 결과가 뒤섞일 수 있어, 순서가 아니라 원래 블록의 .id와 일치하는 id로 요청·결과를 매칭합니다.
Q3도구를 더 쓰지 않을 후속 요청인데도 tools를 보내는 이유는?
대화 기록에 그 도구를 참조하는 블록이 있으므로, Claude가 맥락을 이해하도록 매 요청에 스키마를 함께 보냅니다.
tool_use 블록을 실제로 실행해 tool_result로 되먹이고, 결과를 순서가 아니라 tool_use_id로 짝짓습니다.
// 도구 결과 되먹이기 — 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)를 반드시 함께 보냅니다 — 블록들이 그 도구를 가리키기 때문입니다.");
단일 도구 호출의 전체 왕복을 끝냈습니다. 이제 Claude가 여러 도구를 연달아 부르는 멀티턴 대화로 넘어갑니다. → 도구와 멀티턴 대화
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.