RAG : connecter vos LLMs à vos données internes
Retrieval Augmented Generation (RAG) : architecture, implémentation avec LangChain4j ou Spring AI, choix de la base vectorielle et bonnes pratiques pour un assistant IA sur vos données.
Un LLM sans contexte répond en généraliste. Avec RAG (Retrieval Augmented Generation), il répond depuis vos données. C’est la différence entre un assistant qui dit “je ne sais pas” et un assistant qui cite votre documentation interne avec précision. Voici comment le construire en Java.
Le problème que RAG résout
Les LLMs ont deux limites fondamentales pour un usage en entreprise :
- Données coupées dans le temps : leur connaissance s’arrête à leur date d’entraînement
- Hallucination sur des données propriétaires : ils inventent des réponses sur des données qu’ils n’ont pas vues
RAG contourne ces deux limites. À chaque question, on cherche les passages pertinents dans vos documents, on les donne au LLM avec la question, et le LLM répond depuis ce contexte. Il n’invente pas — il synthétise ce qu’on lui donne.
Architecture d’un pipeline RAG
Phase d'indexation (une fois, offline) :
Documents → Découpage (chunking) → Embedding → Base vectorielle
Phase de requête (à chaque question) :
Question → Embedding → Recherche sémantique → Passages pertinents
→ LLM → Réponse
Embedding : transformer du texte en vecteur numérique (768 à 3072 dimensions selon le modèle). Des textes sémantiquement proches ont des vecteurs proches. C’est ce qui permet la recherche par sens, pas seulement par mots-clés.
Implémentation avec Spring AI
Spring AI (Spring Boot 3.3+) standardise l’intégration des LLMs et des bases vectorielles.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
spring:
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
options:
model: claude-sonnet-4-6
vectorstore:
pgvector:
dimensions: 1536
distance-type: COSINE_DISTANCE
index-type: HNSW
Indexation des documents
@Service
public class DocumentIndexingService {
private final VectorStore vectorStore;
private final DocumentReader pdfReader;
// Indexer un PDF
public void indexDocument(MultipartFile file) throws IOException {
// 1. Lire le PDF
List<Document> documents = new TikaDocumentReader(file.getResource())
.get();
// 2. Découper en chunks (max 800 tokens, overlap 100)
TextSplitter splitter = new TokenTextSplitter(800, 100, 5, 10000, true);
List<Document> chunks = splitter.apply(documents);
// 3. Enrichir les métadonnées
chunks.forEach(chunk -> {
chunk.getMetadata().put("filename", file.getOriginalFilename());
chunk.getMetadata().put("indexed_at", Instant.now().toString());
});
// 4. Vectoriser et stocker (embedding + insertion PgVector)
vectorStore.add(chunks);
log.info("Indexé {} chunks depuis {}", chunks.size(), file.getOriginalFilename());
}
// Indexer une URL (documentation web)
public void indexUrl(String url) {
List<Document> docs = new JsoupDocumentReader(url).get();
TextSplitter splitter = new TokenTextSplitter(600, 80, 5, 10000, true);
vectorStore.add(splitter.apply(docs));
}
}
Recherche et génération (le RAG proprement dit)
@Service
public class RagService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
private static final String SYSTEM_PROMPT = """
Tu es un assistant expert qui répond uniquement à partir des informations fournies dans le contexte.
Si la réponse n'est pas dans le contexte, dis clairement que tu ne trouves pas l'information.
Ne déduis pas et ne complètes pas avec des informations extérieures.
Cite les sources (nom du document) quand c'est pertinent.
""";
public RagResponse answer(String question) {
// 1. Recherche sémantique des passages pertinents
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(5) // 5 passages les plus proches
.withSimilarityThreshold(0.7) // score minimum de similarité
);
if (relevantDocs.isEmpty()) {
return new RagResponse(
"Je ne trouve pas d'information sur ce sujet dans la base de connaissance.",
List.of()
);
}
// 2. Construire le contexte
String context = relevantDocs.stream()
.map(doc -> "Source : " + doc.getMetadata().get("filename") + "\n" + doc.getContent())
.collect(Collectors.joining("\n\n---\n\n"));
// 3. Appeler le LLM avec question + contexte
String answer = chatClient.prompt()
.system(SYSTEM_PROMPT)
.user(u -> u.text("""
Contexte :
{context}
Question : {question}
""")
.param("context", context)
.param("question", question))
.call()
.content();
// 4. Retourner réponse + sources
List<String> sources = relevantDocs.stream()
.map(d -> (String) d.getMetadata().get("filename"))
.distinct()
.toList();
return new RagResponse(answer, sources);
}
}
Choisir sa base vectorielle
| Base | Usage | Points forts |
|---|---|---|
| pgvector | PostgreSQL extension | Simple si vous utilisez déjà Postgres |
| Qdrant | Service dédié | Très performant, filtres avancés |
| Weaviate | Service dédié | GraphQL natif, multi-tenancy |
| Chroma | Embedded / service | Idéal pour prototyper rapidement |
| OpenSearch | Service AWS | Déjà utilisé en prod dans beaucoup d’entreprises |
Pour démarrer : pgvector si vous avez Postgres, Qdrant si vous voulez dédier un service.
Chunking : le point le plus critique
La qualité du chunking détermine 50% de la qualité des réponses RAG.
Trop grands chunks : la recherche retourne des passages trop larges, le LLM est noyé dans du contexte non pertinent.
Trop petits chunks : la recherche retourne des fragments sans contexte, le LLM manque d’information pour répondre.
Bonnes pratiques :
- 400-800 tokens par chunk avec 10-20% d’overlap
- Respecter les frontières naturelles (paragraphes, sections)
- Conserver les métadonnées (titre de section, page, document source)
- Tester avec vos vraies questions et vos vrais documents
Évaluation de la qualité
Un RAG non évalué est un RAG non fiable. Construisez un jeu de test :
// Jeu de test : questions avec réponses attendues
record RagTestCase(String question, String expectedAnswer, String source) {}
@Test
void evaluateRagPrecision() {
List<RagTestCase> testCases = loadTestCases();
int correct = 0;
for (RagTestCase tc : testCases) {
RagResponse response = ragService.answer(tc.question());
// Évaluation par LLM (plus robuste que la comparaison exacte)
boolean isCorrect = evaluator.isAnswerCorrect(
tc.question(), response.answer(), tc.expectedAnswer());
if (isCorrect) correct++;
}
double precision = (double) correct / testCases.size();
log.info("Précision RAG : {}%", Math.round(precision * 100));
assertThat(precision).isGreaterThan(0.80); // seuil minimum
}
Conclusion
RAG est la brique technique qui transforme un LLM généraliste en expert de votre domaine. La clé du succès : des documents bien structurés, un chunking adapté à votre contenu, et une évaluation continue de la qualité des réponses. Une implémentation RAG bien faite répond mieux qu’un moteur de recherche classique sur des questions en langage naturel.
Amine MEGDICHE
Développeur AEM & Java Full Stack — Freelance depuis 2013