Sistemi di Memoria per Agenti: Dare Contesto Persistente alla Tua AI
Ogni agente AI ha un problema di memoria. Ogni chiamata al modello è stateless. Il modello non ricorda l’ultima richiesta. Non ricorda il nome dell’utente, le sue preferenze o le decisioni precedenti. Senza memoria esterna, il tuo agente parte da zero ad ogni turno.
Questa guida copre quattro strategie di memoria — dalla più semplice alla più potente — e quando usare ciascuna. Tutti gli esempi usano TypeScript e l’Anthropic SDK.
Perché gli Agenti Hanno Bisogno di Memoria
Considera un agente di code review. Al primo run, l’utente spiega le convenzioni del suo team: nessun tipo any, gestisci sempre gli errori esplicitamente, preferisci lo stile funzionale. L’agente produce una buona revisione.
Al secondo run, l’utente invia un altro file. L’agente ha dimenticato tutto. Manca le stesse violazioni delle convenzioni che aveva segnalato ieri.
Questo è il problema centrale: gli agenti sono stateless, ma gli agenti utili hanno bisogno di continuità.
La memoria dà agli agenti tre capacità:
- Richiamo — recuperare fatti da prima in una conversazione o da una sessione precedente
- Personalizzazione — adattarsi alle preferenze dell’utente nel tempo
- Coordinazione — nei sistemi multi-agente, condividere lo stato tra agenti
I Quattro Tipi di Memoria
Prima di scrivere codice, è utile nominare le strategie chiaramente. La memoria degli agenti si divide in quattro pattern:
| Tipo | Dove è memorizzata | Ambito | Migliore per |
|---|---|---|---|
| Buffer | Nel contesto (array di messaggi) | Sessione corrente | Conversazioni brevi |
| Riepilogo | Nel contesto (compresso) | Sessione corrente | Conversazioni lunghe |
| Semantica | Esterna (vector DB) | Cross-sessione | Richiamo di conoscenze |
| Episodica | Esterna (key-value store) | Cross-sessione | Fatti utente, preferenze |
Ogni strategia ha un diverso trade-off tra semplicità, costo e capacità. Scegli il minimo che risolve il tuo problema.
Strategia 1: Buffer di Conversazione
La strategia più semplice è passare la cronologia completa della conversazione a ogni chiamata del modello. Il modello vede ogni messaggio precedente e risponde con pieno contesto.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Message { role: "user" | "assistant"; content: string;}
// Il buffer contiene la cronologia completa della conversazioneconst buffer: Message[] = [];
async function chat(userMessage: string): Promise<string> { // Aggiungi il nuovo messaggio dell'utente al buffer buffer.push({ role: "user", content: userMessage });
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, // Passa il buffer completo ad ogni chiamata messages: buffer, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Aggiungi la risposta dell'assistente al buffer buffer.push({ role: "assistant", content: assistantMessage });
return assistantMessage;}
// Esempio d'usoawait chat("My name is Alex and I prefer Python over TypeScript.");await chat("What language should I use for my next project?");// Il modello ricorda la preferenza espressa nel primo messaggioQuando usarlo: Conversazioni sotto i 20 turni. Semplici chatbot. Prototipi.
Il limite: Le finestre di contesto sono finite. Una sessione lunga alla fine supera il limite di token, e i messaggi più vecchi devono essere eliminati. Il modello perde silenziosamente il contesto iniziale, causando comportamenti incoerenti.
Strategia 2: Riepilogo Scorrevole
Quando le conversazioni diventano lunghe, riassumi i vecchi turni invece di eliminarli. Mantieni un riepilogo compresso di ciò che è successo prima di una finestra scorrevole di messaggi recenti.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface ConversationState { summary: string; // Cronologia compressa dei turni più vecchi recentMessages: Array<{ role: "user" | "assistant"; content: string }>; maxRecentTurns: number; // Mantieni gli ultimi N turni verbatim}
const state: ConversationState = { summary: "", recentMessages: [], maxRecentTurns: 6, // 3 turni utente + 3 assistente};
// Comprimi i turni più vecchi nel riepilogo correnteasync function compressSummary( currentSummary: string, messagesToCompress: Array<{ role: string; content: string }>): Promise<string> { const formatted = messagesToCompress .map((m) => `${m.role.toUpperCase()}: ${m.content}`) .join("\n");
const response = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 512, messages: [ { role: "user", content: `Update the conversation summary by adding the new exchanges below.Return only the updated summary. Keep it under 200 words.
Current summary:${currentSummary || "(none)"}
New exchanges:${formatted}`, }, ], });
return response.content[0].type === "text" ? response.content[0].text : currentSummary;}
async function chat(userMessage: string): Promise<string> { // Quando il buffer supera il limite, comprimi la metà più vecchia if (state.recentMessages.length >= state.maxRecentTurns * 2) { const toCompress = state.recentMessages.splice(0, state.maxRecentTurns); state.summary = await compressSummary(state.summary, toCompress); }
state.recentMessages.push({ role: "user", content: userMessage });
// Costruisci l'array dei messaggi: riepilogo di sistema + turni verbatim recenti const messages = [ ...(state.summary ? [ { role: "user" as const, content: `Context from earlier in this conversation:\n${state.summary}`, }, { role: "assistant" as const, content: "Understood. I will use that context.", }, ] : []), ...state.recentMessages, ];
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
state.recentMessages.push({ role: "assistant", content: assistantMessage }); return assistantMessage;}La chiamata di riepilogo usa un modello veloce e economico (Haiku). La conversazione principale usa il modello capace (Sonnet). Questa divisione mantiene bassi i costi preservando la qualità.
Quando usarlo: Sessioni oltre i 20 turni. Agenti di supporto. Agenti per compiti di lunga durata.
Il limite: I riepiloghi perdono dettagli. Fatti specifici espressi all’inizio di una conversazione possono essere compressi in affermazioni vaghe. Per il richiamo preciso di fatti specifici, usa la memoria episodica.
Strategia 3: Memoria Episodica
La memoria episodica memorizza fatti specifici su un utente o sessione in un key-value store. L’agente estrae fatti durante una conversazione e li recupera nelle sessioni future.
import Anthropic from "@anthropic-ai/sdk";import fs from "fs/promises";import path from "path";
const client = new Anthropic();
// In produzione, sostituisci questo file store con Redis o un databaseconst MEMORY_PATH = "./memory.json";
interface EpisodicStore { [userId: string]: Record<string, string>;}
async function loadMemory(): Promise<EpisodicStore> { try { const data = await fs.readFile(MEMORY_PATH, "utf-8"); return JSON.parse(data); } catch { return {}; }}
async function saveMemory(store: EpisodicStore): Promise<void> { await fs.writeFile(MEMORY_PATH, JSON.stringify(store, null, 2));}
// Estrai fatti strutturati dall'ultimo scambioasync function extractFacts( userMessage: string, assistantReply: string): Promise<Record<string, string>> { const response = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 256, messages: [ { role: "user", content: `Extract any personal facts, preferences, or important decisions from this exchange.Return a JSON object of key-value pairs. Return {} if there is nothing worth remembering.
User: ${userMessage}Assistant: ${assistantReply}`, }, ], });
try { const text = response.content[0].type === "text" ? response.content[0].text : "{}"; const match = text.match(/\{[\s\S]*\}/); return match ? JSON.parse(match[0]) : {}; } catch { return {}; }}
// Formatta i fatti memorizzati come iniezione nel prompt di sistemafunction formatMemory(facts: Record<string, string>): string { const entries = Object.entries(facts); if (entries.length === 0) return ""; return ( "What you know about this user:\n" + entries.map(([k, v]) => `- ${k}: ${v}`).join("\n") );}
async function chat(userId: string, userMessage: string): Promise<string> { const store = await loadMemory(); const userFacts = store[userId] ?? {};
const systemPrompt = formatMemory(userFacts);
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: systemPrompt || undefined, messages: [{ role: "user", content: userMessage }], });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Estrai i fatti e persistili per le sessioni future const newFacts = await extractFacts(userMessage, assistantMessage); if (Object.keys(newFacts).length > 0) { store[userId] = { ...userFacts, ...newFacts }; await saveMemory(store); }
return assistantMessage;}
// Sessione 1await chat("user-123", "I prefer concise answers. No bullet points unless necessary.");// Sessione 2 (un processo diverso, giorni dopo)await chat("user-123", "Explain how TCP handshakes work.");// L'agente ricorda la preferenza di formattazione dalla sessione 1Quando usarlo: Agenti rivolti all’utente che funzionano attraverso sessioni. Personalizzazione. Tracciamento delle preferenze.
Il limite: I fatti si accumulano nel tempo. I fatti obsoleti possono produrre comportamenti errati (un utente cambia la sua lingua preferita; la vecchia preferenza è ancora nel store). Aggiungi un meccanismo per aggiornare o far scadere i fatti.
Strategia 4: Memoria Semantica (Ricerca Vettoriale)
La memoria episodica memorizza fatti discreti. La memoria semantica memorizza documenti, codice o frammenti di conversazione indicizzati per significato. Quando l’agente ha bisogno di informazioni, cerca nell’indice usando una query.
Questa è la base del Retrieval-Augmented Generation (RAG).
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Document { id: string; content: string; embedding?: number[];}
// Genera un vettore di embedding per una stringa di testoasync function embed(text: string): Promise<number[]> { // Claude non espone direttamente un endpoint di embeddings. // Usa un modello di embedding dedicato. Questo esempio usa uno stub per illustrazione. // In produzione: usa text-embedding-3-small (OpenAI), embed-english-v3 (Cohere), // o un modello self-hosted come nomic-embed-text. throw new Error("Replace this stub with a real embedding call");}
// Calcola la similarità coseno tra due vettorifunction cosineSimilarity(a: number[], b: number[]): number { const dot = a.reduce((sum, val, i) => sum + val * b[i], 0); const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); return dot / (magA * magB);}
class SemanticMemory { private documents: Document[] = [];
// Indicizza un documento per il recupero successivo async add(id: string, content: string): Promise<void> { const embedding = await embed(content); this.documents.push({ id, content, embedding }); }
// Recupera i top-k documenti più rilevanti per una query async search(query: string, topK = 3): Promise<Document[]> { const queryEmbedding = await embed(query); return this.documents .filter((doc) => doc.embedding) .map((doc) => ({ doc, score: cosineSimilarity(queryEmbedding, doc.embedding!), })) .sort((a, b) => b.score - a.score) .slice(0, topK) .map(({ doc }) => doc); }}
const memory = new SemanticMemory();
async function answerWithContext(question: string): Promise<string> { // Recupera documenti rilevanti const relevant = await memory.search(question); const context = relevant.map((d) => d.content).join("\n\n---\n\n");
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: context ? `Answer the question using the following context:\n\n${context}` : undefined, messages: [{ role: "user", content: question }], });
return response.content[0].type === "text" ? response.content[0].text : "";}
// Esempio: indicizza una base di conoscenza e interrogalaawait memory.add("doc-1", "The team uses React 18 with the app router pattern.");await memory.add("doc-2", "All API routes must validate input with Zod schemas.");await memory.add("doc-3", "Database queries use Drizzle ORM. Avoid raw SQL.");
const answer = await answerWithContext("How should I write a new API endpoint?");// L'agente recupera doc-2 e doc-3 e genera una risposta fondataPer l’uso in produzione, sostituisci il store in-memory con un database vettoriale dedicato. Pgvector (estensione PostgreSQL) è il più semplice se già usi Postgres. Chroma e Qdrant sono buone opzioni standalone.
Quando usarlo: Assistenti di documentazione. Agenti di ricerca del codice. Q&A su knowledge base. Qualsiasi cosa che richieda richiamo da un grande corpus.
Connettere la Memoria a MCP
Se hai seguito la guida al server MCP, puoi esporre la memoria come risorsa MCP. Questo rende il tuo livello di memoria accessibile a qualsiasi client compatibile MCP, incluso Claude Code.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server( { name: "memory-server", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } });
// Strumento: memorizza un fattoserver.setRequestHandler("tools/call", async (request) => { if (request.params.name === "remember") { const { key, value, userId } = request.params.arguments as { key: string; value: string; userId: string; }; await storeEpisodicFact(userId, key, value); return { content: [{ type: "text", text: `Stored: ${key} = ${value}` }] }; }
if (request.params.name === "recall") { const { userId } = request.params.arguments as { userId: string }; const facts = await loadEpisodicFacts(userId); return { content: [{ type: "text", text: JSON.stringify(facts, null, 2) }], }; }});
const transport = new StdioServerTransport();await server.connect(transport);Qualsiasi agente che parla MCP può ora chiamare remember e recall come strumenti standard. La memoria diventa un servizio condiviso piuttosto che un’implementazione per agente.
Scegliere la Strategia Giusta
Usa questa tabella decisionale come punto di partenza:
| Situazione | Strategia |
|---|---|
| Sessione singola, meno di 20 turni | Buffer |
| Sessione singola, di lunga durata | Riepilogo |
| Multi-sessione, preferenze utente | Episodica |
| Grande knowledge base da interrogare | Semantica |
| Tutto quanto sopra | Combina: riepilogo per sessione + episodica/semantica tra sessioni |
Inizia con il buffer. Aggiungi un livello di riepilogo quando le conversazioni si allungano. Aggiungi la memoria episodica quando gli utenti tornano tra sessioni. Aggiungi la memoria semantica quando hai un corpus di documenti da cui recuperare.
Non aggiungere complessità prima di averne bisogno. Un buffer di conversazione è la risposta giusta per la maggior parte dei prototipi. Un eccessivo ingegnerizzazione della memoria fin dall’inizio aggiunge costo, latenza e superficie di manutenzione senza beneficio proporzionale.
Conclusione
La memoria non è una cosa sola — è uno stack di strategie che affrontano problemi diversi a diversi livelli. Il buffer gestisce la sessione corrente. Il riepilogo estende quella sessione. La memoria episodica colma le sessioni. La memoria semantica ancora le risposte in una base di conoscenza.
Scegli il livello che risolve il problema che hai oggi. Avvolgilo dietro un’interfaccia pulita in modo da poter scambiare le implementazioni in seguito. E se i tuoi agenti comunicano tramite MCP, esponi la memoria come strumento condiviso — mantiene ogni agente semplice dando all’intero sistema un cervello persistente.
Articoli Correlati
- Costruire il Tuo Primo Server MCP
- Pattern Multi-Agente: Orchestratori, Worker e Pipeline
- Pattern di Utilizzo degli Strumenti: Interfacce Agente-Strumento Affidabili
- Introduzione allo Sviluppo Agentico