RAG avec Next.js et Groq : système de retrieval augmenté
Un chatbot qui répond « je ne sais pas » dès qu'on sort de son entraînement, c'est une démo, pas un produit. Le RAG (Retrieval-Augmented Generation) règle ça : au lieu d'espérer que le modèle « connaisse » votre documentation, vous lui injectez les bons passages au moment de la génération. Ce guide assemble un pipeline RAG complet et honnête avec Next.js + Groq.
C'est quoi, concrètement, un système RAG ?
Quatre étapes, aucune magie : (1) découper vos documents en morceaux, (2) transformer chaque morceau en vecteur (embedding), (3) à la question, retrouver les morceaux les plus proches, (4) les coller dans le prompt et laisser Groq rédiger la réponse. L'avantage de Groq : l'inférence est si rapide que l'utilisateur ne sent pas le surcoût du contexte ajouté.
Étape 1 : chunking intelligent
Découper par nombre de caractères est la pire option. Préférez un découpage par titre (H2/H3) avec un chevauchement (overlap) de 100-200 caractères pour ne pas couper une phrase en deux.
// lib/rag/chunk.ts
export function chunkDocument(text: string, opts = { size: 800, overlap: 150 }) {
const sentences = text.split(/(?<=[.!?])\s+/)
const chunks: string[] = []
let current = ''
for (const s of sentences) {
if ((current + s).length > opts.size && current) {
chunks.push(current.trim())
current = current.slice(-opts.overlap) + s
} else {
current += ' ' + s
}
}
if (current.trim()) chunks.push(current.trim())
return chunks
}Étape 2 : embeddings (hors Groq)
Groq ne fournit pas d'API d'embeddings. Utilisez un provider compatible OpenAI ou un modèle local via @xenova/transformers. Ici on appelle un endpoint compatible OpenAI ; remplacez l'URL/clé par votre provider.
// lib/rag/embed.ts
const EMBED_URL = process.env.EMBED_URL!
const EMBED_KEY = process.env.EMBED_KEY!
export async function embed(texts: string[]): Promise<number[][]> {
const res = await fetch(EMBED_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${EMBED_KEY}` },
body: JSON.stringify({ input: texts, model: 'text-embedding-3-small' }),
})
const data = await res.json()
return data.data.map((d: any) => d.embedding as number[])
}Étape 3 : similarité cosinus
Pour un prototype, un Map en mémoire suffit. En production, passez à pgvector (ou un vector store managé). La similarité cosinus reste la même dans les deux cas.
// lib/rag/retrieve.ts
function cosine(a: number[], b: number[]) {
let dot = 0, na = 0, nb = 0
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2 }
return dot / (Math.sqrt(na) * Math.sqrt(nb))
}
const STORE: { text: string; vec: number[] }[] = []
export function index(text: string, vec: number[]) { STORE.push({ text, vec }) }
export function retrieve(queryVec: number[], topK = 4) {
return STORE
.map(c => ({ text: c.text, score: cosine(queryVec, c.vec) }))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
}Étape 4 : la route API /api/rag
On enchaîne embed → retrieve → Groq. Le prompt système interdit au modèle d'inventer hors contexte — c'est ce qui rend le RAG fiable.
// app/api/rag/route.ts
import { NextRequest } from 'next/server'
import { createGroq } from 'groq-sdk'
import { embed } from '@/lib/rag/embed'
import { retrieve } from '@/lib/rag/retrieve'
const groq = createGroq({ apiKey: process.env.GROQ_API_KEY! })
export async function POST(req: NextRequest) {
const { question } = await req.json()
const [qVec] = await embed([question])
const hits = retrieve(qVec, 4)
const context = hits.map(h => '- ' + h.text).join('\n')
const completion = await groq.chat.completions.create({
model: 'llama-3.3-70b-versatile',
messages: [
{ role: 'system', content:
'Réponds en français en t'appuyant UNIQUEMENT sur le contexte fourni. ' +
"Si la réponse n'y est pas, dis "je n'ai pas cette information". " +
'Cite le passage pertinent.' },
{ role: 'user', content: `Contexte :\n${context}\n\nQuestion : ${question}` },
],
})
return Response.json({ answer: completion.choices[0].message.content, sources: hits })
}Les 4 erreurs qui tuent un RAG
- Chunks trop grands : au-delà de ~800 tokens, le signal se dilue. Préférez des morceaux ciblés.
- Pas de seuil de score : si le top-1 a un score de 0,12, la question n'est pas dans votre base. Renvoyez « inconnu » plutôt que de halluciner.
- Embeddings et génération de providers différents sans test : mesurez la pertinence réelle sur 20 questions avant de ship.
- Oublier de ré-indexer : une doc qui change sans ré-indexation donne des réponses périmées. Automatisez l'indexation à chaque deploy.
Quand passer à pgvector ?
Dès que vous dépassez quelques milliers de chunks, ou que plusieurs utilisateurs partagent la base, le Map en mémoire ne tient plus. pgvector ajoute une recherche ANN (HNSW) et la persistance. Le code de retrieval devient une requête SQL simple — le reste du pipeline ne change pas.
Partir de la bonne base
Nos templates Next.js embarquent déjà la couche API (route handlers, failover, rate-limit) sur laquelle brancher ce pipeline RAG en quelques heures. Voir les templates
Voir les prix →Conclusion
Un RAG n'est pas plus compliqué qu'une API bien structurée : chunking, embeddings, similarité, génération. Groq absorbe le surcoût du contexte ajouté sans pénaliser la latence. Le vrai travail n'est pas le code, c'est le seuil de pertinence et la fraîcheur de l'index — c'est là que se joue la confiance de l'utilisateur.