DanLevy.net

Smetti di Costruire Agent Instabili: Usa Workflow e Memoria

Pattern deterministici per modelli non deterministici.

Gli LLM hanno questa strana caratteristica: sono brillanti nel comprendere le sfumature ma terribili nel seguire le ricette. Dai a GPT-4 un problema vago e ragionerà sulle possibilità. Dagli una sequenza precisa di passaggi e potrebbe saltare il passaggio 3 perché il passaggio 5 “sembrava più rilevante.”

Non è un bug del modello. È una caratteristica fondamentale dei sistemi probabilistici che cercano di risolvere problemi deterministici.

Ho visto team lottare con questo disallineamento. Costruiscono un agent per gestire i rimborsi clienti, gli forniscono una dozzina di tool e si aspettano che esegua in modo affidabile un processo di business. A volte funziona perfettamente. A volte allucina approvazioni che non sono mai avvenute. A volte rimane bloccato a chiedere le stesse informazioni tre volte.

La soluzione non sono prompt migliori. È sapere quando smettere di chiedere all’LLM di “pensare” e iniziare a dirgli di “obbedire.”


Quando il Deterministico Batte il Creativo

Pensa a cosa succede quando devi elaborare un ticket di supporto. La logica di business nel mondo reale assomiglia a qualcosa del genere:

  1. Recupera i dettagli del ticket dal database
  2. Verifica se l’utente è idoneo per un rimborso (regole della policy)
  3. Conferma che la transazione esiste e non è già stata rimborsata
  4. Calcola l’importo del rimborso
  5. Elabora l’inversione del pagamento
  6. Aggiorna lo stato del ticket
  7. Invia email di conferma

Potresti affidare questo compito a un LLM come esercizio di tool-calling. Secondo la mia esperienza, è chiedere guai. Il modello potrebbe decidere che i passaggi 2 e 3 sono “praticamente la stessa cosa” e saltarne uno. Oppure potrebbe elaborare il rimborso prima di verificare l’idoneità perché l’utente sembrava agitato.

I workflow esistono esattamente per questo scenario. Non sono entusiasmanti, ma è proprio questo il punto.

Costruire un Pianificatore di Attività Meteorologiche

Ecco un esempio pratico che mostra il pattern. Abbiamo bisogno di dati meteorologici concreti e fattuali abbinati a suggerimenti creativi per le attività. Il recupero del meteo non deve mai essere creativo, ma i suggerimenti sì.

src/mastra/workflows/activity-planner.ts
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { Agent } from '@mastra/core/agent';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// Passaggio 1: Recupero dati meteo (Deterministico)
const fetchWeather = createStep({
id: 'fetch-weather',
description: 'Fetches weather forecast for a given city',
inputSchema: z.object({
city: z.string(),
}),
outputSchema: z.object({
location: z.string(),
temperature: z.number(),
conditions: z.string(),
precipitationChance: z.number(),
}),
execute: async ({ inputData }) => {
// ... (fetch logic) ...
const weather = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current=temperature_2m,weather_code&daily=precipitation_probability_mean`).then(r => r.json());
return {
location: inputData.city,
temperature: weather.current.temperature_2m,
conditions: getWeatherCondition(weather.current.weather_code),
precipitationChance: weather.daily.precipitation_probability_mean[0],
};
},
});
// Passaggio 2: L'agent suggerisce attività (Creativo)
const activityPlanner = new Agent({
id: 'activity-planner-agent',
name: 'Activity Planner',
instructions: `You are a local activities expert. Based on weather conditions, suggest 3-5 appropriate activities.
- For rain (>50% precipitation), prioritize indoor activities
- For extreme temperatures, consider climate-appropriate options
- Always include one adventurous and one relaxing option`,
model: openai('gpt-5'),
});
const planActivities = createStep({
id: 'plan-activities',
description: 'Uses AI to suggest activities based on weather',
inputSchema: z.object({
location: z.string(),
temperature: z.number(),
conditions: z.string(),
precipitationChance: z.number(),
}),
outputSchema: z.object({
activities: z.string(),
}),
execute: async ({ inputData }) => {
const prompt = `Weather in ${inputData.location}: ${inputData.temperature}°C...`;
const response = await activityPlanner.generate(prompt);
return { activities: response.text };
},
});
// La Pipeline
export const activityPlannerWorkflow = createWorkflow({
id: 'activity-planner',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ activities: z.string() }),
})
.then(fetchWeather)
.then(planActivities);
activityPlannerWorkflow.commit();

L’LLM non tocca mai l’API meteo. Riceve dati reali come input, poi fa ciò in cui è effettivamente bravo: fornire suggerimenti contestuali. Se inverti la situazione e lasci che l’agent recuperi i dati meteo, alla fine otterrai un forecast soleggiato quando in realtà sta piovendo.

Quando prendere in considerazione i workflow:


Il Problema della Context Window di Cui Nessuno Parla

C’è questo pattern che continuo a vedere. Qualcuno costruisce un chatbot. Funziona alla grande durante il test. Poi in produzione, gli utenti hanno conversazioni più lunghe e improvvisamente il bot si perde.

Lo sviluppatore guarda i log e si rende conto che sta inviando l’intera cronologia delle conversazioni con ogni richiesta. Tutti i 47 messaggi. Sta bruciando token e spazio di contesto per informazioni che sono per lo più irrilevanti.

Peggio ancora, c’è un fenomeno che i ricercatori chiamano “lost in the middle” in cui i modelli performano peggio quando le informazioni rilevanti sono sepolte in un contesto lungo. Il modello letteralmente non riesce a vedere il bosco per gli alberi.

Inviare la cronologia completa delle conversazioni sembra sicuro. Stai dando al modello “tutte le informazioni.” Ma in realtà stai rendendo più difficile per il modello concentrarsi su ciò che conta.

Memoria di Lavoro vs. Archiviazione a Lungo Termine

Il sistema di memoria di Mastra ti offre entrambi. La memoria di lavoro mantiene i messaggi recenti nella context window. Il richiamo semantico cerca nei messaggi storici quando la query corrente sembra correlata.

src/mastra/agents/memory-agent.ts
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
export const memoryAgent = new Agent({
id: 'memory-agent',
name: 'Memory Agent',
instructions: 'You are a helpful assistant with perfect recall of our conversations.',
model: openai('gpt-5'),
memory: new Memory({
storage: new LibSQLStore({
id: 'memory-agent-store',
url: 'file:../mastra.db',
}),
options: {
lastMessages: 20, // Keep last 20 messages in context
semanticRecall: {
enabled: true, // Use embeddings to find old stuff
topK: 5,
threshold: 0.7,
},
},
}),
});

Ecco come si svolge nella pratica. Un utente chiede: “Qual era quel ristorante italiano che hai consigliato il mese scorso?”

Senza richiamo semantico, l’agent vede gli ultimi 20 messaggi. Il consiglio del ristorante era il messaggio 487 di 506. È sparito. L’agent dice “Non ho questa informazione.”

Con il richiamo semantico:

  1. La query viene incorporata: [0.234, -0.567, 0.891, ...]
  2. L’embedding viene confrontato con i messaggi storici
  3. Il messaggio 487 (“Ti consiglio Trattoria Bella - la loro carbonara è incredibile”) ottiene un punteggio di similarità di 0.89
  4. Quel messaggio viene iniettato nel contesto corrente
  5. L’agent risponde: “Ho consigliato Trattoria Bella. La loro carbonara è ciò che ha attirato la mia attenzione.”

L’agent sembra avere una memoria perfetta utilizzando solo una frazione della context window. Non è solo ingegneria intelligente - è funzionalmente necessario una volta che le conversazioni si estendono oltre qualche decina di messaggi.


Coordinamento Tramite Reti di Agent

A volte hai bisogno sia di struttura che di flessibilità. I workflow puri sono troppo rigidi. Gli agent puri sono troppo imprevedibili.

Le reti di agent ti forniscono un coordinatore che decide quale agent specializzato o workflow invocare in base al compito. Pensalo come un load balancer intelligente per le capacità AI.

export const coordinatorAgent = new Agent({
id: 'coordinator-agent',
name: 'Research Coordinator',
instructions: `You are a network of researchers and writers.
- Use researchAgent for gathering facts
- Use writingAgent for producing final content
- Use weatherTool for current weather data
- Use activityPlannerWorkflow for location-based planning
Always produce comprehensive, well-structured responses.`,
model: openai('gpt-5'),
// Available primitives
agents: { researchAgent, writingAgent },
workflows: { activityPlannerWorkflow },
tools: { weatherTool },
// Network requires memory
memory: new Memory({
storage: new LibSQLStore({ id: 'network-store', url: 'file:../network.db' }),
}),
});

Quando interroghi questa rete, il coordinatore analizza la richiesta e instrada di conseguenza:

Questo pattern scala meglio rispetto al tentativo di comprimere tutto in un singolo mega-agent. Gli agent specializzati sviluppano competenze mirate. Il coordinatore gestisce il routing. Ogni pezzo fa ciò in cui è bravo.


Mettere Tutto Insieme

I sistemi AI di produzione reali hanno bisogno di architettura, non solo di prompt. Stai costruendo sistemi distribuiti in cui alcuni nodi sono LLM.

I workflow ti danno garanzie quando hai bisogno che le cose avvengano esattamente nel modo giusto. La memoria ti fornisce contesto senza bruciare il tuo budget di token. Le reti di agent ti permettono di comporre complessità da parti più semplici.

Niente di tutto questo è glamour. Ma dopo aver visto abbastanza “agent completamente autonomi” fallire in produzione, sono arrivato ad apprezzare l’affidabilità noiosa rispetto all’imprevedibilità eccitante.

I tuoi risultati possono variare, ma secondo la mia esperienza, i sistemi che effettivamente vengono rilasciati e rimangono in esecuzione sono quelli che trattano gli LLM come componenti in un’architettura più ampia piuttosto che scatole magiche che risolvono tutto.

Risorse

Leggi la Serie

  1. LLM Routing
  2. Security & Guardrails
  3. MCP & Tool Integrations
  4. Workflows & Memory (Questo Post)