CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API
도구 사용
Implementing multiple turns
이제 도구 사용 루프를 코드로 구현합니다. run_conversation은 Claude가 더 이상 도구를 요청하지 않을 때까지 계속 호출하는 while 루프입니다. 멈출 시점은 stop_reason으로 알고, 한 메시지에 들어 있는 여러 tool_use 블록을 run_tools가 모두 실행해 결과를 돌려보냅니다.
Stephen Grider · Anthropic 기술 스태프
리팩터가 끝났으니 이제 run_conversation 함수를 구현합니다. 실제 구현은 화면 오른쪽에 보이는 의사 코드와 거의 똑같습니다. 이 함수의 전체 목표는 단 하나 — Claude가 더 이상 도구를 쓰겠다고 하지 않을 때까지 계속 Claude를 호출하는 것입니다. 도구를 더 요청하지 않으면, 그건 Claude가 사용자에게 돌려줄 최종 응답이 준비됐다는 신호입니다.
먼저 이해할 것은 Claude가 도구를 쓰고 싶어 하는지를 어떻게 알아내는가입니다. 응답 메시지를 직접 들여다보며 tool_use 블록이 있는지 확인할 수도 있지만, 더 편한 방법이 있습니다. client.messages.create로 직접 호출해 응답을 보면 stop_reason이라는 필드가 있고, 값이 문자열 tool_use로 설정돼 있습니다.
stop_reason은 Claude가 왜 텍스트 생성을 멈췄는지를 알려 줍니다. 값이 tool_use라면 Claude가 도구를 호출해야 한다고 판단했다는 신호입니다. 그러니 우리가 받은 어시스턴트 메시지의 stop_reason이 tool_use이면, 그건 Claude가 도구를 쓰고 싶어 한다는 아주 분명하고 즉각적인 신호입니다. stop_reason에는 다른 값들도 있지만, 가장 자주 확인하게 될 값은 tool_use입니다. 바로 이 값으로 if 문을 구현합니다.
이제 run_conversation을 만듭니다. 메시지 목록을 받아 while 루프를 세웁니다. 루프 안에서 도구를 지원하도록 업그레이드한 chat 함수로 Claude를 호출하는데, 메시지 목록과 함께 Claude가 부를 수 있는 도구들을 넘깁니다. 지금은 도구가 하나뿐이라 get_current_datetime_schema만 넣습니다. 받은 응답을 add_assistant_message로 대화 기록에 추가하고, text_from_message로 출력해 Claude가 지금 무엇을 하는지 봅니다.
여기서 stop_reason을 씁니다. 방금 받은 메시지가 도구를 쓰고 싶어 하는지 확인합니다. 쓰고 싶어 하지 않으면 곧바로 while 루프를 빠져나옵니다 — 즉 response.stop_reason != "tool_use"이면 break합니다. 이 if 문을 지나면 Claude가 도구를 호출하고 싶다는 뜻이므로, 잠시 뒤에 만들 run_tools 함수에 그 메시지를 넘깁니다. run_tools의 목표는 메시지 안의 모든 tool_use 블록을 보고 각각에 알맞은 도구를 실행하는 것입니다.
run_tools는 조금 까다롭습니다. 한 메시지 안에 여러 개의 tool_use 블록이 있을 수 있다고 가정하고 짜야 하기 때문입니다. 예를 들어 Claude에게 10+10과 30+30을 더하라고 하면, tool_use 블록 두 개를 돌려줄 수 있습니다. 하나는 계산기 도구로 10+10을, 다른 하나는 30+30을 평가하라는 식이죠. 그래서 메시지의 content(블록들의 리스트)에서 텍스트 블록은 신경 쓰지 않고 tool_use 블록만 골라냅니다.
각 tool_use 블록마다 name 필드를 보고 알맞은 도구 함수를 찾아 주어진 input으로 실행합니다. 그 출력들을 각각 tool_result 블록으로 만들어 한데 모아 리스트로 반환합니다. 코드로는, 먼저 message.content에서 block.type == "tool_use"인 블록만 모아 tool_request라 부릅니다(이것들은 Claude가 우리에게 도구를 써 달라고 보낸 요청이니까요). 그다음 빈 리스트 tool_result_blocks를 만들고, 각 요청을 순회합니다.
tool_result 블록은 몇 가지 속성이 필요합니다. tool_use_id는 그 도구 실행을 유발한 tool_use 블록의 id와 정확히 같아야 합니다(왼쪽 블록에선 id, 오른쪽 결과 블록에선 tool_use_id로 이름은 다르지만 값은 같아야 합니다). content에는 도구 실행 결과를 문자열로 인코딩해 넣고(json.dumps 사용), 선택적으로 에러가 났으면 is_error를, 그리고 type은 tool_result로 둡니다.
두 가지 개선을 더합니다. 첫째, 에러 처리 — 도구 실행을 try/except로 감싸, 예외가 나면 is_error=True인 tool_result 블록을 만들고 content에 에러 메시지를 넣습니다. 그래야 Claude가 무슨 에러인지 이해하고, 더 나은 인자로 도구를 다시 시도할 수 있습니다. 둘째, 지금은 get_current_datetime 하나만 if로 확인하는데 이는 확장성이 없습니다. 그래서 run_tools 위에 run_tool 헬퍼를 따로 만들어, tool_name과 input을 받아 알맞은 함수를 호출해 결과를 반환하게 합니다. 도구가 늘면 if만 추가하면 됩니다.
마지막으로 run_conversation으로 돌아와 run_tools를 씁니다. run_tools가 돌려준 tool_result 리스트를 add_user_message로 대화 기록에 추가하고, while 루프 바깥에서 messages를 반환합니다. 이제 이 함수는 전체 루프를 담습니다 — Claude 호출 → 어시스턴트 메시지가 도구를 요청하면 도구 실행 → 결과를 user 메시지로 추가 → 루프 위로 돌아가 다시 호출 → Claude가 더 이상 도구를 요청하지 않을 때까지 반복.
테스트해 봅니다. "현재 시각을 HH:MM 형식으로, 그리고 SS 형식으로 알려 줘"라고 물으면 Claude는 보통 이를 두 번의 도구 호출로 나눕니다. 메시지 기록을 보면 user 메시지, 그다음 텍스트 블록과 tool_use 블록을 가진 어시스턴트 메시지(HH:MM 요청), 우리의 tool_result, 그리고 두 번째 도구 호출(이번엔 텍스트 없이 SS 형식 tool_use), tool_result, 마지막으로 최종 텍스트 답이 옵니다. 여러 블록을 올바르게 다루는 것이 왜 중요한지 완벽히 보여 주는 멀티턴 도구 호출 예시입니다.
이 장에서 배우는 것What you'll learn
약 12분run_conversation = Claude가 도구를 그만 요청할 때까지 도는 while 루프
stop_reason != "tool_use"이면 break — 최종 응답 신호
run_tools는 한 메시지의 모든 tool_use 블록을 순회
각 결과를 tool_result 블록(tool_use_id·content·is_error)으로
try/except로 에러를 잡아 is_error=True로 돌려보낸다
run_tool 헬퍼로 이름→함수 라우팅 — 도구 추가가 쉬워진다
tool_use이면 도구를 호출하려는 신호.tool_use_id는 원래 tool_use의 id와 같아야 한다.루프의 목표는 하나입니다 — Claude가 도구를 그만 요청할 때까지 계속 호출하는 것. 그 시점을 어떻게 알까요? 응답의 stop_reason 필드를 보면 됩니다. 값이 tool_use이면 Claude가 도구를 호출하려는 분명한 신호입니다.
# chat 함수 대신 client로 직접 호출해 보면… message = client.messages.create( model=model, max_tokens=1000, messages=messages, tools=[get_current_datetime_schema], ) message.stop_reason # => "tool_use" ← Claude가 도구를 호출하려는 신호
메시지 content를 뒤져 tool_use 블록을 찾아도 되지만, stop_reason 하나만 보면 훨씬 간편합니다. 다른 값들도 있지만 도구 사용 루프에서 가장 자주 확인할 값은 tool_use입니다.
한 어시스턴트 메시지에는 tool_use 블록이 여러 개 있을 수 있습니다(예: 10+10과 30+30을 각각). 그래서 run_tools는 content에서 tool_use 블록만 골라 순회하며 각각 실행하고, 결과를 tool_result 블록으로 만들어 리스트로 돌려줍니다.
import json def run_tool(tool_name, tool_input): if tool_name == "get_current_datetime": return get_current_datetime(**tool_input) # 도구가 늘면 여기에 elif 만 추가하면 된다
그리고 본체인 run_tools. try/except로 에러를 잡아 is_error=True로 돌려보내면, Claude가 에러를 이해하고 더 나은 인자로 다시 시도할 수 있습니다.
def run_tools(message): tool_requests = [ block for block in message.content if block.type == "tool_use" ] # tool_use 블록만 골라낸다 tool_result_blocks = [] for tool_request in tool_requests: try: tool_output = run_tool(tool_request.name, tool_request.input) tool_result_block = { "type": "tool_result", "tool_use_id": tool_request.id, # 원래 tool_use 의 id 와 동일 "content": json.dumps(tool_output), "is_error": False, } except Exception as e: tool_result_block = { "type": "tool_result", "tool_use_id": tool_request.id, "content": f"Error: {e}", "is_error": True, # 에러를 Claude 에 알려 재시도하게 } tool_result_blocks.append(tool_result_block) return tool_result_blocks
tool_result의 tool_use_id는 그 실행을 유발한 tool_use 블록의 id와 정확히 같아야 합니다. 이름은 다르지만(id vs tool_use_id) 값으로 짝을 맞춥니다.
이제 조각을 합칩니다. run_conversation은 Claude를 호출하고, 어시스턴트 메시지를 기록에 추가하고, stop_reason으로 종료를 판단하고, 도구를 요청하면 run_tools로 실행해 결과를 user 메시지로 다시 넣고 — 루프 위로 돌아갑니다.
while True: response = chat(...) →
대기 중break. 루프를 빠져나와 최종 메시지 목록을 반환합니다. Claude가 도구를 더 요청하지 않았으니, 마지막 어시스턴트 메시지가 사용자에게 보낼 최종 응답입니다.루프는 매 반복마다 ① Claude 호출 → ② stop_reason 확인 → tool_use면 ③ 도구 실행 후 결과를 user 메시지로 추가 → 다시 ①. tool_use가 아닐 때 비로소 멈춥니다.
def run_conversation(messages): while True: response = chat(messages, tools=[get_current_datetime_schema]) add_assistant_message(messages, response) print(text_from_message(response)) if response.stop_reason != "tool_use": break # 도구를 더 요청하지 않으면 최종 응답 → 종료 tool_results = run_tools(response) add_user_message(messages, tool_results) # 결과를 user 메시지로 return messages
테스트로 두 번의 도구 호출이 필요한 질문을 던집니다.
messages = []
add_user_message(
messages,
"What is the current time in HH:MM format? "
"Also, what is the current time in SS format?",
)
run_conversation(messages)# messages 기록을 펼쳐 보면 — 두 번의 도구 호출이 잡힌다 [user] What is the current time in HH:MM format? Also... [assistant] TextBlock + ToolUseBlock(date_format="%H:%M") [user] ToolResultBlock(content="13:18") [assistant] ToolUseBlock(date_format="%S") # 이번엔 텍스트 없음 [user] ToolResultBlock(content="50") [assistant] "The current time is 13:18 (HH:MM) and 50 seconds."
run_conversation은 stop_reason != "tool_use"가 될 때까지 도는 while 루프.run_tools는 한 메시지의 모든 tool_use 블록을 순회해 각각 실행한다.tool_result 블록 — tool_use_id는 원래 id와 같고, content는 json.dumps로 인코딩.try/except로 에러를 is_error=True로 돌려보내고, run_tool로 이름→함수를 라우팅한다.Q1while 루프를 언제 빠져나오나요?
stop_reason이 tool_use가 아니면 Claude가 도구를 더 요청하지 않은 것 — 최종 응답 신호이므로 break합니다.
Q2run_tools가 한 메시지의 tool_use 블록을 모두 순회해야 하는 이유는?
예를 들어 10+10과 30+30을 각각 다른 tool_use 블록으로 보낼 수 있으므로, 여러 개를 가정하고 순회합니다.
Q3도구 실행 중 에러가 나면 어떻게 처리하나요?
에러 정보를 돌려보내면 Claude가 무엇이 잘못됐는지 이해하고 더 나은 인자로 도구를 다시 시도할 수 있습니다.
루프와 한 도구는 완성됐습니다. 이제 도구를 여러 개 등록하고 이름으로 라우팅해 봅니다. → 여러 도구 사용하기
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.