LangChain pour piloter vos LLM

  IA Javascript

Introduction

LangChain est un framework open source pour développer des applications alimentées par de grands modèles de langage (LLM).

L’objectif de LangChain est de relier de puissants LLM, tels que GPT-3.5, GPT-4, Ahthropic Claude, Gemini etc, à un éventail de sources de données externes pour créer et récolter les avantages des applications de traitement du langage naturel (NLP).

LangChain est utilisable avec le language Python ou JavaScript.

Dans ce tutoriel, j’utiliserai l’implémentation Javascript de LangChain.

Installation

Vous devez au préalable avoir installé Node.js, sur votre machine.

Ensuite pour installer LangChain, il suffit d’utiliser la commande suivante :

npm install langchain

OpenAI sans LangChain

Avant de parler de LangChain, nous allons utiliser la librairie OpenAI, qui va nous permettre d’appeler le modèle GPT.

Cela va nous permettre de comparer la façon dont fonctionne LangChain, par la suite.

Exemple :

import { config } from "dotenv";
config();

import OpenAI from "openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function askGpt(content) {
  const messages = [{ role: "user", content }];

  const response = await openai.chat.completions.create({
    model: "gpt-3.5-turbo",
    messages: messages,
    temperature: 0,
  });

  return response.choices[0].message.content;
}

Donc dans cet exemple, j’instancie une instance de la classe OpenAI, en lui précissant notre clé API, qui provient du fichier .env.

Ensuite, je définis une fonction askGPT qui appellera l’API d’OpenAI.

Vous constaterez, que je déclare un tableau de messages qu’il faut ensuite passer à la méthode create. Le rôle user permet d’indiquer qu’il s’agit de la question qui provient de l’utilisateur.

Ensuite, je définis une fonction askGPT qui appelera l’API d’OpenAI.

Vous constaterez, que je déclare un tableau de messages qu’il faut ensuite passer à la méthode create.

Ensuite, j’obtiens une réponse asynchrone qui contiendra un tableau choices qui lui même dispose d’autres objets.

Exemple :

function displayResponse(promise) {
  promise
    .then((response) => console.log(response))
    .catch((error) => console.error(error));
}

// 1ère question
const question = "Quelle est la capitale de l'Angleterre ?";
displayResponse(askGpt(question));
// Output :
// La capitale de l'Angleterre est Londres.

// 2ème question
const promptTemplate = `
  Soit très drôle quand tu réponds à la question.
  Question: {question}
  `;
const prompt = promptTemplate.replace("{question}", question);

displayResponse(askGpt(prompt));
// Output :
// La capitale de l'Angleterre est Londres, bien sûr ! J'ai entendu dire qu'ils ont un maire qui ressemble étrangement à un ours en peluche...ou peut-être que c'est juste son manteau en fourrure, qui sait !

Ensuite, je définis une function displayResponse, qui va me permettre d’afficher simplement le contenu de la reponse.

Par la suite, je déclare une question : Quelle est la capitale de l'Angleterre ?.

Et j’utilise ma fonction askGPT, pour appeler le modèle.

Plus bas, je déclare un prompt qui contient une variable question, qui va demander au modèle de répondre en étant drôle.

J’utilise la fonction replace, pour remplacer le placeholder question avec la variable question.

Enfin j’utilise une deuxième fois la fonction askGPT, pour appeler le modèle.

Si j’exécute le script, j’obtiens bien en sortie les deux résultats à mes questions.

La deuxième réponse étant écrite de façon drôle.

Prompt Template avec LangChain

Passons maintenant à l’utilisation de LangChain.

Voici un exemple qui permet d’utiliser le modèle GPT avec LangChain.

import { config } from "dotenv";
config();

import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";

const prompt = ChatPromptTemplate.fromTemplate(
  "Soit très drôle quand tu réponds à la question\n Question: {question}"
);
const model = new ChatOpenAI({ model: "gpt-3.5-turbo" });
const chain = prompt.pipe(model);

const response = await chain.invoke({
  question: "Quelle est la capitale de l'Angleterre ?",
});

console.log(response.content);
// Output :
// La capitale de l'Angleterre est Londres, bien sûr ! J'ai entendu dire qu'ils ont un maire qui ressemble étrangement à un ours en peluche...ou peut-être que c'est juste son manteau en fourrure, qui sait !

Je commence par utiliser la classe ChatOpenAI fourni par LangChain, et je précise le modèle GPT 3.5 turbo.

Ensuite, j’utilise la classe ChatPromptTemplate pour définir mon prompt.

Pour le coup, j’ai repris la même question et prompt que l’exemple précédent.

Ensuite, j’utilise la méthode pipe qui est disponible sur chaque object de type Runnable et qui va me permettre de lier le prompt avec le modèle GPT.

Enfin, j’utilise la méthode invoke pour lancer la requête, en précisant la variable question, qui sera automatiquement remplacée par LangChain.

J’obtiens en réponse un objet qui contient une propriété content qui contient le contenu de la reponse.

Vous constaterez que cet exemple est bien plus simple que le précédent.

Si j’exécute le script, j’obtiens bien en sortie mon objet qui contient la réponse dans le champ content.

Appels multiples (Batch)

Passons à un autre exemple, qui permet de lancer 2 requêtes en parallèle.

import { config } from "dotenv";
config();

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const model = new ChatOpenAI({ model: "gpt-3.5-turbo" });
const promptTemplate = PromptTemplate.fromTemplate(
  "Raconte moi une blague sur un {topic}"
);

const chain = promptTemplate.pipe(model).pipe(new StringOutputParser());

const result = await chain.batch([{ topic: "ours" }, { topic: "chat" }]);

console.log(result);
// Output :
/* [
  "Pourquoi l'ours ne porte-t-il jamais de masque ?\n" +
    '\n' +
    "Parce qu'il a déjà une fourrure protectrice !",
  "Pourquoi les chats n'aiment-ils pas jouer aux cartes dans la jungle ?\n" +
    '\n' +
    "Parce qu'il y a trop de lions en jeu !"
]*/

Dans cet exemple, j’utilise la classe PromptTemplate pour définir le prompt, puis j’utilise le parser StringOutputParser pour définir le format de la sortie, qui sera une chaîne de caractères, et non plus un ogjet contenant la propriété content.

Enfin, j’utilise la méthode batch de ma chaine et lui passe un tableau contenant le paramètre topic, pour mes 2 appels.

Les deux appels au modèle GPT seront effectués en parallèles, et j’obtiendrai un tableau qui contient les réponses en sortie.

Si j’exécute le script, j’obtiens bien en réponse un tableau avec mes 2 réponses.

Composition de Chains

Passons maintenant à la composition de Chain.

L’idée derrière la composition est d’utiliser votre première Chain et passer son résultat à une deuxième Chain.

Voici un exemple qui illustre cette fonctionnalité depuis la version v0.1 de LangChain.

import { config } from "dotenv";
config();

import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({ model: "gpt-3.5-turbo" });

const prompt1 = PromptTemplate.fromTemplate(
  `De quelle ville est originaire {person} ? Répond uniquement avec le nom de la ville.`
);

const prompt2 = PromptTemplate.fromTemplate(
  `Dans quel pays se trouve la ville {city} ? Répond en {language}.`
);

const chain = prompt1.pipe(model).pipe(new StringOutputParser());

const combinedChain = RunnableSequence.from([
  {
    city: chain,
    language: (input) => input.language,
  },
  prompt2,
  model,
  new StringOutputParser(),
]);

const result = await combinedChain.invoke({
  person: "Obama",
  language: "Anglais",
});

console.log(result);
// Output :
// Honolulu is located in the United States, specifically in the state of Hawaii.

J’utilise la classe RunnableSequence qui va me permettre d’exécuter séquentiellement plusieurs Chains.

La méthode from accepte un tableau en argument.

Le premier élément du tableau correspond aux paramètres (city, language) qui seront passés à la seconde Chain.

Il est possible d’utiliser une fonction qui reçoit les paramètres initiaux de votre première Chain, c’est ce qui a été utilisé pour le paramètre language.

Ensuite, les autres éléments du tableau correspondent à la succession de pipe, que nous avions pour la première Chain (prompt2, model, new StringOutputParser()).

Pour terminer, j’appelle la méthode invoke pour exécuter les requêtes, et je récupère en sortie le contenu de ma deuxième Chain.

Donc, concernant les prompts, mon premier demande de quelle ville est originaire une certaine personne, et en deuxième prompt je demande le pays d’origine de cette ville, et aussi une réponse en Anglais.

Si j’exécute le script, j’obtiens bien en réponse le pays d’origine de la ville d’Obama et ma réponse est en anglais.

Voici le même exemple avec la version v0.2 de LangChain, qui vient de sortir il y a quelque jours, au moment ou cet article a été écrit :

import { config } from "dotenv";
config();

import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableLambda } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({ model: "gpt-3.5-turbo" });

const prompt1 = PromptTemplate.fromTemplate(
  `De quelle ville est originaire {person} ? Répond uniquement avec le nom de la ville.`
);

const prompt2 = PromptTemplate.fromTemplate(
  `Dans quel pays se trouve la ville {city} ? Répond en {language}.`
);

const chain = prompt1.pipe(model).pipe(new StringOutputParser());

const composedChain = new RunnableLambda({

  func: async (input) => {

    const result = await chain.invoke(input); 
    return { city: result, language: input.language }; 
  }, 
}) 
  .pipe(prompt2) 
  .pipe(model) 
  .pipe(new StringOutputParser()); 

const result = await composedChain.invoke({
  person: "Obama",
  language: "Anglais",
});

console.log(result);
// Output :
// Honolulu is located in the United States, specifically in the state of Hawaii.

Avec la nouvelle version v2.0, il est désormais possible d’utiliser la classe RunnableLambda. Le constructeur accepte en paramètre un objet avec une propriété func. Cette fonction recevra en paramètre un objet qui contient les paramètres initiaux.

Si vous travaillez avec les Streams il est préférable d’utiliser la classe RunnableLambda, plutôt que la classe RunnableSequence.

Si j’exécute le script. J’obtiens bien le même résultat que dans l’exemple précédent.

Conversation avec historique

Lorsque vous utilisez les LLM sous forme d’API, par défaut chaque appel crée un nouveau contexte, et n’a pas accès par exemple à l’historique de la conversation.

LangChain propose un moyen élégant de conserver l’historique de votre conversation, entre les appels.

Voici un exemple qui simule une conversation avec le chatbot GPT.

import { config } from "dotenv";
config();

import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({ model: "gpt-3.5-turbo" });

const messageHistories = {};

const prompt = ChatPromptTemplate.fromMessages([
  ["system", `Tu es un chatbot utile`],
  ["placeholder", "{chat_history}"],
  ["human", "{input}"],
]);

const chain = prompt.pipe(model).pipe(new StringOutputParser());

const withMessageHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (sessionId) => {
    if (!messageHistories[sessionId]) {
      messageHistories[sessionId] = new InMemoryChatMessageHistory();
    }
    return messageHistories[sessionId];
  },
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

const historyConfig = {
  configurable: {
    sessionId: "mySessionKey",
  },
};

const response = await withMessageHistory.invoke(
  {
    input: "Bonjour, mon prénom est Thibaud !",
  },
  historyConfig
);

console.log(response);

const followupResponse = await withMessageHistory.invoke(
  {
    input: "Quel est mon prénom ?",
  },
  historyConfig
);

console.log(followupResponse);
// Output:
// Bonjour Thibaud ! Comment puis-je t'aider aujourd'hui ?
// Votre prénom est Thibaud.

Dans cet exemple, je commence par utiliser la méthode statique fromMessages de la classe ChatPromptTemplate pour construire un prompt.

Je précise un tableau qui contiendra 3 sous tableaux :

Par la suite, j’utilise la classe RunnableWithMessageHistory qui permet de conserver l’historique de la conversation. Le constructeur, prend en paramètre un objet, et l’entrée getMessageHistory, permet de passer une fonction qui peut être asynchrone, pour récuperer l’historique de la conversation.

En dessous, je déclare un objet historyConfig qui permet de définir la clé de la session pour laquelle j’utilise l’historique.

Enfin, il me reste à lancer l’appel, à l’aide de la méthode invoke en précisant aussi l’objet historyConfig.

Donc au niveau des prompts, j’ai indiqué dans le premier input que je m’appelais Thibaud, et j’ai demandé dans le deuxième prompt de m’indiquer mon prénom.

Donc sans l’historique, le modèle sera incapable de répondre à cette question.

Retrieval Augmented Generation (RAG)

La Retrieval Augmented Generation (RAG) est une technique d’intelligence artificielle qui combine la récupération d’informations (retrieval) et la génération de texte (generation) pour produire des réponses plus précises et informatives.

Elle permet au LLM de rechercher des documents pertinents depuis une base de données et d’utiliser ces informations pour générer des réponses contextuellement enrichies.

La librairie @langchain/community expose une base de données vectorielle HNSWLib pour la recherche de documents.

Pour pouvoir bénéficier de cette base de données, il suffit d’utiliser la commande suivante :

npm install @langchain/community

Voici un exemple qui permet de faire du RAG avec LangChain :

import { config } from "dotenv";
config();

import { HNSWLib } from "@langchain/community/vectorstores/hnswlib";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import {
  RunnablePassthrough,
  RunnableSequence,
} from "@langchain/core/runnables";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { formatDocumentsAsString } from "langchain/util/document";

const model = new ChatOpenAI({ model: "gpt-3.5-turbo" });

const vectorStore = await HNSWLib.fromTexts(
  [
    "Le restaurant 'Le Magnifique' ouvre du lundi au vendredi de 12h à 14h et de 18h à 22h.",
  ],
  [{ id: 1 }],
  new OpenAIEmbeddings()
);
const retriever = vectorStore.asRetriever(1);

const prompt =
  PromptTemplate.fromTemplate(`Répond à la question en te basant uniquement sur le contexte suivant:
{context}

Question: {question}`);

const chain = RunnableSequence.from([
  {
    context: retriever.pipe(formatDocumentsAsString),
    question: new RunnablePassthrough(),
  },
  prompt,
  model,
  new StringOutputParser(),
]);

const result = await chain.invoke(
  "Le restaurant 'Le Magnifique' est il ouvert vendredi à 15h ?"
);

console.log(result);
// Output :
// Non, le restaurant 'Le Magnifique' n'est pas ouvert vendredi à 15h car il est ouvert uniquement de 12h à 14h et de 18h à 22h du lundi au vendredi.

Dans cet exemple, je commence par utiliser la méthode statique fromTexts de la classe HNSWLib pour enrichir la base de données, avec un faux document. J’indique en l’occurence un texte qui stipule les horaires d’ouverture d’un restaurant.

J’utilise aussi en 3e paramètre la classe OpenAIEmbeddings pour utiliser l’API d’OpenAI pour vectoriser les documents.

Ensuite, j’utilise la classe RunnableSequence qui va permettre d’effectuer l’appel, en lui précisant les paramètres d’entrées context et question. Le paramètre context utilise un runnable qui va utiliser la base de données vectorielle HNSWLib.

Le paramètre question utilise la classe RunnablePassthrough qui va permettre d’envoyer la question directement sous forme de texte, depuis la méthode invoke.

Ensuite, comme pour les autres exemples, il me reste juste à lancer l’appel, en utilisant la méthode invoke de ma chaine.

Pour ma quesiton, je demande au modèle si le restaurant est ouvert le vendredi à 15h.

Bien entendu, sans le contexte du RAG, le modèle sera incapable de repondre à cette question.

Si je lance le script, le modèle me répond bien que le restaurant est fermé le vendredi à 15h.

Conclusion

En conclusion, LangChain se présente comme une solution puissante et flexible pour tirer parti des grands modèles de langage (LLM) tels que GPT-3.5, GPT-4 et Claude.

En intégrant ces modèles à une variété de sources de données externes, LangChain permet de développer des applications de traitement du langage naturel (NLP) plus efficaces et contextuellement enrichies.

Grâce à ses fonctionnalités avancées telles que la composition de chaînes, la gestion de l’historique des conversations et la génération augmentée par récupération (RAG), LangChain simplifie et optimise le développement des applications NLP. Que ce soit en JavaScript ou en Python, ce framework open source offre des outils robustes pour les développeurs souhaitant exploiter pleinement le potentiel des LLM.

Commentaires