Pourquoi le RAG et pas le fine-tuning ?
Un LLM a une date de coupure — il ne connaît pas votre documentation interne, vos tickets de support, vos contrats. Le fine-tuning injecte de la connaissance dans les poids du modèle, mais c'est coûteux, long, et vous devez recommencer à chaque mise à jour de vos données.
Le Retrieval-Augmented Generation adopte une approche différente : on récupère dynamiquement les passages pertinents au moment de la requête, on les injecte dans le prompt, et le LLM répond en s'appuyant sur ce contexte. Vos données restent dans votre base — aucun modèle à ré-entraîner.
Architecture
Pipeline en deux phases : ingestion (découpage, embedding, stockage) exécutée hors ligne, et retrieval + génération (recherche vectorielle, construction du prompt, appel LLM) sur chaque requête utilisateur.
pgvector : tout dans PostgreSQL
Plutôt qu'ajouter un service dédié (Pinecone, Qdrant, Weaviate), j'ai opté pour
pgvector — une extension PostgreSQL qui ajoute un type vector
et des index ANN (Approximate Nearest Neighbor). Même base, même infra, même backup.
-- Activer l'extension
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE document_chunk (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL REFERENCES document(id) ON DELETE CASCADE,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL, -- dimensions text-embedding-3-small
chunk_index INT NOT NULL,
metadata JSONB NOT NULL DEFAULT ''
);
-- Index HNSW : meilleur rappel que IVFFlat, recommandé pour < 10M vecteurs
CREATE INDEX ON document_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64); Pipeline d'ingestion
L'ingestion transforme un document brut en vecteurs stockables. Trois étapes : découpage en chunks, embedding via l'API OpenAI, persistance en base.
final class DocumentIngester
{
public function __construct(
private readonly TextSplitter $splitter,
private readonly EmbeddingClient $embeddings,
private readonly ChunkRepository $chunks,
private readonly EntityManagerInterface $em,
) {}
public function ingest(Document $document): void
{
// 1. Découper en chunks de ~512 tokens avec overlap de 64
$texts = $this->splitter->split($document->getContent(), chunkSize: 512, overlap: 64);
// 2. Embedder en batch (max 2048 inputs / appel OpenAI)
$vectors = $this->embeddings->embedBatch($texts);
// 3. Persister
foreach ($vectors as $i => [$text, $vector]) {
$chunk = new DocumentChunk(
document: $document,
content: $text,
embedding: $vector,
chunkIndex: $i,
);
$this->em->persist($chunk);
}
$this->em->flush();
}
} Taille des chunks
512 tokens avec un overlap de 64 est un bon point de départ, mais pas une règle universelle. Des chunks trop courts perdent le contexte ; trop longs, ils noient le signal pertinent. Testez avec vos données réelles en mesurant le rappel sur un jeu d'évaluation.
Recherche vectorielle et construction du prompt
Côté retrieval, on embed la question de l'utilisateur puis on cherche les chunks
les plus proches par similarité cosinus. pgvector s'en charge avec l'opérateur <=>.
public function findSimilar(array $queryVector, int $limit = 5): array
{
$vectorLiteral = '[' . implode(',', $queryVector) . ']';
return $this->getEntityManager()->createNativeQuery(
'SELECT id, content, metadata,
1 - (embedding <=> :vec::vector) AS score
FROM document_chunk
ORDER BY embedding <=> :vec::vector
LIMIT :limit',
$rsm
)
->setParameter('vec', $vectorLiteral)
->setParameter('limit', $limit)
->getResult();
} Les chunks récupérés sont ensuite assemblés en contexte et injectés dans le prompt système avant l'appel au LLM.
public function answer(string $question): string
{
// 1. Embedding de la question
$queryVec = $this->embeddings->embed($question);
// 2. Recherche des chunks pertinents
$chunks = $this->repository->findSimilar($queryVec, limit: 5);
// 3. Construire le contexte
$context = implode("\n\n---\n\n", array_column($chunks, 'content'));
// 4. Prompt avec contexte injecté
$systemPrompt =
Tu es un assistant expert. Réponds uniquement à partir du contexte fourni.
Si la réponse n'est pas dans le contexte, dis-le explicitement.
Contexte :
$context
;
return $this->llm->chat($systemPrompt, $question);
} Conclusion
pgvector dans PostgreSQL couvre 90% des besoins RAG sans infrastructure supplémentaire. Pour les 10% restants — milliards de vecteurs, latence sub-10ms, multi-tenancy massif — un service dédié se justifie. Mais commencer avec ce qu'on maîtrise déjà, c'est souvent la décision d'ingénierie la plus pragmatique.