byteforce

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분
1

run_conversation = Claude가 도구를 그만 요청할 때까지 도는 while 루프

2

stop_reason != "tool_use"이면 break — 최종 응답 신호

3

run_tools는 한 메시지의 모든 tool_use 블록을 순회

4

각 결과를 tool_result 블록(tool_use_id·content·is_error)으로

5

try/except로 에러를 잡아 is_error=True로 돌려보낸다

6

run_tool 헬퍼로 이름→함수 라우팅 — 도구 추가가 쉬워진다

먼저 짚고 갈 용어
stop_reason
Claude가 생성을 멈춘 이유. 값이 tool_use이면 도구를 호출하려는 신호.
run_conversation
메시지 목록을 받아 stop_reason이 tool_use인 동안 반복하는 while 루프 함수.
run_tools
어시스턴트 메시지의 tool_use 블록을 모두 골라 각각 실행하고 tool_result 리스트를 반환.
run_tool
도구 이름과 input을 받아 알맞은 함수로 라우팅하는 헬퍼. 도구가 늘면 if만 추가.
tool_result 블록
도구 실행 결과를 Claude에 돌려보내는 블록. tool_use_id는 원래 tool_use의 id와 같아야 한다.

멈출 때를 아는 법: stop_reason

When to stop the loop

루프의 목표는 하나입니다 — Claude가 도구를 그만 요청할 때까지 계속 호출하는 것. 그 시점을 어떻게 알까요? 응답의 stop_reason 필드를 보면 됩니다. 값이 tool_use이면 Claude가 도구를 호출하려는 분명한 신호입니다.

client 직접 호출 · stop_reason 확인
# chat 함수 대신 client로 직접 호출해 보면…
message = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    tools=[get_current_datetime_schema],
)

message.stop_reason
# => "tool_use"  ← Claude가 도구를 호출하려는 신호
왜 stop_reason?

메시지 content를 뒤져 tool_use 블록을 찾아도 되지만, stop_reason 하나만 보면 훨씬 간편합니다. 다른 값들도 있지만 도구 사용 루프에서 가장 자주 확인할 값은 tool_use입니다.

여러 tool_use 블록 실행: run_tools

Run every tool_use block

한 어시스턴트 메시지에는 tool_use 블록이 여러 개 있을 수 있습니다(예: 10+10과 30+30을 각각). 그래서 run_tools는 content에서 tool_use 블록만 골라 순회하며 각각 실행하고, 결과를 tool_result 블록으로 만들어 리스트로 돌려줍니다.

run_tool · 이름 → 함수 라우팅
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가 에러를 이해하고 더 나은 인자로 다시 시도할 수 있습니다.

run_tools · 블록 순회 + tool_result 조립
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_use_id 주의

tool_resulttool_use_id는 그 실행을 유발한 tool_use 블록의 id정확히 같아야 합니다. 이름은 다르지만(id vs tool_use_id) 값으로 짝을 맞춥니다.

전체 루프: run_conversation

The full while loop

이제 조각을 합칩니다. run_conversation은 Claude를 호출하고, 어시스턴트 메시지를 기록에 추가하고, stop_reason으로 종료를 판단하고, 도구를 요청하면 run_tools로 실행해 결과를 user 메시지로 다시 넣고 — 루프 위로 돌아갑니다.

while 루프 실행 시뮬레이터 · 한 턴씩 진행
반복 0 · stop_reason =
while True: response = chat(...) → 대기 중
stop_reason ≠ tool_usebreak. 루프를 빠져나와 최종 메시지 목록을 반환합니다. Claude가 도구를 더 요청하지 않았으니, 마지막 어시스턴트 메시지가 사용자에게 보낼 최종 응답입니다.

루프는 매 반복마다 ① Claude 호출 → ② stop_reason 확인 → tool_use면 ③ 도구 실행 후 결과를 user 메시지로 추가 → 다시 ①. tool_use가 아닐 때 비로소 멈춥니다.

run_conversation · while 루프
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

테스트로 두 번의 도구 호출이 필요한 질문을 던집니다.

테스트 · HH:MM + SS 두 형식 질문
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."

정리 & 점검

Recap & check
핵심 정리
  • run_conversationstop_reason != "tool_use"가 될 때까지 도는 while 루프.
  • run_tools는 한 메시지의 모든 tool_use 블록을 순회해 각각 실행한다.
  • 결과는 tool_result 블록 — tool_use_id는 원래 id와 같고, contentjson.dumps로 인코딩.
  • try/except로 에러를 is_error=True로 돌려보내고, run_tool로 이름→함수를 라우팅한다.

Q1while 루프를 언제 빠져나오나요?

Q2run_tools가 한 메시지의 tool_use 블록을 모두 순회해야 하는 이유는?

Q3도구 실행 중 에러가 나면 어떻게 처리하나요?

MEMBER SESSION REQUIRED · REGISTRATION IS FREE

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

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

등록하고 이어서 읽기

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