Python & AI Tutorials Logo
LangChain & LangGraph

12: Costruire strumenti per il tuo agente

Nella Parte IV, costruiremo un agente(agent) — un'IA che capisce cosa fare con la richiesta di un utente e poi la esegue, invece di limitarsi a rispondere come un chatbot.

Ecco la differenza in azione. Supponiamo che un utente chieda: "Per favore, annulla l'ordine #12345." Un chatbot direbbe qualcosa del genere: "Vai su La mia pagina > Cronologia ordini e clicca sul pulsante 'Annulla' per quell'ordine." Da lì, spetta all'utente seguire quei passaggi da solo. Un agente invece svolge il lavoro — cerca l'ordine, verifica se è idoneo all'annullamento e lo annulla. Compie un'azione.

Ciò che rende tutto questo possibile sono gli strumenti(tools): uno strumento che cerca un ordine, uno strumento che lo annulla, uno strumento che invia un'email. Con gli strumenti, un LLM smette di essere limitato alla generazione di testo e inizia a portare effettivamente a termine le cose.

Per favore, annulla l'ordine #12345.

chiama get_order(12345)

Ordine trovato,
idoneo all'annullamento

chiama cancel_order(12345)

Annullamento completato

Il tuo ordine è stato annullato.

Utente

Ciclo dell'agente

Strumento di ricerca ordini

Strumento di annullamento ordini

Costruiremo tutto questo pezzo per pezzo nel corso della Parte IV: prima gli strumenti che l'agente userà (questo capitolo), poi il collegamento di questi strumenti a un LLM (Capitolo 13), e infine il ciclo dell'agente che attraversa decidi → agisci → osserva (Capitolo 14).

Questo capitolo copre il primo pezzo — la definizione degli strumenti, il loro collegamento a dati reali e la gestione sicura degli errori.

12.1) Definire gli strumenti con il decoratore @tool

12.1.1) Come funziona uno strumento?

Abbiamo appena visto un agente cercare e annullare un ordine usando gli strumenti. Prima di andare oltre, ecco una cosa che vale la pena chiarire: quando si dice "l'LLM usa uno strumento", sembra che sia l'LLM a chiamarlo direttamente. Non è così. L'LLM non esegue mai nulla da solo — tutto ciò che fa è chiedere che uno strumento venga chiamato con determinati argomenti. L'esecuzione vera e propria avviene nel nostro codice.

Affinché ciò funzioni, l'LLM deve sapere quali strumenti esistono e quando ciascuno è applicabile. Quindi ogni strumento è accompagnato da tre pezzi di metadati:

  • name — un breve identificatore come get_order che l'LLM usa per specificare quale strumento desidera.
  • description — una frase che descrive cosa fa lo strumento e quando usarlo. È ciò che l'LLM legge per scegliere lo strumento giusto per il compito.
  • Schema di input — quali sono i parametri dello strumento: i loro nomi, tipi e significato. L'LLM ne ha bisogno per compilare correttamente gli argomenti.

Niente di tutto questo richiede lavoro extra da parte tua. name proviene direttamente dal nome della funzione, description proviene dalla sua docstring e lo schema di input proviene dai type hint dei parametri. Tutto ciò che devi fare è applicare il decoratore @tool di LangChain.

12.1.2) Costruire il tuo primo strumento

Mettiamo questo in pratica. Scrivi una funzione con type hint e una docstring, poi decorala con @tool.

python
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una data città."""
    return f"È sempre soleggiato a {city}!"

Verifichiamo cosa ha generato @tool per noi.

python
print(get_weather.name)
# Output: get_weather
 
print(get_weather.description)
# Output: Ottieni il meteo attuale per una data città.
 
print(get_weather.args)
# Output: {'city': {'title': 'City', 'type': 'string'}}

Il nome della funzione get_weather è diventato il suo name, la docstring è diventata la sua description e il type hint city: str è diventato il suo schema di input. Tutti e tre i pezzi di metadati della sezione precedente sono stati generati automaticamente. È questo che l'LLM usa per scegliere uno strumento e compilare i suoi argomenti.

Una volta applicato @tool, la funzione diventa un oggetto strumento di LangChain, il che significa che non puoi più chiamarla come una normale funzione — get_weather("Paris") non funzionerà. La chiami invece con .invoke(), lo stesso metodo di esecuzione standard che abbiamo usato per le catene nel Capitolo 6. Gli argomenti vengono passati come un dizionario:

python
result = get_weather.invoke({"city": "Paris"})
print(result)
# Output: È sempre soleggiato a Paris!

12.1.3) Personalizzare name e description

Per impostazione predefinita, name proviene dal nome della funzione e description dalla docstring. Puoi sovrascrivere entrambi.

Passa un nome come primo argomento a @tool:

python
@tool("web_search")
def search(query: str) -> str:
    """Cerca informazioni sul web."""
    return f"Risultati per: {query}"
 
print(search.name)
# Output: web_search

Puoi sovrascrivere anche la descrizione, usando il parametro description. Questo è utile quando vuoi mantenere la docstring come nota per altri sviluppatori, fornendo all'LLM qualcosa di più mirato:

python
@tool("calculator", description="Esegue operazioni aritmetiche. Usalo per qualsiasi problema matematico.")
def calc(expression: str) -> str:
    """Valuta una stringa con un'espressione matematica."""
    return str(eval(expression))  # ATTENZIONE: eval() non è sicuro. Non usarlo mai in produzione.

Attieniti allo snake_case per i nomi degli strumenti — alcuni provider di LLM rifiutano nomi con spazi o caratteri speciali.

12.1.4) Definire uno schema di input con Pydantic

Quando uno strumento accetta diversi parametri, o vuoi descrivere ciascuno di essi individualmente, definisci lo schema di input con un modello Pydantic. Sono gli stessi BaseModel e Field che abbiamo usato per l'output strutturato nel Capitolo 7.

python
from pydantic import BaseModel, Field
from langchain.tools import tool
 
class WeatherInput(BaseModel):
    """Input per le query meteo."""
    location: str = Field(description="Nome della città (es. Seoul, Tokyo)")
    units: str = Field(default="celsius", description="Unità di temperatura (celsius o fahrenheit)")
 
@tool(args_schema=WeatherInput)
def get_weather_detailed(location: str, units: str = "celsius") -> str:
    """Ottieni il meteo attuale con un'unità di temperatura scelta."""
    temp = 22 if units == "celsius" else 72
    return f"Meteo attuale a {location}: {temp} gradi {units[0].upper()}"

Tutto ciò che scrivi in Field(description=...) diventa parte dello schema di input che l'LLM legge, così sa esattamente cosa significa ciascun parametro. La maggior parte delle volte, i type hint e una docstring chiara sono tutto ciò di cui hai bisogno — ricorri a args_schema solo quando hai bisogno di quel livello extra di dettaglio per ciascun parametro.

12.2) Gestire gli errori degli strumenti

Nel mondo reale, gli strumenti possono fallire — una connessione al database si interrompe, o si presenta un input che non avevi previsto. In questa sezione, gestiremo questi errori all'interno dello strumento stesso, in modo che l'agente possa rispondere in modo sensato invece di fermarsi di colpo. Per prima cosa, configuriamo le funzioni su cui si baseranno i nostri strumenti.

12.2.1) Configurazione: funzioni di ricerca prodotti

python
# product_service.py
 
PRODUCTS = {
    1: {"name": "Wireless Mouse", "price": 29.99, "stock": 120},
    2: {"name": "Mechanical Keyboard", "price": 89.99, "stock": 0},
    3: {"name": "USB-C Hub", "price": 45.50, "stock": 35},
    4: {"name": "Laptop Stand", "price": 39.00, "stock": 8},
}
 
def fetch_product(product_id: int) -> dict:
    """Cerca le informazioni di un prodotto tramite ID."""
    product = PRODUCTS.get(product_id)
    if product is None:
        raise ValueError(f"Product with ID {product_id} not found.")
    return product
 
def fetch_stock(product_id: int) -> int:
    """Restituisce la quantità in magazzino per un prodotto."""
    product = PRODUCTS.get(product_id)
    if product is None:
        raise ValueError(f"Product with ID {product_id} not found.")
    return product["stock"]

Entrambe le funzioni sollevano un ValueError quando ricevono un ID prodotto che non esiste.

12.2.2) Gestire gli errori in uno strumento

Avvolgiamo fetch_product in uno strumento get_product.

python
from langchain.tools import tool
from product_service import fetch_product
 
@tool
def get_product(product_id: int) -> str:
    """Cerca un prodotto tramite il suo ID. Restituisce il nome, il prezzo e il livello di magazzino."""
    product = fetch_product(product_id)
    return f"Product {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} in stock."

Con un ID valido, funziona come previsto.

python
print(get_product.invoke({"product_id": 1}))
# Output: Product 1: Wireless Mouse — $29.99, 120 in stock.

Ma passa un ID che non esiste, e fetch_product solleva un ValueError che nessuno cattura — l'esecuzione dell'agente si ferma proprio lì.

python
print(get_product.invoke({"product_id": 99}))
# ValueError: Product with ID 99 not found.

La soluzione è semplice: cattura l'eccezione all'interno dello strumento e restituisci una stringa che l'LLM possa comprendere, invece di lasciarla propagare. Successo o fallimento, lo strumento restituisce sempre una stringa, e l'LLM usa quella stringa per decidere cosa fare dopo.

python
from langchain.tools import tool
from product_service import fetch_product
 
@tool
def get_product(product_id: int) -> str:
    """Cerca un prodotto tramite il suo ID. Restituisce il nome, il prezzo e il livello di magazzino."""
    try:
        product = fetch_product(product_id)
        return f"Product {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} in stock."
    except ValueError as e:
        return f"Error: {e}"
    except Exception as e:
        return f"Unexpected error looking up product {product_id}: {e}"
python
print(get_product.invoke({"product_id": 1}))
# Output: Product 1: Wireless Mouse — $29.99, 120 in stock.
 
print(get_product.invoke({"product_id": 99}))
# Output: Error: Product with ID 99 not found.

Un ID inesistente non solleva più un'eccezione — restituisce invece un messaggio di errore che l'LLM può comprendere.

Applichiamo lo stesso schema a check_stock:

python
from product_service import fetch_stock
 
@tool
def check_stock(product_id: int) -> str:
    """Verifica se un prodotto è attualmente disponibile in magazzino."""
    try:
        stock = fetch_stock(product_id)
        if stock > 0:
            return f"{stock} units available."
        return "Out of stock."
    except ValueError as e:
        return f"Error: {e}"
    except Exception as e:
        return f"Unexpected error checking stock for product {product_id}: {e}"

Gli strumenti costruiti in questo modo possono essere testati direttamente con .invoke(). Assicurati che uno strumento funzioni correttamente da solo prima di collegarlo a un LLM. Altrimenti, quando qualcosa andrà storto all'interno dell'agente più tardi, non sarai in grado di capire se lo strumento è guasto o se il modello ha semplicemente fatto una scelta sbagliata.