Les LLM tels que GPT-4 deviennent des incontournables dans nos vies de tous les jours, en particulier dans le monde des développeurs. Ce sont en effet des outils formidables pour nous aider à écrire des algorithmes parfois complexes, comprendre une erreur ou un bug, ou pour des tâches plus génériques comme expliquer un pattern ou une notion de code avancé.
Et si on pouvait rendre cette technologie encore plus performante en lui donnant la connaissance de l’entièreté du code source de la codebase sur laquelle on travaille au quotidien afin de rendre ces réponses encore plus pertinentes. C’est là qu’entre en jeu la notion de RAG (ou Retrieval Augmented Generation) !
Tout d’abord, avant de partir dans le cœur du sujet, il est important de clarifier ce qu’est le RAG.
RAG is an AI framework for retrieving facts from an external knowledge base to ground large language models (LLMs) on the most accurate, up-to-date information and to give users insight into LLMs’ generative process.
IBM research
On pourrait plus simplement définir le RAG comme une technique d’enrichissement des modèles LLM, avec des sources de données externes afin de lui donner une base de connaissance dans laquelle il pourrait piocher à l’aide de recherche sémantique. Pour nous donner des réponses plus précises et contextuelles à des questions posées en langage naturel par les utilisateurs.
Cet article a pour but de vous montrer un exemple de comment coder son propre système de RAG pour enrichir le modèle GPT avec la codebase d’un repository GitHub, à l’aide de l’API d’OpenAI, Node.js, TypeScript et la bibliothèque Langchain.
Extraction de la donnée
Pour manipuler le modèle GPT-4 et toutes les fonctionnalités qui l’accompagnent, il existe nombre d’outils à la disposition des développeurs. L’un des plus connus est le package open source Langchain, disposant d’une version pour Node.js (vous trouverez plus d’infos et comment l’installer ici).
Pour notre exemple, nous allons extraire le code source du repository qui m’a servi à écrire cet article.
Langchain expose une classe GithubRepoLoader
qui va nous permettre d’extraire chaque fichier de code de notre repository en une liste de documents.
const repositoryUrl = "<https://github.com/Njuelle/github-rag-example>";
const loader = new GithubRepoLoader(repositoryUrl, {
branch: "main",
recursive: true,
// long file should be ignored to avoid "max token" error
ignoreFiles: ["bun.lockb"],
});
const docs = await loader.load();
Résumons nos fichiers pour de meilleurs réponses
Lors de mes premières expérimentations, une chose que j’ai remarquée est que si l’on donne à GPT le code source tel quel en guise de document, la qualité de ses réponses se trouve extrêmement réduite. Pour ainsi dire, il est quasiment incapable de répondre à nos questions. En effet, celui-ci manque de contexte pour retrouver et extraire des informations pertinentes sur notre code source.
Pour améliorer la qualité de notre RAG, une des étapes les plus importantes est la création d’un “summary” de chaque document. Grosso modo, nous allons demander à GPT d’écrire un résumé du fichier de code que nous lui donnons en entrée à l’aide d’un prompt personnalisé.
Ce prompt devra indiquer quelles informations sont à extraire et résumer : nom, type, chemin et description du fichier, packages importés, quelles sont les fonctions présentes, …
Pour ce faire, nous allons pouvoir utiliser la classe PromptTemplate
de Langchain pour créer notre prompt custom.
const summarizeCodePromptTemplate = new PromptTemplate({
template: `You are an excelent developer.
I will give you a source code file, and you will create a summary with these information:
- type of the file
- purpose of the file
- used language and framework
- imported packages / files
- name of all the functions, methods, classes and types that are declared in the file that are not imported
- path of the file
You have to be concise because this summary will be vectorized and persisted in a database to do semantic search.
You should not add more text, only the summary.
Here is the souce code file:
# BEGIN SOURCE CODE FILE
{sourceCodeFile}
# END SOURCE CODE FILE
Here is the path of the file: "{path}"
`,
inputVariables: ["sourceCodeFile", "path"],
});
Ensuite, il nous faut appeler GPT pour lui demander d’exécuter cette tâche. Et une fois la réponse récupérée, nous pouvons enrichir notre document.
const model = new OpenAI({
modelName: "gpt-4-1106-preview",
maxTokens: -1,
});
async function enrichDocument({
model,
document,
}: {
model: OpenAI;
document: Document;
}): Promise<Document> {
const prompt = await summarizeCodePromptTemplate.format({
sourceCodeFile: document.pageContent,
path: document.metadata.source,
});
const summary = await model.call(prompt);
return {
...document,
pageContent: `${summary}\\n- source code: \\n \\`\\`\\`\\n${document.pageContent}\\n\\`\\`\\``,
};
}
const enrichedDocuments = await Promise.all(
documents.map(async (document) => {
return enrichDocument({ model, document });
})
);
Voici un exemple de résumé que GPT nous retourne:
- Type of the file: TypeScript source code
- Purpose of the file: To enrich a document by generating a summary using the OpenAI API
- Used language and framework: TypeScript, langchain framework
- Imported packages / files:
- langchain/document
- langchain/llms/openai
- langchain/prompts
- Declared entities:
- Const: summarizeCodePromptTemplate
- Function: enrichDocument
- Path of the file: "lib/enrichDocument.ts"
Vectorisation des documents
La vectorisation dans l’IA est le processus de conversion des données non numériques, comme le texte ou les images, en un format numérique sous forme de vecteurs. Ce processus permet à GPT de traiter et d’analyser ces données.
Ici, pour le traitement du langage naturel, nous utilisons des “embeddings”, c’est-à-dire des vecteurs de faible dimension utilisés pour représenter des éléments (comme des mots, des phrases, ou même des images) dans un espace vectoriel. Ils capturent les relations sémantiques et les caractéristiques des données.
Une fois de plus, Langchain va nous aider dans cette tâche. Il va nous permettre d’instancier un VectorStore
qui se chargera de convertir chaque document en vecteurs.
⚠️ Ici, pour la simplicité de l’exemple, nous utilisons la classe MemoryVectorStore
, qui va charger nos vecteurs dans la RAM. Dans un vrai contexte d’application, ces embeddings devraient être persistés dans une base de données capable de gérer des vecteurs. Langchain supporte de nombreuses intégrations, vous pouvez trouver cette liste et plus d’informations ici.
const vectorStore = await MemoryVectorStore.fromDocuments(
enrichedDocuments,
new OpenAIEmbeddings()
);
Posons nos questions à GPT
Nous arrivons maintenant à la dernière étape ! Celle où nous allons pouvoir poser nos questions sur la codebase. Pour cela, nous allons utiliser la classe RetrievalQAChain
de Langchain, ainsi que la fonction asRetriever
de notre VectorStore
créé précédemment.
Cela va permettre, dans un premier temps, d’effectuer une recherche de texte sémantique sur l’ensemble de nos embeddings pour extraire les documents les plus pertinents en fonction de notre question. Et ensuite, cela va construire automatiquement un prompt à partir des documents et de la question.
const query =
"Peux tu me donner une explication de la fonction 'enrichDocument' ?";
const { text } = await RetrievalQAChain.fromLLM(
model,
vectorStore.asRetriever()
).call({
query,
});
console.log(text);
Il ne nous reste plus qu’à constater le résultat de la réponse retournée par GPT:
La fonction `enrichDocument` est une fonction asynchrone conçue pour enrichir un objet de type `Document` avec un résumé généré par un modèle d’intelligence artificielle fourni par OpenAI. La fonction fait partie d'un projet utilisant TypeScript et le framework LangChain.
Voici une explication détaillée de ce que fait la fonction `enrichDocument` :
1. Elle prend un objet avec deux propriétés : `model` et `document`.
- `model` est une instance de `OpenAI`, qui est utilisée pour exécuter une requête auprès du modèle d'IA.
- `document` est l'objet de type `Document` qui doit être enrichi.
2. La fonction prépare d'abord une requête pour le modèle d'IA à l'aide d'un `PromptTemplate`, nommé `summarizeCodePromptTemplate`. Ce template contient un texte qui guide le modèle pour générer un résumé qui doit être concis et contient divers détails spécifiés du fichier source comme le type, le but, le langage utilisé, les packages importés, les entités déclarées (noms des fonctions, méthodes, classes et types) et le chemin du fichier.
3. Pour formuler la requête, la fonction remplace les marqueurs dans le template avec le contenu réel (`pageContent`) de l'objet `Document` et le chemin d'accès à ce fichier (`path`).
4. Cette requête est ensuite envoyée au modèle OpenAI via la méthode `call(prompt)` sur l'instance `model`. Le modèle traitera la requête et retournera un résumé généré.
5. Une fois le résumé reçu, la fonction l'ajoute à l'objet `Document` dans la propriété `pageContent`, en conservant le contenu original du document et en préfixant le contenu avec le résumé généré.
6. Enfin, elle renvoie l'objet `Document` enrichi.
Voici un exemple de ce à quoi pourrait ressembler un objet `Document` après avoir été traité par `enrichDocument` :
```typescript
{
...document, // Propriétés initiales du document
pageContent: `
Résumé généré ici
- source code:
\\`\\`\\`
// Contenu original du document
\\`\\`\\`
`,
}
De manière concise, la fonction enrichDocument
utilise un modèle d'IA pour enrichir un document en lui ajoutant un résumé pertinent, qui sera utilisé pour des recherches sémantiques et d'autres opérations de traitement des documents.
Conclusion
Nous venons de réaliser une implémentation très simple d’un système de RAG en extrayant le code source d’un repository GitHub. Vous pouvez retrouver le code complet de cet article sur ce repository. L’implémentation est relativement simple et il peut être intéressant de parler des limitations et de comment aller plus loin.
Concernant les limitations, sur un repository avec une codebase très conséquente, la récupération des fichiers atteint ses limites. En effet, l’implémentation de Langchain ne permet pas une extraction plus efficace en termes de mémoire. Cependant, je travaille actuellement sur une pull request sur le repository de Langchain qui permettrait de charger les documents avec un stream, utilisant les generators.
Aussi, une amélioration pertinente de ce système de RAG pourrait être d’utiliser une approche “Branch Solve Merge” pour améliorer la qualité de la réponse fournie par le modèle. Ou plus simplement utiliser un premier agent qui déterminerait des critères de pertinence d’une réponse. Et ensuite donner ces critères à un deuxième agent qui lui se chargerait de produire une réponse respectant ces critères.
Laisser un commentaire