> Niveau 9 de Agent Harness Patterns, un parcours de patterns sur le fonctionnement des agents d'IA. Version web : https://harnesspatterns.dev/fr/patterns/memory · Tous les patterns (en anglais) : https://harnesspatterns.dev/llms.txt

# 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.

## 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.

**Avec astorlm**

```ts
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)
```

**TypeScript**

```ts
// Long-term memory, from scratch. Plain fetch and node:fs, no SDK.
import { existsSync, readFileSync, writeFileSync } from 'node:fs'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = {
  baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
  model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
  embeddingModel: 'your-embedding-model', // e.g. 'nomic-embed-text', 'text-embedding-3-small'
  apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}

// 1. An embedding: a list of numbers that captures what a text means.
//    Texts that mean similar things get vectors that point the same way.
async function embed(text: string): Promise<number[]> {
  const res = await fetch(`${LLM.baseURL}/embeddings`, {
    method: 'POST',
    headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
    body: JSON.stringify({ model: LLM.embeddingModel, input: text }),
  })
  return (await res.json()).data[0].embedding
}

// How closely two vectors point the same way: 1 = same meaning, 0 = unrelated.
function cosine(a: number[], b: number[]): number {
  let dot = 0, na = 0, nb = 0
  for (let i = 0; i < a.length; i++) {
    dot += a[i]! * b[i]!
    na += a[i]! ** 2
    nb += b[i]! ** 2
  }
  return dot / (Math.sqrt(na) * Math.sqrt(nb))
}

// 2. The diary: a file per customer, outside any session. Each note keeps its vector.
type Note = { text: string; vector: number[] }
const FILE = './memory/c-2291.json'
const diary: Note[] = existsSync(FILE) ? JSON.parse(readFileSync(FILE, 'utf8')) : []

// 3. Two tools: one writes a note, the other searches by meaning.
async function remember({ note }: { note: string }): Promise<string> {
  diary.push({ text: note, vector: await embed(note) })
  writeFileSync(FILE, JSON.stringify(diary))
  return `Saved. ${diary.length} notes about this customer.`
}

async function recall({ query }: { query: string }): Promise<string> {
  const q = await embed(query)
  const hits = diary
    .map((note) => ({ text: note.text, score: cosine(q, note.vector) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, 2)
    .filter((hit) => hit.score > 0.3)
  return hits.map((hit) => `${hit.score.toFixed(2)} ${hit.text}`).join('\n') || 'Nothing saved about that.'
}

type ToolFn = (args: Record<string, string>) => Promise<string>
const tools: Record<string, ToolFn> = {
  remember: (args) => remember({ note: args.note ?? '' }),
  recall: (args) => recall({ query: args.query ?? '' }),
  place_order: placeOrder, // your code
}
const toolSchemas = [/* one JSON Schema per tool: remember(note), recall(query), place_order(…) */]

const SYSTEM =
  '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.'

type ToolCall = { id: string; function: { name: string; arguments: string } }
type Message =
  | { role: 'system' | 'user'; content: string }
  | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
  | { role: 'tool'; tool_call_id: string; content: string }

// 4. The loop from level 2. Every call is a new session: the history starts empty.
export async function runAgent(prompt: string, maxTurns = 10): Promise<string> {
  const messages: Message[] = [
    { role: 'system', content: SYSTEM },
    { role: 'user', content: prompt },
  ]
  for (let turn = 1; turn <= maxTurns; turn++) {
    const res = await fetch(`${LLM.baseURL}/chat/completions`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
      body: JSON.stringify({ model: LLM.model, messages, tools: toolSchemas }),
    })
    const [choice] = (await res.json()).choices
    const reply: Message = choice.message
    messages.push(reply)
    if (choice.finish_reason !== 'tool_calls') return reply.content ?? ''

    for (const call of reply.tool_calls ?? []) {
      const run = tools[call.function.name]
      let output = `Unknown tool: ${call.function.name}`
      try {
        if (run) output = await run(JSON.parse(call.function.arguments))
      } catch (err) {
        output = `Error: ${err instanceof Error ? err.message : err}`
      }
      messages.push({ role: 'tool', tool_call_id: call.id, content: output })
    }
  }
  throw new Error(`No answer after ${maxTurns} turns`)
}

// Two days, two sessions. Nothing but the diary carries over.
await runAgent('Hi! A kilo of Colombia, ground for a moka pot, shipped to Rosario.')
console.log(await runAgent('Hi again! Send me the same as last time.'))
```

**Python**

```python
# Long-term memory, from scratch. Standard library only, no SDK.
import json
import math
import urllib.request
from pathlib import Path

# Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy...
LLM = {
    "base_url": "http://localhost:11434/v1",  # e.g. Ollama's default address
    "model": "your-model",  # e.g. "llama3.1", "gpt-4o-mini"
    "embedding_model": "your-embedding-model",  # e.g. "nomic-embed-text", "text-embedding-3-small"
    "api_key": "YOUR_API_KEY",  # local servers usually ignore it
}

def post(path, payload):
    request = urllib.request.Request(
        f"{LLM['base_url']}{path}",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM['api_key']}"},
    )
    with urllib.request.urlopen(request) as response:
        return json.load(response)

# 1. An embedding: a list of numbers that captures what a text means.
#    Texts that mean similar things get vectors that point the same way.
def embed(text):
    return post("/embeddings", {"model": LLM["embedding_model"], "input": text})["data"][0]["embedding"]

# How closely two vectors point the same way: 1 = same meaning, 0 = unrelated.
def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))

# 2. The diary: a file per customer, outside any session. Each note keeps its vector.
FILE = Path("memory/c-2291.json")
DIARY = json.loads(FILE.read_text()) if FILE.exists() else []

# 3. Two tools: one writes a note, the other searches by meaning.
def remember(note):
    DIARY.append({"text": note, "vector": embed(note)})
    FILE.write_text(json.dumps(DIARY))
    return f"Saved. {len(DIARY)} notes about this customer."

def recall(query):
    q = embed(query)
    hits = sorted(({"text": n["text"], "score": cosine(q, n["vector"])} for n in DIARY), key=lambda h: -h["score"])
    lines = [f"{h['score']:.2f} {h['text']}" for h in hits[:2] if h["score"] > 0.3]
    return "\n".join(lines) or "Nothing saved about that."

TOOLS = {"remember": remember, "recall": recall, "place_order": place_order}  # place_order: your code
TOOL_SCHEMAS = [...]  # one JSON Schema per tool: remember(note), recall(query), place_order(...)

SYSTEM = (
    "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."
)

# 4. The loop from level 2. Every call is a new session: the history starts empty.
def run_agent(prompt, max_turns=10):
    messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}]

    for _ in range(max_turns):
        choice = post("/chat/completions", {"model": LLM["model"], "messages": messages, "tools": TOOL_SCHEMAS})["choices"][0]
        reply = choice["message"]
        messages.append(reply)
        if choice["finish_reason"] != "tool_calls":
            return reply.get("content") or ""

        for call in reply.get("tool_calls", []):
            try:
                output = TOOLS[call["function"]["name"]](**json.loads(call["function"]["arguments"]))
            except Exception as err:
                output = f"Error: {err}"
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": output})

    raise RuntimeError(f"No answer after {max_turns} turns")

# Two days, two sessions. Nothing but the diary carries over.
run_agent("Hi! A kilo of Colombia, ground for a moka pot, shipped to Rosario.")
print(run_agent("Hi again! Send me the same as last time."))
```

## 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).

## Patterns liés

- [0 · Votre boîte à outils](https://harnesspatterns.dev/fr/patterns/your-toolkit.md)
- [7 · Le sac à dos déborde](https://harnesspatterns.dev/fr/patterns/compaction.md)
- [8 · Des skills à la demande](https://harnesspatterns.dev/fr/patterns/skills.md)
- [14 · Sécurité et sandboxing](https://harnesspatterns.dev/fr/patterns/security.md)
- [16 · Les agents proactifs](https://harnesspatterns.dev/fr/patterns/proactive-agents.md)
