Mnemosyne OS
Nouveau La documentation est en ligne — chaque moteur, pas à pas. docs.mnemosyne-os.io →
← Tous les articles

Un multiplicateur ne repêche jamais un zéro

Publié le 17 août 2026 8 min de lecture

longmemevalbenchmarkretrievalhybrid-searchbm25

Demandez à un système de mémoire « Quel service avait Admon dimanche ? » et une seule chose compte : sait-il retrouver l’unique session qui contient Admon ?

Le nôtre n’y arrivait pas. Pas de temps en temps — structurellement.

L’angle mort

Mnemosyne OS classe la mémoire par similarité vectorielle dense : embeddings e5, cosinus, un seul classement. C’est le bon outil pour le sens — « la fois où je me suis plaint de l’hôtel » retrouve la plainte sans partager un mot avec elle. Mais un jeton littéral rare — un nom propre, un identifiant, un numéro de version — est précisément ce qu’un embedder projette près du bruit. Il n’a jamais vu cette chaîne. Il n’a aucun sens à y accrocher. La session qu’il vous faut peut dormir au rang #800.

Mesuré sur LongMemEval full-haystack — même protocole que la campagne des 72,9 %, environ 480 sessions de distraction par question — sur un échantillon de 48 questions : pour 6 questions sur 48, pas une seule session de preuve ne remontait. Et sur les 35 questions où l’on pouvait suivre le chunk exact porteur de la réponse, il manquait au contexte servi 10 fois.

On ne répond pas avec ce que le retrieval ne vous a jamais tendu.

Le correctif séduisant, mesuré à néant

Le code contenait déjà la réponse évidente : un boost de termes. Extraire les jetons distinctifs de la question et, quand un chunk de mémoire en contient un mot pour mot, multiplier son cosinus par 2.

Il s’est passé deux choses, toutes deux instructives.

D’abord, branché naïvement, ce fut une catastrophe. Une question en prose met une majuscule à son premier mot : « What shift was Admon… » boostait « what » — un jeton présent dans à peu près chaque chunk conversationnel de l’univers — et le rappel des preuves s’est effondré de 38/48 à 17/48. La règle qui a survécu : une majuscule n’atteste un nom propre que si le mot n’est pas en tête de phrase.

Ensuite, une fois dompté, il a mesuré neutre de bout en bout. L’arithmétique dit pourquoi. Les sessions qui nous manquaient avaient un cosinus proche de zéro, et 0,30 doublé reste sous 0,80. Un multiplicateur amplifie un signal qui existe ; il n’en crée pas. Les chunks que cet angle mort perd ne sont pas classés bas — ils ne sont classés nulle part.

Un second classement, pas un plus gros multiplicateur

Le correctif qui a marché ne consulte jamais les vecteurs.

Classer le coffre deux fois. Une fois par cosinus, plus profond que le budget (200 au lieu de 32). Une fois lexicalement, avec Okapi BM25 — le bon vieux schéma de pondération de termes, quarante ans d’âge, celui que les produits de « recherche hybride » managés font tourner sous le capot. Puis fusionner les deux classements par Reciprocal Rank Fusion : chaque document marque la somme des 1/(k + rang) sur les canaux.

Des rangs — jamais des scores. Un cosinus vit dans [0,1] ; une somme BM25 est non bornée. La façon classique de rater la recherche hybride, c’est de tenter de normaliser l’un contre l’autre. RRF ne les compare jamais.

La fusion par rangs porte aussi une propriété de sûreté qu’il faut connaître avant de lire le moindre résultat : un document présent dans un seul classement peut au mieux égaler le #1 de l’autre canal — il ne dépassera jamais un document sur lequel les deux canaux s’accordent. Le canal lexical ne peut pas s’emparer du budget ; il ne peut que déloger des résultats vectoriels classés si profond que leur propre réciproque s’est éteinte. Et comme BM25 ne rend rien pour un document qui ne partage aucun terme avec la question, les canaux ne se recouvrent que partiellement — exactement là où la fusion a quelque chose à apporter.

Le budget de contexte n’a pas bougé : toujours 32 sources. Cela change quels chunks sont servis, jamais combien.

Les chiffres

Mesure (échantillon full-haystack de 48 questions)Vecteurs seuls+ canal lexical
Questions dont toutes les sessions de preuve remontent38/4841/48 (+6/−3)
Chunk porteur de la réponse dans le contexte servi (35 suivies)25/3530/35 (+6/−1)
Bout en bout, juge strict29/4837/48 (p apparié = 0,0215)

Le retrieval se mesure de façon déterministe — entrées identiques à l’octet, aucun LLM dans la boucle, reproduction gratuite. Le chiffre de bout en bout passe par un juge LLM, qui est bruité : rejouer des runs identiques à l’octet dans ce montage fait basculer environ 2,6 verdicts sur 48 — un écart sous ~5 questions n’est pas résoluble par ce banc. C’est pourquoi le retrieval est l’instrument, et le taux de réponse la confirmation. Le +8 passe le plancher avec de la marge — et il s’est reproduit sur deux runs indépendants, d’accord verdict par verdict.

Auditez-le. Chaque chiffre ci-dessus se recalcule depuis des lignes par-question publiées : les runs bruts (les deux runs stricts, les fichiers de recall déterministe, le holdout) et les ledgers extraits mécaniquement vivent sur la page de provenance des benchmarks, avec la clé de lecture de la campagne dans lexical-2026-08/. Une commande — node verify.js — redérive chaque cellule et échoue au moindre écart.

Le passage où l’on essaie de se mentir, et où l’on échoue

Il existe deux façons classiques de fabriquer un résultat pareil : régler les hyper-paramètres sur son échantillon, et ne publier que l’échantillon sur lequel on a développé.

Nous n’avons fait ni l’un ni l’autre.

Les paramètres sont les défauts de la littérature — BM25 k1=1,5, b=0,75 ; RRF k=60, la constante du papier d’origine. Délibérément non réglés : notre échantillon de 48 questions tourne environ 13 points plus facile que le benchmark dont il est tiré, et s’ajuster à un échantillon facile fabrique des gains qui ne transfèrent pas.

Puis le holdout : 48 questions fraîches que le canal n’avait jamais vues, tirées après le gel de la conception. Le gain a transféré — +4 questions avec preuves complètes, +2 chunks porteurs de réponse, zéro régression. Pas une question n’est ressortie moins bien servie. Même catégorie dominante de gains (raisonnement temporel) que sur l’échantillon de développement.

Ce que nous refusons de dire

Le contexte de ce travail : plus tôt en août, nous avions mesuré notre propre montage de retrieval augmenté d’une API de recherche hybride managée (Google Vertex AI Search). Ce montage augmenté par Google marquait 37/48 au juge strict. Le canal local atteint désormais le même 37/48 — sans API, sans compte, sans que rien ne quitte votre machine.

Lisez bien cette phrase, pour ce qu’elle ne dit pas. Elle ne dit pas que nous avons battu qui que ce soit. Nous avons comparé deux de nos propres montages — l’un appelant un service externe, l’autre pas — sous un protocole que nous contrôlons. Le produit managé n’a jamais été noté d’une façon qui autoriserait une comparaison de produits, et nous ne ferons pas semblant du contraire.

Les autres réserves restent imprimées à côté du chiffre, à leur place : un seul benchmark ; un échantillon de 48 questions plus mince et plus facile que son ensemble parent ; un juge côté réponse au plancher de bruit connu. Le holdout a levé l’objection que nous craignions le plus — avoir discrètement ajusté notre propre jeu de développement — mais une seconde famille de benchmarks reste due.

Les parties ennuyeuses qui rendent la chose réelle

  • L’index inversé est persistant — de simples tables SQLite dans la base du coffre, un dictionnaire de termes à clés entières. Construction à froid sur un coffre de 2 470 chunks : 19,5 s en naïf (un fsync par posting), moins d’une seconde en une seule transaction. Incrémental ensuite : un ingest n’indexe que ce qu’il ajoute.
  • Pas FTS5, délibérément. Le harnais de benchmark tourne sur node:sqlite, qui n’a pas le module FTS5 — le levier y aurait été immesurable, et un levier non mesuré est indistinguable d’un levier qui n’a jamais tourné. Les défauts de FTS5 (k1=1,2, tokenizer unicode61) auraient de surcroît changé en silence le classement même que nous mesurions.
  • Un index incomplet ne rend rien, et le pipeline retombe sur le classement vectoriel — le comportement d’hier. Un index à moitié construit a des fréquences documentaires fausses et produit un classement confiant, silencieusement faux. Une fonctionnalité absente vaut mieux qu’une réponse fausse.
  • L’isolation mémoire par application est appliquée dans le canal lui-même, avec ses propres tests — pas déléguée à qui voudra bien l’appeler.

Le canal part avec la v1.3.8, actif par défaut. MNEMO_LEXICAL_FUSION=0 restaure le chemin vecteurs-seuls à l’octet près — l’issue de secours que nos propres réserves exigent.


Ce que nous pouvons dire tient en une ligne : un nom propre que votre mémoire n’entendait pas retrouve désormais sa session — localement, mesuré, reproduit deux fois, et confirmé sur des questions jamais vues.

Partager

Citer cet article

Pas de DOI — un billet de blog n'est pas un dépôt. La citation porte sur la page elle-même.

APA

Mnemosyne OS. (18 août 2026). Un multiplicateur ne repêche jamais un zéro. Mnemosyne OS. https://mnemosyne-os.io/fr/blog/a-multiplier-cannot-rescue-a-zero

BibTeX
@misc{mnemosyne-a-multiplier-cannot-rescue-a-zero,
  author       = {{Mnemosyne OS}},
  title        = {Un multiplicateur ne repêche jamais un zéro},
  year         = {2026},
  month        = {8},
  day          = {18},
  howpublished = {Blog post, Mnemosyne OS},
  url          = {https://mnemosyne-os.io/fr/blog/a-multiplier-cannot-rescue-a-zero}
}