Claude Platform Docs
Messages도구

튜토리얼: 도구를 사용하는 에이전트 구축하기

단일 도구 호출에서 프로덕션 수준의 에이전트 루프까지 단계별로 안내하는 가이드입니다.

이 튜토리얼에서는 다섯 개의 동심원 링(ring)으로 캘린더 관리 에이전트를 구축합니다. 각 링은 이전 링에 정확히 하나의 개념을 추가하는 완전하고 실행 가능한 프로그램입니다. 마지막에는 "agentic loop"(에이전트 루프)를 직접 손으로 작성한 다음, 이를 Tool Runner SDK 추상화로 교체하게 됩니다.

예제 도구는 create_calendar_event입니다. 이 도구의 스키마는 중첩 객체, 배열, 선택적 필드를 사용하므로, 단일 평면 문자열이 아닌 현실적인 입력 형태를 Claude가 어떻게 처리하는지 확인할 수 있습니다.

링 1: 단일 도구, 단일 턴

가능한 가장 작은 도구 사용 프로그램입니다. 도구 하나, 사용자 메시지 하나, 도구 호출 하나, 결과 하나로 구성됩니다. 코드에는 주석이 풍부하게 달려 있어 각 줄을 도구 사용 수명 주기에 대응시킬 수 있습니다.

요청은 사용자 메시지와 함께 tools 배열을 전송합니다. Claude가 도구 호출이 필요하다고 판단하면, 응답은 stop_reason: "tool_use"와 함께 도구 이름, 고유한 id, 구조화된 input을 담은 tool_use 콘텐츠 블록을 반환합니다. 여러분의 코드는 도구를 실행한 다음, 호출의 id와 일치하는 tool_use_id를 가진 tool_result 블록에 결과를 담아 다시 보냅니다.

# 링 1: 단일 도구, 단일 턴.

import json

import anthropic

# 클라이언트를 생성합니다. 환경에서 ANTHROPIC_API_KEY를 읽어옵니다.
client = anthropic.Anthropic()

# 도구 하나를 정의합니다. input_schema는 Claude가 이 도구를 호출할 때
# 전달해야 하는 인수를 설명하는 JSON Schema 객체입니다. 이 스키마는
# 중첩 객체(recurrence), 배열(attendees), 선택적 필드를 포함하므로
# 단순한 문자열 인수보다 실제 도구에 더 가깝습니다.
tools = [
    {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "start": {"type": "string", "format": "date-time"},
                "end": {"type": "string", "format": "date-time"},
                "attendees": {
                    "type": "array",
                    "items": {"type": "string", "format": "email"},
                },
                "recurrence": {
                    "type": "object",
                    "properties": {
                        "frequency": {"enum": ["daily", "weekly", "monthly"]},
                        "count": {"type": "integer", "minimum": 1},
                    },
                },
            },
            "required": ["title", "start", "end"],
        },
    }
]

# 사용자의 요청을 도구 정의와 함께 전송합니다. Claude는 요청과
# 도구 설명을 바탕으로 도구 호출 여부를 결정합니다.
response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "auto", "disable_parallel_tool_use": True},
    messages=[
        {
            "role": "user",
            "content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",
        }
    ],
)

# Claude가 도구를 호출하면 응답의 stop_reason은 "tool_use"이고
# content 배열에는 텍스트와 함께 tool_use 블록이 포함됩니다.
print(f"stop_reason: {response.stop_reason}")

# tool_use 블록을 찾습니다. 응답에는 tool_use 블록 앞에 텍스트 블록이
# 있을 수 있으므로 위치를 가정하지 말고 content 배열을 순회하세요.
tool_use = next(block for block in response.content if block.type == "tool_use")
print(f"Tool: {tool_use.name}")
print(f"Input: {tool_use.input}")

# 도구를 실행합니다. 실제 시스템에서는 캘린더 API를 호출하게 됩니다.
# 여기서는 예제를 독립적으로 유지하기 위해 결과를 하드코딩했습니다.
result = {"event_id": "evt_123", "status": "created"}

# 결과를 다시 전송합니다. tool_result 블록은 user 메시지에 들어가며
# 그 tool_use_id는 위 tool_use 블록의 id와 일치해야 합니다. Claude가
# 전체 히스토리를 갖도록 어시스턴트의 이전 응답도 포함됩니다.
followup = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "auto", "disable_parallel_tool_use": True},
    messages=[
        {
            "role": "user",
            "content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",
        },
        {"role": "assistant", "content": response.content},
        {
            "role": "user",
            "content": [
                {
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": json.dumps(result),
                }
            ],
        },
    ],
)

# 도구 결과를 받은 Claude는 최종 자연어 답변을 생성하고
# stop_reason은 "end_turn"이 됩니다.
print(f"stop_reason: {followup.stop_reason}")
final_text = next(block for block in followup.content if block.type == "text")
print(final_text.text)

예상 결과

Output
stop_reason: tool_use
Tool: create_calendar_event
Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['alice@example.com', 'bob@example.com']}
stop_reason: end_turn
I've scheduled your 30-minute sync with Alice and Bob for Monday, March 30 at 10am.

첫 번째 stop_reason은 Claude가 캘린더 결과를 기다리고 있기 때문에 tool_use입니다. 결과를 보낸 후 두 번째 stop_reason은 end_turn이며, 콘텐츠는 사용자를 위한 자연어입니다.

링 2: 에이전트 루프

링 1은 Claude가 도구를 정확히 한 번 호출한다고 가정했습니다. 실제 작업에서는 여러 번의 호출이 필요한 경우가 많습니다. Claude가 이벤트를 생성하고, 확인 내용을 읽은 다음, 또 다른 이벤트를 생성할 수도 있습니다. 해결책은 stop_reason이 더 이상 "tool_use"가 아닐 때까지 계속 도구를 실행하고 결과를 다시 전달하는 while 루프입니다.

또 다른 변경 사항은 대화 기록입니다. 매 요청마다 messages 배열을 처음부터 다시 만드는 대신, 누적 목록을 유지하고 여기에 추가합니다. 모든 턴은 이전의 전체 컨텍스트를 볼 수 있습니다.

# Ring 2: 에이전트 루프입니다.

import json

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "start": {"type": "string", "format": "date-time"},
                "end": {"type": "string", "format": "date-time"},
                "attendees": {
                    "type": "array",
                    "items": {"type": "string", "format": "email"},
                },
                "recurrence": {
                    "type": "object",
                    "properties": {
                        "frequency": {"enum": ["daily", "weekly", "monthly"]},
                        "count": {"type": "integer", "minimum": 1},
                    },
                },
            },
            "required": ["title", "start", "end"],
        },
    }
]


def run_tool(name, tool_input):
    if name == "create_calendar_event":
        return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
    return {"error": f"Unknown tool: {name}"}


# 각 턴이 이전 컨텍스트를 볼 수 있도록 전체 대화 기록을 리스트에 보관합니다.
messages = [
    {
        "role": "user",
        "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
    }
]

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "auto", "disable_parallel_tool_use": True},
    messages=messages,
)

# Claude가 더 이상 도구를 요청하지 않을 때까지 반복합니다. 각 반복에서는 요청된
# 도구를 실행하고, 결과를 기록에 추가한 뒤 Claude에게 계속하도록 요청합니다.
while response.stop_reason == "tool_use":
    tool_use = next(block for block in response.content if block.type == "tool_use")
    result = run_tool(tool_use.name, tool_use.input)

    messages.append({"role": "assistant", "content": response.content})
    messages.append(
        {
            "role": "user",
            "content": [
                {
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": json.dumps(result),
                }
            ],
        }
    )

    response = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=tools,
        tool_choice={"type": "auto", "disable_parallel_tool_use": True},
        messages=messages,
    )

final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)

예상 결과

Output
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.

루프는 Claude가 작업을 어떻게 분해하느냐에 따라 한 번 또는 여러 번 실행될 수 있습니다. 여러분의 코드는 더 이상 이를 미리 알 필요가 없습니다.

링 3: 여러 도구, 병렬 호출

에이전트가 단 하나의 기능만 갖는 경우는 드뭅니다. 두 번째 도구인 list_calendar_events를 추가하여 Claude가 새로운 것을 생성하기 전에 기존 일정을 확인할 수 있도록 합니다.

Claude가 수행할 독립적인 도구 호출이 여러 개 있을 때, 단일 응답에 여러 개의 tool_use 블록을 반환할 수 있습니다. 여러분의 루프는 이 모두를 처리하고 모든 결과를 하나의 사용자 메시지에 함께 담아 다시 보내야 합니다. 첫 번째 블록만이 아니라 response.content의 모든 tool_use 블록을 순회하세요.

# Ring 3: 여러 도구, 병렬 호출.

import json

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "start": {"type": "string", "format": "date-time"},
                "end": {"type": "string", "format": "date-time"},
                "attendees": {
                    "type": "array",
                    "items": {"type": "string", "format": "email"},
                },
                "recurrence": {
                    "type": "object",
                    "properties": {
                        "frequency": {"enum": ["daily", "weekly", "monthly"]},
                        "count": {"type": "integer", "minimum": 1},
                    },
                },
            },
            "required": ["title", "start", "end"],
        },
    },
    {
        "name": "list_calendar_events",
        "description": "List all calendar events on a given date.",
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {"type": "string", "format": "date"},
            },
            "required": ["date"],
        },
    },
]


def run_tool(name, tool_input):
    if name == "create_calendar_event":
        return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
    if name == "list_calendar_events":
        return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
    return {"error": f"Unknown tool: {name}"}


messages = [
    {
        "role": "user",
        "content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
    }
]

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

while response.stop_reason == "tool_use":
    # 하나의 응답에 여러 tool_use 블록이 포함될 수 있습니다. 모든 블록을 처리하고
    # 모든 결과를 하나의 user 메시지에 담아 함께 반환하세요.
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = run_tool(block.name, block.input)
            tool_results.append(
                {
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result),
                }
            )

    messages.append({"role": "assistant", "content": response.content})
    messages.append({"role": "user", "content": tool_results})

    response = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)

예상 결과

Output
I checked your calendar for Monday, March 30 and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.

동시 실행 및 순서 보장에 대한 자세한 내용은 병렬 도구 사용을 참조하세요.

링 4: 오류 처리

도구는 실패합니다. 캘린더 API가 참석자가 너무 많은 이벤트를 거부할 수도 있고, 날짜 형식이 잘못되었을 수도 있습니다. 도구가 오류를 발생시키면 프로그램을 중단시키는 대신 is_error: true와 함께 오류 메시지를 다시 보내세요. Claude는 오류를 읽고 수정된 입력으로 재시도하거나, 사용자에게 명확한 설명을 요청하거나, 제한 사항을 설명할 수 있습니다.

# Ring 4: 오류 처리.

import json

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "start": {"type": "string", "format": "date-time"},
                "end": {"type": "string", "format": "date-time"},
                "attendees": {
                    "type": "array",
                    "items": {"type": "string", "format": "email"},
                },
                "recurrence": {
                    "type": "object",
                    "properties": {
                        "frequency": {"enum": ["daily", "weekly", "monthly"]},
                        "count": {"type": "integer", "minimum": 1},
                    },
                },
            },
            "required": ["title", "start", "end"],
        },
    },
    {
        "name": "list_calendar_events",
        "description": "List all calendar events on a given date.",
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {"type": "string", "format": "date"},
            },
            "required": ["date"],
        },
    },
]


def run_tool(name, tool_input):
    if name == "create_calendar_event":
        if "attendees" in tool_input and len(tool_input["attendees"]) > 10:
            raise ValueError("Too many attendees (max 10)")
        return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
    if name == "list_calendar_events":
        return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
    raise ValueError(f"Unknown tool: {name}")


messages = [
    {
        "role": "user",
        "content": "Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
    }
]

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

while response.stop_reason == "tool_use":
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            try:
                result = run_tool(block.name, block.input)
                tool_results.append(
                    {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
                )
            except Exception as exc:
                # Claude가 재시도하거나 설명을 요청할 수 있도록 실패를 알립니다.
                tool_results.append(
                    {
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(exc),
                        "is_error": True,
                    }
                )

    messages.append({"role": "assistant", "content": response.content})
    messages.append({"role": "user", "content": tool_results})

    response = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)

예상 결과

Output
I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize.

is_error 플래그는 성공적인 결과와의 유일한 차이점입니다. Claude는 플래그와 오류 텍스트를 보고 그에 맞게 응답합니다. 전체 오류 처리 레퍼런스는 도구 호출 처리를 참조하세요.

링 5: Tool Runner SDK 추상화

링 2부터 링 4까지는 동일한 루프를 직접 작성했습니다. API를 호출하고, stop_reason을 확인하고, 도구를 실행하고, 결과를 추가하고, 이를 반복했습니다. Tool Runner는 이 작업을 대신 수행합니다. 각 도구를 함수로 정의하고, 목록을 client.beta.messages.tool_runner()에 전달한 다음, 루프가 완료되면 최종 메시지를 가져오세요. 오류 래핑, 결과 형식 지정, 대화 관리는 내부적으로 처리됩니다.

각 SDK는 일반 함수를 실행 가능한 도구로 변환하고 함수 시그니처에서 입력 스키마를 도출하는 헬퍼를 제공합니다. 아래 탭은 각 언어의 관용적인 형태를 보여줍니다.

# Ring 5: Tool Runner SDK 추상화입니다.

import json

import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()


@beta_tool
def create_calendar_event(
    title: str,
    start: str,
    end: str,
    attendees: list[str] | None = None,
    recurrence: dict | None = None,
) -> str:
    """Create a calendar event with attendees and optional recurrence.

    Args:
        title: Event title.
        start: Start time in ISO 8601 format.
        end: End time in ISO 8601 format.
        attendees: Email addresses to invite.
        recurrence: Dict with 'frequency' (daily, weekly, monthly) and 'count'.
    """
    if attendees and len(attendees) > 10:
        raise ValueError("Too many attendees (max 10)")
    return json.dumps({"event_id": "evt_123", "status": "created", "title": title})


@beta_tool
def list_calendar_events(date: str) -> str:
    """List all calendar events on a given date.

    Args:
        date: Date in YYYY-MM-DD format.
    """
    return json.dumps({"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]})


final_message = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=[create_calendar_event, list_calendar_events],
    messages=[
        {
            "role": "user",
            "content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
        }
    ],
).until_done()

for block in final_message.content:
    if block.type == "text":
        print(block.text)

예상 결과

Output
I checked your calendar for Monday, March 30 and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.

출력은 링 3과 동일합니다. 차이는 코드에 있습니다. 줄 수가 대략 절반이고, 수동 루프가 없으며, 스키마가 구현 바로 옆에 위치합니다.

구축한 내용

하드코딩된 단일 도구 호출에서 시작하여 여러 도구, 병렬 호출, 오류를 처리하는 프로덕션 형태의 에이전트로 마무리한 다음, 이 모든 것을 Tool Runner로 압축했습니다. 그 과정에서 도구 사용 프로토콜의 모든 요소를 살펴보았습니다. tool_use 블록, tool_result 블록, tool_use_id 매칭, stop_reason 확인, is_error 신호 전달이 그것입니다.

다음 단계

스키마 명세 및 모범 사례.

전체 SDK 추상화 레퍼런스.

일반적인 도구 사용 오류 해결.

Was this page helpful?