이 튜토리얼은 다섯 개의 동심원 형태로 캘린더 관리 에이전트를 구축합니다. 각 링은 이전 링에 정확히 하나의 개념을 추가하는 완전하고 실행 가능한 프로그램입니다. 튜토리얼이 끝나면 에이전트 루프를 직접 작성하고, 이를 Tool Runner SDK 추상화로 대체하게 됩니다.
예제 도구는 create_calendar_event입니다. 이 스키마는 중첩 객체, 배열, 선택적 필드를 사용하므로 단순한 단일 문자열이 아닌 실제적인 입력 형태를 Claude가 어떻게 처리하는지 확인할 수 있습니다.
모든 링은 독립적으로 실행됩니다. 어떤 링이든 새 파일에 복사하면 이전 링의 코드 없이도 실행됩니다.
가장 작은 도구 사용 프로그램: 하나의 도구, 하나의 사용자 메시지, 하나의 도구 호출, 하나의 결과. 코드에는 각 줄을 도구 사용 라이프사이클에 매핑할 수 있도록 상세한 주석이 달려 있습니다.
요청은 사용자 메시지와 함께 tools 배열을 전송합니다. Claude가 도구를 호출하기로 결정하면, 응답은 stop_reason: "tool_use"와 도구 이름, 고유한 id, 구조화된 input을 포함하는 tool_use 콘텐츠 블록과 함께 반환됩니다. 코드가 도구를 실행한 후, 호출의 id와 일치하는 tool_use_id를 가진 tool_result 블록으로 결과를 다시 전송합니다.
# Ring 1: Single tool, single turn.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
import json
import anthropic
# Create a client. It reads ANTHROPIC_API_KEY from the environment.
client = anthropic.Anthropic()
# Define one tool. The input_schema is a JSON Schema object describing
# the arguments Claude should pass when it calls this tool. This schema
# includes nested objects (recurrence), arrays (attendees), and optional
# fields, which is closer to real-world tools than a flat string argument.
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"],
},
}
]
# Send the user's request along with the tool definition. Claude decides
# whether to call the tool based on the request and the tool description.
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] next Monday at 10am.",
}
],
)
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
print(f"stop_reason: {response.stop_reason}")
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so scan the content array rather than assuming position.
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}")
# Execute the tool. In a real system this would call your calendar API.
# Here the result is hardcoded to keep the example self-contained.
result = {"event_id": "evt_123", "status": "created"}
# Send the result back. The tool_result block goes in a user message and
# its tool_use_id must match the id from the tool_use block above. The
# assistant's previous response is included so Claude has the full history.
followup = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] next Monday at 10am.",
},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
],
)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "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)예상 출력
stop_reason: tool_use
Tool: create_calendar_event
Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['[email protected]', '[email protected]']}
stop_reason: end_turn
I've scheduled your 30-minute sync with Alice and Bob for next Monday at 10am.첫 번째 stop_reason은 Claude가 캘린더 결과를 기다리고 있기 때문에 tool_use입니다. 결과를 전송한 후, 두 번째 stop_reason은 end_turn이며 콘텐츠는 사용자를 위한 자연어입니다.
링 1은 Claude가 도구를 정확히 한 번 호출한다고 가정했습니다. 실제 작업에서는 여러 번의 호출이 필요한 경우가 많습니다: Claude가 이벤트를 생성하고, 확인 내용을 읽은 후, 또 다른 이벤트를 생성할 수 있습니다. 해결책은 stop_reason이 더 이상 "tool_use"가 아닐 때까지 도구를 계속 실행하고 결과를 피드백하는 while 루프입니다.
또 다른 변경 사항은 대화 기록입니다. 각 요청마다 messages 배열을 처음부터 다시 구성하는 대신, 실행 중인 목록을 유지하고 여기에 추가합니다. 모든 턴은 이전의 완전한 컨텍스트를 볼 수 있습니다.
# Ring 2: The agentic loop.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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}"}
# Keep the full conversation history in a list so each turn sees prior context.
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: [email protected], [email protected], [email protected].",
}
]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
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-4-6",
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)예상 출력
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.루프는 Claude가 작업을 어떻게 분해하느냐에 따라 한 번 또는 여러 번 실행될 수 있습니다. 코드는 더 이상 미리 알 필요가 없습니다.
에이전트는 하나의 기능만 가지는 경우가 드뭅니다. 두 번째 도구인 list_calendar_events를 추가하여 Claude가 새로운 항목을 생성하기 전에 기존 일정을 확인할 수 있도록 합니다.
Claude가 독립적인 여러 도구 호출을 수행해야 할 때, 단일 응답에서 여러 tool_use 블록을 반환할 수 있습니다. 루프는 모든 블록을 처리하고 모든 결과를 하나의 사용자 메시지로 함께 전송해야 합니다. response.content의 첫 번째 블록만이 아닌 모든 tool_use 블록을 반복 처리하세요.
# Ring 3: Multiple tools, parallel calls.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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 next Monday, then schedule a planning session that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
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-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)예상 출력
I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.동시 실행 및 순서 보장에 대한 자세한 내용은 병렬 도구 사용을 참조하세요.
도구는 실패합니다. 캘린더 API가 참석자가 너무 많은 이벤트를 거부하거나 날짜 형식이 잘못될 수 있습니다. 도구에서 오류가 발생하면 충돌 대신 is_error: true와 함께 오류 메시지를 다시 전송하세요. Claude는 오류를 읽고 수정된 입력으로 재시도하거나, 사용자에게 명확한 설명을 요청하거나, 제한 사항을 설명할 수 있습니다.
# Ring 4: Error handling.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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 an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-4-6",
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:
# Signal failure so Claude can retry or ask for clarification.
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-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)예상 출력
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는 플래그와 오류 텍스트를 보고 그에 따라 응답합니다. 전체 오류 처리 참조는 도구 호출 처리를 참조하세요.
링 2부터 4까지는 동일한 루프를 직접 작성했습니다: API 호출, stop_reason 확인, 도구 실행, 결과 추가, 반복. Tool Runner가 이 작업을 대신 수행합니다. 각 도구를 함수로 정의하고, 목록을 tool_runner에 전달하면 루프가 완료된 후 최종 메시지를 가져올 수 있습니다. 오류 래핑, 결과 형식 지정, 대화 관리는 내부적으로 처리됩니다.
Python SDK는 @beta_tool 데코레이터를 사용하여 타입 힌트와 독스트링에서 스키마를 추론합니다. TypeScript SDK는 Zod 스키마와 함께 betaZodTool을 사용합니다.
Tool Runner는 Python, TypeScript, Ruby SDK에서 사용할 수 있습니다. Shell 및 CLI 탭에는 코드 대신 참고 사항이 표시됩니다. 셸 기반 스크립트에는 링 4 루프를 유지하세요.
# Ring 5: The Tool Runner SDK abstraction.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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-4-6",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)예상 출력
I checked your calendar for next Monday 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 신호.
Was this page helpful?