Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 9

La mémoire

L'historique meurt avec la session. La mémoire à long terme, c'est ce que vous enregistrez en dehors, plus un moyen de le retrouver la fois suivante.
1/33 Plis du bandonéon :
  • user
  • assistant
  • tool_result
L'assistant de la boutique d'un torréfacteur de café, dans une petite ferme. La cabane de torréfaction traite les commandes. Le journal près du lit, c'est la mémoire à long terme : il vit hors de la session, donc la nuit ne peut pas y toucher.

EventBus

Le problème

Tout ce qu'un agent sait d'une conversation vit dans son historique, les messages que la boucle renvoie à chaque tour. Quand la session se termine, l'historique part avec elle. Demain, le même client revient, dit « la même chose que la dernière fois », et l'agent n'a aucune idée de ce que c'était.

C'est le Spectre Effaceur, l'amnésie de session. Il ne casse rien. Il fait juste que l'agent pose les mêmes questions tous les jours, oublie les préférences qu'on lui a données et traite un habitué comme un inconnu.

Garder tout l'historique pour toujours ne règle rien non plus. Il grossit sans fin, et Gulp, du niveau 7, l'attend au tournant.

La solution

Enregistrez ce qui compte hors de l'historique, là où la fin d'une session ne peut pas l'atteindre : un fichier, une base de données. Puis donnez à l'agent un moyen de le récupérer. Il y a trois façons courantes, et les vrais agents les combinent souvent :

  • Reprendre la session

    Enregistrer tout l'historique sous un id de session, et le recharger la fois suivante.

    Rien ne se perd, mais tout revient : la requête suivante démarre lourde, et les vieux bavardages encombrent le contexte. Bien pour reprendre une tâche inachevée, pas pour se souvenir d'un client pendant des mois.

  • Des notes dans le prompt

    L'agent enregistre de courtes notes dans un fichier, et le fichier entier va dans le system prompt au début de chaque session.

    Simple et prévisible : le modèle voit toujours toutes les notes. Ça ne passe plus à l'échelle dès que les notes dépassent une page. Le CLAUDE.md et les fichiers de mémoire de Claude Code fonctionnent ainsi.

  • Recherche par le sens

    Chaque note est enregistrée avec son embedding. Un outil recall calcule l'embedding de la question et ne renvoie que les notes les plus proches.

    Ça passe à l'échelle jusqu'à des milliers de notes, et les retrouve même quand les mots ne correspondent pas. Ça coûte un appel d'embedding par note et par recherche, et il faut que le modèle pense à appeler recall.

L'animation montre la troisième. Un embedding est une liste de nombres qu'un modèle calcule pour un texte, de sorte que des textes au sens proche obtiennent des nombres proches. « Ce que le client a commandé la dernière fois » et « Achète 1 kg de Colombie » n'ont aucun mot en commun, mais leurs embeddings pointent dans la même direction, et c'est ainsi que recall trouve la bonne page.

Les personnages

Les mêmes personnages que d'habitude, cette fois dans une ferme.

Un jour une session
Une conversation, de la première demande à la réponse. La nuit y met fin.
Le bandonéon l'historique
Chaque message de la session du jour. Il est vide chaque matin.
Le journal mémoire à long terme
Des notes enregistrées hors de toute session, une ligne par note. remember écrit une page, recall cherche parmi elles.
Le Spectre Effaceur fin de session
Il vient chaque nuit et vide le bandonéon. Il ne peut pas toucher au journal.
Le classement similarité
À quel point le sens de chaque note est proche de la requête, de 0 à 1. Le modèle ne reçoit que les meilleures.
Le torréfacteur place_order
Un outil ordinaire.

Dans le panneau EventBus, remember et recall sont des appels d'outils ordinaires. La boucle ne sait rien de la mémoire : ce sont vos outils, et un fichier dans lequel ils écrivent.

Le code

Avec astorlm : createSemanticIndex avec createOpenAIEmbedder classe les notes par le sens. L'index vit en mémoire, donc l'outil remember l'écrit aussi dans un fichier, et la session suivante le recharge avec addVector.

À partir de zéro : Un appel d'embedding, une similarité cosinus, un fichier JSON et deux outils. La boucle du niveau 2 ne change pas.

import { OpenAIProvider, createLocalAgent, createOpenAIEmbedder, createSemanticIndex, tool } from 'astorlm'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { z } from 'zod'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

// The diary: one file of notes per customer, each saved with its embedding.
// The semantic index lives in memory, so load the saved vectors into it at startup.
const customerId = 'c-2291'
const file = `./memory/${customerId}.json`
const diary = createSemanticIndex({
  embedder: createOpenAIEmbedder({ ...LLM, model: 'your-embedding-model' }), // e.g. 'nomic-embed-text'
})
if (existsSync(file)) JSON.parse(readFileSync(file, 'utf8')).forEach(diary.addVector)

const remember = tool({
  name: 'remember',
  description: 'Save a short note about this customer for future conversations.',
  schema: z.object({ note: z.string().describe('One fact, in your own words.') }),
  execute: async ({ note }) => {
    await diary.add(`note-${diary.size + 1}`, note) // embeds it, then stores it
    writeFileSync(file, JSON.stringify(diary.list())) // outlives the session
    return `Saved. ${diary.size} notes about this customer.`
  },
})

const recall = tool({
  name: 'recall',
  description: 'Search the saved notes about this customer by meaning. Returns the closest ones.',
  schema: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    const hits = await diary.query(query, { topK: 2, threshold: 0.3 })
    return hits.map((hit) => `${hit.score.toFixed(2)} ${hit.text}`).join('\n') || 'Nothing saved about that.'
  },
})

const agent = await createLocalAgent({
  provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
  systemPrompt:
    'You are the shop assistant of a coffee roaster. When a customer tells you something worth keeping ' +
    '(what they buy, how they like it), save it with remember. If they refer to the past, use recall first.',
  tools: [placeOrder, remember, recall], // placeOrder: your code
  maxTurns: 10,
})

// A new session every day: the history starts empty, the diary doesn't.
const last = await agent.run('Hi again! Send me the same as last time.')
console.log(last.content)

Points de vigilance

  • Décidez de ce qui vaut la peine d'être gardé. Enregistrez les faits qui compteront la prochaine fois : préférences, décisions, adresses. Pas tout le chat. Dites-le dans le system prompt, sinon le modèle n'enregistrera rien, ou enregistrera tout.
  • Gardez les mémoires séparées. Un journal par client, et vérifiez à qui il appartient à chaque appel. Une recherche de mémoire qui mélange les clients, c'est une fuite de données qui n'attend qu'à arriver.
  • Les mémoires vieillissent. Le client déménage de Rosario à Córdoba. Enregistrez une date avec chaque note, laissez les notes les plus récentes l'emporter, et donnez aux gens un moyen de voir et de supprimer ce qui est stocké sur eux.
  • Une correspondance n'est pas une preuve. Une recherche renvoie toujours ses notes les plus proches, même quand aucune ne convient. Fixez un score minimum, et quand la meilleure correspondance est faible, faites demander le modèle plutôt que deviner.
  • Ce qu'il lit, il peut y obéir. Une note enregistrée revient plus tard dans le prompt. Ne laissez jamais le texte d'un client devenir des instructions pour une autre session (niveau 14).