Aller au contenu
English

Reclassement et reformulation des requêtes

  • Leçon 9
  • 50 min
  • Module 3 : Créer et comparer la récupération

Ce que vous saurez faire

Placez le bon passage en tête de liste, pas seulement parmi les 20 premiers. Vous ajoutez un deuxième lecteur, plus lent, qui relit les meilleurs candidats. Vous laissez aussi un modèle de langage reformuler la question. Ensuite, vous mesurez les gains de chacun, leur coût en temps et leurs risques d’échec.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

  • La recherche par mots-clés, vectorielle et hybride, les filtres et les k premiers résultats, vus dans Recherche lexicale, dense et hybride.
  • Un adaptateur de fournisseur, le fournisseur simulé et les réponses enregistrées, vus dans Créer des applications fiables avec les API de LLM (le cours). Cette leçon utilise la même approche.

Ce qu’il vous faut sur votre ordinateur

  • Le projet de Recherche lexicale, dense et hybride, avec .venv actif. python -m pytest affiche 44 passed (44 tests réussis), et rerank.py se trouve dans policy_assistant/.
  • Environ 0,5 Go d’espace disque libre pour le modèle de reclassement (reranker).

Aucun compte ni aucune clé : les reformulations des requêtes proviennent d’enregistrements. Testé avec Python 3.12.13 sur macOS (Apple silicon). Les commandes Windows ne sont pas testées.

Une entreprise reçoit 200 candidatures pour un poste. Une recruteuse parcourt les 200 en une heure, à la recherche de quelques mots : le bon diplôme, le bon outil, la bonne ville. Elle en garde 20. Ensuite, une gestionnaire lit attentivement ces 20 candidatures, du début à la fin, et les classe. La gestionnaire est beaucoup plus lente : elle ne pourrait jamais lire les 200. Mais elle lit bien les 20.

La recherche peut fonctionner de la même façon. Une première recherche rapide garde 20 candidats, puis un modèle plus lent et plus attentif lit seulement ceux-là.

Le problème : le bon passage est dans la liste, mais trop bas

Section intitulée « Le problème : le bon passage est dans la liste, mais trop bas »

Camille demande : « Combien de temps dure la garantie d’un barbecue? » La réponse est dans le tableau des garanties en anglais : « BBQs and grills | the Ember GR-310 | 2 years » (barbecues et grils | le Ember GR-310 | 2 ans). Voici la recherche hybride :

Fenêtre de terminal
python -m policy_assistant search "Combien de temps dure la garantie d'un barbecue?" --method hybrid
1. 0.0315 [a540f2d18f86] garantie-legale-quebec v1 | Garantie prolongée | public
2. 0.0315 [09e9e322e55c] garantie-legale-quebec v1 | Comment faire une demande | public
3. 0.0313 [578b6d5a7284] garantie-legale-quebec v1 | Deux garanties différentes | public
4. 0.0310 [ad9138005226] garantie-legale-quebec v1 | Après la fin de la garantie L… | public
5. 0.0301 [4bf0b43abe06] faq-quebec v2 | Comment retourner un article? | public
hybrid, top 5, as of 2026-10-09, audience staff

(La ligne de texte sous chaque résultat est omise ici.) Les cinq segments sont en français, car ils partagent le mot « garantie » avec la question. Aucun ne donne la durée de garantie d’un barbecue. Le tableau des garanties figure dans la liste des 20, au rang 10. La recherche l’a trouvé, mais ne l’a pas placé en tête.

La même chose arrive avec « What is LB-1? » (Qu’est-ce que LB-1?). La recherche hybride place le passage sur le taille-haie au rang 6. Dans les deux cas, la première recherche a fait son travail : recueillir des candidats. Le classement est le point faible.

Un modèle de plongement lit la question seule et le passage seul, puis produit un vecteur pour chacun. Les deux vecteurs sont comparés seulement après. Le modèle ne voit jamais les deux textes en même temps. Il ne peut pas remarquer que « barbecue » correspond à la ligne « BBQs and grills ». Il ne peut pas voir que « garantie », dans un titre français, concerne un autre sujet.

Un encodeur croisé (cross-encoder) lit la question et un passage ensemble, comme une seule entrée. Il renvoie un score : dans quelle mesure ce passage répond à cette question. Son nom décrit son fonctionnement : il encode les deux textes en les croisant. À l’inverse, un modèle qui produit un plongement de chaque texte séparément est un bi-encodeur. Le reclassement consiste à prendre les meilleurs candidats d’une recherche rapide et à leur attribuer un score avec un encodeur croisé. Ensuite, on les trie de nouveau selon ce score.

À gauche, le modèle de plongement e5 : il lit query : What is LB-1? seul et produit 384 nombres. Il lit passage : After each use ... blade oil LB-1 seul et produit 384 nombres. Le cosinus des deux vecteurs donne un nombre; les plongements des passages sont produits une seule fois, à l'ingestion. À droite, l'encodeur croisé : il lit What is LB-1? et After each use ... blade oil LB-1 ensemble et renvoie un score, −0,87. Il s'exécute une fois par candidat, pour chaque question.
Un modèle de plongement lit chaque texte seul. On peut donc produire les vecteurs des passages une fois, à l'avance. Un encodeur croisé lit la paire ensemble : il est plus attentif, mais doit s'exécuter pour chaque question et chaque candidat.
Description textuelle du diagramme

À gauche, le modèle de plongement e5. Il lit « query : What is LB-1? » (requête : Qu’est-ce que LB-1?) seul et renvoie 384 nombres. Séparément, il lit « passage : After each use … blade oil LB-1 … » (passage : Après chaque utilisation… huile pour lame LB-1…) et renvoie 384 nombres. Les deux vecteurs sont comparés avec la similarité cosinus, qui donne un nombre. Les vecteurs des passages sont produits une seule fois, à l’ingestion. Une recherche produit donc seulement le plongement de la question. À droite, l’encodeur croisé. Il lit « What is LB-1? » et « After each use … blade oil LB-1 … » ensemble, comme une seule entrée. Il renvoie un score : −0,87. Il doit s’exécuter une fois pour chaque passage candidat, pour chaque nouvelle question.

Pourquoi ne pas utiliser un encodeur croisé pour tout? Parce qu’on ne peut rien préparer à l’avance. Pour chaque question, il doit relire chaque passage. Avec 185 segments, cela prendrait des secondes par question; avec un million, des heures. C’est pourquoi il relit seulement les 20 premiers résultats d’une recherche rapide. C’est comme la gestionnaire qui lit 20 candidatures, pas 200.

Le modèle de reclassement de Larkfield est cross-encoder/mmarco-mMiniLMv2-L12-H384-v1, un modèle ouvert sous licence Apache 2.0, d’environ 470 Mo. Il a été entraîné sur des paires question-passage dans 14 langues. Le cours a aussi testé un modèle de reclassement uniquement anglais, cross-encoder/ms-marco-MiniLM-L6-v2. Il prenait moins de la moitié du temps : 167 ms contre 391 ms pour 20 candidats, lors de la comparaison. Mais pour les questions françaises, son hit@5 descendait à 0,625, contre 0,875.

Voici « What is LB-1? » avec --method rerank. Le projet prend les 20 premiers résultats de la recherche hybride, après les filtres. L’encodeur croisé attribue ensuite un score aux 20 :

1. -0.8719 [86a0a5266439] product-care-hedge-trimmer-ht550 v1 | After each use | public
After each use Unplug it, clean the blade, and put a few drops of blade oil LB-1 along the blade. F…
2. -5.2345 [d753fb02bc8c] privacy-policy v3 | Why we use it | public
Why we use it We use your information to deliver orders, answer your questions, prevent fraud, and …
3. -5.4942 [a31b42d3e40d] damaged-or-wrong-items v2 | Wrong item | public
Wrong item We send a free return label. We ship the right item as soon as the courier scans the ret…
4. -5.5988 [e50c70a93a32] warranty-claims-procedure v3 | Approval limits | staff
Approval limits - An agent may approve a repair, replacement or refund worth up to 300 dollars. - F…
5. -5.6818 [13f00fce8eaa] privacy-policy v3 | Our help-desk assistant | public
Our help-desk assistant Our help-desk staff use an assistant that drafts answers from Larkfield's p…
rerank, top 5, as of 2026-10-09, audience staff

Le passage sur le taille-haie passe du rang 6 au rang 1. Regardez les scores. Le premier est −0,87, et le deuxième est −5,23 : un écart de plus de 4. L’encodeur croisé tranche beaucoup plus nettement que e5, dont les scores étaient proches les uns des autres.

Les scores ne sont ni des probabilités ni des similarités cosinus. Ce sont des nombres bruts du modèle, et seul leur ordre compte. Un score négatif ne signifie pas « incorrect » : −0,87 est ici le meilleur des 20. Ne fixez pas de seuil comme « garder les scores supérieurs à 0 » sans le tester sur vos questions.

La question française sur le barbecue bénéficie de la même aide. Après le reclassement, le tableau des garanties arrive premier, avec −0,55. Les segments français sur la garantie légale quittent les cinq premiers résultats.

Une façon de mesurer le classement est le rang réciproque moyen (MRR). Pour chaque question, divisez 1 par le rang du premier passage pertinent. Calculez ensuite la moyenne pour toutes les questions. Un passage au rang 1 donne 1, au rang 2 donne 0,5, au rang 5 donne 0,2. Une recherche sans passage pertinent donne 0. Le MRR récompense donc un passage pertinent près du sommet, pas seulement parmi les cinq premiers. Mesurer la récupération l’explique en détail.

La position moyenne du premier passage pertinent. Le reclassement donne le meilleur ordre; reformuler la question avant la recherche l'améliore encore.
Description textuelle du graphique

Un graphique à colonnes du rang réciproque moyen pour les 59 questions de Larkfield ayant un passage pertinent. Il utilise les cinq premiers résultats, avec les filtres activés. BM25 : 0,655. Recherche vectorielle avec e5 : 0,769. Recherche hybride : 0,745. Reclassement, soit les 20 premiers résultats hybrides relus par l’encodeur croisé : 0,831. Reclassement avec la question reformulée : 0,882. L’axe va de 0 à 1.

Reclasser les candidats hybrides fait passer hit@5 de 0.864 à 0.898, et le MRR de 0.745 à 0.831. Pour les questions, cela fait entrer 4 passages pertinents dans les cinq premiers résultats, et en fait sortir 2. Les 4 concernent LB-1, le barbecue en français, une question française sur les retours et une question sur les retours des Fêtes. L’une des 2 est Q60, « Larkfield vend-elle une garantie prolongée? » : rang 3 avant le reclassement, rang 14 après. Chaque changement améliore certaines questions et en dégrade d’autres. Regardez les deux listes, pas seulement le total.

La recherche vectorielle seule garde le meilleur hit@5, 0.915. Mais le reclassement donne le meilleur ordre : son premier résultat est pertinent pour 80 % des questions, contre 68 % pour la recherche vectorielle. L’étape de réponse du module Construire des réponses fondées sur les sources lit les passages dans l’ordre de classement. Le cours garde donc le reclassement comme méthode par défaut.

Passons au coût. Voici les médianes sur l’ordinateur de test, un Mac partagé avec 2 fils d’exécution du processeur pour les modèles :

Étape, pour une question Temps
Recherche BM25 0,26 ms
Produire le plongement de la question avec e5 5,2 ms
Le comparer à 185 vecteurs 0,09 ms
Reclasser 20 candidats 322 ms

Le reclassement est plus de 50 fois plus lent que tout le reste de la recherche. Pour un agent avec une question, 0,3 s convient. Pour un service qui répond à 100 questions par seconde, c’est un problème de processeur. Le coût augmente avec le nombre de candidats : 20 est un choix, pas une loi. Moins de candidats accélère la recherche; plus de candidats peut permettre d’en trouver davantage.

Revenons à la bibliothèque. Avant de chercher dans le catalogue, une bibliothécaire vous aide en demandant : « Que voulez-vous dire par “un cadeau pour ma mère qui jardine”? » Ensuite, elle tape « gants de jardinage » ou « sécateur ». Elle transforme votre question en mots utilisés par le catalogue.

La reformulation des requêtes fait cela avec un modèle de langage. Le modèle reçoit la question de l’agent et renvoie une requête de recherche. La recherche utilise cette requête à la place. Cela aide quand les mots de la question diffèrent beaucoup de ceux des documents. Par exemple, des questions françaises sur des documents anglais, ou des mots courants plutôt que des termes de politique.

La requête (prompt) du projet se trouve dans policy_assistant/prompts/rewrite.md :

policy_assistant/prompts/rewrite.md
[system]
You turn a question from a help-desk agent into one search query for Larkfield's policy documents (an online shop for home and garden products in Canada). The documents are mostly in English, some in French.
- Write the query in English, even when the question is in French.
- Keep every product code, part number, amount and date exactly as written.
- Use the words that a policy document would use (for example "return window", "warranty period", "refund").
- Do not answer the question. Return only the query, on one line.
[user]
{question}

Les reformulations des 67 questions ont été enregistrées avec chat-small (gpt-6-luna sur Azure). Le fournisseur simulé les rejoue, donc vous n’avez besoin d’aucune clé. Voici quelques exemples, avec le rang du premier passage pertinent pour BM25 avant et après la reformulation :

Question Reformulation enregistrée Rang BM25 : avant → après
Puis-je retourner une carte-cadeau? {rw.Q63.rewrite} not in top 20 → 2
How long is the warranty on a garden sprinkler? {rw.Q34.rewrite} 16 → 1
Which pump kit fits the PW-2200? {rw.Q38.rewrite} 1 → 1
How long do we keep chat transcripts with customers? {rw.Q08.rewrite} 1 → 17
Larkfield vend-elle une garantie prolongée? {rw.Q60.rewrite} 2 → not in top 20

Les trois premiers montrent l’aide apportée. La question française sur les cartes-cadeaux devient une suite de mots anglais. BM25 trouve maintenant le passage sur les cartes-cadeaux. « Garden sprinkler warranty period » (durée de garantie d’un arroseur de jardin) est plus court et retire les mots interrogatifs. Le tableau des garanties passe du rang 16 au rang 1. Le code produit PW-2200 reste exactement comme il était écrit, conformément à la requête.

Les deux derniers montrent les effets nuisibles. « Chat transcripts » (transcriptions de clavardage) est devenu « retention period customer service records » (durée de conservation des dossiers du service à la clientèle). La section de confidentialité qui y répond, « How long we keep it » (combien de temps nous les conservons), passe du rang 1 au rang 17. Q60 est le pire cas : l’exemple d’échec ci-dessous l’examine.

Regardez aussi ce que le modèle a ajouté à certaines reformulations. Voici de vraies sorties enregistrées :

  • Q05 : {rw.Q05.rewrite}
  • Q45 : {rw.Q45.rewrite}
  • Q46 : {rw.Q46.rewrite}
  • Q43 : {rw.Q43.rewrite}

Des caractères d’autres langues et systèmes d’écriture, et un &returns inattendu, à la fin d’une requête. Rien dans la requête au modèle ne les demandait. La recherche par mots-clés ignore la plupart de ces caractères, car ils ne correspondent à aucun segment. Mais ils montrent que la sortie d’un modèle n’est jamais propre par défaut. Q46 a aussi perdu une partie de son sens. L’agent demandait : « La FAQ du centre d’aide dit que les remboursements prennent 3 jours ouvrables. Est-ce exact? » La reformulation garde « 3 jours ouvrables » et retire le doute sur la FAQ. Elle cherche maintenant l’affirmation, pas la question.

Méthode hit@5 MRR
BM25 0.831 0.655
BM25 + reformulation 0.898 0.745
Reclassement 0.898 0.831
Reclassement + reformulation 0.932 0.882

La reformulation aide surtout BM25 : 8 questions gagnent un passage pertinent parmi les cinq premiers résultats, et 4 en perdent un. Avec le reclassement, le gain est plus faible. Mais le reclassement avec reformulation est la meilleure combinaison de ce module.

Ce n’est pas gratuit. Cela ajoute un appel au modèle avant chaque recherche : une médiane de 1.33 s ici, quatre fois le temps de reclassement. Les 67 reformulations enregistrées ont coûté 0.0029 $ US au total, donc le coût monétaire est faible. Les coûts les plus importants sont ailleurs. La question de l’agent est maintenant envoyée à un fournisseur de modèles. La recherche échoue quand le fournisseur échoue. Et le modèle peut changer le sens. Dans ce cours, la reformulation est donc une option (--rewrite), mesurée, et non le choix par défaut.

Voici la récupération complète de l’assistant de Larkfield, des 185 segments aux 5 passages que le modèle lira. Quand elle est activée, la reformulation change la requête avant la première étape.

Une suite d'étapes de gauche à droite : 185 segments dans l'index; les filtres d'abord, version en vigueur à la date et étiquette d'accès pour le public visé; les candidats, les 20 premiers de BM25 et les 20 premiers de la recherche vectorielle; la recherche hybride, fusionner les deux listes avec RRF et garder 20 candidats; le reclassement, un encodeur croisé lit la question et un segment 20 fois; k vaut 5, les preuves sélectionnées; vers le modèle du module Construire des réponses fondées sur les sources. Une flèche pointillée part des filtres vers les autres versions et les segments réservés au personnel pour un assistant public : supprimés.
L'entonnoir de récupération, maintenant complet : les filtres retirent ce qui n'est pas autorisé. Deux recherches rapides recueillent les candidats, et l'encodeur croisé choisit l'ordre des cinq premiers.
Description textuelle du diagramme

Sept étapes, de gauche à droite. Les 185 segments de l’index. Les filtres, d’abord : seulement la version en vigueur à la date de la question et les étiquettes d’accès autorisées pour le public visé. Ils retirent les autres versions et, pour un assistant public, les segments réservés au personnel. Les candidats : les 20 premiers de BM25 et les 20 premiers de la recherche vectorielle. La recherche hybride fusionne les deux listes avec la fusion des rangs réciproques et garde 20 candidats. L’encodeur croisé lit la question avec chacun des 20 segments et les classe de nouveau. Les cinq premiers sont les preuves sélectionnées, envoyées au modèle du module Construire des réponses fondées sur les sources.

rerank.py était fourni avec la dernière leçon. Le projet reçoit maintenant l’adaptateur pour les appels au modèle, la requête de reformulation, ses enregistrements et rewrite.py. Travaillez dans ~/projects/policy-assistant, avec .venv actif.

  1. Lisez policy_assistant/rerank.py. La méthode rerank résume toute l’idée de cette leçon en quelques lignes :

    policy_assistant/rerank.py (part)
    def rerank(self, query: str, candidates: list, k: int = 5) -> list[Hit]:
    """candidates: chunks (with .chunk_id and .text). Returns the k best by the cross-encoder's score."""
    if not candidates:
    return []
    scores = self.model.predict([(query, c.text) for c in candidates], batch_size=16)
    order = sorted(range(len(candidates)), key=lambda i: (-float(scores[i]), candidates[i].chunk_id))
    return [Hit(candidates[i].chunk_id, round(float(scores[i]), 4), rank) for rank, i in enumerate(order[:k], 1)]

    predict reçoit une paire par candidat, (question, chunk text), et renvoie un score par paire. Comme embed.py, le fichier fixe la révision du modèle, 1427fd652930e4ba29e8149678df786c240d8825.

  2. Exécutez l’exemple détaillé. La première fois, le modèle de reclassement télécharge environ 470 Mo :

    Fenêtre de terminal
    python -m policy_assistant search "What is LB-1?" --method rerank

    Vérifiez : la première ligne est 1. -0.8719 [86a0a5266439] product-care-hedge-trimmer-ht550 v1 | After each use | public.

  3. Copiez l’adaptateur, ses paramètres, le dossier des requêtes, le fichier d’exemple de paramètres et les reformulations enregistrées depuis l’instantané :

    Fenêtre de terminal
    S=../nextia-learning-labs/C09/snapshots/end-of-m03
    cp $S/policy_assistant/providers.py $S/policy_assistant/config.py policy_assistant/
    cp -R $S/policy_assistant/prompts policy_assistant/
    cp $S/.env.example .
    cp $S/recordings/rewrites.jsonl recordings/

    providers.py est l’adaptateur du cours Créer des applications fiables avec les API de LLM, sans outils ni diffusion en continu. MockProvider rejoue les enregistrements, et OpenAICompatibleProvider appelle un vrai modèle. config.py lit les paramètres dans les variables d’environnement ou un fichier .env. Le fournisseur par défaut est le fournisseur simulé, donc vous n’avez pas besoin de .env maintenant.

  4. Créez le fichier policy_assistant/rewrite.py :

    policy_assistant/rewrite.py
    """Query rewriting: ask a model to turn the agent's question into a better search query.
    It can help (a French question becomes English words that match English documents; "two years without
    buying" becomes "points expiry") and it can hurt (the model drops a date or a code, or answers the
    question instead of searching). Measure it like any other change. The course's rewrites were recorded
    with chat-small; the mock replays them.
    """
    from .config import load_prompt
    MAX_TOKENS = 1000
    def build_request(question: str, model: str) -> dict:
    system, user = load_prompt("rewrite")
    return {"model": model,
    "messages": [{"role": "system", "content": system}, {"role": "user", "content": user.format(question=question)}],
    "max_completion_tokens": MAX_TOKENS}
    def rewrite(question: str, provider, model: str = "chat-small") -> str:
    completion = provider.complete(build_request(question, model))
    text = (completion.text or "").strip().splitlines()
    return text[0].strip().strip('"') if text else question

    rewrite garde seulement la première ligne de la réponse et retire les guillemets qui l’entourent. Si le modèle ne renvoie rien, la question d’origine est utilisée.

    Ne modifiez pas rewrite.md ni build_request. Le fournisseur simulé trouve un enregistrement grâce à une empreinte de la requête complète. Une requête au modèle modifiée est une nouvelle requête, et le fournisseur simulé n’a aucun enregistrement correspondant.

  5. Copiez le dernier fichier de tests et exécutez tous les tests :

    Fenêtre de terminal
    cp ../nextia-learning-labs/C09/snapshots/end-of-m03/tests/test_rewrite.py tests/
    python -m pytest

    La dernière ligne est 48 passed (48 tests réussis). Un des nouveaux tests vérifie les caractères inattendus de la reformulation Q05. Le test conserve la sortie réelle et imparfaite dans les enregistrements.

  1. Reclassez les résultats de la question de Camille sur le barbecue. Comparez-les à la sortie hybride ci-dessus :

    Fenêtre de terminal
    python -m policy_assistant search "Combien de temps dure la garantie d'un barbecue?" --method rerank

    Vérifiez : le premier résultat est warranty-policy v5 | Warranty periods by product c…, avec le score -0.5515.

  2. Mesurez le reclassement sur tout l’ensemble. Cela prend environ 30 secondes, car l’encodeur croisé lit 20 candidats pour chacune des 67 questions :

    Fenêtre de terminal
    python -m policy_assistant eval --method rerank

    Les premières et dernières lignes :

    rerank, top 5, audience staff: hit@5 0.898 (59 questions with relevant passages)
    ...
    missed: Q13, Q28, Q36, Q56, Q57, Q60
  3. Cherchez la question en français sur les cartes-cadeaux avec BM25, sans reformulation, puis avec reformulation :

    Fenêtre de terminal
    python -m policy_assistant search "Puis-je retourner une carte-cadeau?" --method bm25
    python -m policy_assistant search "Puis-je retourner une carte-cadeau?" --method bm25 --rewrite

    La deuxième sortie commence par la requête reformulée, puis affiche trois segments de gift-cards :

    Rewritten query: gift card return policy&returns
    1. 9.7154 [8c4bc4299042] gift-cards v1 | Buying a gift card | public
    2. 8.7215 [52ad3a57c1d1] gift-cards v1 | Lost cards and returns | public
    3. 8.0177 [f27bb50ce8b7] gift-cards v1 | Using a gift card | public

    (La ligne de texte sous chaque résultat est omise ici.) « Lost cards and returns » (cartes perdues et retours) est le passage pertinent.

  4. Mesurez la reformulation avec BM25 et avec le reclassement :

    Fenêtre de terminal
    python -m policy_assistant eval --method bm25 --rewrite
    python -m policy_assistant eval --method rerank --rewrite

    Les premières lignes :

    bm25, top 5, audience staff: hit@5 0.898 (59 questions with relevant passages)
    rerank, top 5, audience staff: hit@5 0.932 (59 questions with relevant passages)

    Comparez la ligne missed: (questions manquées) de BM25 à celle de Recherche lexicale, dense et hybride, sans reformulation : missed: Q05, Q12, Q13, Q29, Q32, Q34, Q37, Q56, Q57, Q63. La reformulation en retrouve 8. Mais Q08, Q28, Q60 et Q61 sont maintenant manquées, alors qu’elles étaient trouvées sans reformulation.

  5. Essayez une question qui n’a jamais été enregistrée :

    Fenêtre de terminal
    python -m policy_assistant search "Can a customer return a used pressure washer?" --method bm25 --rewrite

    Le programme s’arrête avec une trace d’erreur qui se termine par :

    policy_assistant.providers.RecordingNotFound: No recording for this request (key 8250f8ad99fd1353). The mock replays only the questions that were recorded for the course, with the same prompt and the same model.

    C’est prévu. La simulation contient des reformulations seulement pour les 67 questions de l’ensemble. Sans --rewrite, la même recherche fonctionne. Pour reformuler vos propres questions, utilisez un modèle réel. .env.example explique comment faire, avec Ollama sur votre ordinateur ou votre propre clé. C’est facultatif et ce n’est pas testé dans ce cours.

La reformulation a perdu Q08, Q60 et Q61 parce qu’elle a supprimé les mots de l’agent. Une idée : cherchez avec les deux, la question suivie de sa reformulation, dans une seule requête. BM25 peut alors trouver les mots français et les mots anglais.

Avant de lancer la recherche, faites une prédiction : le hit@5 de BM25 augmente-t-il, diminue-t-il ou reste-t-il à 0,898? Exécutez ensuite ceci dans Python, dans le dossier du projet :

Python, in ~/projects/policy-assistant
from policy_assistant.config import Settings, make_provider
from policy_assistant.embed import LocalEmbedder
from policy_assistant.evaluate import load_questions, retrieval_scores, summarise
from policy_assistant.filters import Filters
from policy_assistant.rewrite import rewrite
from policy_assistant.search import Retriever
from policy_assistant.store import Store
store = Store("index.sqlite")
retriever = Retriever(store, LocalEmbedder())
provider = make_provider(Settings.from_env())
rows = []
for q in load_questions().values():
query = q.question + " " + rewrite(q.question, provider)
results = retriever.search(query, "bm25", 5, Filters(as_of=q.as_of))
rows.append(retrieval_scores(q, [r.chunk for r in results], 5))
print(summarise(rows), [r["id"] for r in rows if r["scored"] and not r["hit"]])
Afficher la réponse
{'questions': 59, 'hit': 0.898} ['Q13', 'Q28', 'Q29', 'Q37', 'Q56', 'Q57']

Le total reste à 0,898, mais les questions changent. Q08, Q60 et Q61 sont retrouvées, parce que les mots d’origine sont de retour. Q29, Q37 et Q56 sont de nouveau perdues. Q29 et Q37 sont des questions en français. Leurs mots français font maintenant remonter les segments français, comme avant la reformulation. Le même total cache six questions dont le résultat a changé. C’est pourquoi vous comparez les listes de questions manquées, et pas seulement le score. C’est aussi pourquoi chaque nouvelle idée doit être mesurée avant d’être intégrée à l’assistant.

Erreur courante

Une reformulation dans la mauvaise langue

Symptôme : avec --rewrite, la question de Camille « Larkfield vend-elle une garantie prolongée? » ne trouve rien de pertinent. Sans reformulation, BM25 place la réponse au rang 2 : garantie-legale-quebec | Garantie prolongée, « Nous ne vendons pas de garantie prolongée. » Avec la reformulation :

Rewritten query: Does Larkfield offer an extended warranty?
1. 8.8860 [3c79dce4a330] safety-notice-kt180 v1 | What we offer | public
2. 6.2494 [144a40851685] warranty-policy v5 | What the warranty covers | public

Les cinq résultats portent sur l’avis concernant la bouilloire, la politique de garantie, la livraison d’articles volumineux et les nettoyeurs haute pression. Aucun ne dit si Larkfield vend des garanties prolongées.

Cause : la requête dit « Rédigez la requête en anglais, même quand la question est en français ». Cette règle aide quand la réponse se trouve dans un document anglais. Mais la seule affirmation sur les garanties prolongées se trouve dans le document français du Québec. La reformulation a traduit la question et l’a éloignée du seul document qui y répond. Le modèle a suivi la consigne; la consigne ne convenait pas à cette question.

Correction : traitez la reformulation comme un choix de récupération supplémentaire. Mesurez-la par question, pas seulement au total. Pour cette collection, il y a trois options. Laissez la reformulation désactivée par défaut, comme dans le cours. Ou cherchez avec les mots d’origine et la reformulation ensemble, en acceptant de perdre deux autres questions en français. Ou rédigez une nouvelle requête, enregistrez de nouvelles reformulations et mesurez-les toutes de nouveau. Un test de Q60 avec --rewrite détecterait ce problème s’il revenait.

Pour un vrai service d’IA : un modèle placé avant la recherche peut modifier discrètement ce qui est cherché. Consignez la requête reformulée à côté de l’originale, pour qu’un agent ou un développeur puisse la voir. Mesurez la reformulation sur des questions dans toutes les langues utilisées par vos utilisateurs.

Vérifiez votre compréhension

Dans vos mots : en quoi un encodeur croisé diffère-t-il du modèle de plongement? Pourquoi l'assistant l'utilise-t-il seulement sur les 20 premiers résultats?

Afficher la réponse

Le modèle de plongement lit la question et chaque passage séparément, puis crée un vecteur pour chacun. Il peut donc calculer les plongements des passages une seule fois, à l’ingestion. Une recherche calcule ensuite seulement le plongement de la question et compare les vecteurs, ce qui prend quelques millisecondes. Un encodeur croisé lit la question et un passage ensemble, comme une seule entrée. Il donne un score à cette paire. Il peut voir comment les deux textes correspondent, donc il les classe mieux. Mais il doit s’exécuter de nouveau pour chaque question et chaque passage. Il est donc lent : 322 ms pour 20 candidats ici. Une recherche rapide recueille donc 20 candidats, et l’encodeur croisé classe seulement ces 20 candidats.

Vérifiez votre compréhension

Le reclassement a fait passer le MRR de 0,745 (hybride) à 0,831, et le hit@5 de 0,864 à 0,898. Omar demande : « Donc, c'est mieux pour chaque question? » Que répondez-vous?

Afficher la réponse

Non. Les totaux ont augmenté, mais certaines questions ont reculé. Le reclassement a fait entrer 4 passages pertinents dans les 5 premiers résultats, notamment LB-1 et la question française sur le barbecue. Il en a fait sortir 2, notamment Q60, qui est passée du rang 3 au rang 14. Le total est le résultat net. Avant d’intégrer un changement, examinez les questions qu’il perd autant que celles qu’il gagne. Décidez ensuite si les pertes sont acceptables. Il coûte aussi environ 0,3 s par question.

Vérifiez votre compréhension

Donnez une façon dont la reformulation des requêtes a aidé la recherche de Larkfield, et deux façons dont elle a échoué. Donnez un exemple de chaque cas.

Afficher la réponse

Elle a aidé les questions en français portant sur des documents anglais. « Puis-je retourner une carte-cadeau? » est devenue « gift card return policy » (politique de retour des cartes-cadeaux). BM25 a trouvé le passage sur les cartes-cadeaux pour la première fois. Elle a échoué en traduisant une question dont la seule réponse est en français : Q60 a perdu son passage pertinent. Elle a échoué en changeant le sens. Q46, « La FAQ du centre d’aide dit que les remboursements prennent 3 jours ouvrables. Est-ce exact? », est devenue une recherche de « 3 business days » (3 jours ouvrables), sans le doute. Autre réponse acceptable : elle a ajouté des caractères indésirables provenant d’autres systèmes d’écriture (Q45, Q46, Q05). Ou : elle a remplacé les mots de l’agent, comme pour la question sur les transcriptions de clavardage (Q08, du rang 1 au rang 17).

Ce module a donné à l’assistant de Larkfield quatre méthodes de recherche, deux filtres et deux étapes supplémentaires. L’exercice du module les compare sur les trois types de questions du module.

  1. Démarrez Python dans le dossier du projet. Créez un système de récupération avec les trois modèles :

    Python, in ~/projects/policy-assistant
    from policy_assistant.embed import LocalEmbedder
    from policy_assistant.evaluate import covers, load_questions
    from policy_assistant.filters import Filters
    from policy_assistant.rerank import Reranker
    from policy_assistant.search import Retriever
    from policy_assistant.store import Store
    store = Store("index.sqlite")
    retriever = Retriever(store, LocalEmbedder(), Reranker())
    questions = load_questions()
    methods = ["bm25", "dense", "hybrid", "rerank"]
  2. Pour chaque question, affichez le rang du premier passage pertinent parmi les 20 premiers résultats de chaque méthode :

    Python (continued)
    print("question", *methods, sep="\t")
    for qid in ["Q38", "Q40", "Q44", "Q34", "Q05", "Q12", "Q64", "Q65", "Q66", "Q67"]:
    q = questions[qid]
    ranks = []
    for method in methods:
    results = retriever.search(q.question, method, 20, Filters(as_of=q.as_of))
    first = next((r.rank for r in results if any(covers(r.chunk, p) for p in q.relevant)), "-")
    ranks.append(first)
    print(qid, *ranks, sep="\t")

    Appuyez sur Entrée à la ligne vide après la boucle. Cela prend environ 10 secondes.

  3. Regroupez les lignes. Q38, Q40 et Q44 sont des termes exacts dans une phrase. Q34, Q05 et Q12 sont des paraphrases. Q64 à Q67 sont des identifiants rares utilisés seuls.

  4. Rédigez une note de quatre ou cinq phrases pour Grace : la méthode à utiliser, les preuves et son coût.

Afficher une réponse modèle
question bm25 dense hybrid rerank
Q38 1 1 1 1
Q40 1 1 1 1
Q44 1 1 1 1
Q34 16 2 3 1
Q05 8 2 5 1
Q12 6 1 1 1
Q64 1 - 6 1
Q65 1 4 1 1
Q66 1 3 1 1
Q67 1 3 1 1

Note pour Grace (59 questions, filtres activés, 9 octobre 2026) : « Les termes exacts dans une phrase sont faciles : chaque méthode place le bon passage en premier. Les paraphrases demandent une recherche vectorielle. BM25 les classe du 6e au 16e rang, et la recherche vectorielle au 1er ou au 2e rang. Les identifiants rares utilisés seuls demandent des mots-clés : BM25 classe les quatre en premier, et la recherche vectorielle manque LB-1. Le reclassement est la seule méthode qui place le bon passage en premier pour les dix questions. Sur l’ensemble complet, il donne le meilleur ordre (MRR de 0,831). Il coûte environ 0,3 s par question sur un portable, et aucune donnée ne quitte nos ordinateurs. Je recommande le reclassement par défaut. Je le mesurerais de nouveau après l’ajout de documents ou de questions. »

Rédigez trois questions auxquelles la collection peut répondre. Créez-en une de chaque type : un terme exact dans une phrase, une paraphrase en mots courants et un identifiant rare seul. Trouvez vous-même la section pertinente avec python -m policy_assistant chunks DOC_ID. Prédisez quelle méthode trouve chaque réponse, puis exécutez search (recherche) avec les quatre méthodes. Expliquez chaque résultat en une ou deux phrases.

Afficher un exemple de réponse

Trois questions exécutées sur l’ordinateur de test (les 3 premiers résultats de chaque méthode) :

  • Un identifiant seul, « Qu’est-ce que PK-2400-03? » Les quatre méthodes placent product-care-pw2400 | Parts and part numbers ou warranty-claims-procedure | Pressure washer pumps en premier. Les codes ne font pas tous échouer la recherche vectorielle. Les segments contenant « PK-2400-03 » disent aussi « pump kit » (ensemble de pompe) et « PW-2400 ». Le vecteur dispose donc d’un peu de sens pour établir un lien. LB-1 n’a pas cette aide.
  • Une paraphrase, « Ma bouilloire ne s’éteint pas quand l’eau bout. Que dois-je faire? » Chaque méthode trouve safety-notice-kt180 | The risk en premier. Le deuxième résultat de BM25 est la section sur le gaz du barbecue, car elle partage « What should … do » (que devrait… faire). Les méthodes vectorielles renvoient trois sections de l’avis sur la bouilloire.
  • Un terme exact dans une phrase, « Une carte-cadeau est-elle remboursable? » BM25 classe gift-cards | Buying a gift card en premier et « Lost cards and returns », qui répond à la question, en troisième. La recherche vectorielle et le reclassement placent « Lost cards and returns » en premier.

La leçon de cet exemple : les prédictions par type sont un bon début, mais chaque question peut vous surprendre. C’est pourquoi le cours mesure les résultats sur un ensemble de questions. C’est aussi pourquoi vous devriez y ajouter vos propres questions difficiles.

Vérifiez votre compréhension

Vérification finale : expliquez un cas où la recherche lexicale dépasse la recherche vectorielle, pourquoi cela arrive et comment l'assistant y répond.

Afficher la réponse

« Qu’est-ce que LB-1? » est un bon cas. BM25 classe en premier le passage sur le taille-haie contenant « blade oil LB-1 » (huile pour lame LB-1). LB-1 est un mot rare que seul ce segment contient. BM25 donne un score élevé aux mots rares partagés. La recherche vectorielle avec e5 ne le trouve pas parmi ses 20 premiers résultats. Le modèle n’a aucun sens utile pour ce code, donc la question devient un vague « Qu’est-ce que…? ». Elle mène à des sections générales sur la confidentialité. Plus généralement, la recherche par mots-clés gagne pour les identifiants exacts utilisés seuls : codes de produits, numéros de pièces, codes du personnel. L’assistant conserve les candidats de BM25 (recherche hybride) et laisse l’encodeur croisé les classer. LB-1 arrive donc en premier avec le reclassement.

Vérifiez votre compréhension

Tomás change le modèle de plongement dans la configuration et ne relance pas l'ingestion. Que se passe-t-il, et comment le projet s'en protège-t-il?

Afficher la réponse

L’index contient encore les vecteurs créés par l’ancien modèle. Les plongements des questions seraient calculés par le nouveau. Les vecteurs de deux modèles ne sont pas comparables, même s’ils ont la même longueur. Dans le test du cours, une recherche dans un index e5 avec des vecteurs de questions MiniLM donnait un hit@5 de 0,068. Aucune erreur n’était signalée. Le projet enregistre le nom et la révision du modèle dans le meta de l’index à l’ingestion. search.py les compare au modèle de la question et s’arrête avec IndexMismatch. La correction consiste à exécuter ingest de nouveau avec le nouveau modèle.

Vérifiez votre compréhension

La recherche hybride a obtenu 0,864 et la recherche vectorielle 0,915 sur les questions de Larkfield. Pourtant, beaucoup d'articles disent que la recherche hybride est meilleure. Comment l'expliquez-vous, et que recommandez-vous pour les documents d'une autre entreprise?

Afficher la réponse

La fusion des rangs réciproques additionne les votes des deux listes. Quand une liste est faible pour une question, comme BM25 sur les questions françaises, ses rangs bas votent quand même. Les segments mal classés dans les deux listes peuvent alors faire sortir un bon résultat des 5 premiers. Sur les questions de Larkfield, cela a coûté plus que cela a rapporté. Avec les vecteurs hébergés d’embed-small, la recherche hybride était légèrement meilleure. Aucune méthode n’est donc la meilleure partout. Pour une autre entreprise, créez d’abord un ensemble de questions avec leurs passages pertinents. Incluez des codes, des paraphrases et toutes les langues utilisées par les utilisateurs. Mesurez ensuite chaque méthode sur cet ensemble. Examinez les questions que chacune perd, pas seulement le total.

Vous pouvez maintenant trouver des preuves de trois façons et choisir entre elles à l’aide de chiffres. La recherche vectorielle trouve les paraphrases et les questions françaises. La recherche par mots-clés trouve les codes rares, et le reclassement place le meilleur passage en premier. Les filtres excluent les mauvaises versions et les documents interdits avant tout classement. Le module 4 prend les 5 premiers passages et construit une réponse à partir d’eux, avec une citation pour chaque affirmation.

Si votre projet ne donne pas 48 passed, copiez l’instantané end-of-m03 depuis le dépôt des laboratoires. Procédez comme pour start dans Préparer le projet. Ensuite :

  1. Ouvrez un terminal dans ~/projects/policy-assistant. Un instantané n’a pas de .venv. Créez-en donc un nouveau et activez-le, comme à l’étape 5 de Préparer le projet.
  2. Exécutez python -m pip install -r requirements.txt. Sur Linux, installez d’abord PyTorch pour processeur seulement, comme dans Les plongements pour la recherche.
  3. Exécutez python -m pytest. La dernière ligne est 48 passed (48 tests réussis).
  4. Exécutez python -m policy_assistant ingest, puis python -m policy_assistant eval. La première ligne de eval est rerank, top 5, audience staff: hit@5 0.898 (59 questions with relevant passages).

Récapitulatif

  • Un encodeur croisé lit la question et un passage ensemble, puis donne un score à la paire. Le reclassement des 20 candidats hybrides a fait passer le MRR de 0,745 à 0,831, et le hit@5 de 0,864 à 0,898. Il coûte environ 322 ms par question. C’est le choix par défaut du cours.
  • La reformulation des requêtes demande à un modèle de meilleurs mots pour la recherche. Elle a surtout aidé BM25 (de 0,831 à 0,898). Elle a donné le meilleur résultat avec le reclassement (0,932, MRR de 0,882). Elle a aussi perdu des questions, ajouté des caractères indésirables et changé un sens. Elle reste donc facultative et mesurée.
  • Chaque changement fait monter certaines questions et en fait descendre d’autres. Comparez les questions manquées, pas seulement le total.
  • Le module 3 se termine avec l’instantané end-of-m03 : 48 tests, et eval donne un hit@5 de 0,898 avec le reclassement.

Ensuite, choisissez les passages à inclure dans la requête, en respectant un budget de jetons, et conservez la source de chacun.

Leçon suivante : Assembler le contexte