Tutoriel RAG #
Dans ce tutoriel, nous allons construire un pipeline de génération de réponses à des questions en utilisant la génération augmentée de récupération (RAG) en moins de 100 lignes de code Python.
Nous utiliserons Haystack, un framework open-source pour créer des applications LLM prêtes pour la production, des pipelines de génération augmentée de récupération et des systèmes de recherche de pointe qui fonctionnent de manière intelligente sur de grandes quantités de documents.
Pour ce tutoriel, vous aurez besoin de Python 3.10 installé sur votre système.
Installer Haystack et les dépendances nécessaires #
1pip install haystack-ai
2pip install markdown-it-py mdit_plain pypdf
3pip install gdown
4pip install pdfminer.six
Impor des modules nécessaires #
1# coding: utf-8
2
3from pprint import pprint
4from haystack import Pipeline
5from haystack.components.writers import DocumentWriter
6from haystack.components.converters import TextFileToDocument
7from haystack.components.preprocessors import DocumentSplitter, DocumentCleaner
8from haystack.components.routers import FileTypeRouter
9from haystack.components.joiners import DocumentJoiner
10from haystack.components.embedders import OpenAIDocumentEmbedder, OpenAITextEmbedder
11from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
12from haystack.components.builders import PromptBuilder
13from haystack.components.generators import OpenAIGenerator
14from haystack.utils import Secret
15from haystack.document_stores.in_memory import InMemoryDocumentStore
Données d'exemple #
Dans cet exemple, nous utiliserons un fichier d'exemple documentation.txt
qui contient un aperçu de base d'une solution imaginaire : DataSyncX.
Le fichier peut être téléchargé ici.
Initialisation le DocumentStore #
Configurez un DocumentStore
pour organiser vos documents. Un DocumentStore
stocke les documents sur lesquels le système de réponse aux questions se base pour obtenir des réponses à vos requêtes. Dans ce guide, pour simplifier, nous utiliserons le InMemoryDocumentStore
.
Pour des configurations plus avancées, une base de données vectorielle dédiée comme Milvus est recommandée.
1document_store = InMemoryDocumentStore()
Construction d'un pipeline pour indexer les documents #
Create a pipeline to index documents, here we will only allow the text/plain
media type.
Définitions des objets #
1file_type_router = FileTypeRouter(mime_types=["text/plain"])
2text_file_converter = TextFileToDocument()
3document_joiner = DocumentJoiner()
4document_cleaner = DocumentCleaner()
5document_splitter = DocumentSplitter(
6 split_by="word", split_length=150, split_overlap=50)
7document_writer = DocumentWriter(document_store)
Définition de l'embedder #
Ici, nous appellerons directement l'API Ektos AI pour générer des embeddings en utilisant le modèle d'embedding gte-multilingual-base
.
1document_embedder = OpenAIDocumentEmbedder(
2 api_base_url="https://api.ektos.ai/v1/",
3 api_key=Secret.from_token("YOUR_EKTOS_API_KEY_HERE"),
4 # api_key=Secret.from_token(os.environ.get("EKTOS_API_KEY")),
5 model="gte-multilingual-base")
Assemblage le pipeline #
1preprocessing_pipeline = Pipeline()
2preprocessing_pipeline.add_component(instance=file_type_router, name="file_type_router")
3preprocessing_pipeline.add_component(
4 instance=text_file_converter, name="text_file_converter")
5preprocessing_pipeline.add_component(instance=document_joiner, name="document_joiner")
6preprocessing_pipeline.add_component(instance=document_cleaner, name="document_cleaner")
7preprocessing_pipeline.add_component(
8 instance=document_splitter, name="document_splitter")
9preprocessing_pipeline.add_component(
10 instance=document_embedder, name="document_embedder")
11preprocessing_pipeline.add_component(instance=document_writer, name="document_writer")
12
13preprocessing_pipeline.connect(
14 "file_type_router.text/plain", "text_file_converter.sources")
15preprocessing_pipeline.connect("text_file_converter", "document_joiner")
16preprocessing_pipeline.connect("document_joiner", "document_cleaner")
17preprocessing_pipeline.connect("document_cleaner", "document_splitter")
18preprocessing_pipeline.connect("document_splitter", "document_embedder")
19preprocessing_pipeline.connect("document_embedder", "document_writer")
Ajout des documents à traiter #
1preprocessing_pipeline.run(
2 {"file_type_router": {"sources": ["./documentation.txt"]}})
Vous pouvez déjà exécuter le code pour vérifier que les embeddings sont générés correctement et enregistrés dans la base de données vectorielle en mémoire.
1$ python rag.py
2Calculating embeddings: 100%|███████████████████| 1/1 [00:00<00:00, 4.78it/s]
Création d'un pipeline pour la génération #
Définition du template de prompt #
Concevez un prompt personnalisé pour une tâche de génération de réponses à des questions en utilisant la méthode RAG. Le prompt doit accepter deux entrées : les documents, récupérés à partir d'un document store, et une question soumise par l'utilisateur. Utilisez la syntaxe de boucle de Jinja2 pour combiner le contenu des documents récupérés dans le prompt.
1template = """
2Answer the questions based on the given context.
3Do not add anything else after the answer.
4
5Context:
6{% for document in documents %}
7 {{ document.content }}
8{% endfor %}
9
10Question: {{ question }}
11Answer:
12"""
Définition du prompt builder #
Ensuite, créez une instance de PromptBuilder en utilisant votre modèle de prompt. Le PromptBuilder insérera automatiquement les valeurs requises lorsqu'elles seront fournies, générant ainsi un prompt complet. Cette méthode permet une expérience de réponse aux questions plus personnalisée et efficace.
1prompt_buidler = PromptBuilder(template=template)
Définitions des composants du pipeline: text embedder, retriever et LLM generator #
Ici, nous appellerons à nouveau l'API Ektos AI pour générer des embeddings en utilisant le modèle d'embedding gte-multilingual-base
et le LLM phi-3.5-mini-4k-instruct
pour la génération.
1text_embedder = OpenAITextEmbedder(
2 api_base_url="https://api.ektos.ai/v1/",
3 api_key=Secret.from_token("YOUR_EKTOS_API_KEY_HERE"),
4 # api_key=Secret.from_token(os.environ.get("EKTOS_API_KEY")),
5 model="gte-multilingual-base")
6retriever = InMemoryEmbeddingRetriever(document_store=document_store)
7llm_generator = OpenAIGenerator(
8 api_base_url="https://api.ektos.ai/v1/",
9 api_key=Secret.from_token("YOUR_EKTOS_API_KEY_HERE"),
10 # api_key=Secret.from_token(os.environ.get("EKTOS_API_KEY")),
11 model="phi-3.5-mini-4k-instruct")
Assemblement du pipeline #
1pipe = Pipeline()
2pipe.add_component("embedder", text_embedder)
3pipe.add_component("retriever", retriever)
4pipe.add_component("prompt_builder", prompt_buidler)
5pipe.add_component("llm", llm_generator)
6pipe.connect("embedder.embedding", "retriever.query_embedding")
7pipe.connect("retriever", "prompt_builder.documents")
8pipe.connect("prompt_builder", "llm")
Pour poser une question #
1question = (
2 "What is the phone number to contact DataSyncX support?"
3)
4
5pprint(pipe.run(
6 {
7 "embedder": {"text": question},
8 "prompt_builder": {"question": question},
9 }
10))
Exécution du code #
Lors de l'exécution du script, les étapes suivantes se dérouleront successivement :
- Indexation : Les données (ici
documentation.txt
) sont converties en embeddings et stockées dans une base de données vectorielle en mémoire. - Récupération : Étant donné une requête utilisateur convertie en embeddings, un récupérateur est appelé pour sélectionner les documents les plus pertinents afin d'enrichir la requête future adressée au LLM.
- Enrichissement : La requête envoyée au LLM est enrichie du contexte récupéré en plus de la question initiale.
- Génération : Le LLM est appelé et génère une sortie basée à la fois sur la question et le contexte supplémentaire (les deux étant fournis dans le même prompt).
1$ python rag.py
2Calculating embeddings: 100%|██████████████████████████████████| 1/1 [00:00<00:00, 4.82it/s]
3{'embedder': {'meta': {'model': 'gte-multilingual-base',
4 'usage': {'completion_tokens': 0,
5 'prompt_tokens': 54,
6 'total_tokens': 54}}},
7 'llm': {'meta': [{'finish_reason': 'stop',
8 'index': 0,
9 'model': 'phi-3.5-mini-4k-instruct',
10 'usage': {'completion_tokens': 16,
11 'completion_tokens_details': None,
12 'prompt_tokens': 1780,
13 'total_tokens': 1796}}],
14 'replies': [' +1-800-555-0123']}}
Le LLM a répondu avec la réponse correcte : +1-800-555-0123
, qui se trouvait initialement dans le fichier documentation.txt
.
Remarquez le nombre de prompt_tokens
(1780) ; cela indique que le nombre élevé de tokens est dû au contexte supplémentaire ajouté en plus de la question.
Code complet #
1# coding: utf-8
2
3from pprint import pprint
4from haystack import Pipeline
5from haystack.components.writers import DocumentWriter
6from haystack.components.converters import TextFileToDocument
7from haystack.components.preprocessors import DocumentSplitter, DocumentCleaner
8from haystack.components.routers import FileTypeRouter
9from haystack.components.joiners import DocumentJoiner
10from haystack.components.embedders import OpenAIDocumentEmbedder, OpenAITextEmbedder
11from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
12from haystack.components.builders import PromptBuilder
13from haystack.components.generators import OpenAIGenerator
14from haystack.utils import Secret
15from haystack.document_stores.in_memory import InMemoryDocumentStore
16
17document_store = InMemoryDocumentStore()
18file_type_router = FileTypeRouter(mime_types=["text/plain"])
19text_file_converter = TextFileToDocument()
20document_joiner = DocumentJoiner()
21document_cleaner = DocumentCleaner()
22document_splitter = DocumentSplitter(
23 split_by="word", split_length=150, split_overlap=50)
24document_writer = DocumentWriter(document_store)
25document_embedder = OpenAIDocumentEmbedder(
26 api_base_url="https://api.ektos.ai/v1/",
27 api_key=Secret.from_token("YOUR_EKTOS_API_KEY_HERE"),
28 # api_key=Secret.from_token(os.environ.get("EKTOS_API_KEY")),
29 model="gte-multilingual-base")
30
31preprocessing_pipeline = Pipeline()
32preprocessing_pipeline.add_component(instance=file_type_router, name="file_type_router")
33preprocessing_pipeline.add_component(
34 instance=text_file_converter, name="text_file_converter")
35preprocessing_pipeline.add_component(instance=document_joiner, name="document_joiner")
36preprocessing_pipeline.add_component(instance=document_cleaner, name="document_cleaner")
37preprocessing_pipeline.add_component(
38 instance=document_splitter, name="document_splitter")
39preprocessing_pipeline.add_component(
40 instance=document_embedder, name="document_embedder")
41preprocessing_pipeline.add_component(instance=document_writer, name="document_writer")
42
43preprocessing_pipeline.connect(
44 "file_type_router.text/plain", "text_file_converter.sources")
45preprocessing_pipeline.connect("text_file_converter", "document_joiner")
46preprocessing_pipeline.connect("document_joiner", "document_cleaner")
47preprocessing_pipeline.connect("document_cleaner", "document_splitter")
48preprocessing_pipeline.connect("document_splitter", "document_embedder")
49preprocessing_pipeline.connect("document_embedder", "document_writer")
50
51
52preprocessing_pipeline.run(
53 {"file_type_router": {"sources": ["./documentation.txt"]}})
54
55
56template = """
57Answer the questions based on the given context.
58Do not add anything else after the answer.
59
60Context:
61{% for document in documents %}
62 {{ document.content }}
63{% endfor %}
64
65Question: {{ question }}
66Answer:
67"""
68prompt_buidler = PromptBuilder(template=template)
69text_embedder = OpenAITextEmbedder(
70 api_base_url="https://api.ektos.ai/v1/",
71 api_key=Secret.from_token("YOUR_EKTOS_API_KEY_HERE"),
72 # api_key=Secret.from_token(os.environ.get("EKTOS_API_KEY")),
73 model="gte-multilingual-base")
74retriever = InMemoryEmbeddingRetriever(document_store=document_store)
75llm_generator = OpenAIGenerator(
76 api_base_url="https://api.ektos.ai/v1/",
77 api_key=Secret.from_token("YOUR_EKTOS_API_KEY_HERE"),
78 # api_key=Secret.from_token(os.environ.get("EKTOS_API_KEY")),
79 model="phi-3.5-mini-4k-instruct")
80
81pipe = Pipeline()
82pipe.add_component("embedder", text_embedder)
83pipe.add_component("retriever", retriever)
84pipe.add_component("prompt_builder", prompt_buidler)
85pipe.add_component("llm", llm_generator)
86pipe.connect("embedder.embedding", "retriever.query_embedding")
87pipe.connect("retriever", "prompt_builder.documents")
88pipe.connect("prompt_builder", "llm")
89
90question = (
91 "What is the phone number to contact DataSyncX support?"
92)
93
94pprint(pipe.run(
95 {
96 "embedder": {"text": question},
97 "prompt_builder": {"question": question},
98 }
99))