Recherche documentaire : découpage, rappel, reclassement et citations
Un système de questions-réponses doit retrouver les passages utiles avant de demander au modèle de rédiger sa réponse. Si les preuves n'atteignent jamais le contexte, un générateur plus puissant risque simplement de produire une supposition plus fluide. Cette page suit le chemin du document vers la preuve ; la mémoire des agents traite de ce qu'il faut conserver entre les tâches.
Deux cas se passent de recherche documentaire. Si les documents tiennent en entier dans la fenêtre de contexte, il suffit de les y placer. Pour retrouver un nom de fonction ou un message d'erreur connu dans une base de code, la recherche exacte et la recherche de symboles sont plus précises que la recherche sémantique (voir l'ingénierie du contexte).
Choisir les passages en partant des questions
Imaginons un manuel fictif : une section explique le remplacement de la batterie, puis la suivante précise que l'étanchéité suppose un boîtier intact. À la question « Puis-je immerger l'appareil juste après avoir changé la batterie ? », un paragraphe contenant seulement « batterie » ne suffit pas.
Un passage doit conserver une idée complète sans noyer la preuve dans du texte secondaire. On peut commencer par les titres, paragraphes, fonctions ou limites des tableaux, puis plafonner les sections trop longues. Le chevauchement préserve parfois les phrases à cheval sur deux passages, mais crée des doublons. Une référence d'API, une conversation et un chapitre scientifique n'appellent pas le même découpage.
Chaque passage doit conserver l'identifiant du document, sa position, le chemin de ses titres et sa version. Une ligne de tableau doit permettre de retrouver ses en-têtes et ses unités. Un texte détaché de sa source complique les citations, la déduplication et les mises à jour. La suppression d'un document doit aussi invalider ses anciennes entrées d'index.
Distinguer rappel et reclassement
Le rappel sélectionne des passages plausibles dans le corpus ; le reclassement examine leur rapport précis avec la question. L'exemple de Sentence Transformers combine une recherche lexicale ou un bi-encodeur avec un reclassement par cross-encoder.
Une recherche hybride peut réunir les candidats lexicaux et vectoriels, mais leurs scores n'ont pas nécessairement la même échelle. La fusion par rang constitue une base simple, plutôt que d'additionner directement similarité cosinus et score lexical : la fusion réciproque additionne 1 / (c + rank) dans chaque liste, où c règle l'avantage donné aux premières positions. Cette règle de classement ne produit pas une probabilité de justesse.
Localiser l'échec avant de changer de modèle
Pour la batterie, supposons que le rappel retienne 20 passages, le reclassement en sélectionne 4, et que le contexte puisse en contenir 2. Ces budgets servent à l'exemple et ne constituent pas des réglages conseillés.
Si la condition d'étanchéité arrive 30e, le rappel échoue. Si elle figure parmi les candidats mais descend à la 12e place, le classement échoue. Si elle est dans les 4 premières puis disparaît au profit de doublons, l'assemblage échoue. Si le modèle la reçoit mais l'ignore, la génération échoue. Conserver les identifiants et les positions à chaque étape permet de distinguer ces cas. La réponse finale ne dit pas à elle seule s'il faut modifier les embeddings, le découpage ou les instructions.
Pour une question qui traverse plusieurs sections, on peut retrouver un petit passage puis ajouter ses voisins ou sa section parente. Cette extension ajoute aussi du bruit : il faut la plafonner et préserver les conditions utiles. Les contraintes d'accès, de date et de version interviennent dès la sélection des candidats, avant qu'une réponse ne cite une source inapplicable.
Relier chaque citation à une affirmation
« Voir le manuel » ne suffit pas. Une phrase telle que « le joint doit être vérifié après remplacement » doit pointer vers son passage justificatif. Lorsque deux versions se contredisent, déterminer leurs dates et modèles d'appareil respectifs. Si le conflit persiste, le conserver dans la réponse plutôt que fabriquer une conclusion commune.
Un texte retrouvé fournit des preuves, pas des instructions d'exécution. Une phrase demandant d'ignorer les règles ou d'appeler un outil n'acquiert aucune autorité grâce à son classement. Les contrats d'outils détaillent cette frontière.
Un score n'est qu'un critère de classement, pas un fait. Le passage classé premier doit encore être vérifié dans sa source. L'absence de résultat signifie seulement que ce découpage, cette tokenisation et ce modèle ne l'ont pas fait remonter ; elle ne prouve pas que le corpus ne le contient pas. Avant de modifier du code ou d'écrire une conclusion, rouvrir la source actuelle, car l'index reflète les fichiers tels qu'ils étaient lors de sa construction.
Une base minimale qui tourne
Construire d'abord la plus petite version qui fonctionne, avant de décider s'il faut une base vectorielle, un modèle de reclassement ou un découpage plus fin. Le code Python ci-dessous découpe un dossier selon les titres Markdown, puis lance en parallèle un rappel lexical BM25 et un rappel vectoriel. La fusion par rang réciproque combine les deux listes et renvoie les 3 meilleurs passages sous la forme fichier:ligne. Enfin, il mesure le taux de réussite sur une liste de questions annotées. Il ne dépend que de deux bibliothèques : rank_bm25 pour BM25 et Model2Vec pour charger en local potion-multilingual-128M. Ce modèle, distillé de BGE-M3, gère le chinois et l'anglais ; le premier lancement télécharge environ 0,5 Go, puis il reste rapide même sur CPU.
import pathlib, re, sys
import numpy as np
from model2vec import StaticModel
from rank_bm25 import BM25Okapi
def chunks(folder): # one chunk per Markdown heading, remembering file and line
for path in sorted(pathlib.Path(folder).rglob("*.md")):
lines = path.read_text(encoding="utf-8").splitlines()
heads = [i for i, line in enumerate(lines) if line.startswith("#")] + [len(lines)]
for a, b in zip(heads, heads[1:]):
if "\n".join(lines[a + 1:b]).strip(): # skip headings with nothing under them
yield {"where": f"{path}:{a + 1}", "text": "\n".join(lines[a:b])}
def tokens(text): # English words and single Chinese characters
return re.findall(r"[a-z0-9]+|[\u4e00-\u9fff]", text.lower())
docs = list(chunks(sys.argv[1]))
bm25 = BM25Okapi([tokens(d["text"]) for d in docs])
model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
vectors = model.encode([d["text"] for d in docs])
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)
def search(question, k=3, pool=20, c=60):
lexical = np.argsort(-bm25.get_scores(tokens(question)))[:pool]
q = model.encode([question])[0]
semantic = np.argsort(-(vectors @ (q / np.linalg.norm(q))))[:pool]
fused = {}
for ranking in (lexical, semantic): # reciprocal rank fusion
for rank, i in enumerate(ranking, start=1):
fused[i] = fused.get(i, 0) + 1 / (c + rank)
return [docs[i]["where"] for i in sorted(fused, key=fused.get, reverse=True)[:k]]
rows = [line.rstrip("\n").split("\t") for line in open(sys.argv[2], encoding="utf-8")]
hits = 0
for question, expected in rows:
found = search(question)
hits += expected in found
print("ok " if expected in found else "MISS", question, "->", ", ".join(found))
print(f"recall@3: {hits}/{len(rows)}")
La liste de questions est un fichier séparé par des tabulations : une question par ligne, suivie du fichier:ligne du titre sous lequel se trouve la réponse.
换完电池还能直接浸水吗 notes/manual.md:7
How do I update the firmware? notes/faq.md:7
Enregistrer le code sous rag.py, placer les notes dans un dossier notes/, puis lancer :
pip install rank_bm25 model2vec
python rag.py notes questions.tsv
Avec deux notes tirées du manuel d'appareil ci-dessus et 10 questions, le script en a trouvé 10 sur 10 lors d'un test le 29 octobre 2026 ; chaque ligne de sortie affiche une question et les trois positions trouvées. BM25 laisse la tokenisation à votre charge : le code découpe l'anglais en mots et le chinois en caractères isolés, ce qui suffit pour un petit corpus ; un grand corpus chinois mérite un segmenteur. Les vecteurs restent en mémoire et sont comparés un par un : quelques dizaines de milliers de passages n'ont pas besoin d'index vectoriel.
La liste de questions vaut plus que le code. La relancer après chaque changement de découpage ou de modèle, et repérer les questions qui ne sont plus trouvées. Pour choisir un outil existant, voir le tableau comparatif de la mémoire des agents. L'évaluation de la recherche et de la génération explique les mesures et les contre-exemples ; l'ingénierie du contexte explique comment répartir l'espace de travail du modèle.