I 12 Fattori per Costruire AI Agent Affidabili

Dec 21, 2025 · 14 min read

Una guida pratica per costruire AI agent produttivi e affidabili applicando principi di software engineering consolidati, senza dipendere completamente da framework.

Introduzione

Costruire agent che funzionano davvero in produzione richiede molto più di un semplice loop agentico. La maggior parte degli sviluppatori ha seguito questo percorso: si decide di costruire un agent, si usa una libreria per muoversi velocemente, si raggiunge il 70-80% di qualità (sufficiente per entusiasmare il CEO e ottenere più risorse), ma poi ci si rende conto che quel 70-80% non basta.

Quando si cerca di superare quella soglia di qualità, ci si trova immersi in sette livelli di call stack cercando di capire come viene costruito il prompt o come vengono passati i tool. Alla fine, molti buttano via tutto e ricominciano da zero. O peggio, scoprono che il problema in questione non aveva nemmeno bisogno di un agent.

Note

Non ogni problema necessita di un agent. Un esempio reale: tentare di costruire un DevOps agent che eseguisse comandi make. Dopo due ore di prompt sempre più dettagliati, il risultato era essenzialmente uno script bash che avrebbe richiesto 90 secondi per essere scritto manualmente.

Il Contesto: Da Dove Nasce Questo Framework

Dopo aver parlato con oltre 100 founder, builder e ingegneri che costruiscono agent in produzione, sono emersi pattern chiari:

  1. La maggior parte degli agent produttivi non sono così agentici - sono principalmente software con componenti LLM strategicamente posizionati
  2. Esistono pattern ricorrenti che rendono le applicazioni basate su LLM davvero efficaci
  3. Nessuno fa rewrite completi - piuttosto, applicano piccoli concetti modulari al codice esistente
  4. Non serve un background in AI - è software engineering 101

Proprio come Heroku ha definito i 12 fattori per applicazioni cloud-native 10 anni fa, serviva una definizione chiara di cosa significa costruire agent affidabili.

Il repository GitHub dei 12 Factor Agents ha raggiunto:

  • Front page di Hacker News per un’intera giornata
  • 200k impressioni sui social
  • 4.000 star in 1-2 mesi
  • 14 contributori attivi
Tip

Questo non è un talk anti-framework. Pensalo come una wishlist, una lista di feature request per come i framework possano servire meglio i builder che necessitano alta affidabilità e velocità di sviluppo.

I 12 Fattori

Factor 1: Structured Output - La Vera Magia degli LLM

La cosa più magica che gli LLM possono fare non ha niente a che fare con loop, switch statement, codice o tool.

È trasformare una frase come questa:

"Prenota un volo per San Francisco il prossimo martedì"

In JSON strutturato come questo:

{
  "action": "book_flight",
  "destination": "San Francisco",
  "date": "2025-12-23",
  "flexibility": "exact"
}

Non importa nemmeno cosa fai con quel JSON - gli altri fattori servono a quello. Ma se riesci a ottenere questo, hai un pezzo che puoi integrare nella tua app oggi stesso.

★ Insight ───────────────────────────────────── Structured output come primitiva fondamentale: Gli LLM eccellono nel convertire linguaggio naturale in strutture dati. Questa capacità è più affidabile e utile di loop agentici complessi. Pensa all’LLM come a un parser semantico che puoi chiamare con una API. ─────────────────────────────────────────────────

Factor 4: Tool Use is Harmful (?)

Il titolo è provocatorio e tra virgolette, ma il concetto è importante.

Nel 1968 uscì il paper “Go To Considered Harmful” che argomentava come questa astrazione rendesse il codice terribile.

“Tool use” come concetto può essere fuorviante perché suggerisce che un’entità aliena eterea stia interagendo magicamente con il suo ambiente. In realtà:

  1. L’LLM genera JSON
  2. Il tuo codice deterministico riceve quel JSON
  3. Il codice fa qualcosa con esso
  4. Opzionalmente, reinserisci il risultato nel context

Non c’è niente di speciale nei tool. È solo JSON e codice.

// Non pensare a "tool use magico"
// Pensa a: structured output + switch statement

const action = await llm.generate(prompt);

switch(action.type) {
  case 'api_call':
    const result = await callAPI(action.params);
    break;
  case 'database_query':
    const data = await queryDB(action.sql);
    break;
}

Oppure con un semplice loop:

for (const step of action.steps) {
  await executeStep(step);
}

Factor 8: Own Your Control Flow

Come Siamo Arrivati Qui

Scriviamo DAG (Directed Acyclic Graph) da sempre. Se hai scritto un if statement, hai scritto un grafo diretto. Il codice è un grafo.

Molti hanno familiarità con DAG orchestrator come Airflow o Prefect - l’idea di scomporre processi in nodi ti dà garanzie di affidabilità.

La promessa degli agent era diversa: non devi scrivere il DAG. Dici all’LLM qual è l’obiettivo e lui trova il percorso.

Il modello più semplice è questo:

1. Evento iniziale → Prompt
2. LLM determina next step
3. Aggiungi risultato al context window
4. Ripeti fino a "done"

Il Problema con Loop Ingenui

User Message → Prompt → "Call API" → Risultato nel context
            Prompt → "Process data" → Risultato nel context
            Prompt → "Done" → Final Answer

Questo approccio non scala. Il problema principale: context window lunghe.

Note

Sì, puoi inserire 2 milioni di token in Gemini e otterrai una risposta. Ma nessuno sosterrà che otterrai sempre risultati migliori e più affidabili rispetto a controllare e limitare il numero di token nel context.

L’Anatomia di un Agent

Un agent reale ha quattro componenti essenziali:

  1. Prompt - istruzioni su come selezionare il next step
  2. Switch statement - prende il JSON del model e fa qualcosa
  3. Context building - come costruisci la context window
  4. Loop - determina quando/come/perché uscire

Se possiedi il tuo control flow, puoi fare cose interessanti:

  • Break e switch tra strategie
  • Summarize parti della conversazione
  • LLM as judge per validazione
  • Checkpoint e resume

Factor 3 & 6: State Management - Execution vs Business

Gli orchestrator ti danno execution state:

  • Current step
  • Next step
  • Retry counts
  • Timestamps

Ma hai anche business state:

  • Messaggi della conversazione
  • Dati mostrati all’utente
  • Approvazioni in sospeso
  • Contesto del dominio

Vuoi poter lanciare, mettere in pausa e riprendere questi workflow come qualsiasi API standard.

Esempio: Pause & Resume

1. Request arriva  Carica context dal DB
2. Agent chiama long-running tool
3. Serializza context window nel DB con state_id
4. Return 202 Accepted

... tempo passa, async operation completa ...

5. Callback con state_id + result
6. Carica state dal DB usando state_id
7. Appendi risultato al context
8. Continua l'agent

L’agent non sa nemmeno che del tempo è passato. È solo software.

★ Insight ───────────────────────────────────── Agent come REST API stateless: Separare execution state (gestito da te) da business state (nel DB) permette di costruire agent che si comportano come normali servizi web - scalabili, pausabili, resumable. Questo è software engineering applicato agli agent. ─────────────────────────────────────────────────

Factor 2: Own Your Prompts

Molti iniziano qui: scopri rapidamente che vuoi possedere i tuoi prompt.

Ci sono buone astrazioni che ti danno prompt eccellenti senza scrivere ogni token a mano - prompt che richiederebbero mesi di “prompt school” per replicare.

Ma alla fine, per superare una certa qualità, scriverai ogni singolo token a mano.

Perché? Gli LLM sono funzioni pure:

tokens_in → [LLM] → tokens_out

L’unica cosa che determina l’affidabilità del tuo agent è:

  1. Quanto buoni sono i token in output
  2. E l’unica cosa che determina i token in output (a parte fare fine-tuning del model) è quanto sei preciso con i token in input
Tip

Non so quale prompt è migliore per il tuo caso d’uso. Ma so che più varianti puoi testare, più knob puoi girare, più eval puoi eseguire, più probabilità hai di trovare qualcosa di eccellente.

Factor 5 & 7: Own Your Context Building

Puoi usare lo standard OpenAI messages format, oppure no.

Nel momento in cui stai dicendo all’LLM “scegli il next step”, il tuo unico lavoro è dirgli cosa è successo finora.

Puoi mettere tutte quelle informazioni in un singolo user message:

User: Ecco lo stato corrente:
- Events: [lista eventi]
- Current data: {...}
- Previous attempts: [...]

What should happen next?

Oppure usare system message, o messages strutturati. Modella il tuo event state e thread model come preferisci.

Esempio: Trace Ottimizzato

Invece di:

User: Prenota volo
Assistant: [tool_call: search_flights]
Tool: [risultati 500 linee]
Assistant: [tool_call: book_flight]
Tool: [conferma 200 linee]

Considera:

User: Task: Prenota volo SFO
Assistant: SEARCH → 3 voli trovati (AA123, UA456, DL789)
Assistant: BOOK → AA123 confermato, ref: ABC123

Se non ottimizzi la densità e la chiarezza di come passi informazioni all’LLM, stai perdendo qualità potenziale.

★ Insight ───────────────────────────────────── Context engineering come disciplina core: Prompt + memory + RAG + history = tutto è solo “come ottenere i token giusti nel model”. Ogni token conta. La capacità di sperimentare con diverse rappresentazioni del context è più importante dell’astrazione che usi. ─────────────────────────────────────────────────

Factor 9: Error Handling (Controverso)

Quando il model sbaglia - chiama un’API in modo errato, o chiama un’API che è down - puoi prendere la tool call e l’errore associato, metterli nel context window e farlo riprovare.

Problema: Quanti hanno visto questo approccio andare male? L’agent che inizia a girare a vuoto, perde contesto, si blocca?

La Soluzione: Context Intelligente

Non mettere ciecamente tutto nel context:

// ❌ Naive approach
context.push(toolCall);
context.push(error);

// ✅ Smart approach
if (hasValidToolCall) {
  // Rimuovi tutti gli errori pending
  context = context.filter(msg => !msg.isError);
  context.push(summary(previousErrors)); // Sommario, non dettagli
  context.push(validToolCall);
}

Non inserire l’intera stack trace nel context. Determina cosa vuoi dire al model per ottenere risultati migliori.

Factor 10: Contact Humans with Tools

Questo è sottile ma potente.

Quasi tutti evitano una scelta molto importante all’inizio dell’output: decidere tra tool call e messaggio all’umano.

Invece di avere due “modalità” separate (tool mode vs chat mode):

type AgentOutput =
  | { type: 'done', message: string }
  | { type: 'need_clarification', question: string }
  | { type: 'need_approval', action: Action }
  | { type: 'tool_call', tool: string, params: any }

Vantaggi:

  1. Dai al model diversi modi di comunicare intent
  2. Spingi l’intent sul primo token generation
  3. Il sampling avviene su linguaggio naturale che il model capisce bene

Auto-Out-of-Loop Agents

Questo permette di costruire agent che sanno quando coinvolgere gli umani:

Agent: Analizzo il database... trovato errore critico
Agent: ESCALATE → "Rilevate inconsistenze nei dati utente.
                    Necessaria revisione manuale prima di procedere."
Human: Approva dopo aver verificato
Agent: Continuo con la migrazione...
Note

C’è molto da dire su questo pattern. Sul sito dei 12 Factor Agents c’è un link a un post approfondito sugli auto-out-of-loop agents.

Factor 11 & 12: Small Focused Agents

Abbiamo parlato di perché i loop ingenui non funzionano. Cosa funziona invece?

Micro agents.

[Deterministic DAG]
  [Micro Agent: 3-10 steps]
[Deterministic DAG]
  [Micro Agent: 3-10 steps]
[Deterministic DAG]

Esempio Reale: Deploy Bot

Il deploy pipeline è principalmente CI/CD deterministico:

1. PR merged + tests pass on dev
2. → DEPLOY AGENT START
3.   Agent: "Deploy frontend"
4.   Human: "No, backend first" ← Natural language input!
5.   Agent: Proposes backend deploy
6.   Human: Approves
7.   Agent: Executes backend deploy
8.   Agent: "Now deploying frontend"
9. → DEPLOY AGENT END
10. Run E2E tests against prod (deterministic)
11. If failed → ROLLBACK AGENT START

100 tools, 20 steps max: Facile. Context gestibile, responsabilità chiare.

E Se i Model Migliorano?

“Cosa succede quando potrò mettere 2 milioni di token e farà tutto?”

Assolutamente possibile. Vedrai questo pattern:

[Mostly deterministic code with LLM sprinkles]
[Bigger LLM sections as models improve]
[Eventually: entire API endpoint run by agent]

Ma vorrai comunque sapere come ingegnerizzare per la massima qualità.

Il Sweet Spot

Dal team di NotebookLM:

“Trova qualcosa che è proprio al limite di ciò che il model può fare affidabilmente - che non riesce a fare bene ogni volta - e se riesci a farlo funzionare affidabilmente attraverso l’ingegneria del sistema, avrai creato qualcosa di magico.”

Factor 13 (Bonus): Agents Should Be Stateless

Gli agent dovrebbero essere stateless reducers (o “transducers” per i puristi):

function agent(state: State, event: Event): State {
  // Pure function
  // No side effects
  // Ritorna nuovo state
}

Tu possiedi lo state, lo gestisci come vuoi:

  • Database
  • Redis
  • File system
  • Memory

L’agent è solo una funzione che trasforma stato.

★ Insight ───────────────────────────────────── Stateless agents = testable agents: Separare la logica dell’agent (pure function) dalla gestione dello stato rende tutto più semplice: test, debug, replay, time-travel debugging. Pattern funzionale applicato agli AI agent. ─────────────────────────────────────────────────

Principi Guida

Agents Sono Software

Avete mai scritto uno switch statement? Un while loop? Allora potete costruire agent.

Non serve magia. Serve buona ingegneria del software.

LLM Sono Funzioni Stateless

tokens_in → [LLM] → tokens_out

Implicazione: Assicurati di mettere le cose giuste nel context e otterrai i risultati migliori.

Own State & Control Flow

Possiedi e comprendi il tuo state e control flow perché ti dà flessibilità. Quando usi un framework che nasconde questo, perdi la capacità di:

  • Debug preciso
  • Ottimizzazioni custom
  • Pattern non previsti dal framework

Find the Bleeding Edge

Trova modi per fare meglio di tutti gli altri curando attentamente:

  • Cosa metti nel model
  • Come controlli cosa esce

Non competere su “quanto è grande il mio context window”. Competi su “quanto è denso e rilevante il mio context”.

Agents Are Better With People

Trova modi per far collaborare agent con umani. Gli agent migliori sanno quando chiedere aiuto.

Framework vs Libraries: La Vera Questione

Note

Questo non è un talk anti-framework. È una wishlist.

Il Problema

Molti framework cercano di togliere le parti difficili dell’AI così puoi semplicemente integrare e partire.

La Proposta

I tool dovrebbero fare l’opposto: togliere le altre parti difficili così puoi concentrarti interamente sulle parti difficili dell’AI:

  • Prompt giusti
  • Flow giusto
  • Token giusti
  • Context engineering

Create-12-Factor-Agent

Il team sta lavorando su qualcosa chiamato create-12-factor-agent.

Non è un wrapper. È come shadcn/ui: scaffolding che poi possiedi.

npx create-12-factor-agent my-agent
cd my-agent
# Ora il codice è tuo, modificalo come vuoi

Non hai bisogno di bootstrap. Hai bisogno di ownership.

Patterns & Best Practices

1. DAG Deterministico + Micro Agents

┌─────────────────┐
│ Validate Input  │ ← Deterministic
└────────┬────────┘
┌─────────────────┐
│  Agent: Plan    │ ← 3-5 steps max
│  Execution      │
└────────┬────────┘
┌─────────────────┐
│ Execute Steps   │ ← Deterministic
└────────┬────────┘
┌─────────────────┐
│ Agent: Verify   │ ← 3-5 steps max
│ & Report        │
└────────┬────────┘
┌─────────────────┐
│ Send Results    │ ← Deterministic
└─────────────────┘

2. Context Window Management

class ContextBuilder {
  private maxTokens = 8000; // Lascia spazio per output

  build(state: State): Message[] {
    const messages = [];

    // 1. System prompt (sempre)
    messages.push(this.systemPrompt);

    // 2. Task description (sempre)
    messages.push(this.taskDescription(state));

    // 3. Recent history (riassumi se troppo lungo)
    const history = state.history;
    if (tokenCount(history) > 2000) {
      messages.push(this.summarize(history));
    } else {
      messages.push(...history);
    }

    // 4. Current context (sempre)
    messages.push(this.currentContext(state));

    return messages;
  }
}

3. Error Recovery Pattern

async function executeWithRetry(toolCall, maxRetries = 3) {
  const errors = [];

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await execute(toolCall);
    } catch (error) {
      errors.push(error);

      // Non mettere l'intera stack trace nel context
      const summary = summarizeError(error);

      if (i < maxRetries - 1) {
        // Chiedi all'LLM di riprovare con più informazioni
        toolCall = await llm.generate({
          context: currentContext,
          error: summary,
          previousAttempts: errors.length
        });
      }
    }
  }

  // Dopo max retries, escalate to human
  return await requestHumanHelp({
    task: toolCall,
    errors: errors.map(summarizeError)
  });
}

4. Human-in-the-Loop Pattern

type AgentDecision =
  | { type: 'continue', action: Action }
  | { type: 'needs_approval', action: Action, reason: string }
  | { type: 'needs_input', question: string }
  | { type: 'done', result: any };

async function agentLoop(state: State): Promise<State> {
  while (!state.done) {
    const decision = await agent.decide(state);

    switch (decision.type) {
      case 'continue':
        state = await executeAction(decision.action, state);
        break;

      case 'needs_approval':
        const approved = await askHuman(decision);
        if (approved) {
          state = await executeAction(decision.action, state);
        } else {
          state = await agent.replan(state);
        }
        break;

      case 'needs_input':
        const input = await askHuman(decision.question);
        state = { ...state, additionalInput: input };
        break;

      case 'done':
        state.done = true;
        state.result = decision.result;
        break;
    }
  }

  return state;
}

Strumenti e Risorse

A2 Protocol

Il team sta lavorando su A2 Protocol - un modo per ottenere consolidamento su come gli agent possono contattare gli umani.

Human Layer

C’è roba difficile ma importante (prompt, flow, token) e roba difficile ma meno interessante (infrastructure, state management, human communication).

Il team dietro i 12 Factor Agents sta costruendo strumenti per gestire la seconda categoria, così puoi concentrarti sulla prima.

Open Source

Molto del lavoro è fatto in the open:

  • Repository GitHub: 12 Factor Agents
  • Contributi della community
  • Pattern condivisi
Tip

Cerca il repository “12-factor-agents” su GitHub per la versione sempre aggiornata dei fattori, esempi di codice e contributi della community.

Conclusione

Ricapitolando

  1. Agents are software - puoi costruirli con competenze di software engineering
  2. LLMs are pure functions - garbage in, garbage out. Cura i token
  3. Own your state and control flow - la flessibilità ripaga
  4. Find the bleeding edge - innova sul context engineering, non sulla dimensione del context
  5. Agents are better with people - human-in-the-loop non è un fallimento, è una feature

Le Cose Difficili

Ci sono cose difficili nel costruire agent, ma dovresti probabilmente farle comunque, almeno per ora, e dovresti fare la maggior parte di loro.

Non lasciare che i framework ti nascondano le parti difficili dell’AI. Lascia che ti aiutino con tutto il resto così puoi concentrarti su:

  • Prompt perfetti
  • Flow ottimali
  • Token density massima
  • Context engineering superiore

Prossimi Passi

  1. Inizia piccolo: Prendi un processo esistente e aggiungi un micro agent
  2. Misura: Stabilisci metriche di qualità e affidabilità
  3. Itera: Ottimizza prompt e context in base ai dati
  4. Scala: Aggiungi più micro agent dove servono
Note

Il futuro degli agent non è un singolo super-agent che fa tutto. È un’architettura di micro agent specializzati, orchestrati da codice deterministico, che sanno quando chiedere aiuto agli umani.

Let’s go build something. 🚀


Basato sul talk di Human Layer sui 12 Factor Agents. Per approfondimenti, visita il repository GitHub e unisciti alla conversazione.