Accee a #18220 (EPIC origami – lacunes pedagogiques verifiees) : la ligne Memoire paginee d’agent du tableau Agents est confirmee absente apres verification first-hand (07-KernelMemory-* couvre l’ETL documentaire, pas la memoire conversationnelle d’agent ; 10b/10f/10_LocalLlama/22 mentionnent PagedAttention cote inference KV cache, pas cote memoire d’agent ; rien dans ML/DataScienceWithAgents/ n’evoque fenetre glissante, resume progressif ou eviction de messages anciens).
Le probleme de la memoire non bornable
Un agent conversationnel qui appelle un LLM sur chaque tour accumule des messages dans la fenetre de contexte du modele. Trois symptomes apparaissent des que la conversation dure :
Cout : chaque tour complet repaie l’integralite de l’historique (input tokens). Au-dela de quelques milliers de messages, la facture explose.
Latence : la latence d’un appel LLM depend du nombre de tokens d’entree. La conversation ralentit mecaniquement au fil de l’eau.
Degradation de la qualite : les modeles oublient le milieu de leur contexte (lost-in-the-middle), et les anciens details ne servent plus rien une fois la discussion passee.
La memoire persistante de 14_Persistent_Memory.ipynb (vecteur + similarite cosinus) repond au probleme du transfer cross-session : les lecons survivent aux runs et sont retrocedees par similarite. Elle ne repond pas au probleme du cout intra-session : dans une seule conversation longue, l’historique continue de croitre jusqu’a exploser la fenetre.
Ce notebook introduit la memoire paginee : trois mecanismes qui cooperent pour borner la taille de la fenetre de contexte vue par le LLM a chaque tour, tout en preservant les informations anciennes sous une forme accessible a la demande.
Plan
Definitions et vocabulaire : fenetre glissante, resume progressif, eviction, paging.
Fenetre glissante (rolling window) : ne garder que les N derniers echanges intacts.
Resume progressif : condenser les messages evicques en un resume cumulatif.
Politique d’eviction : FIFO, LRU, importance-scored.
Memory paging (style Generative Agents / MemGPT) : hierarchie core/archival + retrieval a la demande.
Mesure : comparer le cout (en tokens et en messages retenus) sur une longue conversation synthetique, avec et sans pagination.
Trois exercices : fenetre parametrique, eviction LRU sur la memoire vectorielle de NB-14, transfer cross-paging multi-domaines.
0. Setup – structures de donnees pures, sans dependance API
Ce notebook est CPU-pur : il ne depend d’aucun appel LLM pour fonctionner. La pagination est un mecanisme structurel, et les resumes sont simules par troncature + concatenation (un vrai systeme brancherait ici un appel chat(...) de NB-14 ou NB-09 pour generer le resume ; ce notebook montre le squelette, le slot d’appel est marque d’un # HOOK).
from __future__ import annotationsfrom collections import deque, OrderedDictfrom dataclasses import dataclass, fieldfrom typing import Callable, Deque, Dict, List, Optional, Tupleimport jsonimport timeprint("Imports OK : dataclasses, collections, json (memoire paginee pure Python, sans API).")
Imports OK : dataclasses, collections, json (memoire paginee pure Python, sans API).
1. Definitions et vocabulaire
On formalise les trois mecanismes de pagination avant de les implementer. Le vocabulaire suit la litterature Generative Agents (Park et al., 2023) et MemGPT (Packer et al., 2023).
Mecanisme
Question qu’il resout
Granularite
Fenetre glissante (rolling window)
Combien de messages recents garder intacts ?
Message (1 tour = 1 echange user/assistant)
Resume progressif (progressive summarization)
Que faire des messages evicques ?
Bloc resume cumulatif
Eviction (eviction policy)
Lequel supprimer en priorite quand la memoire est pleine ?
FIFO / LRU / importance-scored
Memory paging (core/archival retrieval)
Comment retrouver une information ancienne sans tout recharger ?
Page (sous-unite de memoire adressable)
Un agent reel combine les quatre. Ici on les presente separement pour la clarte pedagogique, puis on les assemble dans une classe PagedMemory.
2. Fenetre glissante (rolling window)
Le mecanisme le plus simple : la memoire courante est un deque(maxlen=N) qui conserve les N derniers messages. Au-dela, les plus anciens sont automatiquement evicques (FIFO).
Avantage : O(1) en insert et en eviction, deterministe. Limite : aucune information n’est preservee au-dela de la fenetre (oubli total). Un message important du tour 3 est perdu au tour N+3.
@dataclassclass Message: role: str# user / assistant / system / tool content: str ts: float= field(default_factory=time.time) importance: float=1.0class RollingWindow:def__init__(self, max_messages: int):if max_messages <1:raiseValueError("max_messages >= 1 requis.")self.max= max_messagesself._buf = deque(maxlen=max_messages)self._evicted_total =0def append(self, msg): ev = []iflen(self._buf) ==self.max: ev.append(self._buf[0])self._buf.append(msg)self._evicted_total +=len(ev)return evdef__len__(self):returnlen(self._buf)def render(self):return"\n".join(f"[{m.role}] {m.content}"for m inself._buf)# Demonstration : 6 messages dans une fenetre de taille 4.rw = RollingWindow(max_messages=4)for i inrange(6): m = Message(role="user"if i %2==0else"assistant", content=f"message #{i}") ev = rw.append(m) label =f" -> evince(s) : {[e.content for e in ev]}"if ev else""print(f"append #{i}: taille={len(rw)}{label}")print()print("Contenu final de la fenetre :")print(rw.render())print(f"Total evicques sur la duree de vie : {rw._evicted_total}")
append #0: taille=1
append #1: taille=2
append #2: taille=3
append #3: taille=4
append #4: taille=4 -> evince(s) : ['message #0']
append #5: taille=4 -> evince(s) : ['message #1']
Contenu final de la fenetre :
[user] message #2
[assistant] message #3
[user] message #4
[assistant] message #5
Total evicques sur la duree de vie : 2
Lecture de la sortie. Les messages #0 et #1 ont ete evicques (sortie evince(s) : ['message #0'] puis ['message #1'] apparaissent sur les appends #4 et #5). Au final, la fenetre contient les messages #2 a #5, dans l’ordre. Le total evicque cumule est 2, et la taille de la fenetre reste bornee a 4 quel que soit le nombre d’appels.
3. Resume progressif
Quand un message est evince de la fenetre glissante, son contenu est perdu. Pour preserver l’information ancienne sous une forme compacte, on accumule un resume cumulatif : a chaque eviction, on concatene le message evince au resume en cours (ou on appelle un LLM pour le paraphraser).
Strategie simplifiee (notebook CPU-pur) : troncature a 80 caracteres par message evince, concatenation par saut de ligne. En production, remplacer par summarize(msg) -> str (cf # HOOK). Le resume reste borne par construction : on peut le summarizer a son tour quand il depasse max_resume_chars.
@dataclassclass ProgressiveSummary: chunks: List[str] = field(default_factory=list) max_chars_per_chunk: int=80def ajouter(self, msg):# HOOK production : remplacer par summarize(msg.content) (appel LLM). c = msg.contentiflen(c) >self.max_chars_per_chunk: c = c[: self.max_chars_per_chunk -3] +"..." prefix =f"[{msg.role}]"self.chunks.append(f"{prefix}{c}")def render(self, max_chars=None): body ="\n--\n".join(self.chunks)if max_chars isnotNoneandlen(body) > max_chars:return body[: max_chars -3] +"..."return bodydef__len__(self):returnsum(len(c) for c inself.chunks)# Demonstration : on couple RollingWindow et ProgressiveSummary.rw2 = RollingWindow(max_messages=3)sm = ProgressiveSummary()for i inrange(7): m = Message(role="user"if i %2==0else"assistant", content=f"Le projet X en est a l'iteration {i} et les tests passent presque tous sauf un edge case.")for evicted in rw2.append(m): sm.ajouter(evicted)print("Fenetre finale :")print(rw2.render())print()print(f"Resume cumulatif ({len(sm)} caracteres) :")print(sm.render(max_chars=300))
Fenetre finale :
[user] Le projet X en est a l'iteration 4 et les tests passent presque tous sauf un edge case.
[assistant] Le projet X en est a l'iteration 5 et les tests passent presque tous sauf un edge case.
[user] Le projet X en est a l'iteration 6 et les tests passent presque tous sauf un edge case.
Resume cumulatif (358 caracteres) :
[user] Le projet X en est a l'iteration 0 et les tests passent presque tous sauf un ...
--
[assistant] Le projet X en est a l'iteration 1 et les tests passent presque tous sauf un ...
--
[user] Le projet X en est a l'iteration 2 et les tests passent presque tous sauf un ...
--
[assistant] Le proj...
Lecture de la sortie. Sur 7 messages avec une fenetre de 3, 4 messages ont ete evicques et verses dans le resume cumulatif. Le resume preserve l’information ancienne sous forme tronquee (80 chars par chunk) ; le lecteur voit que les 3 messages les plus recents sont dans la fenetre et que les 4 precedents survivent sous forme compressee. En branchant un summarize() LLM sur la methode ajouter(), on obtient un resume semantique (au lieu d’une simple troncature).
4. Politique d’eviction – FIFO, LRU, importance-scored
La fenetre glissante est FIFO par construction. Mais on peut choisir une politique d’eviction : quand la memoire est pleine, lequel sacrifier ?
FIFO : le plus ancien (defaut de deque(maxlen=N)).
LRU (Least Recently Used) : celui qu’on n’a pas lu/cite depuis le plus longtemps. Necessite un compteur d’acces par message.
Importance-scored : celui dont l’importance est la plus basse. Necessite un score (calcule par le LLM, par l’usage, par les outils appeles, etc.).
Implementation : LRUMemory et ImportanceMemory comme variantes de RollingWindow (memes operations, politique de remplacement differente).
class LRUMemory:def__init__(self, max_messages: int):if max_messages <1:raiseValueError("max_messages >= 1 requis.")self.max= max_messagesself._store = OrderedDict()self._evicted = []def append(self, msg): ev = []iflen(self._store) >=self.max: evicted_id, evicted_msg =self._store.popitem(last=False) ev.append(evicted_msg)self._evicted.append(evicted_msg)self._store[id(msg)] = msgreturn evdef touch(self, msg):for k, v inlist(self._store.items()):if v is msg:self._store.move_to_end(k)returndef render(self):return"\n".join(f"[{m.role}] {m.content}"for m inself._store.values())def__len__(self):returnlen(self._store)class ImportanceMemory:def__init__(self, max_messages: int):if max_messages <1:raiseValueError("max_messages >= 1 requis.")self.max= max_messagesself._items = []self._evicted = []def append(self, msg): ev = []iflen(self._items) >=self.max: victim =min(self._items, key=lambda m: (m.importance, m.ts))self._items.remove(victim)self._evicted.append(victim) ev.append(victim)self._items.append(msg)return evdef render(self):return"\n".join(f"[{m.role}] (imp={m.importance:.2f}) {m.content}"for m inself._items)def__len__(self):returnlen(self._items)# Demonstration : 5 messages dans une LRU de taille 3.lru = LRUMemory(max_messages=3)msgs = []for i inrange(5): m = Message(role="user", content=f"message #{i}") msgs.append(m) lru.append(m)print("Apres 5 insertions, contenu LRU (du plus ancien au plus recent) :")print(lru.render())print()print("On touche msgs[1], deja evince -> sans effet : touch() ne ressuscite pas un message sorti.")lru.touch(msgs[1])print(lru.render())print()print("On touche msgs[2], le LRU courant (encore en memoire) -> il devient MRU.")lru.touch(msgs[2])print(lru.render())print()ev = lru.append(Message(role="user", content="message #5"))print(f"Insertion de #5 -> le message evince est : {ev[0].content}")print("Contenu final (le message touche a survecu a l'eviction) :")print(lru.render())# Demonstration importance-scored : un message important survit a un message recu mais banal.imp = ImportanceMemory(max_messages=3)for contenu, importance in [("a", 1.0), ("b", 1.0), ("c", 1.0), ("d", 1.0), ("PENSEE-CLE : ne jamais committer un secret", 10.0)]: imp.append(Message(role="user", content=contenu, importance=importance))print()print("ImportanceMemory apres 5 insertions :")print(imp.render())
Apres 5 insertions, contenu LRU (du plus ancien au plus recent) :
[user] message #2
[user] message #3
[user] message #4
On touche msgs[1], deja evince -> sans effet : touch() ne ressuscite pas un message sorti.
[user] message #2
[user] message #3
[user] message #4
On touche msgs[2], le LRU courant (encore en memoire) -> il devient MRU.
[user] message #3
[user] message #4
[user] message #2
Insertion de #5 -> le message evince est : message #3
Contenu final (le message touche a survecu a l'eviction) :
[user] message #4
[user] message #2
[user] message #5
ImportanceMemory apres 5 insertions :
[user] (imp=1.00) c
[user] (imp=1.00) d
[user] (imp=10.00) PENSEE-CLE : ne jamais committer un secret
Lecture de la sortie. Sur la memoire LRU : apres 5 insertions dans une fenetre de 3, le contenu est #2 / #3 / #4 (du plus ancien au plus recent). On touche d’abord msgs[1], deja evince : touch() ne fait rien, la memoire reste #2 / #3 / #4 – un message sorti ne se ressuscite pas. On touche ensuite msgs[2], le LRU courant : il passe en queue (#3 / #4 / #2). L’insertion de #5 evince alors #3 (le LRU) et non #2 : le message touche a survecu a l’eviction, la memoire finale est #4 / #2 / #5. La promotion MRU est donc visible dans la sortie, et non seulement annoncee. Sur l’ImportanceMemory : apres 5 insertions, le message PENSEE-CLE (importance 10) survit alors qu’il a ete insere en dernier, parce que les 4 precedents (tous importance 1) ont ete preferes a tour de role ; a la 5e insertion, le LRU-parmi-les-faibles est elimine et le message important reste.
Le memory paging est le mecanisme des systemes type MemGPT (Packer et al., 2023) et Generative Agents (Park et al., 2023) : la memoire est decoupee en pages (sous-unites adressables), et l’agent ne voit a chaque tour que la page courante dans son contexte. Pour acceder a une page non-courante, il appelle un outil de retrieval qui swap-in la page demandee.
L’analogie est volontairement systeme d’exploitation : la memoire de l’agent est un espace d’adressage virtuel, le contexte du LLM est la memoire physique (RAM), et le swap est un outil de retrieval.
Implementation simplifiee ici : PagedMemory tient une liste de pages ; current_page est celle injectee dans le prompt. swap_to(idx) charge une autre page (cout symbolique simule).
@dataclassclass Page: title: str messages: List[Message]def render(self): body ="\n".join(f" [{m.role}] {m.content}"for m inself.messages)returnf"=== Page: {self.title} ({len(self.messages)} msg) ===\n{body}"class PagedMemory:def__init__(self):self.pages = []self.current =0def add_page(self, page):self.pages.append(page)returnlen(self.pages) -1def swap_to(self, idx):ifnot (0<= idx <len(self.pages)):raiseIndexError(f"page index {idx} hors limites (0..{len(self.pages)-1}).")self.current = idxreturnself.pages[idx]def current_page(self):returnself.pages[self.current]def total_messages(self):returnsum(len(p.messages) for p inself.pages)# Demonstration : 3 pages sur 3 sujets, swap entre elles.pm = PagedMemory()pm.add_page(Page(title="onboarding", messages=[Message(role="user", content="Bonjour, je m'appelle Alice."), Message(role="assistant", content="Bonjour Alice, bienvenue.")]))pm.add_page(Page(title="bug-investigation", messages=[Message(role="user", content="Le test #42 echoue avec une AssertionError."), Message(role="assistant", content="Voyons la trace... la ligne 17."), Message(role="user", content="C'etait bien la ligne 17, merci.")]))pm.add_page(Page(title="preferences", messages=[Message(role="user", content="Je prefere qu'on reponde en francais."), Message(role="assistant", content="Compris, je repondrai en francais.")]))print(f"Memoire paginee : {len(pm.pages)} pages, {pm.total_messages()} messages au total.")print(f"Page courante (idx={pm.current}) : {pm.current_page().title}")print()for idx, page inenumerate(pm.pages): marker =" <-- courante"if idx == pm.current else""print(f"Page #{idx}: {page.title}{marker}")print()print("Swap vers la page preferences (idx=2)...")loaded = pm.swap_to(2)print(loaded.render())
Memoire paginee : 3 pages, 7 messages au total.
Page courante (idx=0) : onboarding
Page #0: onboarding <-- courante
Page #1: bug-investigation
Page #2: preferences
Swap vers la page preferences (idx=2)...
=== Page: preferences (2 msg) ===
[user] Je prefere qu'on reponde en francais.
[assistant] Compris, je repondrai en francais.
Lecture de la sortie. La memoire contient 3 pages (7 messages au total). La page courante est onboarding (idx 0). Apres swap_to(2), la page preferences est chargee dans le contexte. Le gain : un agent qui n’a besoin que du contexte preferences n’envoie au LLM que les 2 messages de cette page (au lieu des 7), divisant le cout d’input par 3.5 sur cet exemple ; en production, le gain peut etre bien superieur sur de longues conversations.
6. Mesure – cout en tokens sur une longue conversation
Comparons le cout de contexte (en caracteres total, proxy des tokens) entre une memoire non bornee (liste classique) et une memoire paginee + resume progressif, sur deux conversations synthetiques (100 et 1000 tours).
# Conversation synthetique : messages alternes user/assistant.MSG_AVG_LEN =120# caracteres par message (proxy de tokens ~1:4)WINDOW =10# taille de la fenetre glissanteRESUME_MAX =200# budget du resume cumulatif (chars)PAGE_SIZE =5# taille d'une pagedef gains_pour(n_turns):"""Cout total (chars) des trois memoires sur n_turns messages.""" conv = [ Message( role="user"if i %2==0else"assistant", content=f"Message #{i:03d} - "+"x"* MSG_AVG_LEN, )for i inrange(n_turns) ]# Memoire non bornee : on injecte TOUT l'historique a chaque tour. total_unbounded =sum(MSG_AVG_LEN * (i +1) for i inrange(n_turns))# Fenetre glissante + resume cumulatif (compact). rw = RollingWindow(max_messages=WINDOW) sm = ProgressiveSummary(max_chars_per_chunk=40) total_windowed =0for m in conv:for evicted in rw.append(m): sm.ajouter(evicted) total_windowed += WINDOW * MSG_AVG_LEN +min(RESUME_MAX, len(sm))# Memory paging a pages de PAGE_SIZE, swap automatique selon le sujet simule. pm = PagedMemory()for i inrange(0, n_turns, PAGE_SIZE): pm.add_page(Page(title=f"page-{i // PAGE_SIZE}", messages=conv[i : i + PAGE_SIZE])) total_paged = n_turns * (PAGE_SIZE * MSG_AVG_LEN + RESUME_MAX)return total_unbounded, total_windowed, total_pagedPROFILS = (100, 1000)resultats = {n: gains_pour(n) for n in PROFILS}print("="*84)print(f"Conversation synthetique : {MSG_AVG_LEN} chars/message, fenetre W={WINDOW}, pages P={PAGE_SIZE}")print("="*84)for n in PROFILS: ub, w, p = resultats[n]print(f"{n:>5} tours : non bornee = {ub:>11,} | fenetre = {w:>10,} (x{ub / w:>5.1f}) | paging = {p:>10,} (x{ub / p:>5.1f})")print()print("Verdict honnete (G.2) : le cout UNI-side varie avec le profil de la conversation.")print(" - non bornee : O(N) par tour -> explosion quadratique sur la duree.")print(" - fenetre : O(W) par tour -> lineaire borne, mais perte d'info ancienne.")print(" - paging : O(P) par tour + cout de swap, lineaire borne, info preservee par pagination.")gw = [resultats[n][0] / resultats[n][1] for n in PROFILS]gp = [resultats[n][0] / resultats[n][2] for n in PROFILS]print(f" - le gain croit avec N : fenetre x{gw[0]:.1f} -> x{gw[1]:.1f}, paging x{gp[0]:.1f} -> x{gp[1]:.1f}, a W et P fixes.")
====================================================================================
Conversation synthetique : 120 chars/message, fenetre W=10, pages P=5
====================================================================================
100 tours : non bornee = 606,000 | fenetre = 137,690 (x 4.4) | paging = 80,000 (x 7.6)
1000 tours : non bornee = 60,060,000 | fenetre = 1,397,690 (x 43.0) | paging = 800,000 (x 75.1)
Verdict honnete (G.2) : le cout UNI-side varie avec le profil de la conversation.
- non bornee : O(N) par tour -> explosion quadratique sur la duree.
- fenetre : O(W) par tour -> lineaire borne, mais perte d'info ancienne.
- paging : O(P) par tour + cout de swap, lineaire borne, info preservee par pagination.
- le gain croit avec N : fenetre x4.4 -> x43.0, paging x7.6 -> x75.1, a W et P fixes.
7. Trois exercices
Les exercices suivent la convention C.1 : pas d’erreur volontaire, le notebook tourne de bout en bout memes exercices non completes. Chaque exercice introduit un mecanisme non couvert dans la demonstration ci-dessus.
Exercice 1 – fenetre parametrique et cout marginal
Implemente une fonction cout_marginal(window_size, n_turns, msg_len) qui calcule le cout total d’une fenetre glissante sur n_turns messages, avec une taille de fenetre window_size et une longueur moyenne msg_len. La fonction doit aussi calculer a partir de quelle taille de fenetre le cout marginal (cout total / nombre de tours) devient inferieur au cout d’une memoire non bornee divise par 2.
Indice : le cout par tour d’une fenetre glissante est constant = window_size * msg_len des qu’on a fait window_size insertions. Le cout par tour d’une memoire non bornee est lineaire : i * msg_len au tour i. Trace les deux courbes et trouve leur intersection.
def cout_marginal(window_size, n_turns, msg_len):"""Exercice 1 : cout total + cout marginal d'une fenetre glissante. Renvoie (cout_fenetre, seuil_atteint, w_seuil). seuil_atteint = 1 si cout marginal < seuil, 0 sinon. w_seuil = taille de fenetre minimale qui satisfait le critere."""# TODO etudiant : implementer le calcul.return (0, 0, 0)_cout, _seuil, _w = cout_marginal(window_size=10, n_turns=100, msg_len=120)print(f"Exercice 1 - cout marginal : cout_total={_cout}, seuil_atteint={_seuil}, w_seuil={_w} (a completer)")
Exercice 2 – eviction LRU sur la memoire vectorielle de NB-14
Reprends MemoireVectorielle du notebook 14 (MyIA.AI.Notebooks/GenAI/Texte/14_Persistent_Memory.ipynb) et ajoute-lui une politique d’eviction importance + recence : a chaque ajouter, si len(entrees) > max_entrees, evince l’entree de score min(recence, importance). La recence est mesuree par l’age en secondes. L’importance est la longueur de la lecon (proxy : une lecon detaillee est plus importante qu’un acquittement sec, comme demontre dans NB-14).
Indice : le score peut etre 0.5 * (1 / (1 + age_en_secondes)) + 0.5 * (len(lecon) / max_lecon_len). A chaque ajout, recalcule les scores et evince le plus bas. Verifie que la lecon correcte (PENSEE-CLE : ne jamais committer un secret) reste en memoire apres plusieurs insertions.
class MemoireVectoriellePaginee:def__init__(self, embedder, max_entrees=50):self.embedder = embedderself.max_entrees = max_entreesself.entrees = []def ajouter(self, spec, lecon, ts=None):# TODO etudiant : ajouter une entree avec ts, vecteur, et evincer si > max_entrees.returnNonedef _score(self, entree, max_lecon_len, now):# TODO etudiant : combiner recence et importance en un score [0, 1].return0.0def retroceder(self, spec_query, k=3):# TODO etudiant : reprendre le code de NB-14 (BoWEmbedder.cosine).return []print("Exercice 2 - MemoireVectoriellePaginee : a completer.")
Exercice 2 - MemoireVectoriellePaginee : a completer.
Exercice 3 (avance) – transfer cross-paging multi-domaines
Etends PagedMemory pour gerer des domaines (par exemple : code, math, preferences). Chaque page porte un domaine, et la methode swap_to_domain(dom) charge la derniere page vue de ce domaine (au lieu de demander un index numerique). Teste : un utilisateur parle de code pendant 10 tours, puis de preferences pendant 5 tours, puis revient au code – le swap-to-domain re-charge-t-il bien la derniere page code vue ?
Indice : ajoute un champ domaine: str a Page, et un Dict[str, int]last_seen[dom] -> page_idx dans PagedMemory. La methode swap_to_domain(dom) consulte last_seen[dom] et appelle swap_to(...).
class PagedMemoryMultiDomaine:def__init__(self):self.pages = []self.last_seen = {}self.current =0def add_page(self, page, domaine):# TODO etudiant : ajouter la page, mettre a jour self.last_seen[domaine] = idx.return0def swap_to_domain(self, domaine):# TODO etudiant : swap vers last_seen[domaine] si present, sinon erreur explicite.returnNoneprint("Exercice 3 - PagedMemoryMultiDomaine : a completer.")
Exercice 3 - PagedMemoryMultiDomaine : a completer.
8. Conclusion et suite
On a introduit les trois mecanismes de la memoire d’agent bornee (fenetre glissante, resume progressif, eviction LRU/importance) et le memory paging style Generative Agents / MemGPT. La mesure sur deux profils (100 puis 1000 tours) montre qu’une fenetre de 10 divise le cout de contexte par 4.4 puis 43.0, et un paging par pages de 5 par 7.6 puis 75.1, par rapport a une memoire non bornee : le facteur croit avec la longueur de la conversation (cout cumule O(N^2) pour la memoire non bornee contre O(N) pour les memoires bornees), tout en preservant l’information ancienne (resume cumulatif ou pagination).
Limites honnetes (G.2) :
Resume simplifie : la demonstration utilise une troncature a 80 chars par message evince. Un vrai systeme appelle un LLM (summarize(msg) -> str) – cf le # HOOK dans ProgressiveSummary.ajouter. Le slot est marque, le code de production reste a brancher.
Cout de swap au paging : swap_to(idx) est suppose gratuit dans la demo (pas de cout de retrieval). En production (MemGPT), le swap implique un round-trip LLM pour decider quelle page charger – c’est le cout cache du paging, qui peut annuler le gain si la conversation switch souvent de sujet.
Importance subjective : la politique importance-scored utilise un proxy (longueur de la lecon) qui n’est qu’un proxy. En production, l’importance est scoree par le LLM ou par des signaux d’usage (messages les plus retrocedes, reponses les mieux notees par l’utilisateur).
Pas de jumeau .NET : ce notebook est Python pur. Un jumeau -CSharp implementerait les memes classes via LinkedList<T> pour la LRU et PriorityQueue<T> pour l’importance – voir la convention notebook-accretion-numbering.md §7.
Suite de l’epic #18220 (plis 2-3) :
#18211 – exploiter un LLM en production (routage, cache, cout, deploiement progressif) – la memoire paginee est l’un des leviers du deploiement progressif (garder en cache les pages les plus accedees).
#18213 – protocoles d’agents A2A et securite MCP – la pagination devient un vecteur d’attaque (un agent malveillant pourrait demander le swap d’une page contenant des secrets).
Suite logique 14c – Generative Agents complet : reflexion, importance scoree par LLM, planification journaliere. Cf Park et al., 2023.
References : Park et al., 2023 (Generative Agents: Interactive Simulacra of Human Behavior) ; Packer et al., 2023 (MemGPT: Towards LLMs as Operating Systems) ; NB-12 (Reflexion), NB-13 (routeur agentique), NB-14 (memoire vectorielle persistente).