Przeglądaj źródła

Mémoire coupée par blocs : moins de réécritures en cache

Quand la mémoire est pleine, elle n'oublie plus un échange à chaque
message : elle grandit jusqu'à MAX_HISTORY_LENGTH + la moitié, puis
revient à MAX_HISTORY_LENGTH d'un coup. Entre deux coupes, le début de
l'historique reste dans le cache de prompt au lieu d'être réécrit à
chaque message (environ 66 % moins cher pour l'historique avec 20
messages, en conversation animée).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 4 dni temu
rodzic
commit
f00b65617e
3 zmienionych plików z 21 dodań i 3 usunięć
  1. 3 1
      .env.exemple
  2. 11 2
      bot/history.py
  3. 7 0
      changelog.txt

+ 3 - 1
.env.exemple

@@ -30,7 +30,9 @@ WEB_SEARCH=off
 WEB_MAX_USES=3
 
 # --- Mémoire -------------------------------------------------------------
-# Nombre de messages gardés en mémoire dans le salon dédié (questions + réponses)
+# Nombre de messages gardés en mémoire dans le salon dédié (questions + réponses).
+# La mémoire peut dépasser ce nombre de moitié avant d'y revenir d'un coup (10 → 14 → 10,
+# 20 → 30 → 20) : son début reste ainsi dans le cache de prompt, ce qui coûte moins cher.
 MAX_HISTORY_LENGTH=10
 # Nombre de messages lus comme contexte quand on mentionne le bot ailleurs
 CONTEXT_MESSAGE_LIMIT=20

+ 11 - 2
bot/history.py

@@ -32,6 +32,8 @@ class ConversationHistory:
     def __init__(self, file_path: Path, max_length: int):
         self.file_path = file_path
         self.max_length = max_length
+        # Marge au-delà de max_length avant de couper (voir _trim) : la moitié, en échanges entiers.
+        self.margin = max(2, max_length // 4 * 2)
         self._data: dict[str, list[dict]] = self._load()
         self._locks: dict[str, asyncio.Lock] = {}
         self._save_lock = asyncio.Lock()
@@ -68,8 +70,15 @@ class ConversationHistory:
     # ------------------------------------------------------------ internes
 
     def _trim(self, messages: list[dict]) -> list[dict]:
-        """Garde les N derniers messages, en commençant toujours par un message utilisateur."""
-        trimmed = messages[-self.max_length:]
+        """Garde au moins les N derniers messages, en commençant toujours par un message utilisateur.
+
+        La mémoire est coupée par blocs : elle grandit jusqu'à N + marge messages, puis revient à N
+        d'un coup. Entre deux coupes, son début ne change pas et reste dans le cache de prompt ;
+        couper un échange à chaque message obligerait à réécrire tout l'historique en cache."""
+        if len(messages) <= self.max_length + self.margin:
+            trimmed = messages
+        else:
+            trimmed = messages[-self.max_length:]
         while trimmed and trimmed[0]["role"] != "user":
             trimmed = trimmed[1:]
         return trimmed

+ 7 - 0
changelog.txt

@@ -17,6 +17,13 @@ Nouveautés
   refuser les PDF) et 20 Mo. Les PDF protégés par un mot de passe sont
   refusés. Comme pour les images, seul le nom du fichier reste en mémoire.
   Nouvelle dépendance : pypdf (pip install -r requirements.txt).
+- Mémoire moins chère : quand elle est pleine, elle n'oublie plus un
+  échange à chaque message, mais plusieurs d'un coup. Elle grandit jusqu'à
+  MAX_HISTORY_LENGTH + la moitié (20 → 30 messages), puis revient à
+  MAX_HISTORY_LENGTH. Entre deux coupes, le début de la conversation reste
+  dans le cache de prompt au lieu d'être réécrit à chaque message : dans
+  une conversation animée, l'historique coûte environ 2 à 3 fois moins
+  cher.
 - Fichiers texte : en plus des .txt, Rika lit les .md, .csv, .json, .log,
   sous-titres (.srt, .vtt), fichiers de configuration, pages HTML et le
   code (.py, .js, .java, .cpp, .cs…).