Implémenter des modèles de récupération pour les pipelines RAG
La génération d’extraction augmentée (RAG) améliore les réponses du modèle de langage en fournissant un contexte pertinent à partir de vos données. Le composant retriever recherche votre base de connaissances et retourne des documents que le LLM utilise pour générer des réponses précises et ancrées. Azure Database pour PostgreSQL avec pgvector sert de récupérateur efficace, stockant vos incorporations de documents et exécutant des requêtes de similarité qui alimentent l’étape de génération.
Cette unité couvre la conception de schémas pour les charges de travail RAG, l’implémentation de requêtes de récupération de blocs, la création de pipelines d’ingestion de documents, le retour des métadonnées de citation et l’évaluation de la qualité de récupération.
Comprendre l’architecture RAG et le rôle de récupérateur
Les systèmes RAG suivent un modèle en trois étapes :
- Incorporation de requêtes : Convertir la question de l’utilisateur en vecteur à l’aide d’un modèle d’incorporation
- Récupération : Effectuer une recherche dans le magasin de vecteurs pour trouver des documents similaires à la projection de requête.
- Génération: Transmettez les documents récupérés en tant que contexte à un LLM, ce qui génère une réponse
PostgreSQL agit en tant que récupérateur dans cette architecture. La qualité de la récupération a un impact direct sur la qualité de la génération. Si le récupérateur retourne des documents non pertinents, le LLM ne dispose pas du contexte nécessaire pour répondre correctement et peut générer des informations incorrectes. Si le moteur de recherche manque des documents pertinents, la réponse peut être incomplète.
Pour un assistant de recherche juridique, une récupération efficace signifie trouver les précédents, les lois et les clauses contractuelles qui traitent de la question du procureur. Le LLM synthétise ensuite ces sources en une réponse cohérente avec des citations.
Concevoir des schémas pour les charges de travail RAG
Les applications RAG fonctionnent généralement avec des blocs de documents plutôt que des documents entiers. Les documents longs dépassent les limites de contexte LLM et peuvent contenir des sections non pertinentes pour une requête spécifique. La segmentation fractionne les documents en segments plus petits et ciblés qui peuvent être récupérés indépendamment.
Documents et blocs distincts
Utilisez deux tables : une pour les documents sources et une pour les blocs avec des incorporations :
CREATE TABLE source_documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
source_url TEXT,
document_type TEXT,
ingested_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE document_chunks (
id SERIAL PRIMARY KEY,
document_id INTEGER REFERENCES source_documents(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
embedding vector(1536),
token_count INTEGER,
start_char INTEGER,
end_char INTEGER,
UNIQUE (document_id, chunk_index)
);
Cette conception offre plusieurs avantages :
- Récupération flexible : Vous pouvez récupérer des blocs individuels ou reconstruire des sections de document
- Suivi des sources : Chaque bloc renvoie à sa source des citations
- Stockage efficace : Les métadonnées de document sont stockées une seule fois, et non dupliquées par bloc
- Mises à jour simples : Le remplacement d’un document signifie supprimer ses fragments et les réintégrer.
Ajouter des métadonnées pour le filtrage et le contexte
Incluez les métadonnées qui aident à filtrer et à reconstruire le contexte :
CREATE TABLE document_chunks (
id SERIAL PRIMARY KEY,
document_id INTEGER REFERENCES source_documents(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
embedding vector(1536),
token_count INTEGER,
section_title TEXT,
page_number INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Les titres de section et les numéros de page permettent aux utilisateurs de vérifier les informations récupérées et d’aider le LLM à comprendre le contexte de chaque bloc.
Créer des index pour les modèles de requête RAG
Les index de similarité vectorielle (HNSW ou IVFFlat) permettent d’incorporer rapidement des recherches, comme décrit précédemment dans ce module. Les charges de travail RAG bénéficient également d’index B-tree qui accélèrent les jointures et filtres uniques à la récupération de segments. Lorsque vous récupérez des blocs, vous revenez fréquemment aux documents sources pour les métadonnées de citation et filtrez ou triez par position de bloc dans un document. Sans ces index, PostgreSQL analyse l’intégralité de la table de blocs pour chaque requête de récupération.
-- B-tree index for document lookups (speeds up JOINs to source_documents)
CREATE INDEX document_chunks_document_id_idx
ON document_chunks (document_id);
-- Composite index for chunk ordering (supports context window queries)
CREATE INDEX document_chunks_doc_chunk_idx
ON document_chunks (document_id, chunk_index);
L’index composite sur (document_id, chunk_index) est particulièrement important pour la recherche dans une fenêtre de contexte, dans laquelle vous allez chercher des segments adjacents pour offrir le contexte environnant. Sans cela, PostgreSQL doit analyser tous les blocs d’un document pour rechercher des voisins.
Implémenter la récupération de blocs pour la construction de contexte
Les requêtes RAG récupèrent des fragments qui deviennent le contexte du LLM. Le schéma de récupération que vous choisissez affecte la qualité des réponses et l’efficacité d’utilisation des jetons. Vous devez équilibrer trois préoccupations concurrentes : récupérer suffisamment de contenu pertinent, rester dans des limites de jetons et fournir un contexte suffisant pour que le LLM comprenne chaque segment.
L’approche la plus simple récupère directement les top-k blocs les plus similaires. Cela fonctionne bien lorsque vos blocs sont autonomes et que vos questions correspondent directement à des blocs uniques. Toutefois, les documents juridiques, les spécifications techniques et le contenu de narration répartissent souvent des concepts sur plusieurs paragraphes. Un bloc peut référencer « les conditions précédentes » ou « comme décrit ci-dessus » sans contenir le texte référencé.
Dans ces cas, la récupération de fenêtres de contexte extrait les blocs adjacents, ainsi que chaque correspondance trouvée. Si le bloc 47 est le plus similaire à la requête, vous récupérez également les blocs 46 et 48. Cela augmente l’utilisation des jetons, mais réduit le risque de réponses incomplètes. Le compromis dépend de votre structure de contenu : les documents hautement structurés avec des limites de section claires nécessitent moins de contexte environnant que le texte narratif.
Les limites de jeton ajoutent une autre contrainte. Les LLMs ont des fenêtres contextuelles fixes, et vous avez besoin d'espace pour l'invite du système, la question de l'utilisateur et la réponse générée. Si vous récupérez 10 blocs en moyenne 500 jetons chacun, vous avez consommé 5 000 jetons avant que le LLM n’écrive un mot unique. Effectuez le suivi des nombres de jetons cumulés lors de la récupération pour rester dans les limites du budget.
La requête suivante combine ces modèles. Il récupère les blocs les plus similaires avec leurs voisins, effectue le suivi des jetons cumulatifs et inclut les métadonnées de citation :
WITH matched_chunks AS (
-- Find the most similar chunks
SELECT id, document_id, chunk_index, embedding <=> $1 AS distance
FROM document_chunks
ORDER BY embedding <=> $1
LIMIT 3
),
context_window AS (
-- Expand to include adjacent chunks for context
SELECT DISTINCT dc.id, dc.document_id, dc.chunk_index, dc.content,
dc.token_count, mc.distance
FROM matched_chunks mc
JOIN document_chunks dc ON dc.document_id = mc.document_id
AND dc.chunk_index BETWEEN mc.chunk_index - 1 AND mc.chunk_index + 1
),
token_limited AS (
-- Track cumulative tokens to respect LLM context limits
SELECT cw.*, sd.title AS source_title, sd.source_url,
SUM(cw.token_count) OVER (ORDER BY cw.distance, cw.chunk_index) AS cumulative_tokens
FROM context_window cw
JOIN source_documents sd ON cw.document_id = sd.id
)
SELECT id, content, source_title, source_url, distance
FROM token_limited
WHERE cumulative_tokens <= 3000
ORDER BY distance, chunk_index;
Ajustez les paramètres en fonction de votre cas d’usage. Pour un assistant de recherche juridique répondant à des questions de clauses spécifiques, vous pouvez récupérer cinq correspondances sans fenêtre de contexte. Pour un bot d'assistance client qui répond à des questions ouvertes sur la documentation des produits, vous pouvez obtenir trois correspondances avec deux segments de texte contextuels pour chacun.
Gérer les pipelines d’ingestion de documents
Les nouveaux documents doivent être traités avant d’être recherchés. Le pipeline d’ingestion fractionne les documents en blocs, génère des incorporations et stocke tout dans PostgreSQL.
La façon dont vous fractionnez des documents affecte directement la qualité de la récupération. La stratégie de segmentation appropriée dépend de votre structure de contenu et de vos modèles de requête :
Blocs de taille fixe : Fractionnez tous les N caractères ou jetons. Cette approche est simple à implémenter et produit un nombre de jetons prévisible, mais elle peut couper des phrases ou des paragraphes en plein milieu. Utilisez la segmentation de taille fixe lorsque votre contenu n’a pas de limites structurelles claires ou si vous avez besoin de tailles de bloc cohérentes pour la planification budgétaire des jetons.
Segments sémantiques : Fractionner à des limites naturelles telles que des paragraphes, des sections ou des phrases. Cela conserve la signification dans chaque bloc, mais produit des tailles variables. Utilisez la segmentation sémantique pour les documents structurés dans lesquels les sections représentent des pensées complètes, telles que des clauses juridiques, une documentation d’API ou des entrées de FAQ.
Blocs qui se chevauchent : Incluez du texte à partir de blocs adjacents pour conserver le contexte aux limites. Par exemple, un chevauchement de 200 caractères entre 1 000 blocs de caractères garantit que les concepts couvrant les limites de segment apparaissent dans au moins un bloc complet. Utilisez le chevauchement lorsque vos requêtes peuvent correspondre au contenu près des limites de segment.
Pour le scénario de l’assistant recherche juridique, la segmentation sémantique au niveau des limites de paragraphe ou de section fonctionne bien, car le texte juridique est organisé en clauses et arguments discrets. Chaque bloc représente un concept juridique complet qui peut être autonome dans le contexte du LLM.
Une fois que vous avez segmenté vos documents et généré des incorporations, insérez-les efficacement à l’aide d’opérations de traitement par lots. La conception à deux tables nécessite d’abord d’insérer le document source pour obtenir son ID, puis d’insérer tous les blocs dans une seule ligne à plusieurs lignes INSERT. Cette approche minimise les allers-retours à la base de données et conserve la relation document-à-fragment intacte. La plupart des API d’incorporation acceptent également plusieurs textes par requête. Vous pouvez donc générer des incorporations pour les blocs d’un document entier dans un appel d’API avant l’insertion.
Les mises à jour de documents nécessitent une décision : devez-vous créer des versions des documents ou les remplacer ? Pour la plupart des applications RAG, le remplacement est plus simple. Supprimez les blocs existants (la ON DELETE CASCADE contrainte gère cette opération automatiquement lorsque vous supprimez le document source), puis remettez le contenu mis à jour. Cette approche garantit que vos résultats de récupération reflètent toujours l’état actuel du document. Si vous avez besoin de l’historique des versions, ajoutez une version colonne et source_documents conservez les anciens blocs en même temps que les nouveaux, en filtrant par version au moment de la requête.
Les index HNSW gèrent correctement les suppressions sans reconstruire. Les index IVFFlat peuvent accumuler la fragmentation après des modifications significatives, nécessitant une reconstruction périodique pour maintenir les performances des requêtes.
Implémenter la récupération avec des citations
Les citations transforment RAG d’une boîte noire en un outil de recherche transparent. Lorsque l’assistant de recherche juridique cite une clause de contrat, le procureur doit vérifier cette citation par rapport au document d’origine. Sans métadonnées de citation, les utilisateurs doivent faire confiance à la sortie du LLM à l'aveugle, un risque important dans les contextes juridiques, médicaux ou financiers où la précision est cruciale.
Les citations effectives nécessitent plus que le contenu de bloc. Vos requêtes de récupération doivent retourner le titre du document source, l’URL ou l’ID de document, le titre de section et le numéro de page lorsqu’ils sont disponibles. Ces métadonnées permettent à votre application de mettre en forme des citations que les utilisateurs peuvent réellement suivre à la source. Le score de distance permet également d’afficher en évidence les citations à haut niveau de confiance tout en signalant les correspondances moins fiables pour que l’utilisateur puisse les vérifier.
Un défi courant se produit lorsque plusieurs blocs du même document correspondent à une requête. Si les segments 12, 15 et 18 du « modèle d’accord d’emploi » apparaissent tous dans vos résultats principaux, les listant comme trois citations distinctes encombrent la réponse. Au lieu de cela, regroupez les blocs par document source et présentez-les sous la forme d’une citation unique avec plusieurs extraits pertinents. Cette approche produit une sortie plus propre et aide les utilisateurs à voir le contexte complet de chaque source.
La requête suivante illustre la récupération groupée de documents. Il classe les blocs au sein de chaque document, limite à trois blocs par document pour éviter d’accablant le contexte et agrège le contenu pour une mise en forme de citation plus propre :
WITH ranked_chunks AS (
SELECT
dc.*,
sd.title AS document_title,
sd.source_url,
dc.embedding <=> $1 AS distance,
ROW_NUMBER() OVER (PARTITION BY dc.document_id ORDER BY dc.embedding <=> $1) AS rank_in_doc
FROM document_chunks dc
JOIN source_documents sd ON dc.document_id = sd.id
WHERE dc.embedding <=> $1 < 0.5
)
SELECT
document_id,
document_title,
source_url,
array_agg(content ORDER BY chunk_index) AS chunks,
MIN(distance) AS best_distance
FROM ranked_chunks
WHERE rank_in_doc <= 3
GROUP BY document_id, document_title, source_url
ORDER BY best_distance
LIMIT 5;
Votre application peut ensuite la mettre en forme en citations conviviales :
« L’employeur peut mettre fin à ce contrat avec un préavis de 30 jours. » — Modèle d’accord d’emploi, section 4.2, page 3
Évaluer et améliorer la qualité de recherche
La qualité de récupération détermine l’efficacité de RAG. Une récupération médiocre conduit à une génération médiocre, quelle que soit la capacité de votre LLM. Si le récupérateur renvoie des blocs non pertinents, le LLM gaspille de la capacité de la fenêtre de contexte sur du texte inutile. Si le récupérateur manque des blocs pertinents, le LLM ne dispose pas des informations nécessaires pour répondre correctement. La mesure de la qualité de récupération séparément de la qualité de génération vous aide à diagnostiquer où votre pipeline RAG a besoin d’amélioration.
Trois métriques capturent différents aspects des performances de récupération :
Précision: Fraction des blocs récupérés qui sont réellement pertinents. Une faible précision signifie que le LLM reçoit du contexte non pertinent, ce qui peut le confondre ou l'amener à générer des informations incorrectes. Améliorez la précision en resserrant les seuils de distance ou en réduisant le nombre de blocs récupérés.
Rappel : Fraction des blocs pertinents récupérés. Un rappel faible signifie que le LLM manque des informations importantes, ce qui conduit à des réponses incomplètes ou incorrectes. Améliorez le rappel en assouplissant les seuils de distance, en augmentant le nombre de segments récupérés ou en ajustant les paramètres d'index comme
ef_search.Rang réciproque moyen (MRR) : À quelle position se situe le premier résultat pertinent dans la liste classée. MRR est importante, car les modèles LLM pondérent plus fortement le contexte antérieur, et les utilisateurs qui analysent les citations remarquent d’abord les premiers résultats. Améliorez MRR en affinant votre modèle d’incorporation ou votre prétraitement de requête.
Pour mesurer ces métriques, vous avez besoin d’un jeu de données d’évaluation : ensemble de requêtes représentatives associées à des jugements humains sur lesquels les blocs sont pertinents. La création de ce jeu de données nécessite un effort initial : une personne doit exécuter des exemples de requêtes et étiqueter les résultats, mais cela vous permet d’apporter des améliorations pilotées par les données plutôt que de deviner. Commencez par 20 à 50 requêtes qui représentent les types de questions que vos utilisateurs posent réellement. Pour chaque requête, récupérez les 10-20 premiers blocs et disposez d’un expert de domaine pour évaluer leur pertinence à une échelle simple (non pertinente, quelque peu pertinente, hautement pertinente).
Avec cette évaluation définie en place, vous pouvez mesurer la précision et le rappel à différentes coupures (precision@5, recall@10) et suivre la façon dont les modifications apportées à votre pipeline affectent ces métriques. Exécutez vos requêtes de récupération sur le jeu d’évaluation, comparez les résultats aux jugements humains et calculez les métriques. La plupart des équipes automatisent cela dans un script de scoring qui s’exécute chaque fois qu’ils modifient des stratégies de segmentation, incorporent des modèles ou des paramètres d’index.
Lorsque les métriques tombent en deçà de l'objectif, expérimentez systématiquement les paramètres qui affectent le compromis entre précision et rappel :
Taille de bloc : Les blocs plus petits améliorent la précision en retournant du contenu plus ciblé, mais peuvent nuire au rappel en fragmentant des informations pertinentes sur plusieurs blocs. Les segments plus volumineux améliorent le rappel mais diluent la pertinence.
Chevauchement de bloc : Un plus grand chevauchement conserve le contexte entre les limites, ce qui aide les requêtes qui correspondent au contenu près des bords du bloc. Toutefois, le chevauchement augmente les exigences de stockage et peut retourner du contenu redondant.
Modèle d’incorporation : Différents modèles capturent différentes relations sémantiques. Un modèle formé sur le texte juridique peut surperformer un modèle à usage général pour la recherche juridique. Considérez les modèles spécifiques au domaine ou affinés si les modèles généraux sont sous-performants.
Paramètres d’index : Un taux plus élevé de
ef_search(HNSW) ou deprobes(IVFFlat) améliore le rappel en recherchant davantage de candidats, au prix d'une latence accrue des requêtes. Commencez par les paramètres par défaut et augmentez uniquement si le rappel est insuffisant.Seuils de distance : Les seuils plus stricts améliorent la précision en excluant les correspondances marginales. Les seuils plus souples améliorent le rappel en incluant plus de candidats. Utilisez votre jeu de données d’évaluation pour trouver le seuil qui équilibre les deux métriques pour votre cas d’usage.