SISTEMA DI ACCESSO COMPUTER

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:

TipoDove è memorizzataAmbitoMigliore per
BufferNel contesto (array di messaggi)Sessione correnteConversazioni brevi
RiepilogoNel contesto (compresso)Sessione correnteConversazioni lunghe
SemanticaEsterna (vector DB)Cross-sessioneRichiamo di conoscenze
EpisodicaEsterna (key-value store)Cross-sessioneFatti 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 conversazione
const 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'uso
await 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 messaggio

Quando 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 corrente
async 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 database
const 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 scambio
async 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 sistema
function 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 1
await 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 1

Quando 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 testo
async 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 vettori
function 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 interrogala
await 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 fondata

Per 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 fatto
server.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:

SituazioneStrategia
Sessione singola, meno di 20 turniBuffer
Sessione singola, di lunga durataRiepilogo
Multi-sessione, preferenze utenteEpisodica
Grande knowledge base da interrogareSemantica
Tutto quanto sopraCombina: 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