Systèmes de Mémoire pour les Agents : Donner un Contexte Persistant à votre IA
Chaque agent IA a un problème de mémoire. Chaque appel au modèle est sans état. Le modèle ne se souvient pas de la dernière requête. Il ne se souvient pas du nom de l’utilisateur, de ses préférences ni de ses décisions précédentes. Sans mémoire externe, votre agent repart de zéro à chaque tour.
Ce guide couvre quatre stratégies de mémoire — de la plus simple à la plus puissante — et quand utiliser chacune. Tous les exemples utilisent TypeScript et le SDK Anthropic.
Pourquoi les Agents Ont Besoin de Mémoire
Considérez un agent de révision de code. Lors de la première exécution, l’utilisateur explique les conventions de son équipe : pas de types any, toujours gérer les erreurs explicitement, préférer le style fonctionnel. L’agent produit une bonne révision.
Lors de la deuxième exécution, l’utilisateur soumet un autre fichier. L’agent a tout oublié. Il manque les mêmes violations de convention qu’il avait signalées la veille.
C’est le problème central : les agents sont sans état, mais les agents utiles ont besoin de continuité.
La mémoire donne aux agents trois capacités :
- Rappel — récupérer des faits de plus tôt dans une conversation ou d’une session précédente
- Personnalisation — s’adapter aux préférences de l’utilisateur au fil du temps
- Coordination — dans les systèmes multi-agents, partager l’état entre les agents
Les Quatre Types de Mémoire
Avant d’écrire du code, il est utile de nommer clairement les stratégies. La mémoire des agents se divise en quatre modèles :
| Type | Où stocké | Portée | Idéal pour |
|---|---|---|---|
| Tampon | En contexte (tableau de messages) | Session courante | Conversations courtes |
| Résumé | En contexte (compressé) | Session courante | Conversations longues |
| Sémantique | Externe (base de données vectorielle) | Inter-sessions | Rappel de connaissances |
| Épisodique | Externe (stockage clé-valeur) | Inter-sessions | Faits utilisateur, préférences |
Chaque stratégie présente un compromis différent entre simplicité, coût et capacité. Choisissez le minimum qui résout votre problème.
Stratégie 1 : Tampon de Conversation
La stratégie la plus simple consiste à transmettre l’historique complet de la conversation à chaque appel au modèle. Le modèle voit chaque message précédent et répond avec le contexte complet.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Message { role: "user" | "assistant"; content: string;}
// Le tampon contient l'historique complet de la conversationconst buffer: Message[] = [];
async function chat(userMessage: string): Promise<string> { // Ajouter le nouveau message de l'utilisateur au tampon buffer.push({ role: "user", content: userMessage });
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, // Transmettre le tampon complet à chaque appel messages: buffer, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Ajouter la réponse de l'assistant au tampon buffer.push({ role: "assistant", content: assistantMessage });
return assistantMessage;}
// Exemple d'utilisationawait chat("Je m'appelle Alex et je préfère Python à TypeScript.");await chat("Quel langage devrais-je utiliser pour mon prochain projet ?");// Le modèle se souvient de la préférence exprimée dans le premier messageQuand l’utiliser : Conversations de moins de 20 tours. Chatbots simples. Prototypes.
La limite : Les fenêtres de contexte sont finies. Une session longue dépasse éventuellement la limite de tokens, et les messages les plus anciens doivent être supprimés. Le modèle perd le contexte initial silencieusement, ce qui entraîne un comportement incohérent.
Stratégie 2 : Résumé Progressif
Quand les conversations s’allongent, résumez les anciens tours au lieu de les supprimer. Maintenez un résumé compressé de ce qui s’est passé avant une fenêtre glissante de messages récents.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface ConversationState { summary: string; // Historique compressé des anciens tours recentMessages: Array<{ role: "user" | "assistant"; content: string }>; maxRecentTurns: number; // Conserver les N derniers tours en verbatim}
const state: ConversationState = { summary: "", recentMessages: [], maxRecentTurns: 6, // 3 tours utilisateur + 3 tours assistant};
// Compresser les anciens tours dans le résumé progressifasync 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: `Mettez à jour le résumé de la conversation en ajoutant les nouveaux échanges ci-dessous.Renvoyez uniquement le résumé mis à jour. Limitez-le à moins de 200 mots.
Résumé actuel :${currentSummary || "(aucun)"}
Nouveaux échanges :${formatted}`, }, ], });
return response.content[0].type === "text" ? response.content[0].text : currentSummary;}
async function chat(userMessage: string): Promise<string> { // Quand le tampon dépasse la limite, compresser la moitié la plus ancienne 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 });
// Construire le tableau de messages : résumé système + tours récents verbatim const messages = [ ...(state.summary ? [ { role: "user" as const, content: `Contexte du début de cette conversation :\n${state.summary}`, }, { role: "assistant" as const, content: "Compris. J'utiliserai ce contexte.", }, ] : []), ...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;}L’appel de résumé utilise un modèle rapide et économique (Haiku). La conversation principale utilise le modèle performant (Sonnet). Cette séparation maintient les coûts bas tout en préservant la qualité.
Quand l’utiliser : Sessions de plus de 20 tours. Agents de support. Agents de tâches longues.
La limite : Les résumés perdent en détail. Les faits spécifiques exprimés tôt dans une conversation peuvent être compressés en déclarations vagues. Pour rappeler des faits précis, utilisez la mémoire épisodique.
Stratégie 3 : Mémoire Épisodique
La mémoire épisodique stocke des faits spécifiques sur un utilisateur ou une session dans un stockage clé-valeur. L’agent extrait des faits pendant une conversation et les récupère lors de sessions futures.
import Anthropic from "@anthropic-ai/sdk";import fs from "fs/promises";
const client = new Anthropic();
// En production, remplacez ce stockage de fichiers par Redis ou une base de donnéesconst 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));}
// Extraire des faits structurés du dernier échangeasync 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: `Extrayez les faits personnels, préférences ou décisions importantes de cet échange.Renvoyez un objet JSON de paires clé-valeur. Renvoyez {} s'il n'y a rien qui vaille la peine d'être mémorisé.
Utilisateur : ${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 {}; }}
// Formater les faits stockés comme une injection dans le prompt systèmefunction formatMemory(facts: Record<string, string>): string { const entries = Object.entries(facts); if (entries.length === 0) return ""; return ( "Ce que vous savez sur cet utilisateur :\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 : "";
// Extraire les faits et les persister pour les sessions futures const newFacts = await extractFacts(userMessage, assistantMessage); if (Object.keys(newFacts).length > 0) { store[userId] = { ...userFacts, ...newFacts }; await saveMemory(store); }
return assistantMessage;}
// Session 1await chat("user-123", "Je préfère des réponses concises. Pas de puces sauf si nécessaire.");// Session 2 (un processus différent, quelques jours plus tard)await chat("user-123", "Explique comment fonctionnent les poignées de main TCP.");// L'agent se souvient de la préférence de formatage de la session 1Quand l’utiliser : Agents orientés utilisateur s’exécutant sur plusieurs sessions. Personnalisation. Suivi des préférences.
La limite : Les faits s’accumulent au fil du temps. Des faits obsolètes peuvent produire un comportement incorrect. Ajoutez un mécanisme pour mettre à jour ou faire expirer les faits.
Stratégie 4 : Mémoire Sémantique (Recherche Vectorielle)
La mémoire épisodique stocke des faits discrets. La mémoire sémantique stocke des documents, du code ou des fragments de conversation indexés par signification. Quand l’agent a besoin d’informations, il recherche dans l’index à l’aide d’une requête.
C’est la base de la Génération Augmentée par Récupération (RAG).
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Document { id: string; content: string; embedding?: number[];}
// Calculer la similarité cosinus entre deux vecteursfunction 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[] = [];
// Indexer un document pour une récupération ultérieure async add(id: string, content: string): Promise<void> { const embedding = await embed(content); // Utiliser un modèle d'embedding dédié this.documents.push({ id, content, embedding }); }
// Récupérer les k documents les plus pertinents pour une requête 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> { // Récupérer les documents pertinents 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 ? `Répondez à la question en utilisant le contexte suivant :\n\n${context}` : undefined, messages: [{ role: "user", content: question }], });
return response.content[0].type === "text" ? response.content[0].text : "";}
// Exemple : indexer une base de connaissances et l'interrogerawait memory.add("doc-1", "L'équipe utilise React 18 avec le modèle app router.");await memory.add("doc-2", "Toutes les routes API doivent valider les entrées avec des schémas Zod.");await memory.add("doc-3", "Les requêtes de base de données utilisent Drizzle ORM. Éviter le SQL brut.");
const answer = await answerWithContext("Comment devrais-je écrire un nouveau endpoint API ?");// L'agent récupère doc-2 et doc-3 et génère une réponse fondéePour une utilisation en production, remplacez le stockage en mémoire par une base de données vectorielle dédiée. Pgvector est l’option la plus simple si vous utilisez déjà PostgreSQL. Chroma et Qdrant sont de bonnes options autonomes.
Quand l’utiliser : Assistants de documentation. Agents de recherche de code. Questions-réponses sur des bases de connaissances.
Connecter la Mémoire à MCP
Si vous avez suivi le guide du serveur MCP, vous pouvez exposer la mémoire comme une ressource MCP. Cela rend votre couche de mémoire accessible à tout client compatible MCP.
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: {} } });
// Outil : stocker un faitserver.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: `Stocké : ${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);Tout agent qui parle MCP peut maintenant appeler remember et recall comme outils standard. La mémoire devient un service partagé plutôt qu’une implémentation par agent.
Choisir la Bonne Stratégie
Utilisez ce tableau de décision comme point de départ :
| Situation | Stratégie |
|---|---|
| Session unique, moins de 20 tours | Tampon |
| Session unique, longue durée | Résumé |
| Multi-sessions, préférences utilisateur | Épisodique |
| Grande base de connaissances à interroger | Sémantique |
| Tout ce qui précède | Combiner : résumé pour la session + épisodique/sémantique entre sessions |
Commencez avec le tampon. Ajoutez une couche de résumé quand les conversations s’allongent. Ajoutez la mémoire épisodique quand les utilisateurs reviennent sur plusieurs sessions. Ajoutez la mémoire sémantique quand vous avez un corpus de documents à récupérer.
N’ajoutez pas de complexité avant d’en avoir besoin. Un tampon de conversation est la bonne réponse pour la plupart des prototypes.
Conclusion
La mémoire n’est pas une seule chose — c’est un ensemble de stratégies qui abordent différents problèmes à différentes portées. Le tampon gère la session courante. Le résumé étend cette session. La mémoire épisodique relie les sessions. La mémoire sémantique ancre les réponses dans une base de connaissances.
Choisissez la couche qui résout le problème que vous avez aujourd’hui. Enveloppez-la derrière une interface propre pour pouvoir échanger les implémentations plus tard. Et si vos agents communiquent via MCP, exposez la mémoire comme un outil partagé — cela garde chaque agent simple tout en donnant au système dans son ensemble un cerveau persistant.
Articles Connexes
- Construire Son Premier Serveur MCP
- Patterns Multi-Agents : Orchestrateurs, Workers et Pipelines
- Patterns d’Utilisation des Outils : Interfaces Agent-Outil Fiables
- Introduction au Développement Agentique