Cos’è LangGraph: agenti come grafi
Immagina un processo aziendale di approvazione acquisti. C’è una richiesta iniziale, va al manager, che può approvarla (e passa alla contabilità) oppure rifiutarla (e torna al richiedente con una motivazione) oppure chiedere chiarimenti (torna al richiedente, poi quando risponde ritorna al manager). Non è un flusso lineare — ci sono branch, loop, stati intermedi e coinvolgimento umano.
LangGraph è esattamente lo strumento per costruire questo tipo di sistema, ma con agenti AI. È la risposta di LangChain a una domanda concreta: come costruire agenti che si comportano in modo prevedibile e affidabile in produzione?
I semplici ReAct agent (un loop LLM + tool) hanno tre limiti fondamentali che LangGraph risolve:
- Stato volatile: i loop classici "ricordano" solo la conversazione. LangGraph ha uno State esplicito — un dizionario tipizzato che ogni nodo può leggere e modificare, che persiste tra step e sopravvive ai crash.
- Flusso imprevedibile: in un loop libero, il modello decide autonomamente cosa fare dopo. Con LangGraph, sei tu a disegnare le transizioni — nessuna sorpresa su quale nodo viene eseguito dopo quale condizione.
- No human oversight: LangGraph ha il human-in-the-loop come cittadino di prima classe — puoi mettere in pausa il grafo, far approvare un’azione da un umano, e riprendere esattamente da dove si era fermato.
LangGraph fa parte dell’ecosistema LangChain ma può essere usato completamente indipendentemente, anche senza aver mai toccato LangChain.
I concetti fondamentali: State, Nodes, Edges
LangGraph ha tre concetti base. Capirli bene è tutto.
State
Lo State è la "memoria di lavoro" del grafo — un TypedDict Python che rappresenta tutto ciò che il grafo sa in un dato momento. Ogni nodo può leggerlo e scriverne parti. Lo State è l’unico modo in cui i nodi si "comunicano" tra loro.
from typing import TypedDict, Annotated
from langgraph.graph import add_messages
class AgentState(TypedDict):
# add_messages è un "reducer": aggiunge invece di sostituire
messages: Annotated[list, add_messages]
# Campi custom per la logica applicativa
task_completato: bool
tentativi: int
dati_raccolti: dictpython
La parola chiave è Annotated: permette di specificare un reducer, ovvero una funzione che determina come lo State viene aggiornato quando più nodi scrivono sullo stesso campo. add_messages è un reducer predefinito che aggiunge i nuovi messaggi alla lista invece di sovrascriverla — il comportamento ovvio per una conversazione.
Nodes
Un nodo è una funzione Python che riceve lo State attuale e restituisce un dizionario con i campi da aggiornare. Non restituisce uno State completo — solo le differenze (delta). I campi non menzionati rimangono invariati.
def nodo_analisi(state: AgentState) -> dict:
# Leggi dallo state
ultima_domanda = state["messages"][-1].content
# Logica del nodo: chiamata LLM, tool, calcolo, ecc.
risposta = llm.invoke(ultima_domanda)
# Ritorna SOLO i campi da aggiornare (non tutto lo state)
return {
"messages": [risposta], # add_messages lo aggiunge
"tentativi": state["tentativi"] + 1 # incrementa il contatore
}python
Edges
Gli edges (archi) definiscono come il grafo si muove da un nodo all’altro. Ci sono due tipi:
- Edge normale: sempre da A a B, senza condizioni.
builder.add_edge("A", "B") - Conditional edge: una funzione Python decide dove andare in base allo State.
builder.add_conditional_edges(...)
Il tuo primo grafo
Prima di affrontare i casi complessi, costruiamo il grafo più semplice possibile: un chatbot che risponde alle domande.
from langgraph.graph import StateGraph, END, START
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from typing import TypedDict, Annotated
from langgraph.graph import add_messages
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
class State(TypedDict):
messages: Annotated[list, add_messages]
# Definisci il nodo: riceve State, restituisce delta
def chatbot(state: State) -> dict:
risposta = llm.invoke(state["messages"])
return {"messages": [risposta]}
# Costruisci il grafo
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
# Definisci il flusso: START → chatbot → END
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
# Compila: valida la struttura del grafo e ottimizza
graph = builder.compile()
# Esecuzione
result = graph.invoke({
"messages": [HumanMessage(content="Ciao! Cos'è LangGraph?")]
})
print(result["messages"][-1].content)python
Il metodo builder.compile() non è solo un builder pattern — valida la struttura del grafo (nodi irraggiungibili, edge verso nodi inesistenti), produce una rappresentazione ottimizzata e abilita funzionalità avanzate come checkpointing e streaming.
Conditional edges: routing intelligente
Il vero potere di LangGraph emerge con i conditional edges. Una funzione "router" ispeziona lo State dopo ogni nodo e decide quale nodo attivare successivamente.
Il pattern classico: un agente con tool use deve decidere se continuare (ha richiesto un tool) o fermarsi (ha la risposta finale).
def dovrei_continuare(state: State) -> str:
"""Router: decide se usare un tool o terminare."""
ultimo_messaggio = state["messages"][-1]
# Se il modello ha generato tool_calls, dobbiamo eseguirli
if hasattr(ultimo_messaggio, "tool_calls") and ultimo_messaggio.tool_calls:
return "usa_tool"
# Altrimenti il modello ha la risposta finale
return "end"
# add_conditional_edges: nodo_sorgente, funzione_router, mappa_output→nodo
builder.add_conditional_edges(
"agente",
dovrei_continuare,
{
"usa_tool": "tool_node", # se router ritorna "usa_tool" → tool_node
"end": END # se router ritorna "end" → fine
}
)python
La funzione router può restituire qualsiasi stringa — la mappa {"output_router": "nome_nodo"} converte l’output del router nel nodo corretto. Questo disaccoppiamento è importante: la logica di routing è nel grafo, non dentro il nodo.
Implementare un ReAct agent
Ecco un agente completo con tool use, costruito con LangGraph. Rispetto agli agent di LangChain classici, hai visibilità e controllo su ogni step del loop:
from langgraph.graph import StateGraph, END, START
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
@tool
def cerca_prodotto(nome: str) -> str:
"""Cerca informazioni su un prodotto nel catalogo."""
catalogo = {
"laptop": "ThinkPad X1: 16GB RAM, i7, 1299€",
"mouse": "MX Master 3: wireless, ergonomico, 89€"
}
return catalogo.get(nome.lower(), "Prodotto non trovato")
@tool
def calcola_sconto(prezzo: float, percentuale: float) -> str:
"""Calcola il prezzo scontato dato prezzo base e percentuale."""
scontato = prezzo * (1 - percentuale / 100)
return f"Prezzo scontato: {scontato:.2f}€"
tools = [cerca_prodotto, calcola_sconto]
# bind_tools dice al modello quali tool può invocare
llm_con_tools = llm.bind_tools(tools)
class State(TypedDict):
messages: Annotated[list, add_messages]
def agente(state: State) -> dict:
risposta = llm_con_tools.invoke(state["messages"])
return {"messages": [risposta]}
def router(state: State) -> str:
ultimo = state["messages"][-1]
if hasattr(ultimo, "tool_calls") and ultimo.tool_calls:
return "tools"
return "end"
builder = StateGraph(State)
builder.add_node("agente", agente)
builder.add_node("tools", ToolNode(tools)) # ToolNode esegue i tool automaticamente
builder.add_edge(START, "agente")
builder.add_conditional_edges("agente", router, {"tools": "tools", "end": END})
builder.add_edge("tools", "agente") # dopo i tool, torna all'agente
graph = builder.compile()
result = graph.invoke({
"messages": [HumanMessage("Cerca il laptop e dimmi quanto costa con il 20% di sconto")]
})python
ToolNode è un nodo prebuilt di LangGraph che gestisce automaticamente l’esecuzione dei tool: analizza le tool_calls nell’ultimo messaggio, chiama la funzione corrispondente e aggiunge il risultato come ToolMessage allo State.
State management avanzato
Per applicazioni reali lo State deve fare di più del semplice accumulare messaggi. I reducer personalizzati permettono di definire esattamente come ogni campo viene aggiornato:
from typing import Annotated
import operator
def mantieni_ultimi_5(left: list, right: list) -> list:
"""Finestra mobile: mantieni solo gli ultimi 5 elementi."""
return (left + right)[-5:]
class State(TypedDict):
messages: Annotated[list, add_messages] # accumula messaggi
errori: Annotated[list, operator.add] # accumula errori (append)
risultati: Annotated[list, mantieni_ultimi_5] # finestra mobile
contatore: int # sostituzione semplice
stato_corrente: str # sostituzione semplicepython
I campi senza Annotated usano il comportamento di default: sostituzione completa. Se un nodo scrive {"contatore": 5}, il valore diventa 5 indipendentemente da cosa c’era prima. I campi con reducer invece combinano il valore precedente con quello nuovo secondo la funzione specificata.
Checkpoints e persistenza
I checkpoint sono snapshot completi dello State salvati dopo ogni step del grafo. Sono la fondazione di tre funzionalità critiche per la produzione: resumability (riprendi dopo un crash), branching (esplora percorsi alternativi), debugging (ispeziona lo stato di ogni step passato).
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.postgres import PostgresSaver # per la produzione
# Checkpointer in memoria (solo per sviluppo/test)
checkpointer = MemorySaver()
# In produzione: PostgreSQL o Redis
# checkpointer = PostgresSaver.from_conn_string("postgresql://...")
# Passa il checkpointer al momento della compilazione
graph = builder.compile(checkpointer=checkpointer)
# Il thread_id identifica la "sessione" — ogni thread ha la sua storia
config = {"configurable": {"thread_id": "sessione_utente_123"}}
# Prima chiamata
graph.invoke({"messages": [HumanMessage("Cerca il laptop")]}, config)
# Seconda chiamata — riprende automaticamente dal checkpoint precedente
graph.invoke({"messages": [HumanMessage("Quanto costa con il 15% di sconto?")]}, config)
# Ispeziona la storia completa degli step
for stato in graph.get_state_history(config):
print(stato.config["configurable"]["checkpoint_id"])
print(f"Step: {stato.metadata.get('step')}")
print(stato.values["messages"])python
Ogni coppia (grafo compilato, thread_id) definisce una "conversazione" persistente. Due chiamate allo stesso grafo con lo stesso thread_id si comportano come due turni della stessa sessione: la seconda vede tutto ciò che è successo nella prima.
Human-in-the-loop
Questa è probabilmente la funzionalità più differenziante di LangGraph rispetto a tutti gli altri framework.
Immagina un agente AI che gestisce acquisti online per conto dell’utente. Vuoi che lavori in autonomia per ordini piccoli, ma che chieda conferma per qualsiasi ordine sopra i 500€. Non vuoi bloccare il flusso — vuoi solo inserire un checkpoint di approvazione nel momento giusto.
LangGraph lo rende possibile con interrupt():
from langgraph.types import interrupt, Command
def nodo_approvazione(state: State) -> dict:
"""Pausa il grafo e chiede approvazione umana."""
# interrupt() serializza lo state, lo salva nel checkpointer
# e FERMA l'esecuzione del grafo. Chi ha chiamato invoke() riceve
# un'eccezione GraphInterrupt con il valore passato qui.
risposta_umana = interrupt({
"messaggio": "Approvare questo acquisto?",
"dettagli": state["acquisto_corrente"],
"importo": state["importo"]
})
# Quando l'umano risponde, l'esecuzione RIPRENDE esattamente qui
# risposta_umana contiene ciò che l'umano ha passato a Command(resume=...)
if risposta_umana["approvato"]:
return {"stato_acquisto": "approvato"}
else:
return {"stato_acquisto": "rifiutato", "motivo": risposta_umana["motivo"]}python
from langgraph.errors import GraphInterrupt
config = {"configurable": {"thread_id": "ordine_42"}}
# Avvia il grafo — si fermerà all'interrupt
try:
result = graph.invoke(initial_state, config)
except GraphInterrupt as e:
# e.value contiene i dati passati a interrupt()
print("In attesa di approvazione:", e.value)
# ... mostra i dettagli all'utente, aspetta la sua decisione ...
risposta = {"approvato": True} # o False, con motivo
# Riprendi passando la risposta come Command(resume=...)
# Il thread_id deve essere lo stesso! LangGraph recupera il checkpoint.
result = graph.invoke(
Command(resume=risposta),
config # stesso thread_id
)python
interrupt() funziona SOLO con un checkpointer configurato. Senza checkpoint il grafo non ha dove salvare lo state tra la pausa e la ripresa. In produzione usa PostgresSaver o RedisSaver — mai MemorySaver che perde tutto al restart.
Pattern human-in-the-loop
Ci sono quattro pattern principali per inserire il controllo umano in un flusso agentico:
- Review-before-action: il grafo mostra cosa sta per fare l’agente ("Sto per inviare questa email a 500 destinatari") e aspetta conferma. Ideale per azioni irreversibili.
- Edit-before-action: come review, ma l’utente può anche modificare i parametri dell’azione prima di approvarla ("Cambia il soggetto da X a Y, poi invia").
- Approval gates: in flussi multi-step con approvazione gerarchica — l’agente completa la bozza, passa al team lead per review, che poi passa al CFO per approvazione finale.
- Escalation: il grafo tenta di risolvere autonomamente; se non riesce dopo N tentativi, o se il contesto è ambiguo, interrompe e passa a un operatore umano con tutto il contesto.
Architetture multi-agent
LangGraph è la scelta naturale per sistemi multi-agent perché lo State condiviso e gli edge condizionali rendono la comunicazione tra agenti esplicita e tracciabile.
Il pattern più comune è il supervisor: un orchestratore che riceve le richieste e le smista ad agenti specializzati, raccogliendo i risultati.
from langchain_core.messages import SystemMessage
from langgraph.graph import MessagesState
import json
def agente_ricerca(state: MessagesState) -> dict:
"""Specialista: cerca informazioni online."""
# ... logica di ricerca con web tools ...
return {"messages": [AIMessage(content="Risultati ricerca: ...")]}
def agente_analisi(state: MessagesState) -> dict:
"""Specialista: analizza dati e trae conclusioni."""
# ... logica di analisi ...
return {"messages": [AIMessage(content="Analisi completata: ...")]}
def supervisor(state: MessagesState) -> dict:
"""Orchestratore: decide quale agente chiamare."""
sistema = """Sei un supervisor con accesso a questi agenti:
- ricerca: cerca informazioni online e su documenti
- analisi: analizza dati e produce conclusioni
- FINISH: quando il task è completato e puoi rispondere all'utente
Rispondi SOLO con JSON: {"next": "ricerca" | "analisi" | "FINISH", "ragione": "..."}"""
risposta = llm.invoke([SystemMessage(sistema)] + state["messages"])
dati = json.loads(risposta.content)
return {"messages": [risposta], "next": dati["next"]}
def router_supervisor(state: MessagesState) -> str:
return state["next"]
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor)
builder.add_node("ricerca", agente_ricerca)
builder.add_node("analisi", agente_analisi)
builder.add_edge(START, "supervisor")
builder.add_conditional_edges(
"supervisor", router_supervisor,
{"ricerca": "ricerca", "analisi": "analisi", "FINISH": END}
)
# Gli agenti specializzati tornano sempre al supervisor
builder.add_edge("ricerca", "supervisor")
builder.add_edge("analisi", "supervisor")python
Questo pattern è quello usato da ChipsBot: un orchestratore centrale che conosce le competenze di ogni agente specialista e smista le richieste ottimizzando per la risposta migliore.
Subgraphs: modularità
I subgraph permettono di costruire grafi riutilizzabili e di comporre sistemi complessi da blocchi più semplici. Un grafo compilato può essere usato come nodo in un altro grafo:
from typing import TypedDict, Annotated
from langgraph.graph import add_messages
class RagState(TypedDict):
messages: Annotated[list, add_messages]
context: str
def crea_sottografo_rag():
"""Restituisce un grafo RAG riutilizzabile."""
builder = StateGraph(RagState)
builder.add_node("retrieval", nodo_retrieval)
builder.add_node("generation", nodo_generation)
builder.add_edge(START, "retrieval")
builder.add_edge("retrieval", "generation")
builder.add_edge("generation", END)
return builder.compile()
# Il subgraph è un grafo compilato
rag_subgraph = crea_sottografo_rag()
# Lo usi come qualsiasi altro nodo nel grafo principale
main_builder = StateGraph(MainState)
main_builder.add_node("rag", rag_subgraph) # il subgraph è il nodo!
main_builder.add_node("post_processing", nodo_post_processing)
main_builder.add_edge(START, "rag")
main_builder.add_edge("rag", "post_processing")python
I subgraph hanno il proprio State interno — non condividono tutto con il grafo padre. Ci deve essere un meccanismo di mappatura tra i campi del grafo padre e quelli del subgraph. LangGraph gestisce questo con trasformazioni opzionali a livello di add_node.
LangGraph vs altri framework
LangGraph non è sempre la scelta giusta. Una comparazione onesta:
| Framework | Modello | State | H-i-t-L | Curva |
|---|---|---|---|---|
| LangGraph | Grafo diretto | ✅ Avanzato | ✅ Nativo | Alta |
| CrewAI | Team di ruoli | ❌ Limitato | ❌ No | Bassa |
| AutoGen | Conversazione | ✅ Medio | ⚠️ Parziale | Media |
| Anthropic SDK | Loop libero | ❌ Manuale | ❌ Manuale | Bassa |
LangGraph è la scelta giusta quando:
- Il flusso ha branch multipli e la logica di routing è importante
- Hai bisogno di human-in-the-loop in punti specifici del flusso
- L’agente è long-running e deve sopravvivere a crash (checkpoints)
- Stai costruendo un sistema multi-agent con coordinazione sofisticata
- Hai bisogno di auditability: ogni step è tracciabile e replayable
Usa qualcosa di più semplice quando:
- Il task è lineare senza branch: una chain LCEL è sufficiente
- Stai prototipando rapidamente: CrewAI è più veloce da configurare
- Usi solo Claude senza tool complessi: Anthropic SDK è più diretto
In produzione: LangGraph Platform
LangGraph Platform (disponibile in versione cloud e self-hosted) aggiunge l’infrastruttura necessaria per portare i grafi in produzione senza costruirla da zero:
- Persistence: PostgreSQL integrato per checkpoints, nessun setup manuale
- Streaming: SSE (Server-Sent Events) per aggiornamenti real-time step-per-step
- Async task queue: per agenti long-running (minuti, ore). Il client può disconnettersi e riconnettersi più tardi per vedere i risultati
- Cron scheduling: esecuzioni periodiche di grafi (es. monitoraggio notturno)
- API REST automatica: ogni grafo viene esposto come endpoint REST con documentazione OpenAPI generata automaticamente
- LangGraph Studio: interfaccia visuale per esplorare grafi, ispezionare State, testare step-by-step
Setup locale per sviluppo:
pip install langgraph-cli
# Crea un nuovo progetto LangGraph
langgraph new my-agent
# Avvia il server di sviluppo locale su :8123
langgraph dev
# Studio disponibile su https://smith.langchain.com/studio
# Collegati al server locale per debug visualebash
Il server locale espone automaticamente tre endpoint per ogni grafo compilato: /invoke (sincrono), /stream (SSE), /threads/{id}/state (ispezione State). LangGraph Studio si connette a questi endpoint per la visualizzazione interattiva.
Per approfondire
- Per capire le basi del framework su cui LangGraph si appoggia: LangChain — Il Framework per Applicazioni LLM
- Per i pattern di orchestrazione multi-agent e il contesto teorico degli agenti: Orchestrazione & Agenti
- Per connettere i nodi del grafo a strumenti esterni via protocollo standard: MCP — Model Context Protocol
- Per dare ai tuoi agenti accesso a conoscenza specifica senza fine-tuning: RAG — Retrieval Augmented Generation
- Per vedere un’implementazione reale di un sistema multi-agent in produzione: Caso Studio: ChipsBot