콘텐츠로 이동

도구 러너 (Tool Runner)

SDK의 도구 러너를 쓰면 에이전트 루프, 오류 래핑, 타입 안전성을 대신 처리해 줘요. 직접 처리할 필요가 없죠. 다만 사람의 승인(human-in-the-loop), 사용자 정의 로깅, 조건부 실행이 필요한 경우에는 대신 수동 루프를 써야 해요.

도구 호출, 도구 결과, 대화 관리를 수동으로 처리하는 대신 도구 러너가 자동으로 이런 일을 해줍니다:

  • Claude가 도구를 호출하면 그 도구를 실행해요.
  • 요청/응답 사이클을 처리하죠.
  • 대화 상태를 관리합니다.
  • 타입 안전성과 유효성 검사를 제공해요.

도구 러너는 베타 단계이며 Python SDK, TypeScript SDK, C# SDK, Go SDK, Java SDK, PHP SDK, Ruby SDK에서 사용할 수 있어요.

기본 사용법

SDK 헬퍼로 도구를 정의한 다음, 도구 러너로 실행하는 구조예요.

SDK의 도구 시그니처에 따라 도구는 결과를 문자열 또는 콘텐츠 블록(텍스트·이미지·문서 블록)으로 반환하므로, 도구는 멀티모달 결과를 돌려줄 수 있어요. 반환된 문자열은 단일 텍스트 콘텐츠 블록이 됩니다. JSON 객체나 숫자 같은 구조화된 데이터를 반환하려면 먼저 문자열로 인코딩하세요.

Python 예시로 직접 보죠.

import json
from anthropic import Anthropic, beta_tool

client = Anthropic()


@beta_tool
def get_weather(location: str, unit: str = "fahrenheit") -> str:
    """Get the current weather in a given location.

    Args:
        location: The city and state, e.g. San Francisco, CA
        unit: Temperature unit, either 'celsius' or 'fahrenheit'
    """
    return json.dumps({"temperature": "20°C", "condition": "Sunny"})


@beta_tool
def calculate_sum(a: int, b: int) -> str:
    """Add two numbers together.

    Args:
        a: First number
        b: Second number
    """
    return str(a + b)


runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[get_weather, calculate_sum],
    messages=[
        {
            "role": "user",
            "content": "What's the weather like in Paris? Also, what's 15 + 27?",
        }
    ],
)
for message in runner:
    print(message)

여기서 @beta_tool 데코레이터가 함수 인수와 독스트링을 검사해서 JSON 스키마를 자동으로 도출해요. 타입 힌트와 독스트링만 잘 적어두면 스키마를 손으로 쓰지 않아도 되는 거죠.

비동기 클라이언트를 쓰는 경우엔 @beta_tool@beta_async_tool로 바꾸고 함수를 async def로 정의하면 돼요.

도구 러너 반복하기

도구 러너는 Claude의 메시지를 산출(yield)하는 이터러블이에요. 각 반복에서 러너는 Claude가 도구 사용을 요청했는지 확인하고, 요청했다면 도구를 실행해 결과를 자동으로 Claude에 다시 보낸 다음 루프를 계속할 수 있도록 Claude의 다음 메시지를 산출해요.

break 문으로 어느 반복에서든 루프를 끝낼 수 있고요. 러너는 Claude가 도구 사용이 없는 메시지를 반환할 때까지, 또는 max_iterations를 설정했다면 그 값에 도달할 때까지 반복합니다.

중간 메시지가 필요 없다면 최종 메시지를 바로 가져올 수도 있어요. Python에서는 runner.until_done()을 쓰면 됩니다.

client = anthropic.Anthropic()
# ...
runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[get_weather, calculate_sum],
    messages=[
        {
            "role": "user",
            "content": "What's the weather like in Paris? Also, what's 15 + 27?",
        }
    ],
)
final_message = runner.until_done()
for block in final_message.content:
    if block.type == "text":
        print(block.text)

고급 사용법

루프 안에서 각 응답 메시지를 읽고, 다음 API 호출 전에 러너의 상태를 수정할 수 있어요. 각 반복은 다음 수명 주기를 따릅니다:

  1. 러너가 현재 상태로 Messages API에 요청을 보내요.
  2. 러너가 응답 메시지를 루프 본문에 산출합니다.
  3. 루프 본문이 실행됩니다. 메시지를 읽고, 선택적으로 러너의 상태를 수정할 수 있죠.
  4. 루프 본문이 반환되면 러너는 메시지 기록을 수정했는지 확인해요.
  5. 메시지 기록을 수정하지 않은 경우: 메시지에 도구 호출이 포함되어 있으면 러너가 어시스턴트 메시지와 도구 결과를 추가한 다음 계속합니다. 도구 호출이 없으면 루프가 종료돼요.
  6. 메시지 기록을 수정한 경우: 러너는 자동 추가를 건너뛰고 사용자의 상태를 변경 없이 사용합니다. 이 경우 아래 '메시지 기록 직접 관리하기'를 참고하세요.

메시지 기록 직접 관리하기

기본적으로 러너는 대화 상태를 대신 관리해요. 각 도구 호출 턴 이후 어시스턴트 메시지와 도구 결과를 자기 메시지 기록에 추가하죠. 턴을 재시도(응답을 버리고 다시 전송)하거나, 후속 메시지를 삽입하거나, 도구 결과를 직접 구성하고 싶다면 메시지 기록을 직접 관리해야 합니다.

루프 본문 안에서 러너의 메시지를 수정하면 직접 관리하는 셈이에요. 정확한 방법은 SDK마다 조금 다르니 언어별 탭을 참고하면 됩니다.

한 반복에서 직접 관리하면 러너는 그 턴의 어시스턴트 메시지나 도구 결과를 추가하지 않아요. 따라서 대화를 유효하게 유지할 책임은 사용자에게 있습니다. 어시스턴트 메시지와 도구 결과를 직접 추가하고(그 턴을 반영하려는 경우), 도구 호출이 없을 때 루프가 여전히 종료될 수 있도록 상태를 조건부로 수정하며, 루프를 제한하기 위해 max_iterations를 전달하세요. 7개 SDK 모두 max_iterations를 지원합니다.

Python에서는 generate_tool_call_response()로 도구 결과를 검사하거나 계산합니다. 루프 안에서 append_messages()를 호출하면 기록을 직접 관리하고 있다는 사실을 러너에 알리게 되므로, 추가하는 내용에 어시스턴트 메시지와 도구 결과를 꼭 포함하세요.

runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    max_iterations=10,
    tools=[get_weather],
    messages=[{"role": "user", "content": "What's the weather in San Francisco?"}],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()
    if tool_response is not None:
        # append_messages()는 상태를 수정됨으로 표시하므로 러너는 이번 반복에서
        # 자동 추가를 건너뜁니다. 어시스턴트 메시지와 tool result를 직접 추가하고
        # 필요한 후속 메시지도 함께 추가하세요.
        runner.append_messages(
            message,
            tool_response,
            {"role": "user", "content": "Please be concise."},
        )
    # 도구 호출이 없으면 상태를 그대로 두어 루프가 종료되도록 합니다.

메시지 기록을 직접 관리하지 않으면서 max_tokens 같은 요청 매개변수만 바꾸고 싶다면 set_messages_params()를 쓰면 돼요. 이 경우 러너는 여전히 어시스턴트 메시지와 도구 결과를 자동으로 추가합니다.

for message in runner:
    runner.set_messages_params(lambda params: {**params, "max_tokens": 2048})

자동 컨텍스트 관리

장기 실행 에이전트의 경우, TypeScript와 Ruby 도구 러너는 자동 컴팩션을 지원해요. 토큰 사용량이 임계값을 초과하면 요약을 생성해서 대화가 "context window"(컨텍스트 윈도우) 한도를 넘어 계속될 수 있게 하는 거죠. 다만 두 SDK 모두 이 클라이언트 측 옵션을 지원 중단하고 서버 측 컴팩션을 권장합니다. 서버 측 컴팩션은 context_management 요청 매개변수를 통해 모든 SDK의 도구 러너에서 동작해요. Python SDK(v1.0 이상)와 Go, Java, C#, PHP 도구 러너에는 클라이언트 측 컴팩션이 포함되어 있지 않습니다.

도구 실행 디버깅

도구가 예외를 던지면 도구 러너가 이를 잡아 is_error: true인 도구 결과로 Claude에 오류를 반환해요. 도구 결과에는 전체 스택 트레이스가 아니라 예외의 메시지(Python에서는 타입과 메시지)가 담깁니다.

SDK가 로깅하는 내용은 언어별로 달라요. Python SDK는 도구가 처리되지 않은 예외를 발생시킬 때마다 표준 logging 모듈을 통해 스택 트레이스를 포함한 전체 예외를 로깅합니다. 그리고 Python, TypeScript, Java SDK는 ANTHROPIC_LOG 환경 변수를 읽어서 요청·응답 세부 정보를 포함한 SDK 로깅을 활성화할 수 있어요:

# info 수준으로 로그 기록
export ANTHROPIC_LOG=info

# 더 자세한 출력을 위해 debug 수준으로 로그 기록
export ANTHROPIC_LOG=debug

Go, Ruby, C#, PHP SDK는 ANTHROPIC_LOG를 읽지 않아요. Python 외에는 어떤 SDK도 실패한 도구를 로깅하지 않으므로, 도구가 실패한 이유를 확인하려면 반환하거나 다시 던지기 전에 도구 함수 내부에서 예외를 잡아 직접 로깅해야 합니다.

도구 오류 가로채기

기본적으로 도구 오류는 Claude에 다시 전달되고, Claude는 이에 적절히 응답할 수 있어요. 하지만 오류를 감지해서 다르게 처리하고 싶을 수 있습니다. 예를 들어 실행을 조기에 중단하거나 사용자 정의 오류 처리를 구현하는 경우죠.

Python과 TypeScript SDK에서는 도구 응답 메서드(Python의 generate_tool_call_response(), TypeScript의 generateToolResponse())를 사용해서 도구 결과를 가로채고 Claude에 전송되기 전에 오류를 확인할 수 있어요. 다른 SDK는 해당 훅을 노출하지 않으니 각 탭에서 가장 가까운 대안을 설명합니다.

client = anthropic.Anthropic()
# ...
runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[my_tool],
    messages=[{"role": "user", "content": "Run my_tool with the query 'hello'."}],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()

    if tool_response is not None:
        # tool_response는 dict입니다: {"role": "user", "content": [...]}
        # 도구 결과에 오류가 있는지 확인합니다
        for block in tool_response["content"]:
            if block.get("is_error"):
                # 옵션 1: 예외를 발생시켜 루프를 중단합니다
                raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")

                # 옵션 2: 로그를 남기고 계속 진행합니다 (Claude가 처리하도록 둡니다)
                # logger.error(f"Tool error: {json.dumps(block['content'])}")

    # 메시지를 정상적으로 처리합니다
    print(message.content)

오류를 만났을 때는 두 가지 선택지가 있어요. 옵션 1은 예외를 발생시켜 루프를 아예 중단하는 것이고, 옵션 2는 로그만 남기고 계속 진행해서 Claude가 처리하도록 두는 겁니다.

C# 도구 러너는 도구 결과를 Claude에 보내기 전에 검사할 수 있는 훅을 노출하지 않아요. 오류 내용을 제어하려면 도구 본문 안에서 BetaToolError를 던지면 됩니다. 러너가 이를 is_error: truetool_result와 사용자가 제공한 내용으로 변환해요. Go 러너도 마찬가지로 바깥 tool_result 블록을 수정하는 훅이 없으니, C#은 pushMessages() 수동 패턴을, 오류 내용을 바꾸려면 "도구 결과 수정하기"의 수동 패턴을 참고하세요.

도구 결과 수정하기

도구 결과가 Claude에 다시 전송되기 전에 수정할 수 있어요. 도구 결과에 프롬프트 캐싱을 활성화하기 위해 cache_control 같은 메타데이터를 추가하거나, 도구 출력을 변환하는 데 유용하죠.

Python과 TypeScript SDK에서는 도구 응답 메서드로 도구 결과를 가져온 다음, 러너가 진행하기 전에 수정합니다. 수정된 결과를 명시적으로 추가할지 제자리에서 변경할지는 SDK에 따라 다르니 각 탭의 코드 주석을 참고하세요.

client = anthropic.Anthropic()
# ...
runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[search_documents],
    messages=[
        {
            "role": "user",
            "content": "Search for information about the climate of San Francisco",
        }
    ],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()

    if tool_response is not None:
        # tool_response는 dict입니다: {"role": "user", "content": [...]}
        # 도구 결과를 수정하여 cache control을 추가합니다
        for block in tool_response["content"]:
            if block["type"] == "tool_result":
                # 이 도구 결과를 캐시하기 위해 cache_control을 추가합니다
                block["cache_control"] = {"type": "ephemeral"}

        # 수정된 응답을 추가합니다 (원본의 자동 추가를 방지합니다)
        runner.append_messages(message, tool_response)

    print(message.content)

도구 결과에 cache_control을 추가하는 건 도구가 후속 API 호출을 위해 캐시하고 싶은 대량의 데이터(예: 문서 검색 결과)를 반환할 때 특히 유용해요. 캐싱 전략에 대한 자세한 내용은 프롬프트 캐싱 문서를 참고하세요. Go 러너는 바깥 tool_result 블록에 훅이 없지만, 핸들러가 반환하는 안쪽 콘텐츠 블록에는 cache_control을 설정할 수 있습니다.

스트리밍

"Streaming"(스트리밍)을 활성화하면 각 턴의 응답을 점진적으로 처리할 수 있어요. 각 반복은 이벤트를 반복할 수 있는 스트림 객체를 산출합니다.

client = anthropic.Anthropic()
# ...
runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[calculate_sum],
    messages=[{"role": "user", "content": "What is 15 + 27?"}],
    stream=True,
)

# 스트리밍 시 runner는 BetaMessageStream을 반환합니다
for message_stream in runner:
    for event in message_stream:
        print("event:", event)
    print("message:", message_stream.get_final_message())

print(runner.until_done())

stream=True를 설정하고 get_final_message()를 사용하면 누적된 메시지를 가져올 수 있어요.

다음 단계

도구 사용을 더 깊게 다루고 싶다면 이런 문서를 이어서 보면 좋아요.

  • 엄격한 도구 사용 — 문법 제약 샘플링으로 Claude의 도구 입력에 JSON Schema 준수를 강제하기.
  • 도구 호출 처리하기tool_use 블록을 파싱하고 tool_result 응답을 포맷하며 is_error로 오류 처리하기.
  • 병렬 도구 사용 — 메시지 기록 가이드 및 문제 해결과 함께 병렬 도구 호출 활성화·포맷·비활성화하기.
  • 도구 정의하기 — 도구 스키마를 지정하고, 효과적인 설명을 작성하며, Claude가 도구를 호출하는 시점 제어하기.