Salta al contenuto
Torna alla home
2026-08-1712 min di lettura
AI workflowsdurable stateretriesaudit trailsevalsorchestration

Motori di workflow AI per sistemi durevoli e auditabili

Design pratico per motori di workflow AI: stato persistente, recovery deterministica, retry, audit trail, eval riproducibili e costi sotto controllo.

Perché conta

Il primo sistema che ho costruito attorno a un large language model sembrava abbastanza semplice: ricevere una richiesta, chiamare un model, chiamare un tool, riassumere il risultato, restituire JSON. Funzionava nei test manuali perché ogni passaggio terminava in un unico processo, su una sola macchina, mentre osservavo i log. La produzione ha reso visibile il contratto nascosto. Una chiamata al model è andata in timeout dopo che un tool aveva già modificato lo stato. Un retry ha prodotto una risposta diversa. Un revisore umano ha approvato un branch, ma l’approvazione non era collegata al prompt esatto e al contesto recuperato che l’avevano prodotta. Una successiva revisione dei costi non riusciva a spiegare perché una richiesta avesse usato cinque volte i token attesi.

È stato il punto in cui ho smesso di trattare un AI workflow engine come un livello di comodità e ho iniziato a trattarlo come parte del perimetro di affidabilità. Il compito centrale non era creare un diagramma a grafo. Il compito centrale era preservare abbastanza stato persistente da poter eseguire una recovery deterministica, auditare ogni transizione, riprodurre le eval e mantenere i costi sotto controllo.

In questo articolo uso “workflow engine” in un senso ingegneristico ristretto: un componente che coordina step, branch, retry, transizioni di stato, chiamate a tool, chiamate a model e decisioni umane nel tempo. Una library può aiutare, e ho usato pattern di orchestrazione basati su graph quando erano adatti. Tengo una nota separata su LangGraph workflow orchestration perché l’esecuzione a grafo si mappa bene su applicazioni AI con branch. Tuttavia, il design persistente conta più del framework specifico.

Due riferimenti esterni influenzano il modo in cui ragiono su questo tema. Temporal descrive la durable execution come la preservazione dello stato di esecuzione affinché le applicazioni possano recuperare dai guasti senza perdere avanzamento, e quel modello è vicino a ciò che voglio quando un processo AI long-running attraversa confini di processo o di macchina: Temporal durable execution. LangGraph documenta la persistenza tramite checkpointers che salvano lo stato del graph agli step di esecuzione, una primitiva utile per lo stato conversazionale e per agent con branch: LangGraph persistence.

Cosa si rompe in produzione

La modalità di guasto che vedo più spesso non è un outage totale. È avanzamento parziale senza un record affidabile. Una chiamata a un tool riesce, ma il processo crasha prima che il layer di orchestrazione registri lo stato successivo. Un retry parte da uno step precedente e chiama di nuovo lo stesso sistema esterno. La risposta finale può sembrare valida, ma la state machine si è discostata dalla realtà.

La seconda modalità di guasto è una recovery non deterministica. I sistemi AI spesso includono output stocastici dei model, retrieval sensibile al tempo, tool mutabili e prompt che cambiano. Se riprovo “eseguendo di nuovo la funzione”, potrei non stare recuperando. Potrei stare creando una nuova esecuzione che somiglia soltanto all’originale. Questa distinzione conta quando un workflow approva un rimborso, aggiorna un ticket, redige contenuto regolamentato o instrada una richiesta di un cliente.

La terza modalità di guasto è il branching invisibile. Un workflow che chiede a un model di scegliere tra “recupera più contesto”, “chiama tool” e “rispondi ora” è già un workflow con branch. Se quella scelta di branch è presente solo nei log, la qualità dell’audit è scarsa. Voglio che la decisione di branch sia registrata come dato: stato di input, motivo della decisione, edge selezionato, configurazione del model, versione del prompt e stato successivo.

La quarta modalità di guasto è l’amplificazione dei retry. Un timeout vicino alla fine di uno step può far ripetere al sistema chiamate costose al model o duplicare chiamate a tool. Senza idempotency key, checkpoint a livello di step e contabilità dei costi, una retry policy può diventare un moltiplicatore di costi. Non mi serve un incidente esotico per giustificare questo design. Una singola dipendenza lenta può bastare.

La quinta modalità di guasto è la deriva delle eval. Se lo stato del workflow non include la versione del prompt, l’identificatore dello snapshot di retrieval, l’identificatore del model, la versione del tool e il branch path, allora un risultato di valutazione successivo è difficile da confrontare. Le eval riproducibili hanno bisogno di input stabili, o almeno di un record preciso delle parti instabili.

Il design che ha retto

Il design che ha retto per me è stato una state machine persistente con record di step espliciti. La state machine non doveva essere complessa, ma doveva essere noiosa. Ogni transizione aveva un nome, un input envelope, un output envelope, un error envelope e un checkpoint persistente. Il workflow poteva ramificare, ma ogni branch era rappresentato come dato invece di essere nascosto nel control flow.

Un record di step semplificato aveva questo aspetto:

{
  "workflow_id": "wf_2026_001",
  "run_id": "run_01",
  "step_id": "retrieve_context_02",
  "attempt": 1,
  "status": "succeeded",
  "started_at": "2026-08-17T10:15:30Z",
  "finished_at": "2026-08-17T10:15:34Z",
  "input_ref": "blob://inputs/retrieve_context_02.json",
  "output_ref": "blob://outputs/retrieve_context_02.json",
  "error_ref": null,
  "prompt_version": null,
  "model": null,
  "tool_name": "knowledge_search",
  "tool_version": "2026-08-01",
  "idempotency_key": "wf_2026_001:retrieve_context_02:attempt_1"
}

Preferisco riferimenti per payload grandi e hash per l’integrità. La riga del database resta leggibile, mentre prompt completo, passaggi recuperati, risposta del model e output del tool restano disponibili. Per sistemi sensibili, separo anche i metadati di audit dal contenuto che può richiedere retention o controllo degli accessi più rigorosi.

Lo stato del workflow era un documento compatto:

{
  "workflow_id": "wf_2026_001",
  "status": "waiting_for_human_review",
  "current_node": "human_review",
  "branch_path": [
    "classify_intent",
    "retrieve_context",
    "draft_answer",
    "policy_check",
    "human_review"
  ],
  "facts": {
    "intent": "billing_question",
    "risk_level": "medium",
    "requires_human": true
  },
  "cost": {
    "model_input_tokens": 4280,
    "model_output_tokens": 812,
    "tool_calls": 3
  }
}

Questa separazione mi ha dato tre proprietà utili.

Primo, lo stato persistente era esplicito. Potevo fermare il worker tra due step e riprendere dall’ultima transizione committed. Se il processo moriva durante uno step, il worker successivo poteva ispezionare l’ultimo record di step, decidere se il side effect fosse sicuro da riprovare e continuare secondo policy.

Secondo, la recovery deterministica è diventata un obiettivo di design invece che un desiderio. Non ho provato a rendere deterministico ogni output del model. Ho invece deciso quali risultati dovevano essere rigiocati e quali riutilizzati. Per le chiamate al model completate, ho memorizzato la risposta e l’ho riutilizzata durante la recovery. Per le chiamate fallite prima dell’arrivo di qualsiasi risposta, ho permesso un nuovo tentativo sotto una retry policy registrata. Per i side effect esterni, ho richiesto un idempotency key o un percorso di riconciliazione manuale.

Terzo, i costi sotto controllo sono diventati parte della state machine. Ho tracciato conteggi di token e chiamate a tool a livello di step, poi ho valutato i budget prima dei branch costosi. Se un workflow aveva già speso gran parte del budget, la transizione successiva poteva scegliere un percorso più economico, richiedere review o fermarsi con una risposta parziale. Il costo non era solo una dashboard a posteriori; era un input dell’orchestrazione.

L’engine non doveva possedere ogni responsabilità. Ho continuato a usare code, storage, tracing e application code normali. OpenTelemetry definisce uno span come un’unità di lavoro o operazione, e trovo utile quel modello di span per osservare gli step del workflow senza rendere il tracing la fonte di verità: OpenTelemetry traces. Lo store persistente del workflow restava autoritativo perché le trace possono essere campionate, scartate o conservate con una policy diversa rispetto allo stato di business.

Branch, retry e pause umane

Un workflow AI con branch ha bisogno di un contratto più rigoroso rispetto a una normale chiamata di funzione. Ogni node deve dichiarare se è pure, retryable, idempotent o se richiede reconciliation. Ho usato una piccola classificazione:

pure: no external side effect; safe to recompute if inputs are stable
recorded: nondeterministic output stored after success; replay uses stored output
idempotent_side_effect: external call protected by an idempotency key
non_idempotent_side_effect: retry disabled; manual reconciliation required
human_gate: pauses until an authenticated decision is recorded

Le etichette erano semplici, ma forzavano conversazioni di design utili. Uno step di retrieval era spesso “recorded” perché l’indice poteva cambiare. Una chiamata al model era “recorded” perché l’output contava più del replay teorico. Un pagamento, aggiornamento di ticket, invio email o cambio di permessi non era mai casuale. Richiedeva un idempotency key o un branch del workflow che si fermasse per la riconciliazione.

La review umana era un altro punto in cui lo stato persistente contava. Non volevo che un revisore approvasse “la draft corrente” se la draft poteva cambiare dopo l’approvazione. Il task di review doveva fare riferimento a un output di draft immutabile, alla versione del prompt che l’aveva prodotto, al risultato del policy check e al contesto visibile. Il record di approvazione diventava quindi una transizione nel workflow, non un commento in un sistema separato.

Anche i retry sono diventati più piccoli. Invece di ritentare l’intero workflow, ho ritentato il node fallito secondo la sua classificazione. Questo ha ridotto il lavoro duplicato e ha reso i guasti più facili da ispezionare. Un retrieval fallito non invalidava una classificazione completata. Uno step finale di formattazione fallito non richiedeva un nuovo policy check a meno che la dipendenza dati non fosse cambiata.

Come lo verifico

Verifico un AI workflow engine a tre livelli: test di transizione, test di recovery ed evaluation run.

I test di transizione controllano che ogni node legga uno stato noto e scriva uno stato successivo atteso. Questi test non sono test di qualità del model. Sono test della state machine. Stubbo gli output di model e tool, poi verifico selezione dei branch, transizioni di stato, aggiornamenti dei costi e record di audit.

Un test minimale in stile Python ha questo aspetto:

def test_policy_failure_routes_to_human_review():
    state = {
        "workflow_id": "wf_test",
        "current_node": "policy_check",
        "facts": {"risk_level": "high"},
        "cost": {"model_input_tokens": 1000, "model_output_tokens": 200}
    }
    output = {
        "allowed": False,
        "reason": "requires specialist approval"
    }

    next_state = apply_policy_check(state, output)

    assert next_state["current_node"] == "human_review"
    assert next_state["status"] == "waiting_for_human_review"
    assert next_state["branch_path"][-1] == "human_review"

I test di recovery sono più importanti. Inietto crash dopo le scritture persistenti, prima delle scritture persistenti e dopo le chiamate esterne. Per ogni guasto iniettato, mi aspetto uno di tre esiti: riprendere dall’ultimo stato committed, ritentare lo step corrente in modo sicuro, oppure fermarsi per riconciliazione. Se un test non riesce a spiegare quale di questi esiti debba verificarsi, il workflow è sottospecificato.

Le evaluation run usano input di workflow registrati e branch path registrati. Tengo l’eval harness separato dall’esecuzione di produzione, ma lo alimento con record dalla forma produttiva. Questo mi dà eval riproducibili senza fingere che ogni sistema upstream sia immutabile. Il record di eval include versione del prompt, identificatore del model, output dei tool o riferimenti di retrieval, flag di policy, outcome atteso, outcome osservato e cost envelope.

I fallimenti di eval più utili non sono solo risposte sbagliate. Cerco anche branch sbagliati, uso eccessivo di tool, citazioni mancanti, bypass di policy e violazioni di budget. È qui che workflow design e valutazione del model si incontrano. Se una risposta è accettabile ma il percorso ha richiesto chiamate costose non necessarie, il workflow richiede ancora lavoro.

Cosa non rifarei

Non nasconderei l’orchestrazione dentro funzioni applicative annidate. Quel pattern sembra veloce all’inizio e diventa difficile da recuperare più avanti. Se un workflow ha branch, retry e side effect esterni, voglio che la state machine sia visibile nei dati.

Non userei i log come audit trail. I log sono utili per la diagnosi, ma non sono un ledger stabile del workflow. L’audit trail ha bisogno di identificatori persistenti, transizioni esplicite, riferimenti ai payload, identità dell’attore per le decisioni umane e una retention policy coerente con il rischio dell’applicazione.

Non ritenterei step non deterministici con leggerezza. Una seconda chiamata al model non è lo stesso evento della prima chiamata al model. Può essere accettabile, ma dovrebbe essere registrata come un secondo attempt con il proprio output, costo e motivo.

Non lascerei il costo fuori dal workflow. Il controllo dei costi non è solo analisi di billing. Incide sulla scelta dei branch, sulla retry policy e sulle condizioni di stop. Un workflow che può spendere senza registrare il motivo è incompleto.

Non inizierei con una decisione massimalista sul framework. Preferisco partire dagli invarianti: stato persistente, recovery deterministica, eval riproducibili e costi sotto controllo. Dopo scelgo l’engine o la library che rende più facile mantenere questi invarianti. A volte è un graph framework. A volte è una piattaforma di durable execution. A volte è una piccola state machine supportata da un database e una queue.

Il design persistente è la parte che mi aspetto di mantenere. Model provider, formati di prompt e library di orchestrazione cambiano. La necessità di sapere cosa è successo, riprendere in sicurezza, valutare in modo ripetibile e limitare i costi è rimasta stabile in ogni workflow AI serio che ho costruito.

FAQ

Cosa dovrebbe persistere un AI workflow engine per una recovery affidabile?

Persisto record di step espliciti con un nome di transizione, input envelope, output envelope, error envelope e checkpoint persistente. Per payload più grandi preferisco riferimenti e hash, così prompt, passaggi recuperati, risposte del model e output dei tool restano disponibili senza rendere illeggibile la riga principale del database.

Come gestisco le chiamate non deterministiche al model durante la recovery?

Non provo a rendere deterministico ogni output del model. Per le chiamate al model completate, memorizzo la risposta e la riutilizzo durante la recovery. Per le chiamate fallite prima dell’arrivo di qualsiasi risposta, permetto un nuovo tentativo sotto una retry policy registrata, con il proprio output, costo e motivo.

Perché le decisioni di branch dovrebbero essere memorizzate come dati del workflow?

Una scelta del model tra recuperare più contesto, chiamare un tool o rispondere è già un branch. Voglio che quella decisione sia registrata come dato: stato di input, motivo della decisione, edge selezionato, configurazione del model, versione del prompt e stato successivo. Se il branch esiste solo nei log, la qualità dell’audit è scarsa.

Come dovrebbero essere delimitati i retry in un workflow AI persistente?

Ritento il node fallito secondo la sua classificazione invece di ritentare l’intero workflow. Un node può essere pure, recorded, un side effect idempotent, un side effect non-idempotent o un human gate. Questo riduce il lavoro duplicato e rende i guasti più facili da ispezionare.

Come rendo riproducibili le valutazioni del workflow?

Uso input di workflow registrati e branch path registrati, con un record di eval che include versione del prompt, identificatore del model, output dei tool o riferimenti di retrieval, flag di policy, outcome atteso, outcome osservato e cost envelope. Questo mi dà eval riproducibili senza fingere che ogni sistema upstream sia immutabile.

Condividi questo articolo

Articoli correlati

  • Lo stato come API: LangGraph dopo tre riscritture

    Lo schema di stato è la scelta chiave in LangGraph: tre iterazioni sul modello e perché i canali con reducer sono la primitiva giusta.

    8 gen 202512 min di lettura
    #LangGraph#LLM#Multi-Agent#Orchestration
  • Cosa insegna il key management bancario sugli eval harness per agenti

    Cinque discipline dalla sicurezza bancaria — stato persistente, recovery deterministica, dual control, audit trail — applicate agli agenti LLM.

    18 apr 20267 min di lettura
    #Agent Evals#Verifiable Systems#LLM Production#Banking#MCP
  • Perché lo stato condiviso rompe i sistemi oltre tre agenti

    Lavagne condivise: ok nelle demo, fragili sotto carico di coordinamento. I failure mode dello stato condiviso e perché vince il message-passing supervisionato.

    10 dic 202412 min di lettura
    #LangChain#Agents#Multi-Agent#Architecture