Quand on a commencé à construire Vibe-Chat, la question centrale était simple : comment faire en sorte que l'IA réponde avec les informations précises de votre boutique, et pas avec des généralités hallucinées ?
La réponse est le RAG — Retrieval-Augmented Generation. L'idée : avant de générer une réponse, l'IA "récupère" des fragments de votre documentation qui sont pertinents pour la question posée, et les intègre dans son contexte. Elle répond donc à partir de vos données réelles, pas de son entraînement général.
Simple en théorie. Diablement complexe à optimiser en production.
Voici ce que nous avons appris après 18 mois de développement et des centaines de boutiques en production.
Vue d'ensemble de l'architecture
Question client
↓
[Embedding de la question] (OpenAI text-embedding-3-small)
↓
[Vector Search] (pgvector, similarité cosinus)
↓
[Top K chunks pertinents] (K=5 par défaut)
↓
[Construction du prompt] (question + contexte + system prompt)
↓
[LLM Generation] (GPT-4o ou Claude 3.5 Sonnet)
↓
Réponse streamée au client
Chaque étape a ses propres enjeux. Détaillons-les.
Les embeddings : le fondement de tout
Quel modèle choisir ?
Nous utilisons text-embedding-3-small d'OpenAI pour deux raisons : le coût et la performance. Pour notre usage (questions e-commerce en français et en anglais), il surpasse son prédécesseur (ada-002) sur la plupart des benchmarks, tout en coûtant 5x moins cher.
text-embedding-3-large offre de meilleures performances, mais le gain marginal ne justifie pas le coût pour des questions e-commerce qui sont généralement courtes et directes.
La dimensionnalité
text-embedding-3-small produit des vecteurs de 1536 dimensions. C'est la taille que nous utilisons en production. Il est possible de réduire à 512 ou 256 dimensions avec une légère perte de qualité (le modèle supporte la réduction via Matryoshka), mais nous n'avons pas jugé nécessaire d'optimiser cela pour l'instant.
L'embedding au moment de l'indexation vs de la requête
Un point important souvent sous-estimé : l'embedding de la question et l'embedding des documents doivent être produits avec le même modèle. Si vous changez de modèle, vous devez ré-indexer tous vos documents.
Nous l'avons appris à nos dépens lors d'une migration. Ça ressemble à une évidence, mais quand vous avez 50 000 chunks en base et que vous découvrez ça en production...
Le chunking : l'étape la plus sous-estimée
Si les embeddings sont le "moteur" du RAG, le chunking est la "carrosserie". Et c'est là que la plupart des implémentations échouent.
Ce qu'on a essayé (et abandonné)
Chunking naïf par taille fixe (ex: 500 tokens tous les 500 tokens) : terrible. Les phrases sont coupées au milieu, le contexte est perdu. Les résultats de retrieval sont médiocres.
Chunking par paragraphe : mieux, mais les documents sans structure claire (imports CSV) donnent de très mauvais résultats.
Chunking par token count avec overlap : notre approche actuelle pour les documents texte. Chunks de 512 tokens avec un overlap de 50 tokens. L'overlap garantit que le contexte d'une phrase qui "chevauche" deux chunks est récupéré dans les deux.
Chunking spécifique pour les produits
Pour les catalogues produits importés via CSV, nous avons une stratégie différente. Chaque produit devient un chunk unique structuré :
Nom: [nom du produit]
Description: [description]
Prix: [prix] [devise]
Catégorie: [catégorie]
Tags: [tags]
Stock: [statut]
SKU: [sku]
Ce format structuré améliore significativement la précision des recommandations produits par rapport à un chunking brut du CSV.
Metadata enrichie
Chaque chunk stocke des métadonnées essentielles : l'identifiant de la source (source_id), le type de document (faq, policy, product), et la position dans le document. Ces métadonnées permettent de filtrer les résultats par type et de construire des réponses plus précises.
La base de données vectorielle : pgvector
Pourquoi pgvector plutôt qu'une solution dédiée ?
Chez Vibe-Chat, notre base de données principale est Supabase (PostgreSQL). Plutôt que d'ajouter une dépendance externe (Pinecone, Weaviate, Qdrant...), nous utilisons pgvector, l'extension PostgreSQL pour les vecteurs.
Les avantages :
- Pas de service supplémentaire à gérer
- Les jointures SQL fonctionnent normalement (on peut filtrer par
chatbot_iddirectement) - Transactions ACID garanties
- Un seul provider à facturer
Les inconvénients :
- Performance en dessous des solutions dédiées à très grande échelle (> 100M vecteurs)
- Moins de fonctionnalités natives (pas de hybrid search natif)
Pour notre cas d'usage (quelques milliers à quelques centaines de milliers de chunks par chatbot), pgvector est parfaitement adapté.
L'index HNSW
Nous utilisons l'index HNSW (Hierarchical Navigable Small World) de pgvector pour accélérer les recherches de similarité cosinus :
CREATE INDEX ON embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Sans index, la recherche vectorielle est une opération O(n) — elle parcourt tous les vecteurs. Avec HNSW, c'est approximativement O(log n). La différence en pratique : quelques secondes vs quelques millisecondes pour 100 000 vecteurs.
La requête vectorielle : l'art du "top K"
Le seuil de confiance
Récupérer les K chunks les plus proches n'est pas suffisant. Il faut aussi s'assurer qu'ils sont suffisamment proches. Un chunk avec une similarité cosinus de 0.45 ne vous apprend rien d'utile sur la question posée.
Nous avons introduit un seuil de confiance configurable par chatbot (défaut : 0.7). Si aucun chunk ne dépasse ce seuil, l'IA répond avec son prompt système uniquement (typiquement : "je n'ai pas cette information, voici comment contacter notre équipe").
Ce mécanisme évite les hallucinations. L'IA qui répond "Je ne sais pas" est infiniment préférable à l'IA qui invente une réponse confiante mais fausse.
Filtrage par chatbot
Chaque chatbot a sa propre base de connaissances. Lors d'une requête vectorielle, on filtre systématiquement par chatbot_id :
SELECT chunk_text, metadata, 1 - (embedding <=> $1) as similarity
FROM embeddings
WHERE chatbot_id = $2
AND 1 - (embedding <=> $1) > $3 -- seuil de confiance
ORDER BY similarity DESC
LIMIT $4; -- K chunks
Simple, mais critique. Sans ce filtre, une question posée sur une boutique de mode pourrait récupérer des chunks d'une boutique tech qui partage le même serveur.
La construction du prompt : l'ingénierie qui fait la différence
Le system prompt
Chaque chatbot a un system prompt personnalisé qui définit sa personnalité, son ton, son domaine d'expertise. Ce prompt est rédigé par le propriétaire de la boutique (ou généré automatiquement selon le secteur).
Structure type :
Tu es [NOM], l'assistant de [BOUTIQUE].
Tu répondes uniquement aux questions liées à [DOMAINE].
Ton ton est [TON].
Si tu ne connais pas la réponse, dis-le clairement et propose de contacter l'équipe.
L'injection des chunks
Les chunks récupérés sont injectés dans le contexte avant la question :
CONTEXTE (informations de la boutique) :
---
[chunk 1]
---
[chunk 2]
---
...
Question du client : [question]
Un point important : nous n'injectons que les chunks qui dépassent le seuil de confiance. Injecter des chunks non pertinents dans le contexte dégrade la qualité des réponses — l'IA essaie de les utiliser même s'ils ne répondent pas à la question.
Gestion de la longueur du contexte
GPT-4o a une fenêtre de contexte de 128K tokens. En pratique, on injecte 5 chunks de 512 tokens = ~2560 tokens de contexte. C'est très en-dessous de la limite, même avec un historique de conversation de 20 messages.
Attention si vous augmentez K au-delà de 10 : le coût augmente linéairement, et les LLMs ont tendance à "perdre" l'information qui est au milieu d'un contexte long (le fameux "lost in the middle" problem).
Le streaming : pourquoi c'est non-négociable
Un LLM qui prend 3 secondes à générer une réponse de 200 mots peut être perçu comme rapide si vous streamer les tokens au fur et à mesure, ou comme lent si vous attendez la réponse complète avant d'afficher quoi que ce soit.
Nous utilisons le streaming OpenAI (Server-Sent Events) depuis le premier jour. L'impact UX est majeur : le "temps de premier token" est de 200 à 400ms, et l'utilisateur voit la réponse se construire en temps réel.
Côté Next.js :
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: buildMessages(context, history, question),
stream: true,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || '';
if (text) controller.enqueue(encoder.encode(`data: ${text}\n\n`));
}
controller.close();
},
});
Les métriques de performance
Ce qu'on mesure en production
Sur chaque message, nous stockons dans messages.metadata :
response_time_ms: temps total de générationchunks_used: nombre de chunks injectésconfidence_score: score du meilleur chunk (ou 0 si RAG vide)model_used: quel LLM a généré la réponse
Ces métriques alimentent nos analytics en temps réel et permettent d'identifier rapidement les dégradations de qualité.
La "couverture RAG"
Une métrique clé que nous trackons : le taux de couverture RAG, c'est-à-dire le pourcentage de réponses qui ont utilisé au moins un chunk pertinent. Si ce taux chute sous 60%, c'est le signe que la base de connaissances est insuffisante ou mal indexée.
Les knowledge gaps
Les questions auxquelles l'IA n'a pas pu répondre (confiance = 0) sont précieuses. Ce sont les lacunes de votre documentation. Nous les agrégeons par similarité sémantique pour identifier les thèmes récurrents non couverts.
Ce qu'on ferait différemment
Avec le recul :
1. Investir plus tôt dans le chunking sémantique. Le chunking par taille fixe avec overlap est correct, mais le chunking sémantique (couper aux frontières de sens, pas aux limites de tokens) donne de meilleurs résultats. Nous avons prévu de migrer.
2. Tester le hybrid search plus tôt. La recherche vectorielle seule rate parfois des correspondances exactes (un numéro de SKU, un nom propre rare). Un système hybride qui combine recherche vectorielle et recherche full-text BM25 serait plus robuste. C'est dans notre roadmap.
3. Implémenter le re-ranking. Après avoir récupéré les top-20 chunks, utiliser un cross-encoder pour re-classer et ne garder que les 5 plus pertinents. Coût supplémentaire, mais qualité significativement meilleure.
Conclusion
Le RAG, bien implémenté, transforme un LLM généraliste en expert de votre domaine. La différence entre un chatbot qui hallucine des informations sur votre boutique et un chatbot qui répond avec précision et confiance se joue entièrement dans la qualité de ce système.
Ce n'est pas simple à construire. Mais une fois en place, la robustesse est remarquable. Nos chatbots répondent correctement à des questions très spécifiques (stock d'un SKU précis, politique de retour d'une gamme particulière) avec une précision que les LLMs "vanilla" ne peuvent tout simplement pas atteindre.
Questions techniques ? On est ouverts. Contactez-nous ou rejoignez notre communauté de développeurs.