Créer un RAG avec LangChainJS, Milvus et Strapi
Fournir des réponses précises et pertinentes aux requêtes des utilisateurs est crucial lors de la création d’applications d’IA. Bien qu’ils soient capables de conversations naturelles, les chatbots traditionnels et les modèles d’IA manquent souvent d’accès à des informations spécifiques et à jour. Leurs réponses proviennent de données d’entraînement qui peuvent être obsolètes ou trop génériques pour des besoins spécialisés.
La génération augmentée par récupération (RAG) répond à cette limitation en combinant les capacités génératives des modèles d’IA avec une base de connaissances personnalisée alimentée par une base de données vectorielle comme Milvus. Au lieu de s’appuyer uniquement sur des connaissances pré-entraînées, RAG recherche activement dans votre contenu afin de trouver des informations pertinentes avant de générer des réponses. Cette approche garantit que les réponses sont à la fois précises et adaptées au contexte.
Comprendre RAG et la recherche vectorielle
RAG comble le fossé entre les réponses d’IA génériques et les connaissances spécialisées en intégrant un mécanisme de récupération au processus de génération. Contrairement aux modèles de langage traditionnels qui s’appuient uniquement sur des connaissances pré-entraînées, RAG récupère d’abord des informations pertinentes depuis une base de connaissances personnalisée afin de compléter les réponses de l’IA.
Par exemple, lorsqu’on interroge un assistant IA sur une politique ou une procédure spécifique, il ne s’appuie pas uniquement sur sa mémoire ; il recherche plutôt dans les documents pertinents avant de fournir une réponse. Ce processus de récupération est alimenté par des modèles d’embeddings et des bases de données vectorielles.
Voyons maintenant comment fonctionne un RAG standard.
Embeddings de texte : Le texte est converti en représentations numériques, ou vecteurs, qui capturent sa signification sémantique par un modèle d’embedding. Par exemple, les phrases Comment retourner un article ? et Quelle est la procédure de remboursement ? peuvent utiliser des mots différents mais partager une signification similaire, reflétée dans leur similarité vectorielle.
Recherche vectorielle : Lorsqu’un utilisateur pose une question, la requête est également transformée en vecteur. Ce vecteur est ensuite comparé à d’autres stockés dans une base de données vectorielle, telle que Milvus, afin de trouver les entrées les plus similaires sur le plan sémantique.
Génération de réponse : Les informations récupérées, ainsi que la requête de l’utilisateur, sont transmises à un grand modèle de langage. Le modèle génère une réponse ancrée dans le contenu récupéré, garantissant que la réponse est pertinente et précise.
Cette combinaison de récupération et de génération garantit que les réponses de l’IA sont adaptées au contexte et ancrées dans les données les plus pertinentes de la base de connaissances.
Dans les sections suivantes, nous allons créer un système de FAQ alimenté par RAG capable de répondre à des questions sur Milvus à l’aide d’une base de connaissances gérée dans Strapi. Ce système intégrera Milvus pour la recherche vectorielle, LangChain.js pour la coordination du workflow, et les modèles de langage d’OpenAI afin de fournir des réponses précises et contextuelles. Le contenu sera organisé dans Strapi, un CMS headless open-source Node.js, et interrogé en temps réel pour ancrer les réponses de l’IA dans vos données personnalisées.
Si vous souhaitez y jeter un rapide coup d’œil, voici le code complet et le backend Strapi.
Configurer votre environnement de développement
Avant d’écrire du code, nous devons configurer les trois composants clés dont nous avons parlé ci-dessus : Milvus pour le stockage vectoriel, Strapi pour la gestion de contenu, et une clé API OpenAI pour générer des embeddings et des réponses. Configurons chacun d’eux.
Installer Milvus
Commençons par configurer Milvus, notre base de données vectorielle. Vous pouvez télécharger le fichier Docker Compose préconfiguré soit manuellement, soit avec wget. Dans votre terminal, exécutez :
wget https://github.com/milvus-io/milvus/releases/download/v2.0.2/milvus-standalone-docker-compose.yml -O docker-compose.yml
Une fois le fichier de configuration en place, démarrez Milvus avec Docker :
sudo docker-compose up -d
Si vous utilisez Docker Compose V2 plutôt que V1, utilisez docker compose au lieu de docker-compose. Vous pouvez vérifier votre version en exécutant docker compose version.
Installation de Strapi
Maintenant, configurons Strapi pour gérer le contenu de notre base de connaissances. Créez un nouveau projet Strapi à l’aide de la commande suivante :
npx create-strapi-app@latest my-project
Choisissez SQLite comme base de données lorsque vous y êtes invité. Après l’installation, Strapi s’ouvrira dans votre navigateur, où vous créerez votre compte administrateur.
Dans le panneau d’administration de Strapi, créez un nouveau type de collection nommé MILVUS-KNOWLEDGEBASE. Ajoutez deux champs essentiels : un champ Title utilisant le type Text et un champ Content utilisant le type Rich text.
Figure- Ajout de champs à une collection Strapi
Figure : Ajout de champs à une collection Strapi
Cette structure nous aidera à organiser efficacement les entrées de notre base de connaissances.
Une fois votre collection configurée, rendez-vous dans Content Manager pour ajouter du contenu d’exemple.
Figure- Collection Strapi remplie avec des données
Figure : Collection Strapi remplie avec des données
Assurez-vous de publier vos entrées après les avoir créées, les entrées non publiées ne seront pas accessibles à notre application.
Après avoir publié les entrées, nous devons rendre le contenu disponible pour le système RAG en configurant les autorisations. Allez dans Settings, puis Roles, et sélectionnez le rôle Public. Trouvez votre collection MILVUS-KNOWLEDGEBASE et activez les autorisations find et findOne.
Obtention de votre clé API OpenAI
Le dernier élément dont nous avons besoin est une clé API OpenAI pour générer des embeddings et des réponses. Visitez la plateforme OpenAI et inscrivez-vous ou connectez-vous. Accédez à la section des clés API et créez une nouvelle clé secrète. Copiez et enregistrez la clé dans un endroit sûr, car nous en aurons besoin plus tard et vous ne pourrez la voir qu’une seule fois.
Création de l’application React
Avec nos services principaux prêts, configurons l’application React qui hébergera notre système RAG. Nous utiliserons l’outil create-react-app pour créer un nouveau projet React et installer les dépendances nécessaires.
Tout d’abord, créez une nouvelle application React :
npx create-react-app rag-app
cd rag-app
Cette commande configure un projet React complet avec tous les outils de build et la configuration nécessaires. Ensuite, installez les packages nécessaires à notre système RAG :
npm install @langchain/community @langchain/openai @zilliz/milvus2-sdk-node axios cors express langchain react-markdown
Voici ce que chaque package fera dans notre application :
@langchain/community: Fournit l’accès aux intégrations avec divers outils et services. Nous l’utiliserons pour l’intégration du magasin vectoriel Milvus.@langchain/openai: Gère les interactions avec les modèles d’OpenAI. Nous l’utiliserons pour créer des embeddings et générer des réponses avec GPT-3.5.@zilliz/milvus2-sdk-node: Le SDK Milvus officiel pour Node.js. Il nous permet de communiquer directement avec notre instance Milvus pour les opérations vectorielles.axios: Il s’agit d’un client HTTP. Nous l’utiliserons pour récupérer du contenu depuis notre API Strapi et effectuer des requêtes entre notre frontend et notre backend.cors: Middleware pour Express qui active le partage des ressources entre origines (Cross-Origin Resource Sharing). Cela permet à notre frontend de communiquer avec notre serveur backend de manière sécurisée.express: Un framework web pour Node.js. Nous l’utiliserons pour créer notre API backend qui coordonne notre frontend, Milvus et OpenAI.langchain: La bibliothèque principale LangChain qui aide à orchestrer notre flux de travail RAG, en gérant le flux entre la récupération de contenu et la génération de réponses.react-markdown: Affiche du contenu Markdown dans nos composants React. Nous l’utiliserons pour afficher les réponses formatées de notre assistant IA.
Création du serveur backend
Maintenant que nous avons terminé la configuration de notre environnement de développement, commençons à créer le serveur qui alimentera notre système RAG. Le serveur gérera plusieurs tâches clés : récupérer du contenu depuis Strapi, convertir du texte en embeddings vectoriels, les stocker dans Milvus et coordonner le flux de travail RAG pour générer des réponses.
Créez un fichier nommé server.mjs et un autre nommé .env à la racine de votre projet. Dans le fichier .env, collez votre clé OpenAI comme suit :
OPENAI_API_KEY= your-api-key
Il s’agit du fichier que nous utiliserons pour stocker nos informations sensibles. Ouvrez maintenant le fichier serveur, et commençons à coder.
1. Configuration du serveur et de l’environnement
Commencez par importer les bibliothèques nécessaires et configurer les variables d’environnement afin de garantir que le système fonctionne en toute sécurité.
import express from "express";
import axios from "axios";
import dotenv from "dotenv";
import cors from "cors";
import { ChatOpenAI } from "@langchain/openai";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Milvus } from "@langchain/community/vectorstores/milvus";
import { Document } from "langchain/document";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
dotenv.config();
L’importation des bibliothèques que nous avons installées dans notre environnement vers notre code garantit que nous pouvons appeler et utiliser leurs fonctions et méthodes dans nos codes. La fonction dotenv.config() charge notre clé API, garantissant qu’elle est accessible dans toute l’application sans l’exposer directement dans le code.
2. Configuration de l’application
Ensuite, centralisez les paramètres de votre application à l’aide d’un objet de configuration. Cette étape permet de conserver tous les paramètres clés au même endroit, ce qui les rend plus faciles à gérer et à ajuster selon les besoins.
const CONFIG = {
PORT: parseInt(process.env.PORT || "30080", 10),
STRAPI: {
URL: "http://localhost:1337/api/milvus-knowledgebases",
TIMEOUT: 5000
},
MILVUS: {
URL: "localhost:19530",
COLLECTION: "rag_collection",
PRIMARY_FIELD: "pk",
VECTOR_FIELD: "vector",
TEXT_FIELD: "text",
TEXT_MAX_LENGTH: 4096,
SEARCH_PARAMS: {
nprobe: 16,
offset: 0
},
BATCH_SIZE: 100
},
CHUNKING: {
SIZE: 2000,
OVERLAP: 200
},
TOP_K: 3
};
Cet objet de configuration définit les paramètres critiques de notre application. Le paramètre PORT détermine le port sur lequel le serveur écoute. La section STRAPI contient l’URL du système de gestion de contenu et une valeur de délai d’attente pour éviter les longues attentes lors de la récupération des données.
La section MILVUS configure la base de données vectorielle, en spécifiant des détails tels que le nom de la collection, les limites de longueur de texte et les paramètres de recherche. nprobe contrôle la précision des recherches vectorielles en ajustant le nombre de clusters que Milvus examine. Enfin, la section CHUNKING décrit comment les documents sont divisés en parties plus petites, notamment la taille des segments et le chevauchement, garantissant qu’aucun contexte n’est perdu pendant le traitement.
3. Initialisation du serveur et des services IA
Une fois la configuration en place, l’étape suivante consiste à initialiser le serveur Express et à configurer les services d’IA. Ceux-ci incluent le grand modèle de langage pour générer des réponses et le service d’embedding pour convertir le texte en vecteurs.
const app = express();
app.use(express.json({ limit: '1mb' }));
app.use(cors());
const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0.7,
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
timeout: 30000,
});
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: CONFIG.CHUNKING.SIZE,
chunkOverlap: CONFIG.CHUNKING.OVERLAP,
separators: ["\n\n", "\n", " ", ""]
});
L’objet app est créé à l’aide d’Express, avec un middleware ajouté pour analyser les requêtes JSON entrantes et gérer les requêtes interorigines à l’aide de CORS. Cela garantit que le serveur peut accepter des charges utiles provenant de différents domaines, ce qui est essentiel pour la communication avec le frontend.
Ensuite, nous créons une instance ChatOpenAI que nous utiliserons pour connecter le système au modèle GPT-3.5 d’OpenAI afin de générer des réponses. Le paramètre temperature contrôle la créativité des réponses, les valeurs plus faibles produisant des sorties plus prévisibles et les valeurs plus élevées générant des réponses plus variées et créatives.. L’instance OpenAIEmbeddings convertira le texte en vecteurs, permettant les recherches sémantiques. Enfin, l’instance RecursiveCharacterTextSplitter divisera le texte en fragments en fonction de la taille et du chevauchement définis dans la configuration.
4. Préparer les documents pour le traitement
Avant de pouvoir stocker ou rechercher des documents à l’aide de Milvus, nous devons nettoyer et structurer le contenu brut dans un format cohérent. Cela garantit que les données sont prêtes pour l’embedding et une récupération efficace plus tard dans le workflow.
let milvusStore = null;
let lastDataHash = null;
const processDocument = (content) => {
if (!content?.Content || !content.Title) return null;
const processedContent = content.Content
.map(section => {
if (section.type === 'paragraph' && section.children) {
return section.children
.map(child => child.text)
.join(' ')
.trim();
}
return null;
})
.filter(Boolean)
.join('\n\n');
return processedContent ? new Document({
pageContent: processedContent,
metadata: {
source: 'milvus_content',
id: content.id,
title: content.Title,
documentId: content.documentId
}
}) : null;
};
Dans le code ci-dessus, les variables let milvusStore et let lastDataHash agissent comme des références globales. milvusStore contient la connexion initialisée à la collection Milvus, et lastDataHash suit si les données documentaires sous-jacentes ont changé depuis la dernière mise à jour.
La fonction processDocument nous aidera à prendre les données brutes de Strapi et à les formater dans un objet Document structuré. Tout d’abord, elle vérifie si les champs de contenu et de titre sont présents. Si elles sont valides, elle parcourt les sections du contenu, extrait le texte des paragraphes et le nettoie en supprimant les espaces inutiles. Ces sections sont réunies en un bloc de texte cohérent. La fonction renvoie ensuite le texte traité enveloppé dans un objet Document, incluant des métadonnées telles que le titre du document, l’ID et la source. Ce format garantit la compatibilité avec Milvus et LangChain pour un traitement ultérieur.
5. Se connecter à Milvus ou l’initialiser
Maintenant que les documents sont prêts, l’étape suivante consiste à les intégrer à Milvus. Nous établirons une connexion à une collection Milvus existante, ou si elle n’existe pas, nous en créerons une et la remplirons avec les documents préparés.
async function getMilvusStore() {
if (milvusStore) {
return milvusStore;
}
try {
// First try to connect to existing collection
try {
console.log('Attempting to connect to existing collection');
milvusStore = await Milvus.fromExistingCollection(
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
return milvusStore;
} catch (error) {
console.log('Collection does not exist, will create new one');
// If collection doesn't exist, fetch data and create it
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Split documents
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
console.log(`Creating new collection with ${allSplitDocs.length} documents`);
milvusStore = await Milvus.fromDocuments(
allSplitDocs,
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
lastDataHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
return milvusStore;
}
} catch (error) {
console.error('Failed to initialize Milvus:', error);
throw error;
}
}`
Cette fonction gère la connexion à la collection Milvus. Si la variable milvusStore contient déjà une connexion active, la fonction la renvoie immédiatement afin d’éviter les opérations inutiles. Sinon, elle tente de se connecter à une collection existante dans Milvus à l’aide de la méthode fromExistingCollection. Si la collection n’existe pas, la fonction procède à la création d’une nouvelle collection. Tout d’abord, elle récupère les données depuis Strapi, traite les documents à l’aide de la fonction processDocument et garantit l’unicité de chaque document en utilisant le documentId comme clé dans une Map. Ensuite, les documents sont divisés en fragments plus petits à l’aide du splitter, ce qui garantit que les documents ne sont pas trop volumineux pour l’embedding ou l’indexation dans Milvus. Les fragments traités sont ensuite stockés dans la collection Milvus à l’aide de la méthode fromDocuments. Le lastDataHash est mis à jour pour refléter l’état actuel des données Strapi, ce qui permet de suivre les changements.
6. Maintenir Milvus à jour
Milvus doit rester synchronisé avec Strapi afin de garantir que la base de données vectorielle contient les données les plus récentes. Mettons en place un processus qui détecte les changements dans le contenu de Strapi, identifie les documents mis à jour ou nouveaux, et met à jour la collection Milvus en conséquence.
// Update Milvus data if needed
async function updateMilvusData() {
try {
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const currentHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
if (currentHash === lastDataHash) {
console.log('Data unchanged, skipping update');
return false;
}
console.log('Content changed, updating Milvus collection');
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Split documents
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
// Delete all existing documents before adding new ones
await milvusStore.delete({});
// Add new documents in batches
for (let i = 0; i < allSplitDocs.length; i += CONFIG.MILVUS.BATCH_SIZE) {
const batch = allSplitDocs.slice(i, i + CONFIG.MILVUS.BATCH_SIZE);
await milvusStore.addDocuments(batch);
console.log(`Added batch ${Math.floor(i / CONFIG.MILVUS.BATCH_SIZE) + 1} of ${Math.ceil(allSplitDocs.length / CONFIG.MILVUS.BATCH_SIZE)}`);
}
lastDataHash = currentHash;
console.log(`Updated Milvus with ${allSplitDocs.length} documents`);
return true;
} catch (error) {
console.error("Error updating Milvus data:", error);
throw error;
}
}
Cette fonction commence par récupérer les dernières données depuis Strapi et par calculer leur hash. Si le hash correspond à lastDataHash, les données n’ont pas changé, et aucune mise à jour n’est nécessaire. Sinon, la fonction traite les documents mis à jour et vide la collection existante dans Milvus à l’aide de la méthode delete. Les nouveaux fragments de documents sont ensuite ajoutés par lots, garantissant un stockage efficace tout en conservant des logs pour le suivi de la progression. Enfin, elle met à jour lastDataHash, afin de garantir que les vérifications futures détectent précisément les changements dans les données.
7. Gestion des requêtes utilisateur
Après avoir alimenté Milvus avec nos données, l’étape suivante consiste à gérer les requêtes utilisateur. Le système récupérera les documents les plus pertinents depuis Milvus en fonction de leur similarité sémantique avec la requête de l’utilisateur. Ces documents seront ensuite utilisés pour générer des réponses contextuelles à l’aide du grand modèle de langage GPT-3.5 d’OpenAI.
async function handleQuery(chatHistory, input) {
const store = await getMilvusStore();
try {
await updateMilvusData();
} catch (error) {
console.warn("Failed to check for updates:", error);
}
const results = await store.similaritySearchWithScore(
input,
CONFIG.TOP_K
);
console.log('\n=== Retrieved Documents from Milvus ===');
results.forEach(([doc, score], index) => {
console.log(`\nDocument ${index + 1} (score: ${score}):`);
console.log('Title:', doc.metadata.title);
console.log('Content:', doc.pageContent.substring(0, 150) + '...');
});
const chain = await createStuffDocumentsChain({
llm: model,
prompt: ChatPromptTemplate.fromMessages([
[
"system",
`You are an AI assistant specializing in Milvus and Zilliz, vector database technologies. Your goal is to provide accurate and helpful answers based on the provided context.
**Guidelines:**
1. **Milvus/Zilliz-Related Queries:**
- If the query is about Milvus or Zilliz and the context includes relevant information, provide a detailed and structured response.
- Use proper Markdown formatting:
- \`\`\` for code blocks
- **Bold** for emphasis
- Bullet points for lists
- ### Headings for sections
- If no relevant information exists in the context, respond with: *"I don't have that information yet."*
2. **Unrelated Queries:**
- If the query is not about Milvus or Zilliz, respond with:
*"This topic is outside my expertise. I specialize in Milvus and Zilliz. Please ask questions related to these technologies."*
**Context:**
{context}
**User Query:**
{input}`
],
...chatHistory,
["user", "{input}"]
]),
documentPrompt: ChatPromptTemplate.fromTemplate("Content: {page_content}\n\n")
});
const response = await chain.invoke({
input,
context: results.map(([doc]) => doc)
});
return {
answer: response,
context: results.map(([doc]) => ({
content: doc.pageContent,
title: doc.metadata.title,
id: doc.metadata.id,
documentId: doc.metadata.documentId
}))
};
}
Cette fonction commence par s’assurer que le magasin vectoriel Milvus est à jour. La fonction updateMilvusData est appelée dans un bloc try-catch afin de gérer les erreurs potentielles sans interrompre le processus de requête. La requête est ensuite transmise à la méthode similaritySearchWithScore de Milvus, qui récupère les k documents les plus similaires dans la base de données. Les documents récupérés sont journalisés. Chaque document inclut des métadonnées telles que le titre et l’ID, ainsi qu’un aperçu tronqué de son contenu. Cela nous aidera à savoir si notre pipeline RAG récupère les bons documents.
Ensuite, les documents sont transmis à une createStuffDocumentsChain de LangChain à l’aide d’un prompt qui indique au modèle de répondre uniquement sur la base du contexte fourni. Cela garantit que GPT-3.5 d’OpenAI génère une réponse ancrée dans le contenu récupéré, sans s’appuyer sur des connaissances externes, ce qui maintient l’exactitude et la pertinence. Enfin, la fonction renvoie à la fois la réponse générée et le contexte des documents récupérés.
8. Configuration des points de terminaison API
Maintenant que le backend peut traiter les requêtes des utilisateurs et interagir avec Milvus, l’étape suivante consiste à exposer cette fonctionnalité via des points de terminaison API. Ces points de terminaison servent de passerelle entre notre backend et le frontend (ou d’autres applications clientes).
app.post("/chat", async (req, res) => {
try {
const { chatHistory, input } = req.body;
if (!input?.trim()) {
return res.status(400).json({
error: "Invalid input",
message: "No input provided"
});
}
const formattedHistory = Array.isArray(chatHistory)
? chatHistory.map(msg =>
msg.role === "user"
? new HumanMessage(msg.content)
: new AIMessage(msg.content)
)
: [];
const response = await handleQuery(formattedHistory, input);
res.json(response);
} catch (error) {
console.error("Chat request error:", error);
res.status(500).json({
error: "Internal server error",
message: process.env.NODE_ENV === 'production'
? "An unexpected error occurred"
: error.message
});
}
});
app.get("/health", async (req, res) => {
try {
const store = await getMilvusStore();
res.json({
status: "ok",
timestamp: new Date().toISOString(),
milvusInitialized: !!store,
config: CONFIG
});
} catch (error) {
res.status(500).json({
status: "error",
error: error.message
});
}
});
Le point de terminaison /chat traite les requêtes POST entrantes qui contiennent la requête d’un utilisateur et l’historique de chat. L’historique de chat est formaté en objets HumanMessage et AIMessage afin de maintenir le contexte. Si la requête est invalide (par exemple, vide), le point de terminaison répond avec un statut 400 Bad Request et un message d’erreur approprié. Si la requête est valide, la fonction handleQuery est appelée pour traiter l’entrée, récupérer les documents pertinents depuis Milvus et générer une réponse. Le résultat est renvoyé sous forme d’objet JSON. Si une erreur se produit, elle est consignée, et le serveur répond avec une erreur 500 Internal Server Error.
En revanche, le point de terminaison /health fournit un moyen de surveiller l’état du serveur. Il vérifie si le store Milvus est initialisé et répond avec un statut ok ou error, ainsi qu’avec la configuration du serveur et l’horodatage actuel.
9. Démarrage du serveur
La dernière étape consiste à démarrer le serveur et à le rendre disponible pour traiter les requêtes.
app.listen(CONFIG.PORT, () => {
console.log(`Server running on http://localhost:${CONFIG.PORT}`);
console.log('Configuration:', CONFIG);
});
La méthode app.listen démarre le serveur sur le port spécifié dans la configuration, en l’occurrence le port 30080. Une fois le serveur en cours d’exécution, elle consigne un message indiquant l’URL à laquelle il est accessible, ainsi que la configuration actuelle. Cela confirme que le backend est prêt à traiter les requêtes.
Figure- Serveur backend du système RAG en cours d’exécution tout en consignant les configurations actuelles
Figure : Serveur backend du système RAG en cours d’exécution tout en consignant les configurations actuelles
Pour exécuter le serveur, allez dans le terminal et exécutez cette commande.
node .\server.mjs
Nous avons maintenant terminé la création du backend de notre système RAG.
Création du frontend de votre système RAG
Le backend du système RAG étant entièrement fonctionnel, l’étape suivante consiste à créer un frontend pour permettre aux utilisateurs d’interagir avec le système. Le frontend sera une interface de chatbot qui se connecte au backend, permettant aux utilisateurs de saisir des requêtes et de recevoir des réponses générées par l’IA.
Création de l’interface utilisateur
Commencez par créer un fichier nommé ChatbotUI.js dans le répertoire src. Ce fichier définira le composant de chatbot et gérera le flux des requêtes utilisateur et des réponses du backend.
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import axios from 'axios';
import './ChatbotUI.css';
const ChatbotUI = () => {
const [chatHistory, setChatHistory] = useState([]);
const [userInput, setUserInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const chatContainerRef = useRef(null);
useEffect(() => {
// Scroll to the latest message when chat history updates
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}, [chatHistory]);
const handleUserInput = (e) => setUserInput(e.target.value);
const handleSendMessage = async () => {
if (userInput.trim() !== '') {
const newMessage = { role: 'user', content: userInput };
const updatedChatHistory = [...chatHistory, newMessage];
setChatHistory(updatedChatHistory);
setUserInput('');
setIsLoading(true);
try {
const response = await axios.post('http://localhost:30080/chat', {
chatHistory: updatedChatHistory,
input: userInput,
});
const botMessage = {
role: 'assistant',
content: response.data.answer || 'Sorry, I didn’t quite get that.',
};
setTimeout(() => {
setChatHistory((prevMessages) => [...prevMessages, botMessage]);
}, 1000); // Simulate typing delay
} catch (err) {
console.error('Error:', err);
setError('Unable to connect to the server.');
} finally {
setIsLoading(false);
}
}
};
return (
<div className="chatbot-ui">
{/* Header */}
<div className="chat-header">
<img src="/milvus_logo.png" alt="Assistant" className="assistant-logo" />
<h2>Chat Assistant</h2>
</div>
{/* Chat Messages */}
<div className="chat-body" ref={chatContainerRef}>
{chatHistory.map((message, index) => (
<div
key={index}
className={`message ${message.role === 'user' ? 'user-message' : 'bot-message'}`}
>
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
))}
{isLoading && (
<div className="bot-message typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
{error && <div className="error-message">{error}</div>}
</div>
{/* Footer */}
<div className="chat-footer">
<input
type="text"
placeholder="Ask me anything..."
value={userInput}
onChange={handleUserInput}
onKeyPress={(e) => {
if (e.key === 'Enter') handleSendMessage();
}}
disabled={isLoading}
/>
<button onClick={handleSendMessage} disabled={isLoading}>
<span className="send-icon">✈</span>
</button>
</div>
</div>
);
};
export default ChatbotUI;
Ce composant utilise le useState de React pour gérer des états tels que l’historique de discussion et la saisie utilisateur. Le hook useEffect garantit que le conteneur de discussion défile automatiquement jusqu’au dernier message. Lorsque l’utilisateur soumet une requête, celle-ci est envoyée au point de terminaison de l’API backend http://localhost:30080/chat via une requête POST axios. La réponse du backend est ensuite affichée dans l’interface de discussion.
Styliser le chatbot
Ensuite, créez un fichier nommé ChatbotUI.css dans le répertoire src. Ce fichier stylisera l’interface du chatbot.
/* Full-Screen Gradient Background */
.chatbot-ui {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #8358ff, #00a6ff); /* Milvus color palette */
font-family: 'Roboto', sans-serif;
color: #fff;
}
/* Chat Header */
.chat-header {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #1e1e2d, #28293e);
width: 100%;
padding: 15px;
border-radius: 15px 15px 0 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.assistant-logo {
width: 50px;
height: 50px;
margin-right: 15px;
border-radius: 50%;
background: radial-gradient(circle, #8358ff, #00a6ff); /* Dynamic logo glow */
animation: pulse 2s infinite;
}
.chat-header h2 {
font-size: 1.8rem;
font-weight: bold;
color: #ffffff;
}
.chat-header p {
font-size: 0.9rem;
color: #a0a0b1;
margin-top: 5px;
}
/* Chat Body */
.chat-body {
flex: 1;
width: 100%;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
background: #1e1e2d;
color: #ffffff;
border-radius: 0 0 15px 15px;
}
.message {
max-width: 80%;
padding: 12px 15px;
border-radius: 10px;
font-size: 1rem;
line-height: 1.5;
animation: fadeIn 0.3s ease-in-out;
}
.user-message {
align-self: flex-end;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: #ffffff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.bot-message {
align-self: flex-start;
background: #29293f;
color: #d4d4e5;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
/* Typing Indicator */
.typing-indicator {
display: flex;
justify-content: flex-start;
gap: 5px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: #00a6ff;
border-radius: 50%;
animation: blink 1.2s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
/* Footer */
.chat-footer {
display: flex;
align-items: center;
width: 100%;
padding: 10px 15px;
background: #1e1e2d;
border-top: 1px solid #28293e;
border-radius: 0 0 15px 15px;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.2);
}
.chat-footer input {
flex: 1;
padding: 12px 15px;
border-radius: 30px;
border: 1px solid #8358ff;
font-size: 1rem;
outline: none;
background: #29293f;
color: #ffffff;
transition: border-color 0.3s ease;
}
.chat-footer input:focus {
border-color: #00a6ff;
}
.chat-footer button {
margin-left: 10px;
padding: 12px 20px;
border: none;
border-radius: 30px;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: white;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.chat-footer button:hover {
transform: scale(1.1);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
}
.chat-footer button:disabled {
background: #29293f;
color: #a0a0b1;
cursor: not-allowed;
}
@keyframes blink {
0%, 80%, 100% {
opacity: 0;
}
40% {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 10px rgba(131, 88, 255, 0.5);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(0, 166, 255, 0.7);
}
}
Les styles CSS créent une interface de chatbot inspirée de la palette de couleurs de Milvus. L’arrière-plan utilise un dégradé avec des nuances de violet profond et de cyan, reflétant l’identité visuelle de Milvus. L’en-tête du chat suit un style de dégradé sombre, tandis que les messages utilisateur sont mis en évidence avec un dégradé lumineux correspondant aux couleurs principales de Milvus. Les messages du bot sont stylisés avec un thème plus sombre et atténué. Cette conception exploite les couleurs de Milvus pour une interface utilisateur unifiée et fonctionnelle.
Figure- interface utilisateur terminée du système RAG
Figure : interface utilisateur terminée du système RAG
Voilà à quoi ressemble l’interface utilisateur finale.
Intégration du chatbot dans l’application
Enfin, mettez à jour le fichier App.js dans le répertoire src afin d’afficher le composant ChatbotUI.
import React from 'react';
import ChatbotUI from './ChatbotUI';
const App = () => {
return (
<div>
<ChatbotUI />
</div>
);
};
export default App;
Cela garantit que le chatbot est affiché comme composant principal au démarrage de l’application. Le composant App sert de point d’entrée à l’application React.
Test de votre système RAG
Après avoir terminé la configuration du backend et du frontend, il est temps de tester l’ensemble du pipeline RAG pour vous assurer qu’il fonctionne comme prévu. Voici comment procéder :
Démarrage du backend
Ouvrez un terminal, accédez au répertoire du projet et démarrez le serveur backend :
node server.mjs
Assurez-vous que le backend fonctionne sur http://localhost:30080. Consultez les journaux pour vérifier que Milvus a été initialisé et est prêt pour les requêtes.
Exécution du frontend
Ouvrez un terminal séparé, accédez au répertoire rag-app et démarrez le frontend React :
npm start
Cela lancera l’application à l’adresse http://localhost:3000 dans votre navigateur par défaut.
Test de l’interface du chatbot
Sur l’interface du chatbot, saisissez une question liée à Milvus ou à Zilliz. Essayez également de saisir des questions en dehors de ces sujets afin de voir si le système répondra avec le message approprié. Enfin, essayez de saisir des questions qui pourraient être en dehors de la base de connaissances Strapi. Voici des exemples de résultats :
Figure- Résultat du système RAG pour une requête liée à Milvus
Figure : Résultat du système RAG pour une requête liée à Milvus
Figure- Résultats des tests du système RAG
Figure : Résultats des tests du système RAG
Les captures d’écran ci-dessus montrent que notre système RAG fonctionne comme prévu et affiche les bons résultats. Il est également ancré dans l’utilisation de notre base de connaissances, c’est pourquoi il ne connaît pas les fondateurs de Milvus, car cette information n’existe pas dans Strapi.
Allez-y et ajoutez davantage de contenu à la base de connaissances, puis testez le système.
Conclusion
L’intégration de LangChain, Milvus et Strapi pour créer un système de génération augmentée par récupération (RAG) montre comment l’IA peut fournir des réponses précises et spécifiques à un domaine, fondées sur des connaissances réelles et à jour. Cette architecture est idéale pour des applications telles que le support client, la gestion des connaissances et les outils éducatifs. Avec une compréhension claire de l’architecture et ce guide étape par étape, vous pouvez désormais créer des systèmes RAG adaptés à vos besoins spécifiques.
Ressources associées
Continuer à lire

Zilliz Cloud On-Demand Compute: Pay Only for What You Use
The customer case behind Zilliz Cloud On-Demand: how a $10K vector search bill came down to under $500, and the engineering changes that made it possible.

Zilliz Cloud Audit Logs Goes GA: Security, Compliance, and Transparency at Scale
Zilliz Cloud Audit Logs are now GA, giving enterprises real-time visibility, compliance-ready trails, and stronger security across AWS, GCP, and Azure.
Milvus/Zilliz + Surveillance: How Vector Databases Transform Multi-Camera Tracking
See how Milvus vector database enhances multi-camera tracking with similarity-based matching for better surveillance in retail, warehouses and transport hubs.


