Technologie

Dev: le chatbot RAG conçu avec Firebase Genkit sur les données Firestore

Armel Yara
Dev: le chatbot RAG conçu avec Firebase Genkit sur les données Firestore

Pourquoi avons-nous implémenté le chatbot assistant “Dev” qui accueille et guide les développeurs sur PublieDev ? C’est ce que nous allons voir dans cet article. 

Dev est un système de Questions/Réponses basé sur les ressources de PublieDev. 

La Génération Augmentée par Récupération (RAG) est le moyen le plus pratique de faire en sorte qu'un grand modèle de langage réponde à des questions sur vos données spécifiques, plutôt que sur ses connaissances générales d'entraînement. C'est ce qui alimente les assistants d'IA d'entreprise, les robots de documentation technique et les systèmes de questions/réponses sur des bases de connaissances privées.

Pour PublieDev, la revue technique des développeurs à comité de lecture, nous avions déjà les deux ingrédients les plus difficiles : une base de données Firestore de publications approuvées, et des vecteurs d'intégration de 384 dimensions stockés sur chaque document. 

Cet article explique comment nous avons assemblé ces ingrédients dans Dev, notre chatbot qui répond à des questions comme "Quelles sont les meilleures apps FinTech ?" avec des réponses fondées sur de vraies publications approuvées de notre plateforme.

QU'EST-CE QUE LE RAG ?

RAG signifie Retrieval-Augmented Generation. C'est une technique au moment de l'inférence qui :

  1. RÉCUPÈRE les documents pertinents d'un corpus à l'aide d'une recherche par similarité

  2. AUGMENTE l'invite du LLM (Large Language Model) avec ces documents comme contexte

  3. GÉNÈRE une réponse conditionnée par ce contexte récupéré

La propriété clé : le LLM est contraint aux informations contenues dans les documents récupérés, et non à sa mémoire paramétrique. Cela réduit donc l'hallucination et rend les réponses auditables ("de quelles publications provient cette réponse ?"). Un RAG n'est pas du Fine-tuning (nous ne mettons pas à jour les poids du modèle), des Embeddings (les embeddings sont utilisés pour la récupération, ils ne sont pas RAG eux-mêmes) ou encore un Vector databases (un Vector DB est une implémentation du composant de récupération)


ANTÉCÉDENTS : POURQUOI NOUS AVONS DÉJÀ CE DONT NOUS AVIONS BESOIN?

Lors d'un sprint de codage précédent, nous avons implémenté la recherche sémantique hybride en utilisant :

  • Des vecteurs creux TF-IDF pour la correspondance lexicale

  • Transformers.js (all-MiniLM-L6-v2, 384 dimensions) pour les embeddings sémantiques

  • La similarité cosinus pour le classement

Chaque publication approuvée dans Firestore a déjà un champ embeding avec un Float32Array de 384 valeurs représentant sa signification sémantique. Cela a été construit pour alimenter la recherche sémantique.

Pour RAG, l'étape de récupération est identique à la recherche sémantique : intégrer la question de l'utilisateur, calculer la similarité cosinus par rapport à tous les embeddings de publication, retourner les top-k les plus pertinents.

Nous avons construit Dev, entièrement sur cette infrastructure existante avec zéro nouvel index Firestore et zéro changement de schéma.

Architecture du flux de Dev

Passons maintenant à l’implémentaion

Étape 1 — Embedding de la Question

Nous réutilisons le service d'intégration côté serveur existant sans modification :

  const embeddingService = require('../services/embeddings');

  const questionEmbedding = await embeddingService.generateEmbedding(question);

Un point à souligner ici : l'intégration de la question doit être générée avec le même modèle qui a été utilisé pour intégrer les publications. Différents modèles produisent des espaces vectoriels incomparables. Étant donné que les publications et les questions passent par all-MiniLM-L6-v2, la comparaison de similarité est valide.

Exigence de mémoire : le chargement du modèle MiniLM nécessite environ 500 Mo de RAM. C'est pourquoi nous avons défini memory: "1GiB" sur la fonction Cloud — identique à la fonction searchPublications.

Étape 2 — Retrieval (Cosine Similarity Ranking)

Nous récupérons toutes les publications approuvées de Firestore et les classons côté client :

  const snapshot = await db.collection('publications')

    .where('status', '==', 'approved')

    .get();

  const ranked = snapshot.docs

    .filter(doc => doc.data().embedding && Array.isArray(doc.data().embedding))

    .map(doc => {

      const data = doc.data();

      const sim = embeddingService.calculateSimilarity(questionEmbedding, data.embedding);

      return { id: doc.id, title: data.title, type: data.type, sim, ...};

    })

    .sort((a, b) => b.sim - a.sim)

    .slice(0, 6);

Pourquoi k=6 ? Empiriquement, Gemini 2.0 Flash fonctionne mieux avec 4 à 8 documents de contexte pour les Q&R. Moins de 4 risque de manquer des publications pertinentes ; plus de 8 dilue le contexte et augmente le coût en jetons sans gain de qualité.

Pourquoi récupérer toutes les publications ? À notre échelle (<1000 publications approuvées), une analyse complète est rapide (200 à 400 ms) et évite la complexité de l'indexation par plus proche voisin approximatif (ANN). 

cosine_similarity(A, B) = (A · B) / (‖A‖ × ‖B‖)

La fonction  calculateSimilarity  dans notre embeddingService:

  calculateSimilarity(a, b) {

    let dot = 0, normA = 0, normB = 0;

    for (let i = 0; i < a.length; i++) {

      dot   += a[i] * b[i];

      normA += a[i] * a[i];

      normB += b[i] * b[i];

    }

    return dot / (Math.sqrt(normA) * Math.sqrt(normB));

  }

Étape 3 — Prompt Engineering pour RAG

Ceci est l'étape la plus importante. Après avoir récupéré les publications principales à l'Étape 2, nous assemblons tout en un seul message structuré pour Gemini.

Pour éviter la confusion entre la question et le contexte, considérez ceci comme un modèle en trois parties :

  1. LES INSTRUCTIONS (Les Règles)

  2. LE CONTEXTE (Les données trouvées spécifiquement pour la question)

  3. LA QUESTION (L'invite finale à laquelle l'IA doit répondre)

Voici à quoi ressemble l'Invite Augmentée finale une fois assemblée :

INSTRUCTIONS

Tu es PublieDev Assistant, un expert des innovations technologiques. Réponds à la question suivante en utilisant UNIQUEMENT les publications fournies. Indique les sources entre crochets, ex : [1].

CONTEXTE

Voici les publications trouvées dans notre base pour répondre à votre demande :

[1] "TraficDay" (Type: app-mobile, Catégorie: logistique)

Description : Application de détection d'obstacles...

[2] "Dr Green" (Type: app-mobile, Catégorie: santé-tech)

Description : Application mobile de santé...

QUESTION

Question de l'utilisateur : Quelles sont les meilleures applications Flutter ?

Contraintes clés de l'invite et pourquoi elles sont importantes :

"UNIQUEMENT en te basant sur les publications fournies"

  • Sans cela, Gemini répond à partir de sa mémoire générale (risque d'hallucination).

  • Cela garantit que la réponse porte strictement sur le contenu de PublieDev.

Exigence de citation "[1]"

  • Force une réponse traçable. Les utilisateurs peuvent vérifier exactement quel projet est mentionné.

  • Rend le modèle crédible et professionnel.

"Même langue que la question"

Notre plateforme est multilingue. Cette seule instruction permet au bot de basculer facilement entre le français et l'anglais en fonction de l'entrée de l'utilisateur.

Étape 4 — Flux Genkit Flow 

  const askPubliDevFlow = ai.defineFlow(

    {name: 'askPublieDev'},

    async ({question}) => {

      const db = admin.firestore();

      const questionEmbedding = await embeddingService.generateEmbedding(question);

      const snapshot = await db.collection('publications')

        .where('status', '==', 'approved').get();

      const ranked = snapshot.docs

        .filter(doc => doc.data().embedding)

        .map(doc => ({...doc.data(), sim: embeddingService.calculateSimilarity(

          questionEmbedding, doc.data().embedding

        )}))

        .sort((a, b) => b.sim - a.sim)

        .slice(0, 6);

      const context = ranked.map((pub, i) =>

        [${i+1}] "${pub.title}" (Type: ${pub.type}, Tags: ${pub.tags?.join(', ')})\n

        +     Description: ${pub.description}

      ).join('\n\n');

      const {text} = await ai.generate({

        model: gemini20Flash,

        prompt: ${systemPrompt}\n\n${context}\n\nQuestion: ${question},

        config: {temperature: 0.3, maxOutputTokens: 300}

      });

      return {

        answer: text.trim(),

        sources: ranked.map(({title, type, category, pdid}) => ({title, type, category, pdid}))

      };

    }

  );

Étape 5 — HTTP Endpoint

  exports.askPublieDev = onRequest(

    {cors: true, memory: '1GiB', timeoutSeconds: 60},

    async (req, res) => {

      const {askPubliDevFlow} = require('./flows/askPublieDev');

      const result = await askPubliDevFlow({question: req.body.question.trim()});

      res.json({success: true, answer: result.answer, sources: result.sources});

    }

  );

Étape 6 — Frontend Chat Widget 

Un IIFE (Immediately Invoked Function Expression) autonome injecté dans n'importe quelle page via une seule balise de script. Il crée :

  • Un bouton flottant de Dev

    Screenshot 2026-05-04 at 4.49.27 AM.png

    en bas, à droite de l’écran

  • Un panneau de discussion animé avec des indicateurs de saisie

  • Des cartes sources indiquant les publications auxquelles chaque réponse fait référence

  • Des puces de suggestion pour les questions courantes

Le widget lit l'URL du point de terminaison à partir de FIREBASE_CONFIG (centralisée dans config.js), avec une solution de secours codée en dur pour la résilience.


QUEL MODÈLE GÈRE L'« EMBEDDING » VS « GÉNÉRATION » ?

Ceci est une source fréquente de confusion dans les systèmes RAG. Nous utilisons deux (2) modèles différents :

Modèle

Rôle

Taille

Où exécuter ?

all-MiniLM-L6-v2

Embedding

~90MB

Node.js (Transformers.js)

Gemini 2.0 Flash

Generation

N/A

Google API (via Genkit)

MiniLM convertit text → 384-float vector (signification sémantique, pas de texte généré) 

Gemini prend l'invite grounding → réponse en langage humain.

Ces modèles servent à des fonctions différentes et ne peuvent pas être intervertis.

Pourquoi utiliser Transformers.js (et non TensorFlow.js) pour les embeddings?

  • Transformers.js est la bibliothèque Hugging Face pour exécuter des modèles transformer en JS/Node.

  • TensorFlow.js est un framework d'apprentissage automatique (ML) général.

  • all-MiniLM-L6-v2 est un modèle PyTorch/Hugging Face — Transformers.js est son environnement d'exécution natif.

  • Utiliser TF.js nécessiterait de convertir le modèle au format TensorFlow SavedModel, ajoutant une complexité inutile.

Ventilation de la latence 

Étapes

Temps

Question embedding (MiniLM)

~80ms

Firestore full scan

~150ms

Cosine ranking (in-memory)

~5ms

Appel Gemini 2.0 Flash API

~600ms

Total

~850ms


Le Cold start ajoute environ 3 à 5 secondes (chargement du modèle MiniLM). Ceci est atténué par l'allocation de mémoire de 1 GiB, qui maintient la fonction "chaude" plus longtemps entre les requêtes.

Résultats

Requête : « Quelles sont les meilleures applications Flutter publiées ? »

Réponse : « Selon les publications disponibles, Dr Green \[1\] est une application mobile en Flutter. Elle est catégorisée comme healthtech et utilise également FastAPI, Firebase, JavaScript et Cloud Run. »

Sources : \[

{title: « Dr Green », type: « app-mobile », category: « healthtech », pdid: « PDID-2026-RS5WTC »}

\]

La citation \[1\] renvoie à la première publication récupérée. La réponse est fondée, traçable et factuellement exacte par rapport aux données de publication stockées.

Limitations

  1. Balayage complet de Firestore — ne s'adapte pas au-delà de \~5000 publications sans indexation ANN

  2. Aucun historique de conversation — chaque question est sans état (pas de contexte multi-tour)

  3. Aucune authentification — le point de terminaison est public (limitation de débit recommandée à l'échelle)

Améliorations futures

  1. Vertex AI Vector Search pour l'indexation ANN à l'échelle

  2. Conversation multi-tour utilisant la gestion de session de Genkit

  3. Limitation de débit par utilisateur via Firebase App Check

Résumé par IA

Trop long ? Obtenez un résumé rapide de cet article généré par l'IA.

Vous Pourriez Aussi Aimer
Recherche de suggestions...
Statistiques de l'article
Engagement des lecteurs avec cet article.

1

Vues

0

Commentaires

📧 Restez informé

Recevez une notification par email à chaque nouvel article ou modification

Commentaires (0)

Aucun commentaire pour le moment. Soyez le premier à commenter !

Laisser un commentaire