Python & AI Tutorials Logo
LangChain & LangGraph

13. Collegare gli strumenti all'LLM

Nel Capitolo 12 abbiamo costruito strumenti(tool) che un agente può usare. Ora è il momento di collegare questi strumenti all'LLM in modo che l'agente possa effettivamente utilizzarli.

Per quanto bene sia costruito uno strumento, l'LLM non può usarlo senza sapere che esiste. Deve sapere cosa è disponibile e scegliere lo strumento giusto per ogni richiesta. Tuttavia, come abbiamo imparato nel Capitolo 12, l'LLM non esegue direttamente gli strumenti. Invece, richiede "chiama questo strumento con questi argomenti", e il codice del nostro agente invoca lo strumento. L'intero meccanismo si chiama tool calling.

In questo capitolo collegheremo gli strumenti all'LLM, eseguiremo le richieste di chiamata agli strumenti e restituiremo i risultati all'LLM — implementando il ciclo completo. Questo ciclo è la base del loop dell'agente che costruiremo nel Capitolo 14.

13.1) Collegare gli strumenti e ispezionare le richieste di chiamata

Affinché l'LLM possa usare gli strumenti, deve prima sapere cosa è disponibile. Quando passiamo all'LLM il name, la description e lo schema di input di ogni strumento, esso apprende quando e come usare ciascuno. Questo processo si chiama tool binding.

Una volta collegati gli strumenti, l'LLM farà una di due cose quando riceve una domanda. Se non è necessario alcuno strumento, risponde con del testo come al solito. Se è necessario uno strumento, restituisce una richiesta strutturata: "chiama questo strumento con questi argomenti". Il nostro agente quindi ispeziona questa richiesta e invoca lo strumento specificato.

Iniziamo con il modo in cui collegare gli strumenti.

13.1.1) Collegare gli strumenti con bind_tools()

Il tool binding è gestito da un unico metodo: bind_tools(). Ogni modello di chat che supporta il tool calling fornisce questo metodo. Chiamare bind_tools() restituisce un nuovo oggetto modello con gli strumenti specificati collegati.

Colleghiamo lo strumento get_weather che abbiamo costruito nel Capitolo 12.

python
from langchain_openai import ChatOpenAI
from langchain.tools import tool
 
llm = ChatOpenAI(model="gpt-5-mini")
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una determinata città."""
    return f"It's always sunny in {city}!"
 
# Collega lo strumento al modello — viene restituito un nuovo oggetto modello con lo strumento collegato
llm_with_tools = llm.bind_tools([get_weather])

Questo è tutto ciò che serve. Il llm_with_tools restituito è un modello che conosce lo strumento get_weather. Nota che il llm originale non è cambiato affatto — continua a non sapere nulla di alcuno strumento.

bind_tools() accetta come argomento una lista di strumenti e restituisce un nuovo oggetto modello con quegli strumenti collegati. Internamente, converte i metadati di ogni strumento (name, description e schema di input) in un formato che il provider dell'LLM comprende, in modo che gli schemi degli strumenti vengano inviati insieme a ogni invocazione del modello.

13.1.2) Ispezionare le richieste di chiamata agli strumenti

Cosa succede quando inviamo una domanda sul meteo a un modello con uno strumento meteo collegato?

python
response = llm_with_tools.invoke("Che tempo fa a Parigi?")
 
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")

Output:

type: <class 'langchain_core.messages.ai.AIMessage'>
 
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
 
content: ''

Poiché llm_with_tools è ancora un modello di chat, restituisce un AIMessage. Ma ci sono due aspetti diversi in questa risposta.

Primo, response.tool_calls contiene una lista di richieste di chiamata agli strumenti. Ogni richiesta è un dizionario con quattro chiavi:

  • name — il nome dello strumento da chiamare. Corrisponde al name dello strumento ricavato dai suoi metadati.
  • args — gli argomenti da passare quando si chiama lo strumento. Il modello ha esaminato lo schema di input e la domanda dell'utente per costruire {'city': 'Paris'}.
  • id — un identificatore univoco per questa chiamata. L'LLM lo usa in seguito per abbinare i risultati dell'esecuzione degli strumenti alle richieste di chiamata.
  • type — sempre 'tool_call'.

Secondo, response.content è una stringa vuota. Questa risposta non è una risposta testuale finale — è una richiesta di chiamata a uno strumento.

Per determinare se la risposta è una richiesta di chiamata a uno strumento, controlla tool_calls invece di content. Se tool_calls non è vuoto, è una richiesta di chiamata a uno strumento. Se è vuoto, l'LLM ha fornito direttamente una risposta testuale.

13.2) Eseguire gli strumenti e restituire i risultati

L'LLM ha richiesto una chiamata allo strumento get_weather. Ora il codice del nostro agente deve eseguire lo strumento e restituire il risultato all'LLM.

L'elaborazione di una richiesta di chiamata a uno strumento comporta tre passi:

  1. Estrarre il nome dello strumento e gli argomenti da tool_calls ed eseguire lo strumento corrispondente.
  2. Convertire il risultato dell'esecuzione in un ToolMessage.
  3. Inviare l'intera conversazione (domanda dell'utente + richiesta di chiamata dell'LLM + risultato dell'esecuzione dello strumento) all'LLM per ricevere una risposta.

Ecco un diagramma di questo processo:

LLMget_weatherAgenteUtenteLLMget_weatherAgenteUtente"Che tempo fa a Parigi?"HumanMessageAIMessage (tool_calls: get_weather)get_weather(city='Paris')"It's always sunny in Paris!"ToolMessageAIMessage ("Il tempo a Parigi è soleggiato!")"Il tempo a Parigi è soleggiato!"

13.2.1) Eseguire lo strumento e creare un ToolMessage

Per prima cosa, estraiamo il nome dello strumento e gli argomenti da tool_calls ed eseguiamo lo strumento corrispondente. Per cercare uno strumento per nome, creiamo un dizionario indicizzato per nome dello strumento.

python
# Un dizionario per cercare gli strumenti per nome
tool_map = {get_weather.name: get_weather}

Successivamente, estraiamo una richiesta di chiamata da tool_calls, troviamo lo strumento corrispondente in tool_map e lo eseguiamo.

python
if response.tool_calls:
    tool_call = response.tool_calls[0]
    # {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
 
    selected_tool = tool_map[tool_call["name"]]
    result = selected_tool.invoke(tool_call["args"])
    print(result)  # Output: It's always sunny in Paris!

Per restituire il risultato dell'esecuzione dello strumento all'LLM, dobbiamo racchiuderlo in un ToolMessage. Un ToolMessage ha due campi obbligatori:

  • content — il risultato dell'esecuzione dello strumento come stringa.
  • tool_call_id — l'id di tool_calls. L'LLM lo usa per identificare a quale richiesta appartiene il risultato.
python
from langchain_core.messages import ToolMessage
 
if response.tool_calls:
    tool_call = response.tool_calls[0]
 
    selected_tool = tool_map[tool_call["name"]]
    result = selected_tool.invoke(tool_call["args"])
 
    tool_message = ToolMessage(
        content=result,
        tool_call_id=tool_call["id"],   # deve corrispondere all'id della richiesta
    )
    print(tool_message)

Output:

content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'

Nel codice qui sopra, abbiamo passato args per eseguire lo strumento, poi abbiamo combinato il risultato con il tool_call_id per costruire il ToolMessage.

C'è un modo più semplice per farlo in un solo passo. Invece di passare solo args a .invoke(), passa l'intero dizionario tool_call. LangChain eseguirà lo strumento e restituirà automaticamente un ToolMessage.

python
if response.tool_calls:
    tool_call = response.tool_calls[0]
 
    selected_tool = tool_map[tool_call["name"]]
    tool_message = selected_tool.invoke(tool_call)
    print(tool_message)

Output:

content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'

Stesso risultato, ma il codice è molto più semplice e non c'è alcuna possibilità di un disallineamento del tool_call_id. Useremo questo approccio d'ora in poi.

13.2.2) Inviare i risultati degli strumenti all'LLM

Ora passiamo il risultato dell'esecuzione dello strumento all'LLM. Inviamo all'LLM l'intera cronologia della conversazione — la domanda dell'utente, la richiesta di chiamata dell'LLM e il risultato dell'esecuzione dello strumento — ed esso genera una risposta basata sul risultato dello strumento.

Mettiamo insieme l'intero processo dall'inizio alla fine in un unico blocco di codice.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
 
# Definisci gli strumenti e collegali
llm = ChatOpenAI(model="gpt-5-mini")
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una determinata città."""
    return f"It's always sunny in {city}!"
 
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
 
# Passo 1: Invia la domanda dell'utente e ricevi la risposta dell'LLM
messages = [HumanMessage("Che tempo fa a Parigi?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
 
# Passo 2: Se è stata richiesta una chiamata a uno strumento, esegui lo strumento e aggiungi il risultato
if ai_msg.tool_calls:
    tool_call = ai_msg.tool_calls[0]
    selected_tool = tool_map[tool_call["name"]]
    tool_message = selected_tool.invoke(tool_call)
    messages.append(tool_message)
 
    # Passo 3: Invia all'LLM l'intera cronologia della conversazione
    final_response = llm_with_tools.invoke(messages)
    print(final_response.content)

Output:

Il tempo attuale a Parigi è soleggiato. Buona giornata!

L'LLM ha generato una risposta testuale finale basata sul risultato dell'esecuzione dello strumento. Nel paragrafo 13.1, content era una stringa vuota — ma dopo aver restituito il risultato dello strumento, abbiamo ricevuto una risposta effettiva.

Verifichiamo quali messaggi si trovano nella lista messages al Passo 3.

python
for m in messages:
    print(f"{type(m).__name__}: {m.content!r}")

Output:

HumanMessage: "Che tempo fa a Parigi?"
AIMessage: ''
ToolMessage: "It's always sunny in Paris!"

Tre messaggi vengono memorizzati in ordine: la domanda dell'utente, la richiesta di chiamata dell'LLM (contenuto vuoto) e il risultato dell'esecuzione dello strumento. L'LLM ha visto la domanda dell'utente, ha deciso che aveva bisogno di uno strumento e ha richiesto una chiamata a get_weather. Il codice dell'agente ha eseguito get_weather e ha aggiunto il risultato alla lista dei messaggi. Una volta ottenuto tutto ciò che gli serviva, l'LLM ha generato la sua risposta finale.

13.3) Fornire all'LLM più strumenti

Gli agenti del mondo reale hanno tipicamente più strumenti. Tutto ciò che abbiamo trattato finora funziona esattamente allo stesso modo con più strumenti.

Aggiungiamo calculate e search_web insieme a get_weather per collegare tre strumenti.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
 
llm = ChatOpenAI(model="gpt-5-mini")
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una determinata città."""
    return f"It's always sunny in {city}!"
 
@tool
def calculate(expression: str) -> str:
    """Valuta una semplice espressione aritmetica come '123 * 456'."""
    return str(eval(expression))  # Attenzione: eval() è un rischio per la sicurezza. Non usarlo in produzione.
 
@tool
def search_web(query: str) -> str:
    """Cerca sul web informazioni aggiornate su un argomento."""
    return f"Top result for '{query}': ..."
 
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}

Poniamo una domanda di calcolo.

python
response = llm_with_tools.invoke("Quanto fa 123 moltiplicato per 456?")
print(response.tool_calls)

Output:

[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]

L'LLM ha richiesto una chiamata a calculate. Ha confrontato la description di ogni strumento con la domanda dell'utente e ha determinato che calculate era lo strumento giusto.

Possiamo eseguire lo strumento e ottenere una risposta finale usando lo stesso schema del paragrafo 13.2.

python
messages = [HumanMessage("Quanto fa 123 moltiplicato per 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
 
if ai_msg.tool_calls:
    for tool_call in ai_msg.tool_calls:
        selected_tool = tool_map[tool_call["name"]]
        tool_message = selected_tool.invoke(tool_call)
        messages.append(tool_message)
 
    final_response = llm_with_tools.invoke(messages)
    print(final_response.content)  # Output: 123 moltiplicato per 456 fa 56.088.

Nel paragrafo 13.2 abbiamo gestito solo la prima richiesta con tool_calls[0], ma qui iteriamo su tutte le richieste con un ciclo for. Il motivo è che l'LLM può richiedere più chiamate agli strumenti contemporaneamente.

Richieste simultanee di più strumenti

Vediamo cosa succede quando vengono richiesti più strumenti contemporaneamente. Il codice è lo stesso — cambia solo la domanda.

python
messages = [HumanMessage("Che tempo fa a Tokyo e quanto fa 123 * 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
 
if ai_msg.tool_calls:
    for tool_call in ai_msg.tool_calls:
        print(f"Tool: {tool_call['name']}")
        selected_tool = tool_map[tool_call["name"]]
        tool_message = selected_tool.invoke(tool_call)
        messages.append(tool_message)
 
    final_response = llm_with_tools.invoke(messages)
    print(final_response.content)

Output:

Tool: get_weather
Tool: calculate
Il tempo a Tokyo è attualmente soleggiato e 123 moltiplicato per 456 fa 56.088.

L'LLM ha richiesto sia get_weather che calculate, e il ciclo for ha eseguito tutte le richieste in tool_calls aggiungendo i loro risultati alla lista dei messaggi. L'LLM ha poi usato questi risultati per generare la risposta finale.