CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API
도구 사용
Multi-turn conversations with tools
지금까지는 도구 하나를 한 번 연결했습니다. 하지만 사용자의 질문은 도구를 여러 개 연달아 부르게 만들 수 있습니다. 핵심은 stop_reason == "tool_use"인 동안 반복하는 것 — Claude가 더는 도구를 원하지 않을 때까지 루프를 돕니다. 이 레슨에서는 그 루프를 만들 준비로 헬퍼들을 리팩터링합니다.
Stephen Grider · Anthropic 기술 스태프
지금까지 도구 하나를 Claude에 연결하는 예제를 끝냈습니다. 하지만 이 프로젝트의 목표는 도구를 여러 개 연결하는 것입니다 — 총 세 개를 쓸 겁니다. 도구 셋을 연결하면 코드 안에서 무슨 일이 벌어질지 먼저 그려 봅시다.
예를 들어 사용자가 “오늘부터 103일 뒤는 무슨 요일이야?”라고 물었다고 합시다. 이걸 답하려면 Claude는 도구 둘을 써야 합니다. 먼저 get_current_datetime으로 오늘 날짜를 알아내고, 그다음 add_duration_to_datetime으로 거기에 103일을 더해야 하죠.
뒤에서 이렇게 흘러갑니다. 처음에 Claude는 곧바로 tool_use 블록으로 답하며 get_current_datetime을 불러 달라고 합니다. 우리가 그 함수를 실행해 오늘 날짜를 알려 주면, Claude는 아직 답하기에 정보가 부족하다는 걸 깨닫습니다 — 이제 오늘 날짜에 103일을 더해야 하니까요. 그래서 또 다른 tool_use 블록으로 add_duration_to_datetime을 불러 달라고 합니다. 우리가 그것도 실행해 결과를 돌려주면, 그제야 Claude가 실제 질문에 답합니다.
이 예를 보여 드리는 이유가 있습니다. 원래 user 메시지를 실제 사람에게서 받는다면, 그 사람이 무엇을 물을지 정확히 예측할 수 없습니다. 도구 호출 여러 번이 필요한 엉뚱한 질문이 올 수도 있죠. 그래서 도구 호출을 앱에 넣을 때는 이런 상황을 허용해야 합니다 — 어떤 질문을 보내든 Claude가 도구를 연달아 여러 번 쓰고 싶어 할 수 있다고 가정해야 합니다.
Claude가 응답할 때마다 우리는 응답을 보고 Claude가 도구를 쓰려는지 확인합니다. 아니라면, 사용자에게 돌려줄 최종 응답이 나온 것입니다. 맞다면, 도구를 실행하고 결과 tool_result 블록을 만들어 user 메시지에 담은 뒤, 같은 while 루프 안에서 Claude를 다시 호출합니다. 의사 코드로는 run_conversation 같은 함수가 초기 메시지 리스트를 받아 while 루프를 돌며 이 과정을 반복합니다.
이 영상의 나머지에서는 이런 run_conversation 함수를 만들 준비로 노트북을 리팩터링합니다. 준비 작업이 네 가지 있습니다. 1) add_user_message·add_assistant_message가 여러 블록을 다룰 수 있게 한다. 2) chat이 tools 목록을 받고, 텍스트가 아니라 전체 메시지를 반환하게 한다. 3) 메시지에서 텍스트만 뽑는 text_from_message 헬퍼를 추가한다. 4) 한 대화 안에서 여러 도구 호출을 지원한다.
1단계 — 헬퍼를 업그레이드합니다. 도구를 쓰면 Claude에게서 블록이 여러 개 든 응답이 옵니다. 그런데 지금 두 헬퍼는 늘 평범한 텍스트 블록 하나만 받는다고 가정하고 그 텍스트를 곧장 content에 넣습니다. 더 유연하게 바꿉니다 — 맨 위에서 from anthropic.types import Message를 하고, 두 번째 인자 이름을 text에서 message로 바꾼 뒤, content를 message.content if isinstance(message, Message) else message로 둡니다. assistant 쪽도 똑같이요.
이렇게 하면 같은 헬퍼에 평범한 문자열도, 블록 리스트도, 전체 Message도 넣을 수 있고 알아서 분해해 줍니다. 예를 들어 응답을 통째로 add_assistant_message(messages, response)처럼 넘기거나, response.content를 넘기거나, 평범한 문자열을 넘겨도 모두 잘 동작합니다. 앞으로 tool_use 블록이 든 메시지를 다루기가 훨씬 쉬워집니다.
2단계 — chat 함수를 고칩니다. tools 인자(기본 None)를 받아 system처럼 있으면 params에 넣습니다. 그리고 이제 첫 블록의 텍스트만 꺼내 반환하지 않고 전체 메시지를 반환합니다. 응답에 블록이 여러 개일 수 있으니, 늘 텍스트 하나라고 가정하던 코드를 버리는 거죠. 텍스트를 꺼내는 게 조금 번거로워지지만 현실을 더 정확히 반영해 안전합니다.
그래서 3단계 — text_from_message 헬퍼를 추가합니다. 메시지의 모든 블록을 보고 텍스트 블록만 골라 그 텍스트를 줄바꿈으로 이어 붙여 돌려줍니다: "\n".join(block.text for block in message.content if block.type == "text"). 방금 없앤 기능을 대체하면서, 메시지에서 텍스트를 뽑기 쉽게 해 줍니다.
리팩터링은 거의 끝났습니다. 마지막 4단계 — 한 대화 안에서 여러 도구 호출을 지원하는 함수를 만듭니다. 메시지 리스트를 받아, Claude가 더는 도구를 부르지 않겠다는 신호를 줄 때까지 계속 호출하는 함수죠. 그 구현은 다음 영상에서 합니다.
이 장에서 배우는 것What you'll learn
약 9분사용자 질문은 도구를 여러 개 연달아 부르게 만들 수 있다(예: “103일 뒤?”)
응답마다 도구를 쓰려는지 확인 — 아니면 최종 답, 맞으면 실행 후 재호출
run_conversation 의사 코드 — tool_use인 동안 while 루프 반복
헬퍼 리팩터 ① add_*_message가 여러 블록(문자열·리스트·Message)을 처리
헬퍼 리팩터 ② chat이 tools를 받고 전체 메시지를 반환
헬퍼 리팩터 ③ text_from_message로 텍스트 블록만 모아 뽑기
"tool_use"면 도구를 더 쓰겠다는 뜻, "end_turn"이면 최종 답.도구 하나를 연결하는 건 끝냈습니다. 하지만 사용자의 질문은 도구를 연달아 여러 번 부르게 만들 수 있습니다. “오늘부터 103일 뒤는 무슨 요일이야?” — 이 한 질문도 get_current_datetime과 add_duration_to_datetime을 차례로 쓰게 합니다. 아래에서 한 단계씩 따라가 보세요.
get_current_datetime 를 불러 줘get_current_datetime 결과: 오늘은 2026-06-23add_duration_to_datetime 를 불러 줘 (+103일)add_duration_to_datetime 결과: 2026-10-04규칙은 단순합니다 — 응답의 stop_reason이 tool_use면 도구를 실행하고 다시 호출, end_turn이면 루프를 끝내고 최종 답을 사용자에게 전달합니다.
실제 사용자가 무엇을 물을지 미리 알 수 없으므로, Claude가 도구를 여러 번 쓰고 싶어 할 수 있다고 가정해야 합니다. 규칙은 단순합니다 — 응답의 stop_reason이 tool_use면 실행 후 다시 호출, 아니면 최종 답입니다.
# 의사 코드 — 도구를 더 안 부를 때까지 while 루프로 반복 def run_conversation(messages): while True: response = chat(messages, tools=tools) if response.stop_reason != "tool_use": return response # 도구를 안 씀 → 최종 응답 # 도구를 쓰려 함 → 실행하고 tool_result 블록을 만들어 # user 메시지로 덧붙인 뒤, 루프 안에서 다시 호출 tool_results = run_tools(response) add_assistant_message(messages, response) add_user_message(messages, tool_results)
도구 호출이 몇 번 필요할지 미리 알 수 없기 때문입니다. 한 번일 수도, 세 번일 수도 있죠. 그래서 횟수를 고정하지 않고 “도구를 더 안 부를 때까지” 반복합니다. 이 함수의 실제 구현은 다음 레슨에서 합니다.
도구를 쓰면 응답에 블록이 여러 개 옵니다. 그런데 지금 헬퍼들은 늘 텍스트 하나만 받는다고 가정합니다. Message를 import하고, 인자를 message로 바꿔 — 문자열·블록 리스트·전체 Message를 모두 받아 분해하게 만듭니다.
# 리팩터 전 — 늘 텍스트 블록 하나만 받는다고 가정 def add_user_message(messages, text): user_message = {"role": "user", "content": text} messages.append(user_message) def add_assistant_message(messages, text): assistant_message = {"role": "assistant", "content": text} messages.append(assistant_message)
from anthropic.types import Message def add_user_message(messages, message): user_message = { "role": "user", "content": message.content if isinstance(message, Message) else message, } messages.append(user_message) def add_assistant_message(messages, message): assistant_message = { "role": "assistant", "content": message.content if isinstance(message, Message) else message, } messages.append(assistant_message)
같은 헬퍼에 전체 응답을 통째로(add_assistant_message(messages, response)), 또는 response.content를, 또는 평범한 문자열을 넣어도 모두 동작합니다. tool_use 블록이 든 메시지를 다루기가 훨씬 쉬워집니다.
chat이 tools를 받게 하고, 첫 블록 텍스트가 아니라 전체 메시지를 반환하게 합니다. 응답 블록이 여러 개일 수 있으니, 늘 텍스트 하나라고 가정하던 코드를 버리는 거죠.
def chat(messages, system=None, temperature=1.0, stop_sequences=[], tools=None): params = { "model": model, "max_tokens": 1000, "messages": messages, "temperature": temperature, "stop_sequences": stop_sequences, } if tools: params["tools"] = tools if system: params["system"] = system message = client.messages.create(**params) return message # 첫 블록 텍스트가 아니라 전체 메시지를 반환
텍스트만 따로 꺼낼 일이 늘 있으니, 그 기능을 text_from_message 헬퍼로 대체합니다 — 텍스트 블록만 골라 줄바꿈으로 이어 붙입니다.
def text_from_message(message): return "\n".join( [block.text for block in message.content if block.type == "text"] )
add_user_message·add_assistant_message 가 여러 메시지 블록을 처리하게 한다 — 문자열·블록 리스트·전체 Message 모두.
chat 이 tools 목록을 받게 하고, 텍스트가 아니라 전체 메시지를 반환하게 한다.
text_from_message 함수를 추가해 메시지의 텍스트 블록에서 텍스트만 모아 뽑는다.
한 대화 안에서 여러 도구 호출을 지원한다 — 도구를 더 안 부를 때까지 반복(다음 레슨).
stop_reason=="tool_use"면 실행 후 재호출, 아니면 최종 답.add_*_message를 유연하게 — 문자열·블록 리스트·전체 Message 처리.chat은 tools를 받고 전체 메시지 반환, text_from_message로 텍스트만 추출.Q1한 대화에서 도구를 언제까지 반복 호출하나요?
도구 호출이 몇 번 필요할지 미리 알 수 없으니, 횟수를 고정하지 않고 Claude가 더는 도구를 안 부를 때까지 while 루프로 반복합니다.
Q2헬퍼를 리팩터한 핵심 이유는?
도구를 쓰면 text 블록과 tool_use 블록이 함께 옵니다. 그래서 문자열·블록 리스트·전체 Message를 모두 받아 분해하도록 바꿉니다.
Q3리팩터 후 chat은 무엇을 반환하나요?
블록이 여러 개일 수 있으니 전체 메시지를 반환하고, 텍스트가 필요하면 text_from_message로 따로 뽑습니다.
루프의 큰 그림과 헬퍼 준비를 마쳤습니다. 이제 run_conversation을 실제로 구현합니다. → 멀티턴 구현하기
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.