Aller au contenu
Tous les articles
tutosragclaudepineconeagent-ia

Builder un agent RAG en 30 minutes avec Claude + Pinecone

Pas-à-pas vidéo : ingestion d'une doc Notion, embeddings Pinecone, agent Claude qui répond sourcé. Code dans le repo.

ÉliseÉlise12 avril 202630 min vidéo
Article Devoria

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.

Ajoutez un développeur à votre équipe, au forfait

1. Briefez (simplement)2. Recevez (rapidement)3. Recommencez (à volonté)
+35

+ de 35 marques
nous font confiance