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-dotenvCrea un file .env:
ANTHROPIC_API_KEY=sk-ant-...
TAVILY_API_KEY=tvly-... # gratis fino a 1000 query al mese2) 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.pyCosa 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.
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 fermatoCombinato 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_messagescon strategiatrimmerper 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.



