byteforce

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) chattools 목록을 받고, 텍스트가 아니라 전체 메시지를 반환하게 한다. 3) 메시지에서 텍스트만 뽑는 text_from_message 헬퍼를 추가한다. 4) 한 대화 안에서 여러 도구 호출을 지원한다.

1단계 — 헬퍼를 업그레이드합니다. 도구를 쓰면 Claude에게서 블록이 여러 개 든 응답이 옵니다. 그런데 지금 두 헬퍼는 늘 평범한 텍스트 블록 하나만 받는다고 가정하고 그 텍스트를 곧장 content에 넣습니다. 더 유연하게 바꿉니다 — 맨 위에서 from anthropic.types import Message를 하고, 두 번째 인자 이름을 text에서 message로 바꾼 뒤, contentmessage.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분
1

사용자 질문은 도구를 여러 개 연달아 부르게 만들 수 있다(예: “103일 뒤?”)

2

응답마다 도구를 쓰려는지 확인 — 아니면 최종 답, 맞으면 실행 후 재호출

3

run_conversation 의사 코드 — tool_use인 동안 while 루프 반복

4

헬퍼 리팩터 ① add_*_message여러 블록(문자열·리스트·Message)을 처리

5

헬퍼 리팩터 ② chattools를 받고 전체 메시지를 반환

6

헬퍼 리팩터 ③ text_from_message로 텍스트 블록만 모아 뽑기

먼저 짚고 갈 용어
멀티턴(도구) 대화
한 질문에 Claude가 도구를 여러 번 연달아 쓰며 주고받는 대화. 도구를 더 안 쓸 때까지 반복된다.
stop_reason
응답이 끝난 이유. "tool_use"면 도구를 더 쓰겠다는 뜻, "end_turn"이면 최종 답.
run_conversation
메시지 리스트를 받아, 도구를 더 안 부를 때까지 while 루프로 Claude를 반복 호출하는 함수(다음 레슨에서 구현).
text_from_message
메시지의 텍스트 블록만 골라 이어 붙여 돌려주는 헬퍼.

한 질문, 여러 도구

One question, many tools

도구 하나를 연결하는 건 끝냈습니다. 하지만 사용자의 질문은 도구를 연달아 여러 번 부르게 만들 수 있습니다. “오늘부터 103일 뒤는 무슨 요일이야?” — 이 한 질문도 get_current_datetimeadd_duration_to_datetime을 차례로 쓰게 합니다. 아래에서 한 단계씩 따라가 보세요.

도구 루프 스테퍼 · stop_reason=="tool_use"인 동안 반복
사용자: “오늘부터 103일 뒤는 무슨 요일이야?” — 이 한 질문이 도구를 두 번 부르게 만듭니다.
사용자
→ Claude
text
오늘부터 103일 뒤는 무슨 요일이야?
Claude
응답 ①
tool_use
get_current_datetime 를 불러 줘
stop_reason =tool_use
도구를 쓰려 함 → 실행하고 루프 계속
우리 서버
→ Claude
tool_result
get_current_datetime 결과: 오늘은 2026-06-23
Claude
응답 ②
tool_use
아직 정보 부족 → add_duration_to_datetime 를 불러 줘 (+103일)
stop_reason =tool_use
또 도구를 쓰려 함 → 실행하고 루프 계속
우리 서버
→ Claude
tool_result
add_duration_to_datetime 결과: 2026-10-04
Claude
응답 ③
text
103일 뒤는 2026년 10월 4일, 일요일입니다.
stop_reason =end_turn
0 / 6

규칙은 단순합니다 — 응답의 stop_reasontool_use면 도구를 실행하고 다시 호출, end_turn이면 루프를 끝내고 최종 답을 사용자에게 전달합니다.

핵심 패턴 — tool_use인 동안 반복

Loop while tool_use

실제 사용자가 무엇을 물을지 미리 알 수 없으므로, Claude가 도구를 여러 번 쓰고 싶어 할 수 있다고 가정해야 합니다. 규칙은 단순합니다 — 응답의 stop_reasontool_use면 실행 후 다시 호출, 아니면 최종 답입니다.

의사 코드 · run_conversation
# 의사 코드 — 도구를 더 안 부를 때까지 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)
왜 while 루프인가

도구 호출이 몇 번 필요할지 미리 알 수 없기 때문입니다. 한 번일 수도, 세 번일 수도 있죠. 그래서 횟수를 고정하지 않고 “도구를 더 안 부를 때까지” 반복합니다. 이 함수의 실제 구현은 다음 레슨에서 합니다.

리팩터 ① 메시지 헬퍼 유연하게

Flexible message helpers

도구를 쓰면 응답에 블록이 여러 개 옵니다. 그런데 지금 헬퍼들은 늘 텍스트 하나만 받는다고 가정합니다. Message를 import하고, 인자를 message로 바꿔 — 문자열·블록 리스트·전체 Message를 모두 받아 분해하게 만듭니다.

리팩터 전 · add_user / add_assistant
# 리팩터 전 — 늘 텍스트 블록 하나만 받는다고 가정
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 · text_from_message

chat & text_from_message

chattools를 받게 하고, 첫 블록 텍스트가 아니라 전체 메시지를 반환하게 합니다. 응답 블록이 여러 개일 수 있으니, 늘 텍스트 하나라고 가정하던 코드를 버리는 거죠.

리팩터 후 · 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 헬퍼로 대체합니다 — 텍스트 블록만 골라 줄바꿈으로 이어 붙입니다.

리팩터 ③ · text_from_message
def text_from_message(message):
    return "\n".join(
        [block.text for block in message.content if block.type == "text"]
    )
멀티턴을 위한 리팩터링 · 네 가지 준비
1

add_user_message·add_assistant_message 가 여러 메시지 블록을 처리하게 한다 — 문자열·블록 리스트·전체 Message 모두.

2

chattools 목록을 받게 하고, 텍스트가 아니라 전체 메시지를 반환하게 한다.

3

text_from_message 함수를 추가해 메시지의 텍스트 블록에서 텍스트만 모아 뽑는다.

4

한 대화 안에서 여러 도구 호출을 지원한다 — 도구를 더 안 부를 때까지 반복(다음 레슨).

핵심 정리
  • 사용자 질문은 도구를 연달아 여러 번 부르게 만들 수 있다.
  • 규칙: stop_reason=="tool_use"면 실행 후 재호출, 아니면 최종 답.
  • add_*_message를 유연하게 — 문자열·블록 리스트·전체 Message 처리.
  • chattools를 받고 전체 메시지 반환, text_from_message로 텍스트만 추출.

Q1한 대화에서 도구를 언제까지 반복 호출하나요?

Q2헬퍼를 리팩터한 핵심 이유는?

Q3리팩터 후 chat은 무엇을 반환하나요?

MEMBER SESSION REQUIRED · REGISTRATION IS FREE

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

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

등록하고 이어서 읽기

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