Python & AI Tutorials Logo
LangChain & LangGraph

16. Componenti predefiniti e routing multi-ramo

Nel Capitolo 15 abbiamo assemblato manualmente un grafo per un agente(agent) — un nodo per il modello, un nodo per gli strumenti(tool) e un arco condizionale che decide se continuare il ciclo o fermarsi. Questa è essenzialmente la struttura standard per gli agenti che chiamano strumenti, quindi LangChain e LangGraph la forniscono come componenti predefiniti che puoi utilizzare invece di scrivere ogni volta la stessa impalcatura da zero.

Nella prima metà di questo capitolo, ricostruiremo l'agente del Capitolo 15 usando componenti predefiniti. Sostituiremo il nodo di esecuzione degli strumenti e la funzione di routing con ToolNode e tools_condition, e infine sostituiremo l'intero assemblaggio del grafo con una singola chiamata a create_agent. Vedrai che il comportamento rimane identico a quello del Capitolo 15, mentre il codice si riduce notevolmente.

Nella seconda metà, combineremo i componenti predefiniti con l'approccio di assemblaggio manuale del grafo del Capitolo 15 per costruire un agente più complesso. L'agente che costruiremo instrada ogni richiesta a un gestore diverso — le consulenze complesse vanno a un modello ad alte prestazioni, mentre le domande semplici ricevono risposta da un modello più economico e più piccolo. Si tratta di una struttura multi-ramo in cui il percorso di elaborazione diverge in base al tipo di richiesta.

16.1) Componenti predefiniti e create_agent

In questa sezione sostituiremo la funzione tool_node e la funzione should_continue del grafo del Capitolo 15 con i componenti predefiniti ToolNode e tools_condition. Successivamente, salteremo del tutto l'assemblaggio manuale e creeremo l'intero grafo con una singola chiamata a create_agent. La cosa da tenere d'occhio a ogni passaggio è che il codice diventa più breve mentre il comportamento dell'agente rimane identico a quello del Capitolo 15.

16.1.1) ToolNode e tools_condition

ToolNode è un nodo predefinito che gestisce per te l'esecuzione degli strumenti. Quando l'ultimo messaggio nello stato(state) (l'AIMessage restituito dall'LLM) contiene tool_calls, esegue gli strumenti richiesti e aggiunge i risultati a messages come oggetti ToolMessage. Svolge lo stesso lavoro della funzione tool_node che abbiamo scritto nel Capitolo 15. In aggiunta, quando l'LLM richiede più strumenti contemporaneamente, li esegue in parallelo.

Supporta anche la gestione delle eccezioni durante l'esecuzione degli strumenti. Se imposti ToolNode(tools, handle_tool_errors=True), il grafo non andrà in crash nemmeno quando uno strumento solleva un'eccezione. L'eccezione viene convertita in un ToolMessage che trasporta i dettagli dell'errore, il quale viene passato all'LLM in modo che possa vedere il fallimento e riprovare con argomenti corretti.

Crei un ToolNode passandogli una lista di strumenti. Useremo gli stessi due strumenti del Capitolo 15.

python
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una città."""
    fake_data = {"Tokyo": "18°C, nuvoloso", "Cairo": "31°C, soleggiato"}
    return fake_data.get(city, f"Nessun dato meteo per {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Valuta una semplice espressione aritmetica. Esempio: '3 * 21'."""
    return str(eval(expression))  # Attenzione: eval() è un rischio per la sicurezza. Non usare in produzione.
 
tools = [get_weather, calculate]
 
# tool_map + funzione tool_node del Capitolo 15 sostituiti da questa singola riga
tool_node = ToolNode(tools)

ToolNode costruisce internamente una mappatura nome-strumento a partire dalla lista di strumenti, proprio come il tool_map del Capitolo 15. In fase di esecuzione cerca ogni strumento in base al nome richiesto dall'LLM e lo chiama. In altre parole, il dizionario tool_map, il ciclo for sui tool_calls e il codice che costruisce e raccoglie gli oggetti ToolMessage — tutto questo ora risiede all'interno di ToolNode.

tools_condition è la funzione di routing predefinita che sostituisce la funzione should_continue del Capitolo 15. Osserviamo di nuovo la funzione should_continue del Capitolo 15.

python
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
    """Decide se eseguire gli strumenti o terminare il grafo."""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tool_node"
    return END

Restituiva "tool_node" (il nome registrato del nostro nodo strumenti) quando l'ultimo messaggio conteneva tool_calls, e END altrimenti. tools_condition funziona esattamente allo stesso modo, con una differenza nel nome che restituisce. Mentre should_continue era scritta per restituire "tool_node" — il nome che avevamo registrato nel nostro grafo — tools_condition è cablata per restituire "tools".

Come abbiamo appreso nella Sezione 15.2.4, il valore restituito da una funzione di routing è il nome del nodo successivo da eseguire. Se nel grafo non esiste alcun nodo con quel nome, il routing fallisce. Quindi, quando si usa tools_condition, il nodo degli strumenti deve essere registrato con il nome "tools".

python
from langgraph.prebuilt import ToolNode, tools_condition
 
builder.add_node("tools", ToolNode(tools))                  # Registra con il nome "tools"
builder.add_conditional_edges("llm_call", tools_condition)  # Instrada verso "tools" o END

In questo modo, quando tools_condition restituisce "tools", si connette esattamente al ToolNode che abbiamo appena registrato.

Ora riassembliamo il grafo completo del Capitolo 15, includendo tutti i pezzi rimanenti. Gli strumenti, lo stato e il nodo llm_call sono invariati rispetto al Capitolo 15.

python
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una città."""
    fake_data = {"Tokyo": "18°C, nuvoloso", "Cairo": "31°C, soleggiato"}
    return fake_data.get(city, f"Nessun dato meteo per {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Valuta una semplice espressione aritmetica. Esempio: '3 * 21'."""
    return str(eval(expression))  # Attenzione: eval() è un rischio per la sicurezza. Non usare in produzione.
 
tools = [get_weather, calculate]
 
class AgentState(MessagesState):
    llm_calls: int
 
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)
 
def llm_call(state: AgentState):
    """Chiama l'LLM e restituisce la sua risposta."""
    response = model_with_tools.invoke(state["messages"])
    return {
        "messages": [response],
        "llm_calls": state.get("llm_calls", 0) + 1,
    }
 
builder = StateGraph(AgentState)
 
builder.add_node("llm_call", llm_call)
builder.add_node("tools", ToolNode(tools))  # ToolNode invece della funzione tool_node del Capitolo 15
 
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition)  # tools_condition invece di should_continue del Capitolo 15
builder.add_edge("tools", "llm_call")
 
agent = builder.compile()

Confronta questo con il codice del Capitolo 15. Il dizionario tool_map, la funzione tool_node e la funzione should_continue sono tutti spariti. Il lavoro che svolgevano è ora gestito da ToolNode(tools) e tools_condition. Il nodo degli strumenti è registrato come "tools" per corrispondere al nome verso cui tools_condition instrada. Eseguiamolo con la stessa domanda del Capitolo 15.

python
result = agent.invoke({
    "messages": [HumanMessage(content="Ottieni la temperatura al Cairo, poi moltiplica il numero per 3.")],
    "llm_calls": 0,
})
 
print(result["messages"][-1].content)
print(f"\nChiamate LLM totali: {result['llm_calls']}")

Output:

Temperatura attuale al Cairo: 31°C. Moltiplicata per 3 = 93.
 
Chiamate LLM totali: 3

Il risultato è identico a quello del Capitolo 15. L'agente cerca il meteo, esegue il calcolo e produce la risposta finale — lo stesso comportamento è preservato, mentre il codice che dobbiamo scrivere e mantenere si è ridotto.

E se volessi registrare il nodo degli strumenti con un nome diverso da "tools"? In tal caso, passa un dizionario di mappatura come terzo argomento di add_conditional_edges, specificando a quale nodo debba connettersi ciascun valore restituito da tools_condition. Poiché tools_condition restituisce o "tools" o END, li usi come chiavi e li mappi ai nodi di destinazione. Per esempio, se registri il nodo degli strumenti come "run_tools":

python
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
    "llm_call",
    tools_condition,
    {"tools": "run_tools", END: END}   # ritorno "tools" → nodo run_tools, ritorno END → termina
)

ToolNode e tools_condition sostituiscono singole parti del grafo — quelle noiose — ma l'aggiunta dei nodi e il loro collegamento sono ancora a carico nostro. Potremmo delegare anche quell'assemblaggio? È esattamente ciò che fa create_agent.

16.1.2) create_agent

create_agent è una funzione factory di LangChain che gestisce l'intero assemblaggio del grafo per un agente che chiama strumenti. Passale un modello e una lista di strumenti, e costruirà un grafo con la stessa struttura che abbiamo assemblato nella sezione 16.1.1, già compilato e pronto per l'esecuzione. Proviamolo.

python
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
 
@tool
def get_weather(city: str) -> str:
    """Ottieni il meteo attuale per una città."""
    fake_data = {"Tokyo": "18°C, nuvoloso", "Cairo": "31°C, soleggiato"}
    return fake_data.get(city, f"Nessun dato meteo per {city}.")
 
@tool
def calculate(expression: str) -> str:
    """Valuta una semplice espressione aritmetica. Esempio: '3 * 21'."""
    return str(eval(expression))  # Attenzione: eval() è un rischio per la sicurezza. Non usare in produzione.
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    system_prompt="Sei un assistente utile.",
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="Ottieni la temperatura al Cairo, poi moltiplica il numero per 3.")],
})
print(result["messages"][-1].content)

Output:

Temperatura attuale al Cairo: 31°C. Moltiplicata per 3 = 93.

Nessuna definizione dello stato, nessuna funzione dei nodi, nessun add_node o add_edge. Una singola chiamata a create_agent ha fatto tutto questo, e il risultato è identico a quello della sezione 16.1.1.

Ciò che accade internamente è esattamente ciò che già conosciamo. create_agent crea un nodo di chiamata all'LLM a partire dal modello che passi, costruisce un ToolNode dalla lista degli strumenti e li connette con un arco tools_condition e un arco di ritorno al ciclo. Il risultato è un grafo con struttura a ciclo identico a quello che abbiamo assemblato nella sezione 16.1.1.

tool_calls presenti

nessun tool_calls

START

Nodo di chiamata LLM

ToolNode

END

Osserviamo i parametri. create_agent non è in realtà una novità per noi — l'abbiamo usato brevemente nel Capitolo 11 quando abbiamo costruito il RAG conversazionale, ma non siamo entrati nel dettaglio dei parametri. Esaminiamoli uno per uno.

  • model: L'LLM che l'agente utilizzerà. L'approccio più semplice è passare una stringa del provider come "openai:gpt-5-mini". Se hai bisogno di configurare direttamente i parametri del modello, passa un'istanza del modello già inizializzata come ChatOpenAI(model="gpt-5-mini"). Internamente, il nodo di chiamata all'LLM utilizza questo modello.
  • tools: La lista di strumenti che l'agente può utilizzare. Da questi viene costruito internamente un ToolNode.
  • system_prompt: Istruzioni comportamentali per l'agente. Viene anteposto come messaggio di sistema alla lista dei messaggi a ogni chiamata all'LLM.
  • checkpointer: Salva lo stato della conversazione in modo che l'agente possa ricordare i turni precedenti. È lo stesso parametro che abbiamo usato con InMemorySaver() e thread_id nel Capitolo 11 per implementare conversazioni multi-turno. Ne tratteremo il funzionamento in dettaglio nel Capitolo 17.
  • response_format: Usalo quando vuoi la risposta finale dell'agente come output strutturato. Passa un modello Pydantic (lo stesso concetto del Capitolo 7), e l'oggetto validato sarà disponibile in result["structured_response"].
  • middleware: Registra funzioni da eseguire in punti specifici del ciclo di esecuzione dell'agente. È il parametro che abbiamo usato per registrare trim_old_messages nel Capitolo 11. Lo spiegheremo in dettaglio più sotto.

Vediamo come funziona response_format nella pratica.

python
from pydantic import BaseModel
from langchain.agents import create_agent
 
class WeatherReport(BaseModel):
    city: str
    temperature: str
    condition: str
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    response_format=WeatherReport,
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="Che tempo fa a Tokyo?")],
})
print(result["structured_response"])

Output:

city='Tokyo' temperature='18°C' condition='nuvoloso'

L'agente ha chiamato lo strumento get_weather, poi ha organizzato le informazioni in un oggetto WeatherReport conforme allo schema.

middleware

Il ciclo dell'agente ha fasi distinte. Chiama l'LLM, esegue gli strumenti, chiama di nuovo l'LLM — queste fasi si ripetono. Il middleware ti permette di inserire le tue funzioni prima o dopo queste fasi. Specifichi il momento con un decoratore: @before_model significa subito prima della chiamata all'LLM, e @after_model significa subito dopo che l'LLM ha risposto. Registra la funzione nel parametro middleware, e verrà eseguita nel punto designato ogni volta.

Abbiamo già usato il middleware nel Capitolo 11. Abbiamo decorato una funzione trim_old_messages con @before_model e l'abbiamo registrata — poiché veniva eseguita prima di ogni chiamata all'LLM, poteva tagliare la lista dei messaggi ogni volta.

Costruiamo un middleware che stampa il numero di messaggi subito prima di ogni chiamata all'LLM, in modo da poter vedere esattamente quando viene eseguito.

python
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
 
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
    """Stampa il numero di messaggi subito prima di ogni chiamata all'LLM."""
    print(f"[before_model] Sto per chiamare l'LLM, messaggi attuali: {len(state['messages'])}")
 
agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[get_weather, calculate],
    middleware=[log_llm_call],
)
 
result = agent.invoke({
    "messages": [HumanMessage(content="Ottieni la temperatura al Cairo, poi moltiplica il numero per 3.")],
})
print(result["messages"][-1].content)

Output:

[before_model] Sto per chiamare l'LLM, messaggi attuali: 1
[before_model] Sto per chiamare l'LLM, messaggi attuali: 3
[before_model] Sto per chiamare l'LLM, messaggi attuali: 5
Temperatura attuale al Cairo: 31°C. Moltiplicata per 3 = 93.

Il middleware log_llm_call è stato eseguito tre volte. L'LLM è stato chiamato tre volte durante l'elaborazione della richiesta dell'utente, e il middleware è stato eseguito subito prima di ogni chiamata. Il numero di messaggi ci indica lo stato in ciascun punto: prima della prima chiamata c'era solo la domanda dell'utente (HumanMessage) — 1 messaggio. Dopo ogni iterazione del ciclo, sono stati aggiunti l'AIMessage che richiedeva una chiamata a uno strumento e il ToolMessage con il risultato, arrivando a 3, poi a 5.

Una cosa da sapere: prima di LangChain 1.0, questo ruolo era ricoperto da una funzione chiamata create_react_agent sul lato LangGraph, ed è ora deprecata. Se vedi from langgraph.prebuilt import create_react_agent in tutorial o post di blog più vecchi, sappi che si tratta di una versione precedente del create_agent che stai imparando ora.

Abbiamo ricostruito l'agente del Capitolo 15 in modo conciso usando ToolNode, tools_condition e create_agent. Nella prossima sezione, combineremo questi componenti predefiniti con l'assemblaggio manuale del grafo per costruire un agente più complesso.

16.2) Costruire un agente multi-ramo

L'agente multi-ramo che costruiremo in questa sezione determina prima con che tipo di richiesta ha a che fare, poi gestisce ogni tipo con un modello diverso o un diverso insieme di strumenti. Assembleremo il grafo complessivo manualmente usando l'approccio del Capitolo 15, e useremo create_agent per le parti che necessitano di un ciclo di chiamata agli strumenti.

16.2.1) Requisiti e progettazione

Costruiremo l'agente di assistenza clienti menzionato nell'introduzione di questo capitolo. Ecco i requisiti:

  • Richieste semplici ("Quali sono i vostri orari di apertura?") → Un modello piccolo a basso costo risponde direttamente.
  • Consulenze complesse ("Il mio ordine è arrivato danneggiato — dovrei ottenere una sostituzione o un rimborso?") → Risponde un modello ad alte prestazioni.
  • Ricerche di ordini ("Qual è lo stato di consegna dell'ordine #12345?") → Se ne occupa un agente con uno strumento di ricerca ordini.

Ogni tipo di richiesta necessita di una diversa configurazione di modello e strumenti, quindi ci serve un grafo che classifichi prima ogni richiesta e poi la instradi al gestore corretto. Ecco la struttura:

richiesta semplice

consulenza complessa

ricerca ordine

START

classify

tipo di richiesta?

simple_handler

complex_handler

order_agent

END

Quando arriva una richiesta, il nodo classify determina di quale tipo di richiesta si tratta e registra il risultato nello stato. Un arco condizionale legge quindi il tipo registrato dallo stato e instrada verso il nodo appropriato. simple_handler gestisce le richieste semplici, e complex_handler si occupa delle consulenze complesse. order_agent usa uno strumento di ricerca ordini per controllare lo stato di consegna e rispondere. Poiché order_agent è un nodo standard di chiamata agli strumenti, lo costruiremo con create_agent. Ora costruiamo ogni pezzo in ordine.

16.2.2) Il nodo di classificazione e la funzione di routing

Per prima cosa, definiamo lo stato. Aggiungeremo un campo intent per memorizzare il risultato della classificazione. I tre tipi saranno rappresentati dai valori "simple", "complex" e "order".

python
from langgraph.graph import MessagesState
 
class State(MessagesState):
    intent: str    # Risultato della classificazione: "simple", "complex", "order"

Successivamente c'è il nodo di classificazione. Usa un LLM per determinare a quale dei tre tipi appartiene la richiesta dell'utente, e registra il risultato in intent. Riceviamo il risultato della classificazione tramite output strutturato, che abbiamo appreso nel Capitolo 7. Quando dichiariamo il campo intent dello schema con un tipo Literal, la risposta dell'LLM è vincolata a uno dei valori dichiarati.

python
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
 
class IntentRoute(BaseModel):
    """Risultato della classificazione per una richiesta di un cliente."""
    intent: Literal["simple", "complex", "order"] = Field(
        description=(
            "simple: domande generali come orari di apertura o saluti. "
            "complex: consulenze che richiedono un ragionamento attento, come contestazioni o rimborsi. "
            "order: richieste di ricerca di un ordine specifico."
        )
    )
 
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
 
def classify(state: State):
    """Classifica il tipo di richiesta del cliente."""
    question = state["messages"][-1].content
    result = classifier_llm.invoke(
        f"Classifica la richiesta del cliente.\n\nRichiesta: {question}"
    )
    return {"intent": result.intent}

Abbiamo usato il modello più piccolo (gpt-5.4-nano) per la classificazione. Decidere "di quale tipo è questa richiesta?" è un compito semplice che non necessita di un modello ad alte prestazioni. E poiché il nodo di classificazione è un gateway attraverso cui passa ogni richiesta, è preferibile un modello economico e veloce.

Successivamente c'è la funzione di routing. Restituisce semplicemente il risultato della classificazione memorizzato nello stato.

python
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
    """Determina il nodo successivo in base al risultato della classificazione."""
    return state["intent"]

Il nodo classify ha già deciso quale nodo dovrebbe essere eseguito successivamente e lo ha registrato in intent, quindi la funzione di routing restituisce semplicemente quel valore così com'è.

16.2.3) Nodi gestori per tipo

Ora costruiamo i gestori per ciascuno dei tre tipi di richiesta.

Il gestore delle richieste semplici chiama un modello piccolo una volta. In un vero agente di assistenza clienti applicheresti il RAG per cercare risposte nei documenti interni, ma abbiamo mantenuto il gestore semplice per restare concentrati sull'argomento di questo capitolo.

python
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
 
def simple_handler(state: State):
    """Rispondi alle richieste semplici con un modello piccolo."""
    response = simple_llm.invoke(state["messages"])
    return {"messages": [response]}

Il gestore delle consulenze complesse usa un modello ad alte prestazioni. Per lo stesso motivo del gestore delle richieste semplici, l'abbiamo mantenuto semplice — si limita a generare una risposta.

python
complex_llm = ChatOpenAI(model="gpt-5.4")
 
def complex_handler(state: State):
    """Rispondi alle consulenze complesse con un modello ad alte prestazioni."""
    response = complex_llm.invoke(state["messages"])
    return {"messages": [response]}

Il gestore delle ricerche di ordini deve usare uno strumento di ricerca ordini, il che significa che richiede un ciclo di chiamata agli strumenti. Poiché la sua struttura è identica a quella di un agente standard che chiama strumenti, lo costruiremo con create_agent.

python
from langchain.tools import tool
from langchain.agents import create_agent
 
@tool
def get_order_status(order_id: str) -> str:
    """Cerca lo stato di consegna di un ordine tramite il numero d'ordine."""
    fake_data = {"12345": "In transito, previsto per domani", "67890": "Consegnato"}
    return fake_data.get(order_id, f"Ordine {order_id} non trovato.")
 
order_agent = create_agent(
    model="openai:gpt-5.4-mini",
    tools=[get_order_status],
)

16.2.4) Assemblare ed eseguire il grafo

Colleghiamo tutti i nodi che abbiamo costruito in un grafo. Posizioneremo classify al punto di partenza, lo collegheremo ai tre gestori tramite un arco condizionale e configureremo ciascun gestore affinché termini una volta finito.

python
from langgraph.graph import StateGraph, START, END
 
builder = StateGraph(State)
 
builder.add_node("classify", classify)
builder.add_node("simple", simple_handler)
builder.add_node("complex", complex_handler)
builder.add_node("order", order_agent)      # Registra il grafo di create_agent come nodo
 
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent)
builder.add_edge("simple", END)
builder.add_edge("complex", END)
builder.add_edge("order", END)
 
agent = builder.compile()

Eseguiamo una richiesta di ciascun tipo e vediamo quale gestore la elabora.

python
from langchain_core.messages import HumanMessage
 
for question in [
    "Quali sono i vostri orari di apertura?",
    "Il mio ordine è arrivato danneggiato. Dovrei ottenere una sostituzione o un rimborso?",
    "Qual è lo stato di consegna dell'ordine 12345?",
]:
    result = agent.invoke({"messages": [HumanMessage(content=question)]})
    print(f"D: {question}")
    print(f"[{result['intent']}] R: {result['messages'][-1].content}\n")

Output:

D: Quali sono i vostri orari di apertura?
[simple] R: Non ho orari di apertura fissi—sono disponibile 24/7.
...
 
D: Il mio ordine è arrivato danneggiato. Dovrei ottenere una sostituzione o un rimborso?
[complex] R: Se il tuo ordine è arrivato danneggiato, generalmente dovresti avere diritto a una **sostituzione/scambio o a un rimborso completo**.
...
 
D: Qual è lo stato di consegna dell'ordine 12345?
[order] R: L'ordine 12345 è **in transito** ed è **previsto per domani**.

Ogni richiesta è stata elaborata attraverso un percorso diverso. simple_handler ha risposto alla richiesta semplice con un modello piccolo, complex_handler ha risposto alla consulenza complessa con un modello ad alte prestazioni, e order_agent ha chiamato lo strumento get_order_status per rispondere alla ricerca dell'ordine.