Génération augmentée par récupération : une recherche documentaire intelligente pour les entreprises
RAG combine la recherche vectorielle et l'IA générative pour fournir des réponses précises à partir de bases documentaires privées. Le système extrait le contenu pertinent dans des fichiers PDF, Excel, du code source et bien plus encore, puis génère des explications strictement fondées sur les documents sources — réduisant considérablement les hallucinations.
Le processus commence par un prétraitement : les documents sont analysés, découpés en morceaux intelligents en tenant compte de leur type, puis convertis en embeddings stockés dans une base de données vectorielle.
Extraction de texte selon les formats de fichiers
Le système prend en charge plusieurs formats sans perte de données. Chaque format dispose d'un extracteur dédié.
SUPPORTED = {
'.pdf': lambda f: ''.join(p.extract_text() or '' for p in PyPDF2.PdfReader(f).pages),
'.docx': lambda f: '\n'.join(p.text for p in Document(f).paragraphs),
'.pptx': lambda f: '\n'.join(
shape.text
for slide in Presentation(f).slides
for shape in slide.shapes
if hasattr(shape, "text") and shape.text.strip()
),
'.xlsx': extract_xlsx,
'.csv': extract_csv,
# .txt, .json, .py, .js, etc.
}
Le résultat est un texte propre, prêt à être traité.
Découpage intelligent par type de contenu
Le texte n’est pas divisé par taille fixe — il est segmenté sémantiquement. Tout d’abord, le type de contenu est détecté :
def detect_content_type(text):
if re.search(r'<table|</td>|<tr>', text, re.IGNORECASE):
return 'table'
tab_lines = sum(1 for line in text.splitlines() if '\t' in line)
if tab_lines > max(3, len(text.splitlines()) * 0.4):
return 'table'
if re.search(r'function\s+\w+\s*\(|\bdef\s+\w+\s*\(|\bclass\s+\w+\b', text):
return 'code'
return 'prose'
- Tableaux : les en-têtes sont répétés dans chaque morceau
- Code : découpage par fonctions ou classes sans rompre la logique
- Texte continu : frontières sémantiques basées sur les embeddings de phrases
Pour les tableaux :
def split_table_by_rows(text, chunk_size):
lines = [l for l in text.splitlines() if l.strip()]
header = lines[0]
chunks = []
cur_lines = [header]
for line in lines[1:]:
if sum(len(l) for l in cur_lines) + len(line) >= chunk_size:
chunks.append('\n'.join(cur_lines))
cur_lines = [header, line]
else:
cur_lines.append(line)
if cur_lines:
chunks.append('\n'.join(cur_lines))
return chunks
Le découpage sémantique utilise la similarité cosinus entre phrases adjacentes avec un seuil adaptable (percentile 25).
Génération des embeddings
Les morceaux sont vectorisés grâce à des modèles comme text-embedding-3-small (1536 dimensions). Les vecteurs et métadonnées sont stockés dans la base de données.
def get_embeddings(texts, batch_size=100):
embeddings = []
for i in range(0, len(texts), batch_size):
resp = requests.post(
"https://openrouter.ai/api/v1/embeddings",
headers=HEADERS,
json={
"model": "openai/text-embedding-3-small",
"input": texts[i:i + batch_size],
"encoding_format": "float"
},
).json()
embeddings.extend(d["embedding"] for d in resp.get("data", []))
return np.array(embeddings)
Recherche en plusieurs étapes
La requête est transformée en vecteur, reformulée en trois variantes via un modèle LLM, puis traitée par une recherche hybride.
Recherche hybride : vecteurs + BM25
- Recherche vectorielle : similarité cosinus dans l’espace des embeddings
- BM25 : TF-IDF avec k1=1.5, b=0.75 pour des correspondances exactes
Les résultats sont fusionnés via RRF :
# Fusion RRF
for rank, idx in enumerate(sorted_by_vector):
rrf_scores[idx] += 1.0 / (k_rrf + rank)
for rank, idx in enumerate(sorted_by_bm25):
rrf_scores[idx] += bm25_normalized[idx] * (1.0 / (k_rrf + rank))
Ré-évaluation et filtres
Les 50 meilleurs candidats sont évalués par LLM, dont 5 à 10 sont sélectionnés. Règles d’inclusion forcée :
- Citations entre guillemets « » ou " "
- Nombres ou identifiants (regex : \b(№|N|number)\s*\d+\b)
quote_matches = re.findall(r'«([^»]+)»|"([^"]+)"', query)
for match in quote_matches:
quote = (match[0] or match[1]).strip()
if len(quote) > 5:
for i, chunk in enumerate(chunks_data):
if quote in chunk['content']:
forced_indices.append(i)
break
Optimisations de performance
Recherche hiérarchique
D’abord, les documents pertinents sont identifiés via des annotations ; ensuite, les morceaux à l’intérieur sont recherchés.
doc_summaries = [chunks_data_by_doc[doc_id][0]['content'][:500] for doc_id in all_doc_ids]
doc_embs = get_embeddings(doc_summaries)
sims = np.dot(doc_embs_normalized, query_embedding_normalized)
top_doc_ids = [all_doc_ids[i] for i in np.argsort(sims)[::-1][:5]]
chunks_data = [ch for ch in chunks_data if ch['doc_id'] in top_doc_ids]
Recherche itérative
Pour les requêtes complexes, plusieurs tours affinent les résultats grâce aux retours du modèle LLM.
Points clés
- RAG minimise les hallucinations en générant des réponses strictement à partir de morceaux pertinents
- La recherche hybride (vecteurs + BM25 + RRF) améliore la précision de 20 à 30 % par rapport à la recherche purement vectorielle
- Le découpage sémantique préserve le contexte dans les tableaux et le code, réduisant le bruit
- L’approche hiérarchique accélère la recherche dans de grandes corpora (>10 000 documents)
- Les filtres obligatoires garantissent la récupération de faits contenant des nombres ou du texte entre guillemets
— Editorial Team
Aucun commentaire pour le moment.