Tutorial: Costruisci un agente che usa strumenti
Una guida passo passo da una singola chiamata a uno strumento fino a un ciclo agentico pronto per la produzione.
Questo tutorial costruisce un agente per la gestione del calendario in cinque anelli concentrici. Ogni anello è un programma completo ed eseguibile che aggiunge esattamente un concetto all'anello precedente. Alla fine avrai scritto a mano l'"agentic loop" (ciclo agentico) e poi lo avrai sostituito con l'astrazione SDK Tool Runner.
Lo strumento di esempio è create_calendar_event. Il suo schema usa oggetti annidati, array e campi opzionali, così vedrai come Claude gestisce forme di input realistiche anziché una singola stringa piatta.
Anello 1: Singolo strumento, singolo turno
Il programma più piccolo possibile che usa strumenti: uno strumento, un messaggio utente, una chiamata allo strumento, un risultato. Il codice è ampiamente commentato così puoi associare ogni riga al ciclo di vita dell'uso degli strumenti.
La richiesta invia un array tools insieme al messaggio utente. Quando Claude determina che è necessaria una chiamata a uno strumento, la risposta ritorna con stop_reason: "tool_use" e un blocco di contenuto tool_use contenente il nome dello strumento, un id univoco e l'input strutturato. Il tuo codice esegue lo strumento, poi invia il risultato in un blocco tool_result il cui tool_use_id corrisponde all'id della chiamata.
# Anello 1: Singolo strumento, singolo turno.
import json
import anthropic
# Crea un client. Legge ANTHROPIC_API_KEY dall'ambiente.
client = anthropic.Anthropic()
# Definisci uno strumento. L'input_schema è un oggetto JSON Schema che descrive
# gli argomenti che Claude deve passare quando chiama questo strumento. Questo schema
# include oggetti annidati (recurrence), array (attendees) e campi
# opzionali, più vicino agli strumenti reali rispetto a un semplice argomento stringa.
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"],
},
}
]
# Invia la richiesta dell'utente insieme alla definizione dello strumento. Claude decide
# se chiamare lo strumento in base alla richiesta e alla descrizione dello strumento.
response = client.messages.create(
model="claude-opus-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.",
}
],
)
# Quando Claude chiama uno strumento, la risposta ha stop_reason "tool_use"
# e l'array content contiene un blocco tool_use insieme a eventuale testo.
print(f"stop_reason: {response.stop_reason}")
# Trova il blocco tool_use. Una risposta può contenere blocchi di testo prima del
# blocco tool_use, quindi scansiona l'array content invece di presumere la posizione.
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}")
# Esegui lo strumento. In un sistema reale questo chiamerebbe la tua API calendario.
# Qui il risultato è hardcoded per mantenere l'esempio autonomo.
result = {"event_id": "evt_123", "status": "created"}
# Invia il risultato. Il blocco tool_result va in un messaggio user e
# il suo tool_use_id deve corrispondere all'id del blocco tool_use sopra. La
# risposta precedente dell'assistente è inclusa così Claude ha la cronologia completa.
followup = client.messages.create(
model="claude-opus-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),
}
],
},
],
)
# Con il risultato dello strumento a disposizione, Claude produce una risposta finale
# in linguaggio naturale e stop_reason diventa "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)Cosa aspettarsi
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.Il primo stop_reason è tool_use perché Claude sta aspettando il risultato del calendario. Dopo aver inviato il risultato, il secondo stop_reason è end_turn e il contenuto è linguaggio naturale per l'utente.
Anello 2: Il ciclo agentico
L'Anello 1 presupponeva che Claude chiamasse lo strumento esattamente una volta. I compiti reali spesso richiedono diverse chiamate: Claude potrebbe creare un evento, leggere la conferma, poi crearne un altro. La soluzione è un ciclo while che continua a eseguire strumenti e a restituire i risultati finché stop_reason non è più "tool_use".
L'altra modifica è la cronologia della conversazione. Invece di ricostruire l'array messages da zero a ogni richiesta, mantieni una lista progressiva e aggiungi elementi ad essa. Ogni turno vede il contesto precedente completo.
# Anello 2: Il ciclo agentico.
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}"}
# Conserva l'intera cronologia della conversazione in una lista così ogni turno vede il contesto precedente.
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
}
]
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Itera finché Claude non smette di richiedere strumenti. Ogni iterazione esegue lo
# strumento richiesto, aggiunge il risultato alla cronologia e chiede a Claude di continuare.
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",
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)Cosa aspettarsi
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.Il ciclo potrebbe essere eseguito una o più volte a seconda di come Claude scompone il compito. Il tuo codice non ha più bisogno di saperlo in anticipo.
Anello 3: Strumenti multipli, chiamate parallele
Gli agenti raramente hanno una sola capacità. Aggiungi un secondo strumento, list_calendar_events, così Claude può controllare la pianificazione esistente prima di creare qualcosa di nuovo.
Quando Claude ha più chiamate a strumenti indipendenti da effettuare, potrebbe restituire diversi blocchi tool_use in una singola risposta. Il tuo ciclo deve elaborarli tutti e inviare tutti i risultati insieme in un unico messaggio utente. Itera su ogni blocco tool_use in response.content, non solo sul primo.
# Anello 3: Strumenti multipli, chiamate parallele.
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-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# Una singola risposta può contenere più blocchi tool_use. Elaborali tutti
# e restituisci tutti i risultati insieme in un unico messaggio utente.
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",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)Cosa aspettarsi
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.Per saperne di più sull'esecuzione concorrente e sulle garanzie di ordinamento, consulta Uso parallelo degli strumenti.
Anello 4: Gestione degli errori
Gli strumenti falliscono. Un'API di calendario potrebbe rifiutare un evento con troppi partecipanti, oppure una data potrebbe essere malformata. Quando uno strumento genera un errore, invia il messaggio di errore con is_error: true invece di andare in crash. Claude legge l'errore e può riprovare con un input corretto, chiedere chiarimenti all'utente o spiegare la limitazione.
# Anello 4: Gestione degli errori.
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-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:
# Segnala il fallimento così Claude può riprovare o chiedere chiarimenti.
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",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)Cosa aspettarsi
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.Il flag is_error è l'unica differenza rispetto a un risultato riuscito. Claude vede il flag e il testo dell'errore, e risponde di conseguenza. Consulta Gestire le chiamate agli strumenti per il riferimento completo sulla gestione degli errori.
Anello 5: L'astrazione SDK Tool Runner
Gli Anelli dal 2 al 4 hanno scritto a mano lo stesso ciclo: chiamare l'API, controllare stop_reason, eseguire gli strumenti, aggiungere i risultati, ripetere. Il Tool Runner lo fa per te. Definisci ogni strumento come una funzione, passa la lista a tool_runner e recupera il messaggio finale una volta completato il ciclo. L'incapsulamento degli errori, la formattazione dei risultati e la gestione della conversazione sono gestiti internamente.
Ogni SDK fornisce un helper che trasforma una funzione ordinaria in uno strumento eseguibile e deriva lo schema di input dalla sua firma; le schede qui sotto mostrano la forma idiomatica per ciascun linguaggio.
# Anello 5: l'astrazione 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",
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)Cosa aspettarsi
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.L'output è identico a quello dell'Anello 3. La differenza è nel codice: circa la metà delle righe, nessun ciclo manuale, e lo schema vive accanto all'implementazione.
Cosa hai costruito
Hai iniziato con una singola chiamata a uno strumento codificata in modo fisso e hai finito con un agente di forma adatta alla produzione che gestisce strumenti multipli, chiamate parallele ed errori, per poi condensare tutto questo nel Tool Runner. Lungo il percorso hai visto ogni elemento del protocollo di uso degli strumenti: blocchi tool_use, blocchi tool_result, corrispondenza di tool_use_id, controllo di stop_reason e segnalazione con is_error.
Prossimi passi
Specifica dello schema e best practice.
Il riferimento completo dell'astrazione SDK.
Risolvi gli errori comuni nell'uso degli strumenti.
Was this page helpful?