Aller au contenu principal

L'Ordre des choses : quand la neutralité a besoin d'un mélange

· 6 minutes de lecture
Jean-Noël Schilling
Locki one / french maintainer

Comment un message WhatsApp a révélé un biais systémique involontaire — et pourquoi l'open source a permis de le corriger en quelques heures

Le message

Il est arrivé en début d'après-midi, entre deux réunions. Une citoyenne — vive, à l'aise avec la technique, avec trente minutes de pause — avait remarqué quelque chose que nous n'avions pas vu.

"C'est normal que Lardic soit toujours cité en premier sur votre comparatif ?"

Elle avait cliqué sur plusieurs thèmes dans l'outil de comparaison. À chaque fois, la même liste apparaissait en premier : Construire l'Avenir. Puis Guillon. Puis sa propre liste. Puis Bosser. Toujours le même ordre. Même après quinze minutes d'inactivité. Même après avoir rechargé la page.

"Si vous voulez mettre en avant Lardic, faites le, mais ne vous présentez pas comme neutre car ce n'est pas honnête."

Elle avait raison. Pas sur l'intention — il n'y en avait pas — mais sur l'effet. Un outil qui revendique la neutralité mais présente une liste en premier, à chaque fois, dans chaque comparaison, n'est pas neutre. La perception compte autant que le principe. Et à deux semaines d'une élection municipale, elle compte davantage.

La chaîne

Remonter à la cause a pris moins de temps que la conversation.

Depuis Python 3.7 (2018), les dictionnaires conservent l'ordre d'insertion — ce qui était un détail d'implémentation de CPython 3.6 est devenu une garantie du langage. Quand nous avons défini les quatre listes électorales dans la configuration, nous les avons écrites dans un ordre particulier — alphabétique par slug interne : ca, paa, spae, csnf. Cet ordre s'est propagé, inchangé, à travers chaque couche :

  1. L'interface extrayait les clés du dictionnaire sous forme de liste : ["ca", "paa", "spae", "csnf"]
  2. La couche de recherche interrogeait ChromaDB pour chaque liste, dans cet ordre
  3. Le constructeur de contexte concaténait les extraits, liste par liste, dans cet ordre
  4. Le LLM recevait le prompt avec les extraits de Construire l'Avenir en premier

Les LLM ont tendance à présenter l'information dans l'ordre où ils la reçoivent. Le modèle n'était pas biaisé. Le prompt était ordonné. Et l'ordre avait été fixé le jour où quelqu'un avait tapé un dictionnaire Python.

Personne n'a choisi cela. Personne ne l'a remarqué. C'est justement le problème.

La correction

La correction vit à deux endroits, ceinture et bretelles.

Dans le code (app/agents/ocapistaine/features/compare.py) : avant de construire le contexte que le LLM va lire, l'ordre des listes est mélangé avec random.shuffle(). Chaque requête obtient une permutation différente. Quatre listes produisent 24 ordres possibles, tous également probables.

Dans le prompt (app/rag/prompts.py) : une instruction explicite demande au modèle de suivre l'ordre des extraits qu'il reçoit, plutôt que d'imposer le sien. Le LLM ne choisit pas qui passe en premier — c'est le hasard.

Coût total : zéro. Pas de changement de modèle, pas de ré-indexation, pas d'infrastructure. Deux lignes de random.shuffle() et une phrase dans le prompt système.

Pourquoi l'open source compte ici

La citoyenne qui a signalé le problème n'avait pas le temps de lire le code — elle l'a dit elle-même. Mais elle savait que le code était . C'est la différence entre « faites-moi confiance, je suis neutre » et « voici comment ça fonctionne, vérifiez par vous-même ».

Les prompts qui instruisent l'IA sont open source. La logique de recherche est open source. La configuration qui définit quelles listes existent et comment elles sont comparées est open source. N'importe qui — un candidat, un journaliste, un citoyen avec trente minutes — peut retracer le chemin exact d'une question à sa réponse.

Où regarder

Pour ceux qui veulent vérifier — ou qui veulent simplement comprendre comment l'outil traite leur programme :

Quoi
Prompt de comparaison (ce qu'on dit à l'IA)app/rag/prompts.py — la variable COMPARE_SYSTEM_PROMPT
Prompts synchronisés (version de production)app/prompts/local/ocapistaine_rag.jsonocapistaine.compare_system
Logique de recherche (comment les documents sont récupérés)app/rag/retrieval.py — fonction search_compare()
Constructeur de contexte (comment les résultats sont assemblés)app/agents/ocapistaine/features/compare.py — le mélange + concaténation
Persona de l'agent (principes de neutralité)app/agents/ocapistaine/prompts.py — « Neutralité absolue entre les listes »
Reformulation (comment les questions sont pré-traitées)app/prompts/local/ocapistaine_rag.jsonocapistaine.refine_system

Tous les fichiers sont dans le dépôt OCapistaine, branche dev.

Le pipeline d'information, étape par étape

Quand un citoyen pose une question de comparaison, voici exactement ce qui se passe :

  1. Reformulation — Un appel LLM léger corrige l'orthographe, résout les noms propres (ex. « van praet » devient « Van Praët ») et détecte la catégorie thématique. Il s'appuie sur un répertoire de noms de candidats connus.

  2. Recherche — ChromaDB est interrogé indépendamment pour chaque liste. La même question est recherchée dans les documents indexés de chaque programme. Les résultats reviennent avec un score de distance mesurant la pertinence sémantique.

  3. Mélange — L'ordre des listes est randomisé. Aucune liste n'est systématiquement première.

  4. Construction du contexte — Les extraits de chaque liste sont assemblés dans un prompt unique, avec des en-têtes de section clairs par liste.

  5. Synthèse — Le LLM (actuellement Mistral) lit tous les extraits et produit une comparaison structurée, en suivant les règles du prompt : neutre, factuel, pas de sources dans le texte, format structuré.

  6. Affichage — La réponse est montrée au citoyen, avec les documents sources listés séparément en dessous.

Le modèle ne voit jamais quelle liste « nous » préférons — parce que nous n'en préférons aucune.

L'écart de données

Le mélange corrige l'ordre. Il ne corrige pas la profondeur.

Notre collection ChromaDB contient 511 fragments. La répartition n'est pas uniforme :

ListeFragmentsPart
Documents de référence39277%
Passons à l'Action5511%
Construire l'Avenir316%
S'unir pour Audierne-Esquibien275%
Cap sur Notre Futur61%

Une liste avec 6 fragments produira des réponses plus minces qu'une liste avec 55, quel que soit l'ordre. Ce n'est pas un biais — c'est la disponibilité des données. Les listes qui n'ont pas encore publié leur programme complet auront naturellement moins de matière à comparer.

La solution est simple : publiez le programme, et nous l'indexerons. Le pipeline d'ingestion est prêt. Le puits est ouvert.

La leçon

Construire un outil neutre n'est pas une décision ponctuelle. C'est une discipline continue. On peut écrire « neutralité absolue » dans son prompt système et le penser sincèrement — et quand même voir un dictionnaire Python le compromettre.

La citoyenne qui a remarqué ne cherchait pas la mauvaise foi. Elle cherchait la rigueur. C'est le genre de regard critique qu'un outil civique devrait accueillir, pas craindre.

Nous avons corrigé en quelques heures. Non pas parce que le problème était simple — le biais d'ordre dans la présentation d'informations est un phénomène bien étudié en sciences cognitives — mais parce que l'architecture était suffisamment transparente pour que remonter de la cause à l'effet soit direct.

Les prompts sont ouverts. Le code est ouvert. Le pipeline de données est documenté. Et maintenant, l'ordre est aléatoire.

Comme il aurait dû l'être dès le départ.

Voir aussi : Le Puits de Kvasir | Ò Capistaine, My Capistaine | Le Manifeste du Phare