Un'applicazione che assegna una categoria a un ticket e una che prepara un acquisto da approvare possono usare lo stesso modello AI. Il lavoro attorno al modello cambia parecchio: nella prima basta ottenere un risultato valido; nella seconda bisogna ritrovare una proposta dopo ore, riconoscere chi l'ha approvata e gestire l'ordine inviato a un altro sistema.
È da questa differenza che partirei per scegliere fra LangChain, LangGraph, OpenRouter e Langfuse. Sono strumenti combinabili, con alcune sovrapposizioni. Adottarli tutti insieme significa anche mantenere tutte le loro integrazioni. Preferisco assegnare prima un compito a ogni livello e tenere fuori quello che ancora non serve.
A cosa servono i quattro strumenti
| Strumento | Responsabilità utile in questo confronto | Quando entra nella scelta |
|---|---|---|
| LangChain | Costruire un agente componendo modello, tool, prompt e middleware, cioè regole che intervengono durante l'esecuzione. | Il modello deve scegliere strumenti e proseguire il lavoro in più turni; integrazioni e regole ricorrenti giustificano un harness condiviso. |
| LangGraph | Orchestrare un processo come grafo: i nodi svolgono operazioni, gli archi stabiliscono il passaggio successivo e lo stato conserva i dati del lavoro. | Occorre controllare esplicitamente rami, attese e riprese del processo. |
| OpenRouter | Accedere a modelli attraverso un'API e controllare il routing verso i provider che li servono. | Servono più modelli o regole di accesso e instradamento da gestire in un punto comune. |
| Langfuse | Ricostruire le esecuzioni tramite trace, gestire prompt e valutare risultati. | Bisogna capire quale passaggio ha prodotto un errore, confrontare varianti o lavorare su un insieme di casi di valutazione. |
Un harness è il codice attorno al modello che ne governa l'uso: esegue i tool richiesti, restituisce i risultati e applica limiti e condizioni di arresto. Gli agenti LangChain usano LangGraph come runtime; LangGraph può essere utilizzato anche senza LangChain. Scegliere LangChain e progettare direttamente un grafo sono quindi due livelli diversi di controllo. LangChain overview, LangGraph overview.
Anche OpenRouter comprende più del gateway: il suo Agent SDK gestisce il ciclo modello-tool, approvazioni e stato persistibile. Langfuse si integra inoltre con applicazioni che non usano questi framework. La tabella orienta la scelta; nei due casi seguenti contano le responsabilità effettivamente assegnate. OpenRouter Agent SDK, Langfuse e OpenTelemetry.
Una categoria per un ticket: partirei dall'SDK diretto
Prendo un caso circoscritto: arriva un ticket, il modello propone una categoria fra quelle ammesse e l'applicazione la salva. Il percorso può stare nel servizio esistente:
- Il servizio legge il ticket autorizzato e prepara la richiesta.
- L'SDK del provider invia una chiamata al modello.
- Il codice valida la struttura ricevuta e controlla che la categoria esista.
- Salva il risultato accettato; un output invalido va alla revisione prevista dal prodotto.
La categoria può essere formalmente valida e comunque sbagliata: riconoscere quel caso richiede ticket con classificazioni di riferimento e un criterio di accettazione. Il controllo del formato e quello sul significato hanno lavori diversi.
In questa architettura terrei inizialmente fuori i quattro strumenti.
| Componente escluso | Motivo | Che cosa farebbe cambiare la scelta |
|---|---|---|
| LangChain | Il codice decide già l'unica chiamata; manca un ciclo agente da gestire. | Il modello deve consultare strumenti e decidere come proseguire, oppure si accumulano integrazioni e politiche comuni. |
| LangGraph | Il caso si conclude nella richiesta e non prevede una ripresa successiva. | Compaiono attese, rami e recuperi che il processo deve conservare. |
| OpenRouter | Il provider è già scelto e il suo SDK soddisfa l'accesso richiesto. | Serve confrontare più modelli o centralizzare regole di routing. |
| Langfuse | I log esistenti rispondono ancora alla domanda diagnostica iniziale. | Occorre correlare passaggi, versioni dei prompt e risultati su molti casi. |
Conserverei l'identificativo del ticket, il modello richiesto, la fase dell'operazione, la durata, l'esito e il dettaglio dell'errore. Questi record aiutano a distinguere un problema nella chiamata da uno nella validazione o nel salvataggio. Nel classificatore affido al servizio applicativo il limite di tempo e il numero massimo di tentativi. Anche i retry dell'SDK devono rientrare in quel budget; se non sono controllabili, li disabilito e li gestisco nel servizio. Quando il budget finisce, il ticket passa alla revisione prevista.
Questa partenza segue un criterio che condivido nell'analisi di Anthropic: usare la soluzione più semplice adatta al compito e aggiungere complessità quando risolve un bisogno riconoscibile. Building effective agents.
Un acquisto che aspetta una decisione: rendere esplicito il processo
Ora il requisito cambia. L'applicazione raccoglie dati autorizzati, prepara una proposta d'acquisto e attende un responsabile. Una proposta incompleta torna alla preparazione; una rifiutata si chiude; una approvata può diventare un ordine. L'attesa può durare ore e il servizio può riavviarsi nel frattempo.
Qui sceglierei LangGraph se voglio rappresentare questi passaggi e i relativi rami nel codice del workflow. L'architettura proposta è questa:
- Preparazione. Raccoglie i dati e produce una proposta con versione, importo e destinatario.
- Validazione. Controlla i requisiti dell'acquisto. Se mancano dati, torna alla preparazione con gli errori da correggere.
- Approvazione. Conserva la proposta e sospende il processo. L'interfaccia mostra al responsabile la versione precisa su cui decide; il backend autentica l'utente e verifica che possa approvarla.
- Ripresa. Ricarica lo stesso caso. Un rifiuto chiude la pratica; una proposta modificata richiede una nuova approvazione.
- Invio dell'ordine. Un nodo separato ricontrolla i permessi, invia l'azione approvata e registra l'identificativo restituito dal sistema esterno. Se l'esito è incerto, passa a una riconciliazione prima di tentare un nuovo ordine.
Nell'acquisto il workflow conserva nello stato la scadenza del caso e i tentativi già consumati. Ogni nodo e client lavora entro il budget rimasto; un errore non lo azzera alla ripresa. All'esaurimento il caso passa alla revisione manuale. L'esito incerto dell'ordine segue il percorso di riconciliazione, anziché autorizzare un nuovo invio.
In LangGraph il checkpointer salva gli snapshot dello stato del singolo processo, identificato da un thread_id. Per ritrovarlo dopo un riavvio serve uno storage durevole: InMemorySaver conserva i dati soltanto nella RAM del processo. Questo riguarda l'uso della libreria; Agent Server gestisce l'infrastruttura di persistenza. Uno Store, invece, serve a conservare dati applicativi fra thread: non occorre aggiungerlo per la sola attesa di questo caso. Persistence.
L'interruzione si esprime con interrupt(); la ripresa usa Command(resume=...) sullo stesso thread. Il nodo interrotto riparte dall'inizio, perciò tengo l'invio dell'ordine nel nodo successivo all'approvazione. L'azione esterna richiede comunque una chiave stabile di idempotenza: ripetere la stessa richiesta deve permettere di recuperare l'ordine già creato, senza crearne un secondo. Se il destinatario non supporta questo contratto, va progettata una procedura di riconciliazione. Interrupts.
Resta un punto da provare: l'ordine può essere stato accettato mentre il salvataggio locale fallisce. Nel grafo lo stato risulta incompleto, nel sistema esterno l'acquisto esiste già. Separare i nodi rende leggibile il passaggio; la gestione di quell'esito incerto appartiene all'integrazione con il destinatario.
Quando terrei fuori anche LangGraph
Una sola approvazione prima di un tool può essere gestita dall'Agent SDK di OpenRouter con requireApproval e uno StateAccessor collegato a un backend persistente. I tool deferred possono attendere un sistema esterno e ricevere il risultato da un altro processo. Il solo “devo aspettare una persona” non basta a scegliere un grafo. Tool approval e stato, Deferred tools.
Se quel loop soddisfa già il processo, lo terrei. Per la proposta con correzioni, rifiuti e riconciliazione sceglierei il grafo quando rende più chiaro chi controlla ogni transizione. Se l'applicazione ha già una macchina a stati persistente adeguata, valuterei prima di estenderla: introdurre LangGraph significa anche mantenerne modello di stato, dipendenze e politiche di recupero.
Dentro un nodo una chiamata diretta può bastare. LangChain diventa pertinente se proprio quel passaggio richiede un agente con tool; OpenRouter può fornire l'accesso ai modelli; Langfuse può osservare il percorso. Non li aggiungerei per completare lo stack.
LangChain: delegare il ciclo agente e le integrazioni
Il caso interessante per LangChain è un assistente che consulta più strumenti e decide quali usare in base alle risposte. L'harness esegue il ciclo, mentre i middleware permettono di intervenire su chiamate, tool, limiti e retry. Middleware.
La scelta ha senso quando sostituisce integrazioni e regole ripetute con un comportamento che il team sa leggere e modificare. Per una breve sequenza fissata dal codice, scrivere quelle funzioni direttamente può restare più comprensibile.
Controllerei soprattutto dove finiscono i retry. Se il workflow ripete un nodo, il client ritenta la chiamata e il tool ripete l'azione esterna, un solo errore può generare tentativi a più livelli. Assegnerei al task un budget complessivo e renderei visibile ogni tentativo, compresi quelli falliti. Il contratto del tool rimane decisivo: input, risultato e autorizzazioni sono approfonditi nella guida a MCP.
OpenRouter: decidere separatamente modello, provider e fallback
Un modello può essere servito da più provider. Nel routing di OpenRouter scelgo quindi sia il modello sia le condizioni per raggiungere chi lo esegue. Il campo provider.only limita i provider ammessi; order esprime una preferenza. Con allow_fallbacks attivo, il servizio può cercare altri provider oltre a quelli preferiti. Provider routing.
Il fallback fra modelli è un'altra decisione: una lista models autorizza a provare modelli alternativi dopo un errore. Il modello effettivamente usato compare nella risposta. Per un servizio può essere una scelta di disponibilità; per un confronto fra modelli può cambiare proprio il modello che si intende valutare. Model fallbacks.
Per questo separerei i due permessi nella configurazione e nei dati dell'esecuzione. Se una capacità è indispensabile, provider.require_parameters: true esclude i provider che non supportano i parametri richiesti. Il supporto del parametro resta distinto dalla correttezza della risposta.
Prima dell'adozione seguirei anche il percorso dei dati. La policy di OpenRouter e quella del provider sono due verifiche diverse; l'assenza di logging dei contenuti non significa assenza di metadati conservati. Un gateway aggiunge un punto comune di configurazione e un servizio da includere nella diagnosi degli errori. Data collection.
Langfuse: ricostruire un errore e poi valutarne l'effetto
Una trace collega le operazioni di una richiesta: chiamata al modello, ricerca di documenti, tool e passaggi applicativi. Nel caso dell'acquisto vorrei vedere quale proposta è stata approvata, quale tentativo ha raggiunto il sistema esterno e dove è andato perso l'esito. Questo è il motivo per introdurre osservabilità strutturata. Langfuse observability.
La valutazione risponde a una domanda ulteriore. Nel classificatore, per esempio, una trace senza errori può contenere la categoria sbagliata. In Langfuse posso collegare valutazioni manuali o automatiche a casi ed esecuzioni, ma devo scegliere il criterio: corrispondenza con una categoria di riferimento, completezza della proposta, rispetto dell'approvazione. Evaluation.
Prima di esportare dati sceglierei cosa raccogliere e per quanto conservarlo. Il masking rimuove o sostituisce dati prima dell'esportazione. Va controllato sul percorso effettivo: nella documentazione Python corrente mask_otel_spans è raccomandato per i nuovi setup, mentre il precedente mask ha una copertura diversa. L'hook non modifica gli altri percorsi di esportazione né tutte le proprietà di uno span, cioè di una singola operazione tracciata: dati sensibili presenti altrove richiedono controlli separati. Userei valori sintetici riconoscibili per controllare input, output, metadati ed errori prima che escano dall'applicazione. Masking.
Anche l'hosting pesa nella scelta. Langfuse Cloud è gestito dal fornitore; con il self-hosting il team si prende in carico infrastruttura, aggiornamenti e conservazione. La configurazione Docker Compose presentata per test locali non offre da sola alta disponibilità e backup. Self-hosting.
La prova con cui sceglierei l'architettura dell'acquisto
Prima di accettare la proposta progettuale, farei passare il candidato e l'alternativa più semplice attraverso lo stesso caso: una proposta resta in attesa, il worker viene arrestato, un nuovo processo riprende la pratica e riceve due volte la stessa approvazione. Il risultato atteso è la versione corretta, autorizzata dal responsabile, con un solo ordine identificabile.
Fisserei anche modalità di durabilità dei checkpoint e garanzie del backend: scrivere in uno storage durevole e confermare il salvataggio prima di avanzare sono decisioni collegate. Le modalità di LangGraph le distinguono esplicitamente. Checkpointers.
Aggiungerei un arresto dopo l'accettazione dell'ordine esterno e prima del salvataggio locale. È il punto che distingue una ripresa dimostrata durante l'attesa dal recupero di un'azione dall'esito incerto. In un caso separato modificherei la proposta durante l'attesa e consegnerei l'approvazione della versione precedente: nessun ordine deve partire finché quella corrente non viene approvata.
Se entrambe le architetture soddisfano questi requisiti, la ripresa non è più il motivo per aggiungere un livello. La scelta si sposta su quanto sia leggibile quel processo e sul lavoro necessario per mantenerlo quando cambiano approvazioni, rami e integrazioni.

