Aller au contenu principal
~/GiwiSoft
3140d5e38539

RAG (Retrieval Augmented Generation) : donnez un cerveau à vos LLM avec votre documentation technique

Giwi 4 min de lecture DevOps, IA

Vous avez déployé un LLM local avec Ollama, mais ses connaissances s’arrêtent à sa date d’entraînement. Impossible de lui poser des questions sur votre propre code, votre documentation interne, ou vos procédures ops. C’est là que le RAG entre en jeu.

RAG = Retrieval Augmented Generation. On injecte du contexte externe dans le prompt du LLM pour qu’il réponde à partir de VOS données.

J’ai mis en place une stack RAG complète pour interroger ma documentation technique (procédures ops, schémas d’infra, notes de projet). Voici comment.

Comment ça marche

Le principe est simple en apparence :

  1. Indexation : on découpe les documents en chunks, on les vectorise, on les stocke dans une base vectorielle
  2. Recherche : à chaque question, on cherche les chunks les plus pertinents par similarité sémantique
  3. Génération : on fournit ces chunks au LLM en contexte, il répond à partir de ces seuls documents

La stack que j’utilise

Composant Technologie Rôle
LLM Ollama + llama3.2 Génération des réponses
Embeddings Ollama + nomic-embed-text Vectorisation des chunks
Base vectorielle ChromaDB Stockage et recherche des vecteurs
Framework RAG LangChain.js Orchestration pipeline
Source des docs Fichiers Markdown Documentation technique

Indexation des documents techniques

Première étape : charger et découper ma documentation. J’utilise LangChain.js avec un loader Markdown.

import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
import { TextLoader } from "langchain/document_loaders/fs/text";

const loader = new DirectoryLoader("./docs", {
".md": (path) => new TextLoader(path),
});

const docs = await loader.load();

const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});

const chunks = await splitter.splitDocuments(docs);
console.log(`${chunks.length} chunks créés`);

Le choix du chunkSize est important. Pour de la documentation technique (procédures, configs), je trouve que 1000 caractères avec 200 de overlap donne un bon équilibre entre granularité et contexte.

Vectorisation et stockage

On utilise ChromaDB qui tourne dans un conteneur Docker :

# docker-compose.yml
chromadb:
image: chromadb/chroma:latest
ports:
- "8000:8000"
volumes:
- chroma-data:/chroma/chroma
environment:
- IS_PERSISTENT=TRUE

Puis on vectorise et stocke :

import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OllamaEmbeddings } from "@langchain/ollama";

const embeddings = new OllamaEmbeddings({
model: "nomic-embed-text",
baseUrl: "http://localhost:11434",
});

const vectorStore = await Chroma.fromDocuments(chunks, embeddings, {
collectionName: "tech-docs",
url: "http://localhost:8000",
});

console.log("Indexation terminée");

La recherche RAG

Quand un utilisateur pose une question, on cherche les 4 chunks les plus pertinents :

const retriever = vectorStore.asRetriever(4);
const results = await retriever.invoke(
"Comment redémarrer le serveur Nginx après un changement de config ?"
);

// results contient les chunks les plus proches sémantiquement

Génération de la réponse

On injecte ces chunks dans le prompt du LLM :

import { ChatOllama } from "@langchain/ollama";
import { PromptTemplate } from "@langchain/core/prompts";

const llm = new ChatOllama({
model: "llama3.2",
temperature: 0.1,
});

const prompt = PromptTemplate.fromTemplate(`
Tu es un assistant technique. Réponds à la question en français
uniquement à partir du contexte fourni.

Contexte :
{context}

Question : {question}
`);

const chain = prompt.pipe(llm);

const response = await chain.invoke({
context: results.map((r) => r.pageContent).join("\n\n"),
question: "Comment redémarrer Nginx ?",
});

Pourquoi temperature à 0.1 ? Pour une RAG technique, on veut des réponses factuelles et reproductibles, pas créatives.

Améliorations possibles

Une fois la base fonctionnelle, j’ai ajouté quelques optimisations :

Hybrid search : ChromaDB supporte la recherche vectorielle ET par mots-clés. Utile pour les termes techniques précis (noms de packages, commandes).

Parent Document Retriever : au lieu de renvoyer des chunks isolés, on renvoie le document parent entier quand un chunk est matché. Meilleur contexte pour le LLM.

const parentSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 2000 });
const childSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 400 });
// On indexe les enfants, on retrieves les parents

Reranking : on utilise un petit modèle de cross-encoding pour réordonner les résultats avant de les passer au LLM. Ça améliore significativement la qualité des réponses.

Intégration avec mon workflow

J’ai emballé tout ça dans une API Express avec une interface web minimaliste :

app.post("/ask", async (req, res) => {
const { question } = req.body;
const context = await retriever.invoke(question);
const response = await llm.invoke(
prompt.format({ context, question })
);
res.json({ answer: response.content });
});

Conclusion

Le RAG est devenu mon outil principal pour explorer ma propre documentation. Plus besoin de fouiller dans 50 fichiers Markdown : je pose une question en langage naturel, j’obtiens une réponse sourcée.

La stack complète tient sur un Raspberry Pi 4 (Ollama + ChromaDB + l’app Node.js). Les embeddings et l’inférence sont un peu lents, mais pour de la consultation occasionnelle, ça fait le job.

Et vous, avez-vous mis en place une RAG pour votre documentation technique ? Quelle stack utilisez-vous ?