Costruire un agente AI vuol dire mettere in piedi una sequenza di passaggi in cui il modello chiama strumenti, valuta il risultato, decide come continuare e — quando serve — chiede aiuto a un umano. Se questo flusso lo scrivi a forza di if e while ti accorgi in fretta che diventa illeggibile e fragile. LangGraph è il framework di LangChain che modella tutto questo come grafo: ogni nodo è una funzione, ogni arco una transizione condizionale. La versione 1.1.6 rilasciata il 10 aprile 2026 aggiunge persistenza durevole, checkpoint via SQLite/PostgreSQL, time-travel e human-in-the-loop nativo. In questa guida costruiremo passo passo un agente che fa ricerca documentale, ragiona, chiama strumenti e si ferma quando deve.

A chi serve, cosa otterrai, prerequisiti

Serve a chi ha già scritto un primo prototipo di agente con OpenAI/Claude function calling e si è accorto che fa fatica a gestire: il ramo \"l'utente vuole rilanciare la domanda\", la possibilità di tornare indietro di due passi, l'interruzione per chiedere conferma a un umano, la ripartenza di una sessione dopo un crash. Otterrai un agente di esempio che riceve una domanda di ricerca, sceglie tra web search e vector store interno, valuta se la risposta è abbastanza completa o se serve un altro giro, e quando è incerto chiede a un umano di confermare l'output prima di restituirlo.

Prerequisiti tecnici: Python 3.10+; un editor con cui sei a tuo agio; una chiave API valida (gli esempi usano Anthropic ma il codice gira anche con OpenAI cambiando una riga); 30 minuti di tempo. Costo: 1-2 dollari di token sui modelli premium per fare gli esercizi, zero se userai Qwen via Ollama.

1) Installa l'ambiente

python -m venv .venv
source .venv/bin/activate   # su Windows: .venv\\Scripts\\activate
pip install "langgraph>=1.1" "langchain>=0.3" langchain-anthropic langchain-openai langchain-community tavily-python python-dotenv

Crea un file .env:

ANTHROPIC_API_KEY=sk-ant-...
TAVILY_API_KEY=tvly-...   # gratis fino a 1000 query al mese

2) Capisci i tre concetti fondamentali

LangGraph si basa su tre cose. Stato: un oggetto Python tipizzato (un TypedDict) che attraversa tutto il grafo e viene aggiornato dai nodi. Nodo: una funzione che prende lo stato e restituisce un aggiornamento parziale. Arco condizionale: una funzione che, letto lo stato, restituisce il nome del prossimo nodo da eseguire.

3) Il primo grafo: agente con un solo strumento

Crea agent.py:

from typing import Annotated, TypedDict, Sequence
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import BaseMessage, ToolMessage, HumanMessage
from langchain_community.tools.tavily_search import TavilySearchResults
import os

from dotenv import load_dotenv; load_dotenv()

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]

search = TavilySearchResults(max_results=3)
model = ChatAnthropic(model="claude-sonnet-4-6-20250101").bind_tools([search])

def call_model(state: AgentState):
    return {"messages": [model.invoke(state["messages"]) ]}

def call_tool(state: AgentState):
    last = state["messages"][-1]
    outs = []
    for call in last.tool_calls:
        out = search.invoke(call["args"])
        outs.append(ToolMessage(content=str(out), tool_call_id=call["id"]))
    return {"messages": outs}

def should_continue(state: AgentState):
    return "tools" if state["messages"][-1].tool_calls else END

graph = StateGraph(AgentState)
graph.add_node("model", call_model)
graph.add_node("tools", call_tool)
graph.add_edge(START, "model")
graph.add_conditional_edges("model", should_continue, {"tools":"tools", END:END})
graph.add_edge("tools", "model")

app = graph.compile()

if __name__ == "__main__":
    out = app.invoke({"messages":[HumanMessage(content="Chi ha vinto il GP F1 di Imola 2026?")]})
    print(out["messages"][-1].content)

Esegui:

python agent.py

Cosa succede sotto il cofano: il grafo entra in model, Claude vede la domanda e produce una tool_call verso Tavily (web search); l'arco condizionale should_continue instrada a tools; il nodo tools esegue la ricerca e mette il risultato come ToolMessage; si torna a model, Claude legge il risultato e questa volta risponde senza chiamare strumenti; should_continue finisce a END.

In LangGraph ogni nodo del grafo è una funzione che legge e scrive lo stato dell'agente.

4) Aggiungi persistenza: l'agente non perde la memoria

Per rendere lo stato durabile (sopravvive a un crash, riprende dove si era fermato) basta un checkpointer. Cambia la compilazione:

from langgraph.checkpoint.sqlite import SqliteSaver

memory = SqliteSaver.from_conn_string("checkpoints.sqlite")
app = graph.compile(checkpointer=memory)

config = {"configurable": {"thread_id": "sessione-utente-42"}}
out = app.invoke({"messages":[HumanMessage(content="Ricorda che mi chiamo Andrea.")]}, config)
out2 = app.invoke({"messages":[HumanMessage(content="Come mi chiamo?")]}, config)
print(out2["messages"][-1].content)   # "Andrea"

Ogni thread_id è una conversazione persistente. Se chiudi il programma e lo riapri con lo stesso thread_id, l'agente ricorda. Per produzione usa PostgresSaver al posto di SQLite.

5) Branching condizionale piu' complesso

Aggiungiamo una valutazione: dopo la risposta del modello, un critic decide se la risposta è soddisfacente o se serve un altro giro di ricerca.

def critic(state: AgentState):
    last = state["messages"][-1].content
    judge = ChatAnthropic(model="claude-haiku-4-5-20251001").invoke([
        HumanMessage(content=f"La risposta sotto contiene dati specifici e fonti? Rispondi solo SI o NO.\n\n{last}")
    ]).content
    return {"messages":[]}, judge

def route_after_model(state: AgentState):
    if state["messages"][-1].tool_calls: return "tools"
    _, judge = critic(state)
    return END if "SI" in judge.upper() else "model"

graph.add_conditional_edges("model", route_after_model, {"tools":"tools","model":"model", END:END})

Ora se il critic giudica la risposta povera, il grafo torna a \"model\" che, vedendo lo storico, rilancia con un'altra chiamata a strumento. Con un haiku al posto del Sonnet il giudice costa il 90% in meno e gira in 200 ms.

6) Human-in-the-loop: ferma l'agente prima di un'azione delicata

Per certe azioni (mandare una mail, fare un pagamento, scrivere su un database) vuoi una conferma umana. LangGraph 1.1 lo gestisce con interrupt_before:

app = graph.compile(checkpointer=memory, interrupt_before=["tools"])

result = app.invoke({"messages":[HumanMessage(content="Manda una mail a mario@example.com")]}, config)
# il grafo si ferma prima di tools
print("L'agente sta per fare:", app.get_state(config).next)

# Approvazione manuale:
confirm = input("Confermi? [s/n] ")
if confirm == "s":
    app.invoke(None, config)   # riprende da dove si era fermato

Combinato con i checkpoint, questo significa che puoi mostrare a un utente in interfaccia web ciò che l'agente sta per fare, fargli premere un bottone Approva/Rifiuta, e proseguire o rifiutare.

7) Time-travel: torna indietro in una conversazione

LangGraph salva tutti gli stati intermedi. Per riportare l'agente a uno snapshot precedente:

history = list(app.get_state_history(config))
for s in history:
    print(s.config, len(s.values["messages"]), "msgs")

# riparti dal terzultimo stato
three_ago = history[2]
app.update_state(three_ago.config, {"messages":[HumanMessage(content="Rifrasala in modo piu formale.")]})
app.invoke(None, three_ago.config)

È utilissimo per debug e per esperimenti A/B in qa.

Errori comuni e soluzioni

  • \"Recursion limit reached\": il grafo ha trovato un loop tools↔model. Aggiungi graph.compile(recursion_limit=12) e logga ogni iterazione per capire dove gira a vuoto.
  • \"Tool not bound\": hai dimenticato model.bind_tools([...]). Senza il bind, il modello non sa che esiste lo strumento e risponderà a parole.
  • Errore SQLite \"database is locked\" in produzione: passa a PostgreSQL con PostgresSaver.from_conn_string(\"postgresql://...\").
  • Stato che cresce a dismisura: usa add_messages con strategia trimmer per tagliare la storia oltre N token, oppure introduci un nodo di sommarizzazione periodica.

Quando NON usare LangGraph

Tre casi in cui non vale la pena. Primo, agente single-tool single-step: per \"prendi questa domanda, fai una ricerca web, rispondi\" basta create_react_agent di LangChain o direttamente la function calling del modello. Secondo, automazione semplice fra app SaaS: per \"quando arriva una mail con allegato, salva su Drive e mandami una notifica\" è meglio n8n o Make. Terzo, prototipi rapidi che cambiano tutti i giorni: una pipeline LangGraph richiede un minimo di disciplina sullo stato; per i 5 giorni di POC va bene anche un singolo file con while.

Alternative e come proseguire

Alternative principali sono AutoGen di Microsoft (più orientato a multi-agente con dialoghi tra agenti), CrewAI (sintassi più semplice, meno potente sullo stato), OpenAI Agents SDK (ottimo se vivi nell'ecosistema OpenAI e usi Managed Runs). LangGraph è oggi la scelta più robusta per casi enterprise dove serve durabilità, audit log, branching complesso e gestione fine dello stato.

Per andare avanti: la guida ufficiale docs.langchain.com ha pattern pronti (Plan-and-Execute, Reflexion, Multi-Agent Hierarchies); il repository github.com/langchain-ai/langgraph include una cartella examples/ con notebook in 20 minuti ciascuno; per il deploy in produzione c'è LangGraph Cloud di LangChain (managed) oppure il container open source con langgraph up. Una volta capiti i quattro pattern di base — stato condiviso, nodo, arco condizionale, checkpointer — costruire qualunque agente complesso diventa una questione di scegliere quanti nodi mettere fra START e END, e dove inserire i punti di controllo umano.