Accueil / Articles / Sélectionner et ajuster les modèles d’incorporation pour des systèmes RAG en production.

Sélectionner et ajuster les modèles d’incorporation pour des systèmes RAG en production.

Apprenez comment les modèles d’incorporation transforment le texte en vecteurs exploitables, pourquoi le vocabulaire du domaine perturbe la recherche sémantique, et comment sélectionner, compresser et affiner les modèles pour des systèmes RAG en production.

6521 mots

Ce est le cinquième volet d’une série consacrée à la création de systèmes de génération améliorée par la recherche de niveau professionnel, qui suit le parcours allant des documents bruts à un système capable de répondre à des questions réelles. Les étapes précédentes de cette série ont porté sur le nettoyage et la normalisation du contenu extrait, puis sur sa division en unités de recherche par segmentation. Une fois ces segments en main, il s’agit ensuite de les transformer en éléments que l’index de recherche puisse réellement comparer.

Imaginez le même gestionnaire de relations présenté précédemment, toujours bloqué face à une ligne de crédit de 12 millions d’euros pour un client corporate à haut risque. Pour répondre à cette demande, il faut localiser une exigence de diligence renforcée cachée dans une clause de politique, un seuil d’approbation figurant dans une ligne de tableau, ainsi qu’une séquence de contrôle décrite dans un diagramme de processus. L’étape de segmentation a déjà isolé ces éléments en parties distinctes et traçables.

Même ainsi, rien de tout cela ne peut encore être recherché par un index vectoriel.

C’est un modèle d’incorporation qui transforme chaque morceau de texte en un vecteur numérique de longueur fixe. Le même modèle convertit ensuite une requête entrante en un vecteur, et l’index récupère les morceaux qui se trouvent le plus près de celui-ci dans cet espace vectoriel. Il n’est pas garanti qu’une phrase comme « Group Credit Committee approval » dans un document de politique et « GCC sign-off threshold » dans la question d’un utilisateur se trouvent réellement proches l’une de l’autre — cela dépend entièrement du modèle choisi et de ce qu’il a appris pendant l’entraînement.

C’est cette dépendance qui est au cœur de cette partie de la série.

Choisir un modèle d’incorporation revient à parier sur le degré de similitude entre le vocabulaire et les formulations de vos documents et celles des requêtes de vos utilisateurs. Si vous sélectionnez le mauvais modèle pour votre domaine, vous passerez des semaines à essayer de résoudre ce qui ressemble à un bug de récupération, mais qui est en réalité un problème de représentation — les vecteurs eux-mêmes sont mal positionnés, donc aucune mise à jour de l’index ne pourra y remédier.

Ce qu’un modèle d’incorporation calcule réellement

En son essence, un modèle d’incorporation prend en entrée une séquence de tokens et produit un vecteur dense, généralement entre 384 et 3072 dimensions selon le modèle spécifique. Ce vecteur a pour but d’être une codification compressée de la signification de l’entrée.

L’hypothèse de travail sous-jacente à la récupération est que les entrées ayant un sens similaire se retrouvent avec des vecteurs proches l’un de l’autre dans cet espace. La proximité est le plus souvent mesurée à l’aide de la similarité cosinus, qui prend en compte l’angle séparant deux vecteurs plutôt que leur distance brute — une propriété qui la rend insensible aux différences de longueur du texte.

Considérons une règle de conformité stipulant que les clients entreprises classés comme à haut risque doivent passer par des vérifications de diligence renforcée avant que toute proposition de crédit ne puisse être soumise à examen. Un modèle polyvalent performant placerait probablement son vecteur près de formulations similaires en matière de conformité provenant d’autres banques, près de documents juridiques concernant les obligations de diligence, ainsi que près des directives réglementaires relatives à la gestion des clients à haut risque.

Cependant, un gestionnaire de relations pourrait formuler la même nécessité sous-jacente de manière très différente, en demandant par exemple quels sont les étapes à suivre avant de pouvoir soumettre une demande de crédit. Le fait que cette formulation courante soit proche ou non du langage des politiques formelles dépend vraiment de la quantité de données d’entraînement qui ont associé des questions opérationnelles informelles à un langage de conformité formel. Les modèles entraînés principalement sur du texte web généraliste n’absorbent souvent jamais cette association spécifique lorsque le domaine est spécialisé.

Cette disparité entre la formulation des requêtes quotidiennes et celle des documents spécialisés est connue sous le nom de désaccord de vocabulaire, et c’est la cause principale des problèmes de qualité de récupération dans les systèmes RAG d’entreprise.

Tokenisation et fenêtre de contexte

La leçon pratique pour l’utilisation de RAG en production est que les limites des segments déterminées précédemment dans le pipeline doivent rester dans les limites de contexte imposées par le modèle d’incorporation choisi. Tout segment dépassant cette limite est coupé silencieusement, et le vecteur résultant ne reflète que une partie du texte original — un mode de défaillance qui n’apparaîtra nulle part dans les journaux du pipeline.

L’espace sémantique et ses points de défaillance

La plupart des modèles d’incorporation actuels sont des encodeurs de type transformer entraînés avec un objectif contrastif : les paires de textes ayant des significations similaires sont rapprochées dans l’espace vectoriel, tandis que les paires dissimilaires sont éloignées. Après un entraînement suffisant, le modèle présente une disposition géométrique où la proximité sert de proxy à la similarité sémantique.

Cette architecture fonctionne bien tant que les requêtes et les documents partagent la terminologie, le style d’écriture et la structure conceptuelle de ce sur quoi le modèle a été entraîné. Pour les systèmes RAG d’entreprise, elle a tendance à échouer de plusieurs manières prévisibles :

  • La terminologie spécifique au domaine provoque des échecs facilement négligés. Un analyste qui demande concernant le « seuil de déclaration SAR pour la structuration » pourrait ne pas obtenir de résultat correspondant à un extrait de politique formulé comme « critères de soumission du rapport d’activité suspecte pour la structuration des transactions », si le modèle n’a jamais appris que ces deux formulations signifient la même chose.
  • Les abréviations ne se comportent pas de manière cohérente. Des termes tels que "GCC" (Group Credit Committee), "EDD" (Enhanced Due Diligence) et "RFI" (Request for Information) peuvent être intégrés par un modèle polyvalent en fonction de leurs significations les plus courantes ailleurs — Conseil de coopération du Golfe, Livraison de documents électroniques, Interférence radiofréquence.
  • Les identifiants réglementaires n’ont aucune signification intrinsèque pour les modèles généraux. Un code comme CRD-EU-047, traité par un modèle d’intégration polyvalent, est considéré comme une chaîne de caractères arbitraire. Un modèle entraîné spécifiquement sur du texte réglementaire le classerait plutôt parmi les autres identifiants de réglementation crédituelle de l’UE appartenant à la même famille conceptuelle.
  • Les seuils numériques ne sont que partiellement bien gérés par les modèles généraux. L’expression "10 millions d’euros" est souvent intégrée à côté d’autres chiffres monétaires. Lorsqu’elle apparaît en même temps que des informations relatives aux pouvoirs d’approbation, un modèle affiné sur ce domaine peut comprendre le lien entre cette somme spécifique et le contrôle de gouvernance qu’elle déclenche — ce que les modèles généraux ont moins de chances de saisir correctement.
  • Comprendre précisément où les embeddings polyvalents ont tendance à échouer est tout aussi important que de savoir quel modèle occupe la première place dans les classements publics.

    Choisir un modèle d’embedding en 2025

    Le domaine des modèles d’embedding s’est considérablement réduit. La comparaison qui suit couvre les modèles les plus importants pour les systèmes RAG d’entreprise dans la banque et les services financiers à mi-2025.

    Il n’existe pas de solution gagnante universelle pour tous les scénarios d’entreprise. Votre choix dépend du mélange de langues que vous devez prendre en charge, du budget de latence et des infrastructures dont vous disposez, du fait que l’inférence locale soit même une option pour vous, ainsi que de l’ampleur du fossé terminologique entre votre domaine et celui sur lequel les modèles polyvalents ont été entraînés — un fossé qui détermine si le réglage fin vaut la peine des efforts.

    Récupération asymétrique et indication au modèle du type d’entrée qu’il reçoit

    Une distinction que de nombreuses équipes négligent est la récupération asymétrique. Lorsque vous récupérez des extraits, la requête et le fragment auquel elle est comparée sont structurellement très différents. Les requêtes ont tendance à être courtes, formulées sous forme de questions, et manquent souvent d’une grande partie du vocabulaire présent dans une réponse correcte. En revanche, les extraits sont plus longs, présentés comme des faits, et riches en termes propres au domaine.

    modèles E5 ajoutent un préfixe « query: » ou « passage: » au texte d’entrée afin que le modèle sache quel rôle il doit incarner. Cohere’s Embed v3 permet d’indiquer cela via un paramètre input_type, dont les valeurs acceptées incluent « search_query », « search_document », « classification » et « clustering ».

    Fournir le mauvais type d’entrée lors de l’indexation ou de la recherche affecte discrètement les scores de similarité d’une manière difficile à relier à une cause racine. Si vous incarnez un document comme s’il s’agissait d’une requête, le vecteur obtenu est adapté à la géométrie des requêtes plutôt qu’à celle des passages. La récupération ne échoue pas complètement — elle perd simplement en précision, ce qui peut facilement passer inaperçu lors de tests occasionnels.

    Dans un environnement de production, ne comptez pas sur le fait que les développeurs se souviendront de configurer cela correctement — imposez cette règle via la configuration. Tant l’appel d’incorporation au moment de l’indexation que celui au moment de la requête doivent déclarer explicitement le type de leur entrée chaque fois que le modèle utilisé prend en charge cette option.

    import cohere
    from typing import List
    
    co = cohere.Client(api_key="your_api_key")
    
    def embed_documents(chunks: List[str]) -> List[List[float]]:
        """Embed document chunks for indexing with explicit document input type."""
        response = co.embed(
            texts=chunks,
            model="embed-english-v3.0",
            input_type="search_document",
            embedding_types=["float"]
        )
        return response.embeddings.float
    
    def embed_query(query: str) -> List[float]:
        """Embed a search query with explicit query input type."""
        response = co.embed(
            texts=[query],
            model="embed-english-v3.0",
            input_type="search_query",
            embedding_types=["float"]
        )
        return response.embeddings.float[0]
    

    Vecteurs épars : où la correspondance par mots-clés surpasse la recherche sémantique

    Les embeddings denses capturent le sens. En revanche, les représentations éparses indiquent quels termes sont présents et à quel degré ils doivent être pondérés. Pour une grande partie des types de requêtes dans les systèmes RAG d’entreprise, la récupération éparsse surpasse de loin la récupération dense, et dans la plupart des systèmes de production, combiner les deux est plus efficace que d’en utiliser un seul.

    BM25 comme référence fiable

    BM25 reste l’approche standard pour la recherche pilotée par des mots-clés. Il évalue la pertinence en fonction de la fréquence d’apparition d’un terme dans un document, de sa rareté dans l’ensemble du corpus, ainsi que d’un facteur de normalisation qui tient compte de la longueur du document. Il n’y a pas de modèle à entraîner, aucune exigence en matière de GPU, et aucune appel API d’embedding requis.

    Considérons une requête telle que « CRD-EU-047 approval authority threshold ». BM25 classera en tête tout fragment contenant ces termes littéraux. Un modèle dense, en revanche, pourrait ne pas mettre en évidence ce fragment à moins que son corpus d’entraînement n’ait établi une forte association entre ce code de politique spécifique et le concept d’autorité d’approbation.

    from rank_bm25 import BM25Okapi
    import re
    from typing import List, Tuple
    
    def tokenise(text: str) -> List[str]:
        """Simple whitespace and punctuation tokeniser for BM25."""
        return re.findall(r'\b\w+\b', text.lower())
    
    class BM25Index:
        def __init__(self, documents: List[str]):
            self.documents = documents
            tokenised = [tokenise(doc) for doc in documents]
            self.bm25 = BM25Okapi(tokenised)
    
        def search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
            """Return (doc_index, score) pairs for the top_k results."""
            tokens = tokenise(query)
            scores = self.bm25.get_scores(tokens)
            ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
            return ranked[:top_k]
    

    La recherche dense est efficace pour capturer la similarité conceptuelle, tandis que la recherche sparse permet de trouver des correspondances exactes avec des termes et des identifiants. Leur combinaison — la recherche hybride — s’avère particulièrement utile pour les corpus bancaires, où le langage réglementaire est précis et riche en identifiants.

    Dans les corpus basés sur un langage de politique stable et précisément défini, BM25 seul offre souvent un taux de rappel similaire à celui de la recherche dense pour des recherches factuelles restreintes, avec une fraction seulement du coût d’infrastructure. Son principal point faible réside dans la synonymie : une requête utilisant « exigences EDD » ne récupérera pas un passage mentionnant uniquement « exigences d’analyse approfondie » à moins que la phrase exacte ne se recoupe quelque part.

    SPLADE : vecteurs sparses qui apprennent l’expansion du vocabulaire

    SPLADE (Sparse Lexical and Expansion Model) occupe une position intermédiaire entre la simple correspondance de mots-clés et la recherche dense complète. Lors de l’indexation, un modèle de langage masqué est utilisé pour enrichir à la fois les documents et les requêtes d’un vocabulaire sémantiquement pertinent qui n’est pas nécessairement présent dans le texte original. Le résultat est un vecteur dispersé dont chaque dimension correspond à un token de vocabulaire spécifique, pondéré en fonction de l’importance de ce token pour l’entrée.

    Ainsi, un passage codé en SPLADE abordant les exigences EDD peut accorder plus d’importance à des termes tels que « diligence envers le client », « évaluation des risques » et « vérification de l’identité », même si aucune de ces expressions exactes n’apparaît dans le texte source. Cette extension améliore la capacité de rappel pour les requêtes utilisant des synonymes, tout en conservant l’efficacité et la lisibilité propres aux représentations sparses adaptées à un index inversé.

    Le compromis réside dans un coût d’inférence supplémentaire lors de la création de l’index et une empreinte de stockage plus importante que celle du BM25 classique. Cependant, pour les corpus des services financiers où le même concept réglementaire est décrit différemment selon les juridictions et les versions des documents, cette extension peut considérablement élargir la portée des recherches.

    Matryoshka Embeddings : taille vectorielle ajustable pour un contrôle des coûts

    L’apprentissage par représentation Matryoshka (MRL) crée des embeddings où les N premières dimensions forment déjà une représentation complète et autonome de l’entrée — les dimensions supplémentaires ajoutent des détails plus fins sans écraser ce qui a été précédemment stocké.

    Cette technique tire son nom des poupées russes en cascade : un vecteur Matryoshka de 1536 dimensions contient une représentation fonctionnelle de 256 dimensions dans ses premiers 256 slots, une représentation fonctionnelle de 512 dimensions dans ses premiers 512 slots, et ainsi de suite.

    La famille de modèles text-embedding-3 d’OpenAI prend en charge cela directement via un paramètre de dimensions.

    from openai import OpenAI
    from typing import List
    
    client = OpenAI()
    
    def embed_with_matryoshka(
        texts: List[str],
        dimensions: int = 256,
        model: str = "text-embedding-3-large"
    ) -> List[List[float]]:
        """
        Embed texts at a specified sub-dimension.
        Lower dimensions reduce storage and index cost.
        Measure retrieval quality drop before committing to a dimension.
        """
        response = client.embeddings.create(
            input=texts,
            model=model,
            dimensions=dimensions
        )
        return [item.embedding for item in response.data]
    

    Les embeddings Matryoshka imbriquent des représentations de plus en plus détaillées à l’intérieur d’un seul vecteur : les 256 premières dimensions fournissent déjà une représentation utilisable pour la récupération, et chaque niveau supplémentaire ajoute de la précision sémantique au prix d’un coût de stockage proportionnel.

    Le véritable avantage en termes de production réside dans la capacité d’ajuster l’équilibre entre stockage et qualité selon les besoins, sans devoir réentraîner un modèle ou reconstruire l’index à partir de zéro. Pour un corpus relatif aux politiques bancaires, on peut évaluer les performances de récupération avec 256, 512, 1024 et 3072 dimensions, et constater que 512 dimensions permettent d’obtenir 97 % du taux de rappel total tout en utilisant seulement 17 % du stockage requis par les vecteurs complets.

    Cet équilibre est rarement aussi clair en pratique que ce qu’il semble sur un graphique d’évaluation. Les distinctions fines au sein du domaine — en particulier entre des concepts réglementaires étroitement liés — se trouvent souvent précisément dans la partie à haute dimension du vecteur. Testez avec votre propre corpus et vos schémas de requêtes réels avant de choisir une dimensionnalité réduite pour un usage en production.

    Compresser les embeddings sans sacrifier trop de précision

    Les embeddings standards stockent chaque dimension sous forme de nombre flottant à 32 bits. En augmentant ce chiffre pour un million de fragments de documents, chacun ayant 1536 dimensions, on obtient environ 6 Go de vecteurs bruts avant même d’ajouter les coûts liés à l’indexation. À l’échelle enterprise, cette empreinte de stockage et les coûts mémoire associés ne peuvent plus être considérés comme des erreurs d’arrondi.

    La quantification résout ce problème en réduisant le nombre de bits utilisés pour représenter chaque dimension. Trois techniques prédominent en pratique : la quantification scalaire (conversion de float32 en int8), la quantification binaire (conversion de float32 en un seul bit) et la quantification par produit (compression de chaque vecteur en un code plus court).

    Quantification scalaire : int8

    La quantification scalaire affecte l’intervalle continu float32 à 256 valeurs entières discrètes. Chaque dimension passe de 4 octets à 1, réduisant ainsi l’espace de stockage de 75 %. Comme les modèles d’incorporation à haute dimension répartissent l’information de manière dispersée sur de nombreuses dimensions, aucune dimension isolée ne possède beaucoup d’importance ; par conséquent, la précision perdue à cause de ce arrondi est généralement mineure.

    import numpy as np
    from typing import Tuple
    
    def quantise_to_int8(
        embeddings: np.ndarray
    ) -> Tuple[np.ndarray, float, float]:
        """
        Scalar quantisation to int8.
        Returns quantised array plus the scale and zero_point needed for dequantisation.
        """
        min_val = embeddings.min()
        max_val = embeddings.max()
        scale = (max_val - min_val) / 255.0
        zero_point = -round(min_val / scale)
        quantised = np.clip(
            np.round(embeddings / scale) + zero_point,
            0, 255
        ).astype(np.uint8)
        return quantised, scale, zero_point
    
    def dequantise_from_int8(
        quantised: np.ndarray,
        scale: float,
        zero_point: float
    ) -> np.ndarray:
        """Reconstruct approximate float32 embeddings from int8."""
        return ((quantised.astype(np.float32) - zero_point) * scale)
    

    Quantification binaire

    La quantification binaire va encore plus loin, réduisant chaque dimension à un seul bit qui indique simplement si la valeur float originale était positive ou négative. Cela permet de réduire l’espace de stockage d’environ 97 % par rapport à float32. Comme la représentation n’est plus continue, la similarité est mesurée à l’aide de la distance de Hamming plutôt que de la similarité cosinus.

    Cette technique fonctionne le mieux sur des modèles dont les distributions de sortie sont naturellement bien centrées, de sorte que pour une entrée donnée, environ la moitié des dimensions se situent de chaque côté de zéro. Si les dimensions d’un modèle sont biaisées plutôt que équilibrées, la quantification binaire entraîne une perte de qualité nettement plus importante. Cohere a conçu Embed v3 en tenant compte de cette contrainte, et l’évaluation publiée par Anthropic de ce modèle indique une dégradation inférieure à 1 % dans les résultats de recherche, ainsi qu’une réduction de 97 % de l’espace de stockage sur leurs ensembles de test. Considérez ce chiffre comme un point de départ et non comme une garantie, et validez-le avec votre propre corpus avant de vous y fier.

    import numpy as np
    
    def quantise_to_binary(embeddings: np.ndarray) -> np.ndarray:
        """
        Binary quantisation: positive dimensions become 1, negative become 0.
        Packs 8 dimensions per byte using numpy packbits.
        """
        binary_matrix = (embeddings > 0).astype(np.uint8)
        return np.packbits(binary_matrix, axis=1)
    
    def hamming_similarity(
        query_binary: np.ndarray,
        corpus_binary: np.ndarray
    ) -> np.ndarray:
        """Compute normalised Hamming similarity for binary embeddings."""
        n_bits = corpus_binary.shape[1] * 8
        xor = np.bitwise_xor(
            query_binary,
            corpus_binary
        )
        hamming_distances = np.unpackbits(xor, axis=1).sum(axis=1)
        return 1.0 - (hamming_distances / n_bits)
    

    Dans un environnement de production, la quantification binaire est généralement utilisée comme première étape d’un processus de récupération en deux temps : un index binaire permet une recherche rapide et large des candidats potentiels, tandis qu’une étape à précision complète réévalue ensuite les meilleurs résultats. Cette configuration permet d’économiser la majeure partie de l’espace de stockage tout en restituant la précision là où elle est essentielle, à l’étape finale de classement.

    Ajustement fin pour RAG spécifique au domaine

    L’ajustement fin est la solution appropriée une fois que vous avez confirmé que les embeddings polyvalents ne suffisent pas réellement pour vos données. L’objectif est d’apprendre au modèle que le vocabulaire, les abréviations et les liens conceptuels propres à votre domaine se trouvent proches les uns des autres dans l’espace sémantique.

    Le réglage fin n’est pas toujours justifié, et ce n’est pas non plus toujours la solution adéquate. Si vos problèmes de récupération sont dus à une mauvaise segmentation des données, comme abordé précédemment dans cette série, ajuster le modèle d’embedding ne sera d’aucune aide. Si la cause racine réside dans la manière dont le classement est configuré ou dans la façon dont les prompts sont construits, le réglage fin vise complètement le mauvais niveau du système. Avant d’y consacrer des ressources, analysez vos échecs de récupération par type de requête afin de déterminer où se situe réellement le problème.

    Lorsque les embeddings généraux échouent

    Dans le cadre spécifique du RAG bancaire, quelques schémas d’échec récurrents justifient l’investissement dans le réglage fin :

    Les abréviations propres à un domaine donné sont mal interprétées. Un modèle polyvalent pourrait associer « NPA » à l’Association des parcs nationaux plutôt qu’à « actif non performant », et il pourrait seulement établir un lien faible entre « KYC » et les concepts de conformité et d’intégration qui dominent en réalité les recherches bancaires.

    Les liens entre des concepts apparentés dans différents documents disparaissent. Une recherche sur « dispositions de restructuration des facilités » devrait faire apparaître du texte de politique concernant les « cadres de modification des prêts », mais un modèle entraîné principalement sur du contenu web général n’a peut-être jamais vu ces formulations associées suffisamment souvent pour établir ce lien.

    Les codes et identifiants réglementaires ne reçoivent pas l’attention qu’ils méritent. Les numéros de version des politiques, les codes de réglementation et les indicateurs de juridiction devraient influencer significativement le classement, mais les modèles d’incorporation généraux ont tendance à les considérer comme des tokens de faible valeur ne transmettant que peu de signal sémantique.

    Les seuils numériques perdent leur contexte réglementaire. Une valeur telle que « 10 millions d’euros » présentée isolément ne devrait pas correspondre automatiquement à une requête concernant « l’autorité d’approbation pour les grandes expositions » — ce lien ne se crée que si le modèle a été entraîné sur des données sectorielles qui relient ce chiffre à sa signification réglementaire.

    Création de paires d’entraînement à partir de données spécifiques au domaine

    L’ajustement fin des modèles sentence-transformers selon un objectif contrastif dépend de paires positives : des exemples qui associent une requête à un passage que le modèle doit apprendre à considérer comme lié. Les exemples négatifs peuvent être sélectionnés manuellement ou extraits automatiquement du corpus environnant.

    Dans un contexte RAG bancaire, ces paires positives peuvent être réunies à partir de quelques sources pratiques :

    Des ensembles existants de questions-réponses déjà produits par les équipes de conformité et de crédit, où chaque question est liée à son passage source.

    La structure naturelle des documents de politique, où un en-tête associé au paragraphe qui le suit constitue un exemple positif prêt à l’emploi.

    Les requêtes des analystes enregistrées, associées aux passages qui ont réellement été récupérés lorsque la réponse était correcte.

    Les requêtes générées automatiquement par un LLM pour chaque fragment de texte, en utilisant ce même fragment comme passage positif de référence.

    Parmi ces méthodes, la génération de requêtes synthétiques constitue généralement l’approche la plus praticable lorsque peu de données étiquetées sont déjà disponibles.

    from openai import OpenAI
    import json
    from typing import List, Dict
    
    client = OpenAI()
    
    def generate_training_queries(
        chunk: str,
        chunk_metadata: Dict,
        n_queries: int = 3
    ) -> List[Dict]:
        """
        Generate synthetic query-passage pairs for fine-tuning.
        The chunk itself is the positive passage for each generated query.
        """
        prompt = f"""You are generating training data for a banking RAG system.
    Given the following policy passage, generate {n_queries} realistic questions
    that a credit analyst, compliance officer, or relationship manager might ask
    that this passage directly answers. Each question should use natural language
    and may use different terminology than the passage itself.
    
    Passage:
    {chunk}
    
    Return a JSON array of objects with keys "query" and "difficulty".
    Difficulty should be "narrow" (single fact) or "synthesis" (multiple facts).
    Return only the JSON array, no other text."""
    
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"}
        )
    
        try:
            result = json.loads(response.choices[0].message.content)
            queries = result.get("queries", result) if isinstance(result, dict) else result
            return [
                {
                    "query": q["query"],
                    "passage": chunk,
                    "document_id": chunk_metadata.get("document_id"),
                    "chunk_id": chunk_metadata.get("chunk_id"),
                    "difficulty": q.get("difficulty", "narrow")
                }
                for q in queries
            ]
        except (json.JSONDecodeError, KeyError):
            return []
    

    Entraînement contrastif avec perte de type triplet

    L’objectif d’entraînement le plus efficace pour les modèles d’encodage orientés vers la récupération est l’apprentissage contrastif, qui utilise soit des négatifs au sein du lot, soit des négatifs difficiles choisis délibérément. Sentence-transformers prend en charge ce modèle grâce à MultipleNegativesRankingLoss, qui utilise chaque autre exemple d’un lot d’entraînement comme négatif implicite pour une paire ancre-positive donnée.

    from sentence_transformers import SentenceTransformer, InputExample
    from sentence_transformers.losses import MultipleNegativesRankingLoss
    from torch.utils.data import DataLoader
    from typing import List, Dict
    import logging
    
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    
    def build_training_examples(
        pairs: List[Dict]
    ) -> List[InputExample]:
        """
        Convert query-passage pairs into InputExample objects.
        MultipleNegativesRankingLoss expects (anchor, positive) pairs.
        Negatives are sampled automatically from other items in the batch.
        """
        return [
            InputExample(texts=[pair["query"], pair["passage"]])
            for pair in pairs
            if pair.get("query") and pair.get("passage")
        ]
    
    def fine_tune_embedding_model(
        base_model_name: str,
        training_pairs: List[Dict],
        output_path: str,
        epochs: int = 3,
        batch_size: int = 16,
        warmup_steps: int = 100
    ) -> SentenceTransformer:
        """
        Fine-tune a sentence-transformers model on domain-specific query-passage pairs.
    
        base_model_name: HuggingFace model identifier or local path.
        training_pairs: List of dicts with "query" and "passage" keys.
        output_path: Directory to save the fine-tuned model.
        """
        model = SentenceTransformer(base_model_name)
        logger.info(f"Loaded base model: {base_model_name}")
        logger.info(f"Training on {len(training_pairs)} query-passage pairs")
    
        examples = build_training_examples(training_pairs)
        loader = DataLoader(examples, shuffle=True, batch_size=batch_size)
        loss = MultipleNegativesRankingLoss(model)
    
        total_steps = len(loader) * epochs
        logger.info(f"Training for {epochs} epochs, {total_steps} total steps")
    
        model.fit(
            train_objectives=[(loader, loss)],
            epochs=epochs,
            warmup_steps=warmup_steps,
            output_path=output_path,
            show_progress_bar=True,
            checkpoint_path=output_path,
            checkpoint_save_steps=len(loader)
        )
    
        logger.info(f"Fine-tuned model saved to: {output_path}")
        return model
    

    Résultats des benchmarks : un corpus bancaire avant et après affinage

    Considérez un test de récupération effectué sur un corpus relatif aux politiques de crédit d’entreprise, composé de 847 extraits tirés de documents de politique, de matrices d’approbation, de procédures renforcées de diligence et de directives AML. L’ensemble d’évaluation comprend 120 requêtes réparties en quatre catégories : recherches factuelles ciblées, questions de seuil, questions nécessitant plusieurs preuves et questions de synthèse.

    Les avantages du réglage fin du domaine sont les plus marqués pour les requêtes de type seuil, comme celles visant à connaître le seuil d’approbation applicable aux clients entreprises à haut risque dans l’UE. C’est là que les abréviations bancaires et le vocabulaire réglementaire diffèrent le plus fortement de ce qu’un modèle polyvalent a pu observer lors du pré-entraînement ; combler cette lacune de vocabulaire permet donc d’obtenir la plus grande amélioration en termes de rappel. Les requêtes à plusieurs sources d’information et les requêtes de synthèse voient une amélioration moindre grâce au réglage fin seul, mais elles bénéficient considérablement une fois que la récupération hybride est intégrée.

    Considérez ces chiffres comme une illustration de ce que peut apporter un projet de fine-tuning bien mené sur un ensemble de données bancaires correctement étiqueté, et non comme une promesse. La composition de votre propre corpus, le mélange des requêtes et la précision de l’étiquetage influenceront les résultats. Ce qui est le plus important, c’est d’analyser la qualité de la récupération par type de requête plutôt que de présenter un seul score global, car chaque type de requête a tendance à échouer pour des raisons différentes.

    Considérations de productivité pour l’incorporation en lot à grande échelle

    Fournir 100 000 fragments à une API d’incorporation un par un est à la fois lent et coûteux. Les pipelines d’incorporation de niveau production, eux, traitent leurs entrées en lot afin d’améliorer la productivité, de respecter les limites de débit, de se remettre efficacement des pannes et de garantir une génération de résultats déterministe.

    Gestion en lot via les API des fournisseurs

    L’endpoint d’embeddings d’OpenAI permet jusqu’à 2 048 entrées par appel. L’endpoint Embed de Cohere est limité à 96 textes par requête, sauf si l’on utilise leur API batch dédiée pour des tâches plus volumineuses. L’exécution de l’inférence localement avec sentence-transformers offre des tailles de lot configurables, limitées uniquement par la mémoire GPU disponible.

    import time
    import logging
    from typing import List, Optional
    from openai import OpenAI, RateLimitError, APIError
    
    logger = logging.getLogger(__name__)
    client = OpenAI()
    
    def embed_in_batches(
        texts: List[str],
        model: str = "text-embedding-3-large",
        batch_size: int = 512,
        max_retries: int = 3,
        retry_delay: float = 2.0,
        dimensions: Optional[int] = None
    ) -> List[List[float]]:
        """
        Embed a large list of texts using batched API calls with retry logic.
    
        texts: Pre-chunked text strings. Caller is responsible for ensuring
               no text exceeds the model's token limit.
        batch_size: Number of texts per API call. Stay well below the API limit
                    to avoid hitting per-request token limits.
        dimensions: Optional Matryoshka dimension reduction for supported models.
        """
        all_embeddings: List[List[float]] = []
        total_batches = (len(texts) + batch_size - 1) // batch_size
    
        for batch_idx in range(0, len(texts), batch_size):
            batch = texts[batch_idx: batch_idx + batch_size]
            current_batch = batch_idx // batch_size + 1
            logger.info(f"Embedding batch {current_batch}/{total_batches} "
                        f"({len(batch)} texts)")
    
            kwargs = {
                "input": batch,
                "model": model
            }
            if dimensions is not None:
                kwargs["dimensions"] = dimensions
    
            attempt = 0
            while attempt < max_retries:
                try:
                    response = client.embeddings.create(**kwargs)
                    # Preserve input order: API returns items sorted by index
                    sorted_items = sorted(response.data, key=lambda x: x.index)
                    all_embeddings.extend([item.embedding for item in sorted_items])
                    break
    
                except RateLimitError:
                    attempt += 1
                    wait = retry_delay * (2 ** attempt)
                    logger.warning(f"Rate limit hit on batch {current_batch}. "
                                   f"Waiting {wait:.1f}s before retry {attempt}/{max_retries}")
                    time.sleep(wait)
    
                except APIError as e:
                    attempt += 1
                    logger.error(f"API error on batch {current_batch}: {e}. "
                                 f"Retry {attempt}/{max_retries}")
                    if attempt >= max_retries:
                        raise
                    time.sleep(retry_delay)
    
        logger.info(f"Embedding complete. Total vectors: {len(all_embeddings)}")
        return all_embeddings
    

    Exécution de l’inférence localement avec sentence-transformers

    Certaines organisations sont soumises à des contraintes de résidence des données qui interdisent l’envoi de documents réglementaires vers une API externe. Dans ces cas, l’inférence locale avec sentence-transformers constitue la solution idéale.

    from sentence_transformers import SentenceTransformer
    import numpy as np
    from typing import List, Optional
    import logging
    
    logger = logging.getLogger(__name__)
    
    class LocalEmbeddingPipeline:
        """
        Production-ready local embedding pipeline using sentence-transformers.
        Suitable for data-residency-constrained banking environments.
        """
    
        def __init__(
            self,
            model_name_or_path: str,
            device: str = "cpu",
            batch_size: int = 64,
            normalise: bool = True
        ):
            self.model = SentenceTransformer(model_name_or_path, device=device)
            self.batch_size = batch_size
            self.normalise = normalise
            self.device = device
            logger.info(f"Loaded model: {model_name_or_path} on {device}")
    
        def embed(
            self,
            texts: List[str],
            show_progress: bool = True
        ) -> np.ndarray:
            """
            Embed a list of texts. Returns an (N, D) numpy array.
            Normalises to unit length if normalise=True (required for cosine similarity).
            """
            embeddings = self.model.encode(
                texts,
                batch_size=self.batch_size,
                show_progress_bar=show_progress,
                normalize_embeddings=self.normalise,
                convert_to_numpy=True
            )
            logger.info(f"Embedded {len(texts)} texts. "
                        f"Output shape: {embeddings.shape}")
            return embeddings
    
        def embed_query(self, query: str) -> np.ndarray:
            """Embed a single query. Returns a 1D array."""
            return self.embed([query], show_progress=False)[0]
    

    Vérification de la longueur des tokens avant embedding

    Si un bloc de texte dépasse la limite de tokens d’un modèle, il est tronqué sans avertissement. Dans un corpus de politiques, ce troncage silencieux peut éliminer précisément la clause ou le seuil numérique qui justifiait à l’origine sa récupération. La vérification du nombre de tokens avant l’incorporation permet de détecter ce problème avant même qu’il n’atteigne votre index.

    from transformers import AutoTokenizer
    from typing import List, Tuple
    import logging
    
    logger = logging.getLogger(__name__)
    
    def validate_chunk_lengths(
        chunks: List[str],
        model_name: str,
        max_tokens: int,
        truncation_strategy: str = "warn"
    ) -> Tuple[List[str], List[int]]:
        """
        Validate that all chunks are within the model's token limit.
    
        truncation_strategy:
            "warn"  - Log a warning for oversized chunks and include them (will be truncated by model).
            "skip"  - Remove oversized chunks and return only valid ones.
            "raise" - Raise ValueError on the first oversized chunk.
    
        Returns (validated_chunks, oversized_indices).
        """
        tokeniser = AutoTokenizer.from_pretrained(model_name)
        oversized = []
    
        for idx, chunk in enumerate(chunks):
            token_count = len(tokeniser.encode(chunk, add_special_tokens=True))
            if token_count > max_tokens:
                oversized.append(idx)
                msg = (f"Chunk {idx} has {token_count} tokens, "
                       f"exceeds model limit of {max_tokens}. "
                       f"First 80 chars: {chunk[:80]!r}")
                if truncation_strategy == "raise":
                    raise ValueError(msg)
                else:
                    logger.warning(msg)
    
        if truncation_strategy == "skip" and oversized:
            valid = [c for i, c in enumerate(chunks) if i not in set(oversized)]
            logger.info(f"Removed {len(oversized)} oversized chunks. "
                        f"{len(valid)} chunks remain.")
            return valid, oversized
    
        return chunks, oversized
    

    Attacher des métadonnées et de l’origine aux embeddings

    Un vecteur d’embedding brut ne suffit pas à lui seul pour que un système RAG réglementé fonctionne correctement. Chaque vecteur doit être accompagné de métadonnées structurées afin que les étapes de récupération, de réclassement et de génération ultérieures puissent déterminer l’origine du contenu, appliquer des permissions d’accès, restreindre les résultats selon la juridiction et faire référence à un document source fiable.

    En revenant au scénario de proposition de crédit de 12 millions d’euros, chaque morceau intégré doit inclure, au minimum, les champs indiqués ici :

    from dataclasses import dataclass, field
    from typing import Optional, List
    import uuid
    
    @dataclass
    class EmbeddedChunk:
        """
        Production embedding record for a banking policy RAG system.
        The vector enables retrieval. The metadata enables everything else.
        """
        # Vector
        vector: List[float]
        vector_dimensions: int
        embedding_model: str
        embedding_model_version: str
    
        # Content
        text: str
        content_type: str          # "narrative", "table_row", "proposition", "image_description"
    
        # Provenance
        document_id: str
        document_version: str      # e.g. "7.2"
        policy_id: Optional[str]   # e.g. "CRD-EU-047"
        jurisdiction: Optional[str] # e.g. "EU"
        effective_date: Optional[str]
    
        # Chunk structure
        chunk_id: str = field(default_factory=lambda: str(uuid.uuid4()))
        parent_id: Optional[str] = None
        section: Optional[str] = None
        page_number: Optional[int] = None
        source_artifact_path: Optional[str] = None  # path to original image/table
    
        # Access control
        classification: str = "INTERNAL"            # "PUBLIC", "INTERNAL", "CONFIDENTIAL"
        permitted_roles: List[str] = field(default_factory=list)
    
        # Indexing
        indexed_at: Optional[str] = None
        indexing_pipeline_version: Optional[str] = None
    

    Assurer la cohérence de ces métadonnées à chaque étape du processus, depuis le découpage en morceaux jusqu’à l’intégration et à l’indexation vectorielle, n’est pas seulement une bonne pratique. Dans un contexte bancaire réglementé, obtenir une réponse techniquement correcte mais basée sur une version de politique obsolète constitue une faille en matière de conformité. La fonction du vecteur est de trouver le morceau ; celle des métadonnées est de confirmer que ce morceau provient de la version actuelle et correcte de la source.

    Évaluation de la qualité de l’intégration

    Les classements publics tels que MTEB indiquent des scores de récupération polyvalents pour une large gamme de jeux de données académiques. Ces chiffres sont utiles pour éliminer les modèles dont les performances sont clairement faibles. Cependant, ils s’avèrent insuffisants lorsque la tâche consiste à choisir le meilleur modèle pour un corpus spécialisé comme la bibliothèque de politiques internes d’une banque.

    La seule mesure qui compte vraiment est la performance d’un modèle avec vos propres documents, en utilisant vos propres requêtes, évaluée selon les jugements de pertinence que vous avez vous-même définis.

    Création d’un ensemble d’évaluation pour la récupération

    Un ensemble de test pour la récupération, conçu pour un pipeline d’incorporation RAG bancaire, doit couvrir plusieurs types de requêtes :

    Des recherches factuelles ciblées qui correspondent à un seul fragment fiable, comme demander avec quelle fréquence les clients entreprises à haut risque doivent subir leur examen annuel au minimum.

    Questions basées sur des seuils qui combinent une condition numérique spécifique avec la règle de gouvernance y associée, comme demander quelle autorité d’approbation est requise lorsque les installations d’une entreprise européenne dépassent 10 millions d’euros.

    Questions nécessitant plusieurs éléments de preuve, où une réponse complète dépend de la collecte de plus d’un élément, comme demander quels contrôles doivent être effectués avant même que une proposition de crédit d’entreprise à haut risque ne puisse être soumise.

    Questions de synthèse qui tirent du contenu de plusieurs sections en même temps, comme demander une description complète du cadre de contrôle AML régissant les prêts d’entreprise à haut risque.

    Questions interdocuments, pertinentes chaque fois que des politiques se renvoient mutuellement à travers différents documents.

    import numpy as np
    from typing import List, Dict, Set
    
    def recall_at_k(
        retrieved_ids: List[str],
        relevant_ids: Set[str],
        k: int
    ) -> float:
        """
        Compute Recall@k for a single query.
        relevant_ids is the ground truth set of chunk identifiers.
        retrieved_ids is the ordered list of retrieved chunk identifiers.
        """
        if not relevant_ids:
            return 0.0
        top_k_retrieved = set(retrieved_ids[:k])
        return len(top_k_retrieved & relevant_ids) / len(relevant_ids)
    
    def mean_reciprocal_rank(
        retrieved_ids: List[str],
        relevant_ids: Set[str]
    ) -> float:
        """Compute MRR for a single query."""
        for rank, chunk_id in enumerate(retrieved_ids, start=1):
            if chunk_id in relevant_ids:
                return 1.0 / rank
        return 0.0
    
    def evaluate_embedding_model(
        model_name: str,
        evaluation_queries: List[Dict],
        corpus_chunks: List[Dict],
        k_values: List[int] = [1, 5, 10, 20]
    ) -> Dict:
        """
        Evaluate an embedding model on a labelled retrieval dataset.
    
        evaluation_queries: List of dicts with "query" and "relevant_chunk_ids" keys.
        corpus_chunks: List of dicts with "chunk_id" and "text" keys.
        Returns per-query-type and aggregate retrieval metrics.
        """
        from sentence_transformers import SentenceTransformer
    
        model = SentenceTransformer(model_name)
    
        corpus_texts = [c["text"] for c in corpus_chunks]
        corpus_ids = [c["chunk_id"] for c in corpus_chunks]
        corpus_embeddings = model.encode(corpus_texts, normalize_embeddings=True)
    
        results_by_type: Dict[str, List] = {}
        all_recall: Dict[int, List[float]] = {k: [] for k in k_values}
        all_mrr: List[float] = []
    
        for query_item in evaluation_queries:
            query = query_item["query"]
            relevant = set(query_item["relevant_chunk_ids"])
            query_type = query_item.get("query_type", "unspecified")
    
            query_embedding = model.encode(query, normalize_embeddings=True)
            scores = corpus_embeddings @ query_embedding
            ranked_indices = np.argsort(scores)[::-1]
            retrieved = [corpus_ids[i] for i in ranked_indices]
    
            mrr = mean_reciprocal_rank(retrieved, relevant)
            all_mrr.append(mrr)
    
            for k in k_values:
                r = recall_at_k(retrieved, relevant, k)
                all_recall[k].append(r)
    
            if query_type not in results_by_type:
                results_by_type[query_type] = {"mrr": [], "recall": {k: [] for k in k_values}}
            results_by_type[query_type]["mrr"].append(mrr)
            for k in k_values:
                results_by_type[query_type]["recall"][k].append(
                    recall_at_k(retrieved, relevant, k)
                )
    
        aggregate = {
            "model": model_name,
            "n_queries": len(evaluation_queries),
            "mrr": float(np.mean(all_mrr)),
            "recall": {k: float(np.mean(all_recall[k])) for k in k_values}
        }
    
        per_type = {
            qt: {
                "mrr": float(np.mean(data["mrr"])),
                "recall": {k: float(np.mean(data["recall"][k])) for k in k_values},
                "n_queries": len(data["mrr"])
            }
            for qt, data in results_by_type.items()
        }
    
        return {"aggregate": aggregate, "by_query_type": per_type}
    

    La évaluation des métriques de récupération ne doit pas se faire de manière isolée, sans tenir compte de leur impact sur la qualité de la réponse finale. Supposons que le Recall@5 s’améliore de trois points, mais que le taux de blocs presque identiques récupérés augmente de 30 % — cet équilibre n’est pas nécessairement bénéfique pour le LLM, car lui fournir trois passages presque identiques au lieu d’un passage utile ne rajoute aucune preuve réelle.

    L’approche correcte consiste à évaluer l’ensemble du processus, de la requête initiale jusqu’à la réponse finale présentée à l’utilisateur. Le rôle d’un modèle d’embedding se limite à introduire les bonnes preuves dans la fenêtre de contexte. Le fait que ces preuves aboutissent à une réponse à la fois précise et conforme dépend de chaque étape qui suit.

    Le pipeline d’embedding en tant qu’infrastructure

    Lorsqu’un bloc a terminé son traitement dans le pipeline d’incorporation, il doit en ressortir avec un vecteur associé, des métadonnées complètement remplies, ainsi qu’un identifiant stable qui vous permettra de le récupérer, de l’actualiser ou de le supprimer ultérieurement sans endommager les enregistrements voisins dans l’index.

    Il s’agit d’une composante d’infrastructure, et non d’un script ponctuel. Les pipelines d’incorporation de niveau production destinés à des environnements bancaires réglementés exigent ce qui suit :

    • Idempotence. Si un bloc est réincorporé en raison d’un changement de version de modèle, cela doit écraser l’enregistrement existant plutôt que de créer un double.
  • Suivi des versions. Chaque fois que le modèle d’incorporation change, vous devez soit reconstruire l’index à partir de zéro, soit le partitionner en fonction de la version du modèle. Permettre aux vecteurs provenant de différents modèles de coexister dans le même index génère des scores de similarité auxquels on ne peut pas se fier.
  • Propagation des contrôles d’accès. Si un morceau de données a été marqué comme CONFIDENTIEL lors de son ingestion, cette étiquette doit rester intacte après l’incorporation et figurer dans l’index vectoriel. La couche de récupération doit alors la respecter.
  • Observabilité. Vous devez enregistrer le temps de latence d’incorporation par lot, le nombre de tokens, les taux d’erreur API ainsi que les échecs au niveau des morceaux de données, tous accompagnés de métadonnées structurées. Une troncation qui modifie silencieusement le comportement de récupération est précisément le type de problème que votre système de surveillance doit détecter, et non quelque chose d’invisible.
  • Suivi des coûts. L’inclusion des coûts via une API s’accumule rapidement lorsque l’on opère à grande échelle. Détaillez les dépenses par type de document et par exécution du pipeline, afin de pouvoir évaluer le choix du modèle et la réduction de la dimensionnalité en fonction des coûts réels, plutôt que sur des suppositions.
  • Rien de tout cela n’est optionnel une fois que l’on est en production. Ce sont ces critères qui distinguent un pipeline qui ne fonctionne que dans une démonstration d’un pipeline qu’il est possible d’exploiter, de contrôler et de maintenir sur le long terme dans un environnement réglementé.

    Retour à la proposition de crédit de 12 millions d’euros

    La question initiale du gestionnaire de relation n’a pas changé : quelles autorisations sont nécessaires pour valider cette transaction, et quels contrôles doivent être respectés avant de pouvoir la soumettre ?

    • La partie 4 expliquait comment concevoir des unités de récupération qui conservent ces réponses dans leur forme originale : la clause de politique EDD, la ligne du tableau d’approbation indiquant l’autorité GCC, ainsi que le diagramme de flux de travail décrivant les contrôles avant soumission.
    • Dans cette partie, nous avons transformé ces unités de récupération en vecteurs recherchables, en utilisant un modèle testé sur des terminologies spécifiques au secteur bancaire, affiné pour comprendre comment les abréviations réglementaires se rapportent à leur contexte de conformité, et intégré avec des métadonnées complètes sur l’origine permettant de confirmer ultérieurement la version de la politique et la juridiction concernée.

    Lorsque la requête arrive, l’index vectoriel localise la ligne de la matrice d’approbation concernant les expositions à haut risque dépassant 10 millions d’euros, la clause EDD, ainsi que le diagramme montrant la séquence de contrôle — et il les restitue accompagnés de métadonnées confirmant que ces éléments font tous partie de la police CRD-EU-047, version 7.2, sous juridiction européenne, en vigueur depuis le 15 janvier 2026.

    Ce qui parvient à la couche de génération est des preuves précises, complètes et traçables.

    C’est là la différence entre un pipeline d’embedding qui se contente de charger des blocs dans une base de données vectorielle et un autre qui conserve tout ce dont on a besoin pour obtenir des réponses fiables et auditable.

    Au préalable de passer à l’indexation vectorielle : une liste de contrôle

    Au moment d’intégrer des blocs embeddés dans l’index vectoriel, vérifiez ce qui suit :

    • Le paramètre de type d’entrée est-il correctement défini sur le modèle d’incorporation pour les appels d’indexation ainsi que pour les appels de recherche ? Une telle incohérence nuit silencieusement à la précision des résultats obtenus.
    • A-t-on vérifié la longueur des tokens de chaque morceau avant son incorporation ? Une troncature invisible modifie ce que représente réellement le texte incorporé, sans qu’aucune erreur ne soit signalée au cours du processus.
    • Chaque enregistrement vectoriel indique-t-il l’ensemble des informations de provenance : version du document, identifiant de la politique, juridiction et date d’entrée en vigueur ?
    • Le modèle d’incorporation a-t-il été véritablement testé sur votre propre corpus de domaine et vos schémas de recherche, plutôt que choisi uniquement en fonction des classements dans des benchmarks publics ?
    • Si vous avez affiné le modèle, disposez-vous de chiffres de performance avant et après l’ajustement, mesurés sur un ensemble de requêtes laissé de côté ?
  • Vos décisions de quantification sont-elles basées sur des résultats provenant de votre propre ensemble d’évaluation de récupération, plutôt que sur des hypothèses tirées de benchmarks publiés ?
  • Le pipeline est-il idempotent, conscient des versions, et observable, comme le exigent les systèmes de production réglementés par des normes ?
  • Si l’un de ces critères n’est pas respecté, le index vectoriel ne vous avertira pas — il stockera simplement tout ce que vous lui fournissez. Rien dans la base de données ne signalera un vecteur créé à partir d’un morceau tronqué, une requête contenant un type de données incorrect, ou un morceau intégré à l’aide d’une version de modèle décalée par rapport au reste de l’index. Ces lacunes ne se manifestent pas immédiatement ; elles réapparaissent plus tard sous forme de problèmes de qualité de récupération qui, vu de l’extérieur, ressemblent à des problèmes liés au LLM.

    Dans une précédente partie de cette série, la partie 4 montrait comment transformer des documents propres en unités de recherche tout en préservant leur structure. Cette partie explique comment convertir ces unités de recherche en vecteurs recherchables tout en conservant leur provenance.

    Ce qui suit concerne le point où ces vecteurs rencontrent réellement l’index : la recherche vectorielle dense, la récupération sparse, les approches hybrides, les algorithmes d’approximation du voisin le plus proche, ainsi que le filtrage des métadonnées qui détermine quels vecteurs peuvent être pris en compte pour la recherche.

    Lectures complémentaires

  • Les petits modèles spécialisés dépassent discrètement les GIGANTES LLM — Découvrez comment un modèle logique de 3 milliards de paramètres surpasse un modèle de 120 milliards de paramètres en raisonnement formel sur du matériel ordinaire, et pourquoi l’adaptation à la tâche prime sur la taille brute du modèle.