Le résultat à la fin du tuto
Un endpoint Next.js /api/ask qui prend une question, retrouve les passages pertinents dans une doc Notion ingérée, et répond avec sources cliquables. En 30 minutes chrono, sur ton ordi.
Pré-requis : un compte Anthropic (clé API), un compte Pinecone (free tier suffit), un export Markdown d'une doc Notion (5-50 pages).
Étape 1 — Ingestion (5 min)
On prend l'export Notion (export.zip qu'on déplie en docs/), et on chunke chaque fichier en blocs de ~500 tokens avec un overlap de 100 tokens.
import { encode } from "gpt-tokenizer";
function chunk(text: string, size = 500, overlap = 100) {
const tokens = encode(text);
const chunks: string[] = [];
for (let i = 0; i < tokens.length; i += size - overlap) {
chunks.push(tokens.slice(i, i + size).join(""));
}
return chunks;
}
Pourquoi 500 tokens et non 1 000 ? Plus le chunk est petit, plus le retrieval est précis. Plus il est gros, plus le contexte autour aide. 500 est un bon compromis pour la doc technique.
Étape 2 — Embeddings (5 min)
On utilise les embeddings Voyage AI (voyage-3 à 256 dims) — moins chers que ceux d'OpenAI, plus précis sur du code/technique.
const res = await fetch("https://api.voyageai.com/v1/embeddings", {
method: "POST",
headers: { Authorization: `Bearer ${VOYAGE_KEY}` },
body: JSON.stringify({
model: "voyage-3",
input: chunks,
}),
});
const { data } = await res.json();
const vectors = data.map((d, i) => ({
id: `chunk-${i}`,
values: d.embedding,
metadata: { text: chunks[i], source: chunks[i].source },
}));
Étape 3 — Pinecone upsert (3 min)
Création d'un index 256 dims, distance cosine, namespace devoria-docs.
import { Pinecone } from "@pinecone-database/pinecone";
const pc = new Pinecone({ apiKey: PINECONE_KEY });
const index = pc.index("devoria-docs");
await index.namespace("devoria-docs").upsert(vectors);
À cette étape, votre doc est indexée. Vérifiez via le dashboard Pinecone qu'il y a bien le bon nombre de vectors.
Étape 4 — L'agent Claude (10 min)
Le pattern qu'on utilise : embed la question, retrieve top 5 passages, demande à Claude de répondre uniquement avec ces passages, en citant les sources.
import Anthropic from "@anthropic-ai/sdk";
const ai = new Anthropic({ apiKey: ANTHROPIC_KEY });
export async function ask(question: string) {
const { embedding } = await embed(question);
const { matches } = await index
.namespace("devoria-docs")
.query({ vector: embedding, topK: 5, includeMetadata: true });
const context = matches
.map(
(m, i) => `[Source ${i + 1}: ${m.metadata.source}]\n${m.metadata.text}`,
)
.join("\n\n---\n\n");
const res = await ai.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system:
"Tu réponds uniquement avec les passages fournis. Cite les sources entre crochets. Si la réponse n'est pas dans les passages, dis-le.",
messages: [
{
role: "user",
content: `Contexte:\n${context}\n\nQuestion: ${question}`,
},
],
});
return res.content[0].type === "text" ? res.content[0].text : "";
}
Le system prompt est volontairement strict. Sans ça, Claude a tendance à compléter avec sa connaissance générale, ce qui casse le RAG.
Étape 5 — Endpoint Next.js (3 min)
// app/api/ask/route.ts
import { ask } from "@/lib/rag";
export async function POST(req: Request) {
const { question } = await req.json();
const answer = await ask(question);
return Response.json({ answer });
}
Test :
curl -X POST http://localhost:3000/api/ask \
-H "Content-Type: application/json" \
-d '{"question":"Comment marche notre auth?"}'
Vous devriez recevoir une réponse sourcée en 1-2 secondes.
Étape 6 — Le piège du re-ranking (4 min)
Le topK: 5 retourne les 5 chunks les plus proches en cosine similarity. Mais "proche" ≠ "pertinent". Sur 30 % de nos questions tests, le bon chunk était à la position 4 ou 5, pas position 1.
Solution : ajouter un re-ranker (Cohere rerank-english-v3.0 ou Voyage rerank-2). On reranke les 20 premiers, on garde les 5 meilleurs.
const reranked = await fetch("https://api.cohere.com/v1/rerank", {
method: "POST",
headers: { Authorization: `Bearer ${COHERE_KEY}` },
body: JSON.stringify({
query: question,
documents: matches.slice(0, 20).map((m) => m.metadata.text),
top_n: 5,
}),
});
Ce step ajoute 200 ms de latence et améliore la précision de 15-20 %. Worth it.
Le repo complet
github.com/devoria/rag-starter (open-source bientôt) contient le code complet, plus :
- Streaming (réponse token par token)
- Chunk metadata enrichi (titres de sections, dates)
- Eval set avec 50 questions/réponses pour mesurer la qualité
Quand utiliser un RAG vs un fine-tune
- RAG : doc qui change souvent, < 10 k chunks, besoin de citation des sources.
- Fine-tune : style/format spécifique, > 100 k exemples, pas besoin de citation.
Pour 90 % des cas B2B, RAG suffit. Et c'est 20× moins cher à maintenir.
Si vous voulez qu'on builde un RAG sur votre doc interne, parlez-nous. On livre en 3-5 jours selon la taille de votre corpus.






