Quand une IA répond à une question comme « liste tous les exclusions » avec seulement 5 réponses au lieu de 9, elle ne vous dit pas qu’elle en a oublié. Voici comment détecter et corriger cette faille.
QU'EST-CE QU'UNE QUESTION EN LISTE ?
Imaginez que vous demandez à votre moteur de Recherche : « Quels sont tous les exclusions dans ce contrat ? » et qu’il vous répond avec une liste de cinq éléments, bien formatée, en vous laissant penser que c’est complet. Pourtant, le contrat en contient neuf. L’IA n’a pas menti sciemment, mais elle a échoué à comprendre que la question nécessitait tous les éléments, pas juste les plus visibles. Ces questions-là s’appellent des questions en liste.
Dans un système classique de récupération de passages (RAG), l’IA cherche d’abord les passages les plus pertinents (top-k), puis extrait la réponse. Mais pour une question en liste, la réponse n’est pas dans un seul passage : elle est répartie dans tout le document. L’IA va donc manquer des éléments, simplement parce qu’ils ne sont pas dans les passages les plus similaires.
Prenez l’exemple d’un document qui liste les six fonctions du cadre NIST CSF : Organizational Context, Risk Management Strategy, Roles, Responsibilities, and Authorities, Policy, Oversight, et Cybersecurity Supply Chain Risk Management. Si l’IA ne récupère que les quatre premières, elle affichera une réponse qui semble complète, mais qui est en réalité incomplète. L’utilisateur n’a aucun moyen de savoir que deux fonctions manquent.
POURQUOI LES MODÈLES ACTUELS ÉCHOUENT SUR CES QUESTIONS
Les modèles d’IA modernes, même ceux avec un grand contexte, échouent sur les questions en liste pour la même raison que les systèmes RAG classiques : ils s’appuient sur une logique de « top-k ». Ils récupèrent un nombre fixe de passages (par exemple, les 20 plus similaires) et extraient la réponse de ces passages seulement. Or, pour une question en liste, la réponse n’est pas dans les 20 passages les plus similaires, mais dans des passages dispersés, parfois formulés différemment.
Pire encore, les éléments d’une liste sont souvent similaires entre eux. Par exemple, dans un document sur les techniques de régularisation d’un modèle Transformer, les éléments « Residual Dropout », « Label Smoothing » et « Attention Dropout » sont tous liés à la régularisation. Si l’IA récupère trop de passages, elle va se retrouver avec des doublons, des résumés, des références croisées, et des paragraphes d’introduction. L’IA devra alors trier le vrai du faux, ce qui est impossible sans une stratégie dédiée.
Le problème n’est pas la taille du contexte, mais la logique de récupération. Même avec un contexte de 100 000 tokens, l’IA va manquer des éléments si elle ne cherche pas tous les passages pertinents.
COMMENT RECONNAÎTRE UNE QUESTION EN LISTE
Pour qu’un système d’IA gère correctement les questions en liste, il doit d’abord les reconnaître. Voici quelques indices qui permettent de détecter une question en liste :
- Des mots comme « tous », « toutes », « lister », « énumérer », « donner-moi tous » dans la question.
- Des formulations comme « Quels sont tous les… ? » ou « Quelles sont toutes les… ? ».
- Des questions qui demandent une quantité : « Combien y a-t-il de… ? ».
Voici un exemple de code qui permet de détecter ces questions :
LISTING_MARKERS = [
r"\b(?:what|which)\s+(?:are\s+)?all\s+(?:the\s+)?",
r"\blist\s+(?:all\s+)?(?:the\s+)?",
r"\benumerate\s+(?:all\s+)?",
r"\bgive\s+me\s+(?:all\s+)?(?:the\s+)?",
r"\bevery\s+",
r"\bhow\s+many\s+", # counting questions are also listing
]
def islistingquestion(question: str) -> bool:
"""Heuristique : cette question demande-t-elle un ensemble d'éléments plutôt qu'un seul fait ?"""
import re
return any(re.search(p, question, re.IGNORECASE) for p in LISTING_MARKERS)
Ce code utilise une liste de motifs (expressions régulières) pour détecter les questions en liste. Par exemple, la question « Quels sont tous les sous-catégories de GOVERN ? » sera détectée comme une question en liste, tandis que « Quel est le prix de la liste ? » ne le sera pas.
Cependant, cette méthode a des limites. Elle peut manquer des cas comme « Que couvre GOVERN ? » si GOVERN contient plusieurs catégories, ou donner de faux positifs comme « Quelle est la liste des prix ? ». Pour une détection plus précise, un système peut utiliser un classifieur par IA qui analyse l’intention derrière la question et retourne un verdict clair : « Cette question est une question en liste » ou « Ce n’est pas une question en liste ».
TROIS STRATÉGIES POUR RÉCUPÉRER TOUS LES ÉLÉMENTS
Une fois qu’une question est identifiée comme une question en liste, le système doit activer une stratégie de récupération adaptée. Voici trois méthodes pour récupérer tous les éléments d’une liste, classées par efficacité et complexité.
STRATÉGIE 1 : LA RÉCUPÉRATION STRUCTURELLE
La méthode la plus simple et la plus fiable pour récupérer une liste est d’utiliser la structure du document. Les auteurs de documents d’entreprise (contrats, normes, manuels, articles) organisent souvent leurs listes avec des marqueurs clairs : titres de sections, listes à puces, énumérations numérotées, lignes de tableau, codes de sous-catégories. Ces marqueurs sont une indication directe que le document contient une liste.
Prenons l’exemple d’un document sur le cadre NIST CSF. La question « Quelles sont toutes les catégories sous GOVERN ? » peut être résolue en récupérant les enfants de la section « GOVERN » dans la table des matières. Voici comment cela fonctionne en code :
def listviastructure(
*,
section_hint: str | None,
line_df: pd.DataFrame,
toc_df: pd.DataFrame | None,
page_range: tuple[int, int] | None = None,
) -> list[dict]:
"""Récupération via les marqueurs structurels du document."""
# Stratégie A : enfants de la table des matières.
if sectionhint is not None and tocdf is not None and not toc_df.empty:
if "parentid" in tocdf.columns:
children = tocdf[tocdf["parentid"] == sectionhint]
if not children.empty:
return children.to_dict(orient="records")
# Stratégie B : énumération par expression régulière sur une région.
if page_range is not None:
lo, hi = page_range
region = linedf[(linedf["pagenum"] >= lo) & (linedf["page_num"] <= hi)]
else:
region = line_df
items = region[region["text"].astype(str).str.match(_ENUM_LINE_PATTERN)]
return items.to_dict(orient="records")
Cette méthode fonctionne particulièrement bien pour les documents structurés. Par exemple, dans un article sur les techniques de régularisation d’un modèle Transformer, la section 5.4 pourrait s’intituler « Regularization ». En détectant les en-têtes en gras (comme « Residual Dropout », « Label Smoothing »), le système peut récupérer directement la liste des techniques.
STRATÉGIE 2 : LA RÉCUPÉRATION PAR MOTIFS
Lorsque les éléments d’une liste ne suivent pas un motif structurel strict mais ont une forme reconnaissable, une récupération par motifs peut être utilisée. Par exemple, les codes de sous-catégories dans le cadre NIST CSF suivent un motif strict : GV.XX-NN, où XX est un code de catégorie (OC, RM, RR, PO, OV, SC) et NN est un numéro de séquence.
Voici comment récupérer ces codes avec une expression régulière :
def listviapattern(line_df: pd.DataFrame, pattern: str) -> list[dict]:
"""Trouve chaque occurrence du motif pattern dans line_df, en évitant les doublons."""
import re
rx = re.compile(pattern)
matches: list[dict] = []
seen: set[str] = set()
for , row in linedf.iterrows():
text = str(row.get("text", ""))
if not text:
continue
for m in rx.finditer(text):
code = m.group(0)
if code in seen:
continue
seen.add(code)
matches.append({
"code": code,
"pagenum": int(row.get("pagenum", 0)),
"linenum": int(row.get("linenum", 0)),
"context": text,
})
return matches
# Exemple d'utilisation avec le motif GV.XX-NN
pattern = r"\bGV\.[A-Z]{2}-\d{2}\b"
items = listviapattern(line_df, pattern)
Cette méthode permet de récupérer de manière déterministe tous les éléments qui suivent un motif précis, sans avoir besoin d’embeddings ou de top-k. Elle est exhaustive par construction.
Les motifs peuvent être utilisés pour récupérer des codes, des identifiants, des clauses numérotées, des références normalisées, ou des identifiants de formule. Beaucoup de documents d’entreprise contiennent ce type de motifs.
STRATÉGIE 3 : LA RÉCUPÉRATION SÉMANTIQUE AVEC BOUCLE DE RÉCUPÉRATION
Lorsque les éléments d’une liste ne suivent ni un motif structurel ni un motif fixe (par exemple, des listes en prose libre), une récupération sémantique avec une boucle de récupération est nécessaire. Cette méthode utilise l’IA pour identifier les éléments pertinents, mais elle ne se limite pas à la récupération top-k. Elle élargit progressivement la recherche jusqu’à ce que tous les éléments soient trouvés ou qu’un nombre maximal d’itérations soit atteint.
Voici comment cela fonctionne :
class ListingResult(BaseModel):
items: list[ListingItem]
islikelycomplete: bool
reasonifincomplete: str | None = None
suggestedadditionalkeywords: list[str] = Field(default_factory=list)
def listviasemantic(
*,
question: str,
line_df: pd.DataFrame,
toc_df: pd.DataFrame | None,
initial_keywords: list[str],
llm_extract: LLMExtractFn,
broad_retrieve: Callable | None = None,
max_iterations: int = 3,
) -> list[ListingItem]:
"""Récupération large + extraction par IA dans une boucle bornée."""
if broad_retrieve is None:
broadretrieve = defaultbroadretrieve
accumulated: list[ListingItem] = []
seen_ids: set[str] = set()
keywords = list(initial_keywords)
for in range(maxiterations):
candidates = broadretrieve(linedf, toc_df, keywords)
new = [c for c in candidates if c.get("id") not in seen_ids]
if not new:
break
seen_ids.update(c.get("id") for c in candidates if c.get("id") is not None)
result = llm_extract(question, new, accumulated)
accumulated = mergeitems(accumulated, result.items)
if result.islikelycomplete:
break
keywords = list(set(keywords + result.suggestedadditionalkeywords))
return accumulated
Cette méthode fonctionne en trois étapes :
- Récupération large : Le système récupère un grand nombre de passages pertinents, pas seulement les top-k les plus similaires.
- Extraction par IA : L’IA extrait les éléments pertinents des passages récupérés et détermine si la liste est complète.
- Boucle de récupération : Si la liste n’est pas complète, l’IA suggère des mots-clés supplémentaires pour élargir la récupération, et le processus recommence. Cette boucle s’arrête lorsqu’un nombre maximal d’itérations est atteint, ou lorsque la liste est jugée complète.
Par exemple, pour la question « Quelles sont toutes les techniques utilisées pour améliorer la qualité de traduction ? », les éléments pourraient être dispersés dans le document : dropout (section 5.4), label smoothing (section 5.4), beam search (section 6.1), checkpoint averaging (section 6.1), warmup schedule (section 5.3). La récupération sémantique permet de trouver tous ces éléments, même s’ils ne sont pas dans les passages les plus similaires.
ÉLIMINER LES DOUBLONS ET VALIDER LA COMPLÉTUDE
Une fois que tous les éléments ont été récupérés, le système doit les fusionner, éliminer les doublons, et valider que la liste est complète. Voici comment cela fonctionne.
FUSIONNER LES DOUBLONS
Les éléments d’une liste peuvent apparaître plusieurs fois dans un document. Par exemple, une sous-catégorie mentionnée dans l’introduction et dans l’annexe. Pour éviter les doublons, le système utilise une fusion de surface (exact-string) et, si nécessaire, une fusion sémantique par IA.
def deduplicate_items(
items: list[ListingItem], llm_dedupe: DedupeFn | None = None,
) -> list[ListingItem]:
"""Élimination des doublons de surface, avec fusion sémantique optionnelle par IA."""
# Élimination des doublons de surface d'abord (chaîne exacte, insensible à la casse sur canonical_name).
by_lower: dict[str, ListingItem] = {}
for it in items:
k = it.canonical_name.strip().lower()
if k in by_lower:
cur = by_lower[k]
cur.surfaceforms = sorted(set(cur.surfaceforms + it.surface_forms))
cur.citations.extend(it.citations)
else:
by_lower[k] = it
surfaceunique = list(bylower.values())
if llmdedupe is None or len(surfaceunique) <= 1:
return surface_unique
return llm_dedupe(surface_unique)
Cette méthode élimine d’abord les doublons exacts (en ignorant la casse), puis fusionne les formes de surface (par exemple, « GV.SC » et « Cybersecurity Supply Chain Risk Management »). Si nécessaire, une fusion sémantique par IA peut être utilisée pour regrouper des éléments similaires mais formulés différemment.
VALIDER LA COMPLÉTUDE DE LA LISTE
Pour valider que la liste est complète, le système utilise trois sources d’information :
- Indice de cardinalité : Le document contient-il une indication explicite du nombre total d’éléments ? Par exemple, « Le Cadre Core consiste en six fonctions : ».
- Signal structurel : Le nombre d’éléments a-t-il cessé d’augmenter entre deux itérations ?
- Signal par IA : L’IA estime-t-elle que la liste est complète ?
Voici comment cela fonctionne en code :
def assess_completeness(
items: list,
itemsseenlast_iteration: list | None,
document_text: str,
llm_signal: bool | None,
) -> tuple[bool, Literal["structural", "cardinality", "llm_assessment"]]:
"""Combine les trois sources de complétude avec une priorité stricte."""
cardinality = detectcardinalitycue(document_text or "")
if cardinality is None:
cardinality = countexplicitenumeration(document_text or "")
if cardinality is not None:
return (len(items) >= cardinality, "cardinality")
if itemsseenlast_iteration is not None and len(items) == len(
itemsseenlast_iteration
):
return (True, "structural")
if llm_signal is not None:
return (bool(llmsignal), "llmassessment")
return (False, "llm_assessment")
Par exemple, si le document indique « Le Cadre Core consiste en six fonctions : », le système vérifiera que la liste contient au moins six éléments. Si le nombre d’éléments n’a pas changé entre deux itérations, le système considérera que la liste est complète. Enfin, si l’IA estime que la liste est complète, le système validera cette estimation.
PRÉSENTER UNE RÉPONSE FIABLE À L'UTILISATEUR
Une fois que la liste est validée, le système doit présenter une réponse claire et fiable à l’utilisateur. Voici la structure de la réponse finale :
class ListingItem(BaseModel):
canonical_name: str
surfaceforms: list[str] = Field(defaultfactory=list)
citations: list[Citation] = Field(default_factory=list)
extraction_confidence: float = 1.0
class ListingAnswer(BaseModel):
items: list[ListingItem]
is_complete: bool
completenesssource: Literal["structural", "cardinality", "llmassessment"]
notfounditems: list[str] = Field(default_factory=list)
notes: str | None = None
La réponse finale contient :
- La liste des éléments, avec leurs formes de surface et leurs citations.
- Un indicateur de complétude (is_complete), avec la source de cette information.
- Une liste des éléments non trouvés (si la liste n’est pas complète).
- Des notes supplémentaires (par exemple, des explications sur les éléments manquants).
Par exemple, si la liste n’est pas complète, le système pourrait afficher :
EXEMPLE CONCRET : RÉCUPÉRER LES CATÉGORIES DE GOVERN
Prenons un exemple concret avec le cadre NIST CSF. La question est : « Quelles sont toutes les catégories sous GOVERN ? ». Voici comment le système procède :
- Détection : La question est détectée comme une question en liste grâce aux mots « toutes les catégories ».
- Stratégie 1 : Récupération structurelle : Le système récupère les enfants de la section « GOVERN » dans la table des matières. Cela donne une liste de six catégories : Organizational Context (GV.OC), Risk Management Strategy (GV.RM), Roles, Responsibilities, and Authorities (GV.RR), Policy (GV.PO), Oversight (GV.OV), et Cybersecurity Supply Chain Risk Management (GV.SC).
- Validation : Le système vérifie que la liste contient six éléments, ce qui correspond à l’indice de cardinalité dans le document (« The Framework Core consists of six Functions : »). La liste est donc validée comme complète.
- Présentation : Le système affiche la liste complète des six catégories, avec leurs citations et leurs formes de surface.
Si la récupération structurelle avait manqué une catégorie (par exemple, GV.SC), le système aurait activé la stratégie 3 (récupération sémantique) pour élargir la recherche et trouver l’élément manquant.
UNE FAILLE CRITIQUE DANS LES SYSTÈMES D'IA MODERNES
Les questions en liste révèlent une faille critique dans les systèmes d’IA modernes : leur incapacité à gérer les listes complètes. Cette faille n’est pas due à un manque de contexte ou à une mauvaise extraction, mais à une mauvaise stratégie de récupération. Les systèmes RAG classiques, même avec un grand contexte, échouent sur les questions en liste parce qu’ils s’appuient sur une logique de top-k.
Pour résoudre ce problème, il faut :
- Détecter les questions en liste dès l’étape de compréhension de la question.
- Utiliser une stratégie de récupération adaptée (structurelle, par motifs, ou sémantique).
- Valider la complétude de la liste avec des indices de cardinalité, des signaux structurels, ou des évaluations par IA.
- Présenter une réponse claire et fiable, en indiquant si la liste est complète ou non.
Sans ces étapes, les utilisateurs risquent de recevoir des réponses incomplètes, présentées comme complètes, ce qui peut avoir des conséquences graves dans des domaines comme le droit, la santé, ou la finance.
COMMENT TESTER VOUS-MÊME CES STRATÉGIES
Si vous voulez tester ces stratégies par vous-même, vous pouvez utiliser le notebook compagnon disponible sur GitHub : doc-intel/notebooks-vol1. Ce notebook vous permet de :
- Extraire les six catégories GOVERN depuis la table des matières.
- Récupérer les 31 codes de sous-catégories GV.XX-NN avec une expression régulière.
- Observer comment la récupération sémantique trouve la troisième technique de régularisation manquante grâce à l’indice de cardinalité.
Ce notebook est un excellent point de départ pour comprendre comment intégrer ces stratégies dans vos propres systèmes d’IA.
CONCLUSION : NE LAISSEZ PAS VOS IA VOUS FAIRE CROIRE QU'ELLES ONT TOUT COMPRIS
Les questions en liste sont un cas d’usage critique pour les systèmes d’IA, mais elles sont souvent ignorées par les benchmarks et les pipelines RAG classiques. Pourtant, elles révèlent une faille majeure : l’incapacité à gérer les listes complètes. Sans une stratégie dédiée, les IA peuvent retourner des réponses incomplètes, présentées comme complètes, ce qui peut induire en erreur les utilisateurs.
Pour résoudre ce problème, il faut :
- Détecter les questions en liste dès l’étape de compréhension de la question.
- Utiliser une stratégie de récupération adaptée (structurelle, par motifs, ou sémantique).
- Valider la complétude de la liste avec des indices de cardinalité, des signaux structurels, ou des évaluations par IA.
- Présenter une réponse claire et fiable, en indiquant si la liste est complète ou non.
En intégrant ces étapes dans vos pipelines RAG, vous pouvez transformer une faille critique en une force, et offrir à vos utilisateurs des réponses fiables et complètes.
- Towards Data Science
L'indépendance de CLODCO est votre garantie.
Pour que l'actualité de l'IA reste sans filtre et sans concession, votre soutien est indispensable. Votre contribution est le seul moteur de notre liberté éditoriale.
Soutenir CLODCO


