Skip to content

LlamaIndex 2026 — data layer specialist ​

TL;DR — Si LangChain veut être un framework all-in (modèles, prompts, agents, RAG), LlamaIndex s'est resserré sur ce qu'il fait le mieux : la couche données. En 2026, on l'utilise pour 3 choses : LlamaParse (le meilleur parser PDF/DOCX/PPT du marché, surtout sur tableaux complexes et schémas), les index avancés (Knowledge Graph, Document Summary, Tree, Composable Graph), et les Workflows (alternative à LangGraph, plus event-driven). LlamaCloud (managed) gère ingestion + parsing + indexing en SaaS. Règle freelance FR : LangChain pour orchestrer, LlamaIndex pour ingérer.

🧠 Mental model ​

                ┌─────────────────────────────────────────────────┐
                │             LLAMAINDEX 2026 STACK                │
                └─────────────────────────────────────────────────┘

  Documents bruts                  Index                    Query
  ───────────────                ─────────              ─────────────
                                                                       
  PDF (DCE, bilans)  ─┐                                                
  DOCX (contrats)     │   ┌──────────────┐    ┌──────────────┐         
  PPT (decks)         ├──▶│ LlamaParse   │───▶│ Nodes        │         
  HTML (web)          │   │ (cloud OCR + │    │ + metadata   │         
  CSV/Parquet         │   │  layout AI)  │    │ + relations  │         
  Notion/Confluence  ─┘   └──────────────┘    └──────┬───────┘         
                                                     │                  
                                                     ▼                  
                                    ┌──────────────────────────────┐    
                                    │  Indexes                      │    
                                    │  • VectorStoreIndex           │    
                                    │  • TreeIndex (résumé hiér.)   │    
                                    │  • KnowledgeGraphIndex        │    
                                    │  • DocumentSummaryIndex       │    
                                    │  • KeywordTableIndex          │    
                                    │  • ComposableGraph (mix)      │    
                                    └──────────────┬───────────────┘    
                                                   │                     
                                                   ▼                     
                                    ┌──────────────────────────────┐    
                                    │  Query Engines                │    
                                    │  • RouterQueryEngine          │    
                                    │  • SubQuestionQueryEngine     │    
                                    │  • FLAREInstructQueryEngine   │    
                                    │  • KnowledgeGraphRAGRet.      │    
                                    └──────────────┬───────────────┘    
                                                   │                     
                                                   ▼                     
                                    ┌──────────────────────────────┐    
                                    │  Workflows (event-driven)     │    
                                    │  step → step → step           │    
                                    │  + branching, loops, HITL     │    
                                    └──────────────────────────────┘    

Analogie : LlamaIndex = ETL spécialisé LLM.
LlamaParse = Tesseract + AWS Textract sous stéroïdes (avec LLM en post-process).
Workflows = AWS Step Functions pour LLMs (event-driven, vs LangGraph qui est state-graph).

Diff vs LangChain en une phrase : LangChain pense « chaîne d'appels LLM ». LlamaIndex pense « comment indexer 50K PDF et les interroger ». Sur un RAG simple ils se valent. Sur ingestion industrielle de docs hétérogènes, LlamaIndex gagne nettement.

🛠️ Code minimal ​

python
# pip install llama-index llama-index-llms-anthropic llama-index-embeddings-voyageai
#             llama-parse llama-index-vector-stores-qdrant

from llama_index.core import VectorStoreIndex, Settings, SimpleDirectoryReader
from llama_index.llms.anthropic import Anthropic
from llama_index.embeddings.voyageai import VoyageEmbedding
from llama_parse import LlamaParse

# 1. Config globale
Settings.llm = Anthropic(model="claude-opus-4-8", temperature=0)
Settings.embed_model = VoyageEmbedding(model_name="voyage-3-large")

# 2. Parse + index
parser = LlamaParse(result_type="markdown", premium_mode=True)
documents = SimpleDirectoryReader(
    "./pdfs",
    file_extractor={".pdf": parser},
).load_data()

index = VectorStoreIndex.from_documents(documents)

# 3. Query
query_engine = index.as_query_engine(similarity_top_k=6, response_mode="compact")
response = query_engine.query("Quel est le ratio de solvabilité Banque X en 2024 ?")
print(response)
for src in response.source_nodes:
    print(f"- {src.metadata['file_name']} p.{src.metadata.get('page')}: score={src.score:.3f}")

🧩 Câbler Anthropic proprement (le détail qui sépare le junior du staff) ​

LlamaIndex abstrait le LLM, mais les défauts de l'intégration coûtent cher en prod. Trois choses que le wrapper Anthropic(...) ne fait pas pour toi et que tu dois piloter :

1. Tiering modèle par rôle. Tu n'as pas UN modèle, tu en as trois jobs distincts dans un pipeline RAG/KG. Ne les sers jamais avec le même tier :

Rôle dans le pipelineModèlePourquoi
Extraction KG (SchemaLLMPathExtractor, 1 appel/chunk × 50K)claude-haiku-4-5 (1 $/5 $)Volume massif, tâche structurée bornée par un schéma Pydantic → un petit modèle suffit. Opus ici = facture ×25 pour ~+3 % de précision.
Synthèse de réponse (response_synthesizer) sur le hot pathclaude-sonnet-4-6Le user attend la réponse, latence compte, qualité doit être bonne mais pas chirurgicale.
Routing / décomposition (RouterQueryEngine, SubQuestionQueryEngine)claude-haiku-4-5Choix de route = classification, pas de génération longue.
Eval / golden judge offlineclaude-opus-4-8 (5 $/25 $ @ 1M)Le juge doit être ≥ au modèle jugé, et c'est offline donc le coût est amorti.
python
from llama_index.llms.anthropic import Anthropic

EXTRACTOR_LLM  = Anthropic(model="claude-haiku-4-5",  temperature=0, max_retries=4, timeout=60)
SYNTH_LLM      = Anthropic(model="claude-sonnet-4-6", temperature=0, max_retries=4, timeout=60)
JUDGE_LLM      = Anthropic(model="claude-opus-4-8",   temperature=0, max_retries=4, timeout=120)

2. Adaptive thinking, PAS de budget_tokens. Sur 4.8 l'extended thinking se pilote en mode adaptatif + un niveau d'effort, jamais via un budget de tokens (qui renvoie 400). Réserve-le à la synthèse hybride difficile, surtout pas à l'extraction de masse :

python
# Synthèse d'une réponse hybride multi-source : on autorise du raisonnement.
SYNTH_LLM = Anthropic(
    model="claude-sonnet-4-6",
    temperature=0,
    thinking_dict={"type": "adaptive"},      # adaptatif, le modèle décide combien réfléchir
    # effort piloté côté output_config / generation_kwargs : low | medium | high
    max_tokens=4000,
)
# ❌ NE PLUS FAIRE : thinking={"type": "enabled", "budget_tokens": 8000}  -> HTTP 400 sur 4.7/4.8

3. Prompt caching sur le préfixe stable. Le parsing_instruction LlamaParse, le schéma KG, et le system prompt de synthèse sont identiques sur 50K appels. Sans cache_control, tu paies le plein tarif input à chaque chunk. Le wrapper Anthropic LlamaIndex expose le caching ; place le marqueur sur le plus long préfixe invariant (system + outils + schéma), jamais sur la partie variable (le chunk). Sur l'extraction KG de 50K docs, le caching du schéma + instruction divise typiquement la facture input par 3-5.

Mental model coût : dans un pipeline LlamaIndex, le LLM est appelé bien plus souvent que tu ne crois. Un seul index.query() « simple » peut déclencher : 1 appel routing + N appels de décomposition (sub-question) + 1 appel par node retrieved en mode refine + 1 appel reranking. Toujours logger.info(resp.raw.usage) et agréger par trace_id, sinon tu découvres la facture en fin de mois.

🎬 Cas d'usage concrets ​

Cas 1 — Ingestion 50K PDF archive (banque privée Paris, 1880 → 2024) ​

Contexte — Banque privée familiale, 50 K dossiers clients archivés depuis 1880 (registres manuscrits scannés), bilans annuels jusqu'en 2024 (PDF complexes avec tableaux financiers), correspondance. Mission : indexer le tout pour répondre à des questions de compliance KYC historique.

Pourquoi LlamaIndex — LlamaParse en mode premium_mode gère OCR + reconstruction de tableaux + layout. Sur scans manuscrits dégradés, j'ai testé Tesseract (catastrophe), AWS Textract (mieux, mais 0.04 $/page), LlamaParse premium ($0.045/page) avec post-process LLM = qualité supérieure (recall 92 % vs Textract 78 % sur tableau de bilan).

Pipeline retenu :

  1. LlamaParse premium → Markdown structuré + métadonnées par page.
  2. MarkdownNodeParser chunking respectueux de la structure (titres, tableaux préservés en bloc).
  3. Embeddings Voyage-3-large.
  4. Qdrant + métadonnées (année, type doc, client_id).
  5. RouterQueryEngine : route vers KeywordTableIndex pour recherches par nom propre, vs VectorStoreIndex pour questions sémantiques.

Pitfall réel — 50K PDF, parsing séquentiel = 11 jours. Passage en parallèle avec aparse_files + queue Redis = 18 h. Coût LlamaParse : ~$2 200 (50K × ~10 pages × $0.0045 estimé après remise volume).

ROI — Mission 32 j × 1300 € = 41 600 € HT. Le client a comparé à un dev maison estimé à 4-5 mois.

Cas 2 — Knowledge Graph sur BODACC + Infogreffe (legaltech Bordeaux) ​

Contexte — Startup legaltech qui vend de la "due diligence accélérée" à des fonds de PE. Veut un KG des entreprises françaises avec leurs dirigeants, filiales, procédures collectives, modifications statutaires. Sources : BODACC, Infogreffe, Pappers API.

Pourquoi LlamaIndex KG — KnowledgeGraphIndex + PropertyGraphIndex (v0.11+) qui combine vector embeddings ET triples (sujet, prédicat, objet). On peut requêter "Quelles sociétés dont le dirigeant a aussi dirigé une société en LJ ?" → mix Cypher-like + sémantique.

Pipeline :

  1. Ingestion API BODACC → documents JSON.
  2. SchemaLLMPathExtractor avec schéma Pydantic (Entité = Société | Personne | Procédure ; Relations = DIRIGE, FILIALE_DE, EN_PROCEDURE).
  3. Stockage : Neo4j pour graph + Qdrant pour vectors (intégration native LlamaIndex).
  4. Query engine custom : sub-question router (graph query si entité nommée détectée, vector sinon).

Gain mesurable — Question "Sociétés liées à M. Dupont en LJ depuis 2023" : avec LlamaIndex KG, 4 hops graph + 2 sources sémantiques = réponse en 2.8s avec 94 % de précision. Sans KG (vector pur), précision 61 %.

ROI — Build initial 45 j × 1400 € = 63 K€ HT. Mission de TMA récurrente 4 j/mois.

Cas 3 — Catalogue produits mode avec tableaux et images (e-commerce Sentier) ​

Contexte — Marketplace mode FR/IT, 80K références. Catalogues fournisseurs en PDF (lookbooks luxe : photos + tableaux de matières + grille de tailles + composition). Objectif : assistant interne pour acheteurs ("Trouve-moi des trenchs en gabardine coton-poly < 380 €, dispo en 38-42").

Pourquoi LlamaIndex — LlamaParse multimodal extrait images + texte + tableaux. DocumentSummaryIndex génère un résumé par produit, utilisable comme première couche de filtrage avant retrieval fin sur les détails techniques.

Architecture :

  • Niveau 1 : DocumentSummaryIndex (résumé court par produit) → filtrage rapide.
  • Niveau 2 : VectorStoreIndex sur détails techniques (matière, composition).
  • Niveau 3 : ImageVectorIndex (CLIP-like embeddings) pour recherche visuelle.
  • Orchestration : ComposableGraph qui route selon le type de requête (texte / visuel / mixte).

ROI client — Acheteurs gagnent ~1h/jour de feuilletage manuel. 12 acheteurs × 1h × 220 j × 35 €/h = 92 K€/an de productivité débloquée. Build : 28 j × 1300 € = 36 400 € HT.

🛠️ Exemple end-to-end ​

Use case réel : ingestion + interrogation de dossiers DCE (Dossier de Consultation des Entreprises) en architecture/BTP. Chaque DCE = 30-200 PDF mêlant CCTP, plans, BPU, mémoires techniques. Requêtes hybrides "trouve-moi les marchés où le matériau X est imposé" (KG) + "résume les exigences thermiques RT2020" (vector).

python
"""
DCE BTP — pipeline LlamaParse + LlamaIndex hybride vector + Knowledge Graph
Mission : Cabinet de maîtrise d'œuvre, 600 DCE actifs, recherche transverse.
"""
import os, asyncio
from pathlib import Path
from typing import Literal

from llama_index.core import (
    Settings, SimpleDirectoryReader, StorageContext,
    VectorStoreIndex, PropertyGraphIndex, load_index_from_storage,
)
from llama_index.core.node_parser import MarkdownNodeParser
from llama_index.core.indices.property_graph import SchemaLLMPathExtractor
from llama_index.core.query_engine import RouterQueryEngine, SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.core.selectors import LLMSingleSelector
from llama_index.llms.anthropic import Anthropic
from llama_index.embeddings.voyageai import VoyageEmbedding
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore
from llama_parse import LlamaParse
from qdrant_client import QdrantClient

# ----------------------------------------------------------------------------
# 1. CONFIG
# ----------------------------------------------------------------------------
Settings.llm = Anthropic(model="claude-opus-4-8", temperature=0, max_tokens=2000)
Settings.embed_model = VoyageEmbedding(model_name="voyage-3-large", embed_batch_size=64)
Settings.chunk_size = 1024
Settings.chunk_overlap = 128

DCE_DIR = Path("./dce_archives")
STORAGE_DIR = Path("./storage")
QDRANT_URL = os.environ["QDRANT_URL"]
NEO4J_URL = os.environ["NEO4J_URL"]

# ----------------------------------------------------------------------------
# 2. SCHEMA KG  (entités et relations métier BTP)
# Schéma FERMÉ : SchemaLLMPathExtractor n'extrait QUE ces types + triples validés.
# C'est ce qui coupe 60-80 % des triples parasites vs un extracteur libre.
# ----------------------------------------------------------------------------
ENTITY_TYPES = Literal[
    "Marche", "Lot", "Materiau", "NormeTechnique",
    "Acteur", "Localisation", "Equipement",
]
RELATION_TYPES = Literal[
    "CONTIENT_LOT", "IMPOSE_MATERIAU", "EXIGE_NORME",
    "ATTRIBUE_A", "SITUE_A", "INCLUT_EQUIPEMENT",
]
VALID_TRIPLES = [
    ("Marche", "CONTIENT_LOT", "Lot"),
    ("Lot", "IMPOSE_MATERIAU", "Materiau"),
    ("Lot", "EXIGE_NORME", "NormeTechnique"),
    ("Marche", "ATTRIBUE_A", "Acteur"),
    ("Marche", "SITUE_A", "Localisation"),
    ("Lot", "INCLUT_EQUIPEMENT", "Equipement"),
]

# ----------------------------------------------------------------------------
# 3. INGESTION (LlamaParse premium pour DCE complexes)
# ----------------------------------------------------------------------------
parser = LlamaParse(
    result_type="markdown",
    premium_mode=True,           # OCR + tables + figures
    language="fr",
    parsing_instruction=(
        "Tu parses un DCE (Dossier Consultation Entreprises) du BTP français. "
        "Préserve les structures suivantes en blocs distincts : "
        "CCTP par lot, BPU (Bordereau Prix Unitaires) en tableaux, "
        "DPGF, références normatives (NF, EN, DTU). "
        "Ne reformule pas les exigences techniques."
    ),
    skip_diagonal_text=True,
)

async def parse_dce_folder(folder: Path) -> list:
    """Parse récursivement un dossier DCE → docs LlamaIndex."""
    file_extractor = {".pdf": parser, ".docx": parser}
    reader = SimpleDirectoryReader(
        input_dir=str(folder),
        recursive=True,
        file_extractor=file_extractor,
        filename_as_id=True,
    )
    docs = await reader.aload_data()
    # Métadonnées : nom DCE = dossier parent
    for d in docs:
        rel = Path(d.metadata["file_path"]).relative_to(folder)
        d.metadata["dce_name"] = rel.parts[0] if len(rel.parts) > 1 else "root"
        d.metadata["doc_type"] = _infer_doc_type(rel.name)
    return docs

def _infer_doc_type(filename: str) -> str:
    n = filename.lower()
    if "cctp" in n: return "CCTP"
    if "bpu" in n: return "BPU"
    if "dpgf" in n: return "DPGF"
    if "memoire" in n or "mémoire" in n: return "Memoire"
    if "plan" in n: return "Plan"
    return "Autre"

# ----------------------------------------------------------------------------
# 4. INDEX HYBRIDE  (vector + property graph)
# ----------------------------------------------------------------------------
def build_vector_index(docs):
    qdrant = QdrantClient(url=QDRANT_URL)
    vector_store = QdrantVectorStore(client=qdrant, collection_name="dce_vector")
    storage = StorageContext.from_defaults(vector_store=vector_store)
    node_parser = MarkdownNodeParser()
    nodes = node_parser.get_nodes_from_documents(docs)
    return VectorStoreIndex(nodes, storage_context=storage, show_progress=True)

def build_graph_index(docs):
    graph_store = Neo4jPropertyGraphStore(
        username=os.environ["NEO4J_USER"],
        password=os.environ["NEO4J_PASS"],
        url=NEO4J_URL,
    )
    extractor = SchemaLLMPathExtractor(
        llm=Settings.llm,
        possible_entities=ENTITY_TYPES,
        possible_relations=RELATION_TYPES,
        kg_validation_schema=VALID_TRIPLES,
        strict=True,
    )
    return PropertyGraphIndex.from_documents(
        docs,
        kg_extractors=[extractor],
        property_graph_store=graph_store,
        show_progress=True,
    )

# ----------------------------------------------------------------------------
# 5. QUERY ENGINE HYBRIDE  (router → vector OR graph OR sub-question)
# ----------------------------------------------------------------------------
def build_hybrid_engine(vector_index, graph_index):
    vector_engine = vector_index.as_query_engine(
        similarity_top_k=8,
        response_mode="compact",
    )
    graph_engine = graph_index.as_query_engine(
        include_text=True,
        sub_retrievers=["llm_synonym", "vector_context"],
    )

    sub_q_engine = SubQuestionQueryEngine.from_defaults(
        query_engine_tools=[
            QueryEngineTool(
                query_engine=vector_engine,
                metadata=ToolMetadata(
                    name="vector_dce",
                    description="Cherche dans le contenu textuel des DCE (CCTP, BPU, mémoires).",
                ),
            ),
            QueryEngineTool(
                query_engine=graph_engine,
                metadata=ToolMetadata(
                    name="kg_dce",
                    description="Cherche dans le graphe entités (Marchés, Lots, Matériaux, Normes).",
                ),
            ),
        ],
    )

    router = RouterQueryEngine(
        selector=LLMSingleSelector.from_defaults(llm=Settings.llm),
        query_engine_tools=[
            QueryEngineTool(
                query_engine=vector_engine,
                metadata=ToolMetadata(
                    name="texte",
                    description="Questions sémantiques, résumés, citations exactes."
                ),
            ),
            QueryEngineTool(
                query_engine=graph_engine,
                metadata=ToolMetadata(
                    name="graphe",
                    description="Questions structurelles : relations entre marchés, matériaux, normes."
                ),
            ),
            QueryEngineTool(
                query_engine=sub_q_engine,
                metadata=ToolMetadata(
                    name="multi",
                    description="Questions complexes nécessitant décomposition en sous-questions."
                ),
            ),
        ],
    )
    return router

# ----------------------------------------------------------------------------
# 6. MAIN
# ----------------------------------------------------------------------------
async def main():
    if not (STORAGE_DIR / "built").exists():
        print(">> Parsing DCE folder…")
        docs = await parse_dce_folder(DCE_DIR)
        print(f"   {len(docs)} documents parsed.")

        print(">> Building vector index…")
        vec = build_vector_index(docs)
        vec.storage_context.persist(persist_dir=str(STORAGE_DIR / "vector"))

        print(">> Building knowledge graph…")
        kg = build_graph_index(docs)
        kg.storage_context.persist(persist_dir=str(STORAGE_DIR / "graph"))

        (STORAGE_DIR / "built").touch()
    else:
        print(">> Loading existing indexes…")
        vec_sc = StorageContext.from_defaults(persist_dir=str(STORAGE_DIR / "vector"))
        vec = load_index_from_storage(vec_sc)
        kg_sc = StorageContext.from_defaults(persist_dir=str(STORAGE_DIR / "graph"))
        kg = load_index_from_storage(kg_sc)

    engine = build_hybrid_engine(vec, kg)

    questions = [
        # Question vector (sémantique)
        "Quelles sont les exigences thermiques RT2020 pour les lots de menuiserie extérieure dans le DCE Lyon-Confluence ?",
        # Question graph (structurelle)
        "Liste les marchés où le matériau 'béton bas carbone' est imposé.",
        # Question hybride (sub-question)
        "Compare les normes acoustiques exigées entre les DCE de logements collectifs vs tertiaire sur 2024-2025.",
    ]
    for q in questions:
        print(f"\n## {q}\n")
        resp = engine.query(q)
        print(resp)
        for s in resp.source_nodes[:3]:
            print(f"   src: {s.metadata.get('file_name')} dce={s.metadata.get('dce_name')}")

if __name__ == "__main__":
    asyncio.run(main())

Métriques mesurées sur 80 DCE pilotes (cabinet MOE Bordeaux) :

  • Parsing : 6.2 s/PDF moyenne (LlamaParse premium).
  • Indexing : 11 min pour 80 DCE (~3 200 docs après split).
  • Query latence p50 : 3.1 s (router + retrieve + generate).
  • Précision sur 50 questions test : 87 % (validée par chef de projet).
  • Coût ingestion totale : ~$340 LlamaParse + ~$60 LLM extraction KG + ~$15 embeddings = ~$415.

🎯 Patterns courants ​

1. Composable Graph — indexer N DCE comme sous-index puis méta-index par-dessus :

python
from llama_index.core import ComposableGraph
graph = ComposableGraph.from_indices(
    SummaryIndex, [dce1_index, dce2_index, ...],
    index_summaries=[f"DCE {n}" for n in dce_names],
)

2. Metadata filters avancés sur retrieval :

python
from llama_index.core.vector_stores import MetadataFilter, MetadataFilters
filters = MetadataFilters(filters=[
    MetadataFilter(key="dce_name", value="lyon-confluence"),
    MetadataFilter(key="doc_type", value="CCTP"),
])
engine = index.as_query_engine(filters=filters, similarity_top_k=6)

3. Hybrid search BM25 + vector :

python
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
fusion = QueryFusionRetriever(
    [vector_index.as_retriever(), BM25Retriever.from_defaults(nodes=nodes)],
    similarity_top_k=8, num_queries=4, mode="reciprocal_rerank",
)

4. Workflows event-driven (alternative LangGraph) :

python
from llama_index.core.workflow import Workflow, step, Event, StartEvent, StopEvent

class IngestEvent(Event): path: str
class IndexEvent(Event): nodes: list

class IngestPipeline(Workflow):
    @step
    async def parse(self, ev: StartEvent) -> IngestEvent:
        return IngestEvent(path=ev.path)
    @step
    async def index(self, ev: IngestEvent) -> IndexEvent:
        ...
    @step
    async def finalize(self, ev: IndexEvent) -> StopEvent:
        return StopEvent(result="ok")

5. Reranking :

python
from llama_index.postprocessor.cohere_rerank import CohereRerank
engine = index.as_query_engine(
    node_postprocessors=[CohereRerank(top_n=4, model="rerank-multilingual-v3.0")],
)

6. Streaming réponse :

python
engine = index.as_query_engine(streaming=True)
for tok in engine.query("...").response_gen:
    print(tok, end="", flush=True)

7. Citation node parser — préserve les références exactes pour citation finale.

📊 Observabilité, scale & failure modes (vue staff) ​

Un pipeline LlamaIndex en prod casse rarement à l'endroit où le junior regarde. La couche données est batch, longue, coûteuse, et silencieuse — l'observabilité s'y joue différemment d'un service requête/réponse.

Instrumenter le pipeline. LlamaIndex émet des spans OpenTelemetry via llama-index-callbacks / instrumentation native. Branche un collector (Langfuse, Phoenix/Arize, ou OTel → ton backend) pour voir chaque appel LLM, chaque retrieve, chaque rerank avec tokens et latence. Sans ça, tu ne sais pas que ton RouterQueryEngine fait 4 appels là où tu en imaginais 1.

python
from llama_index.core import set_global_handler
set_global_handler("langfuse")   # ou "arize_phoenix"
# chaque query() devient une trace : route -> retrieve -> rerank -> synth, avec usage par span

Les failure modes réels et leur signature :

Failure modeSignature observableCause racineFix
Recall qui s'effondre silencieusementEval recall@k chute, pas d'erreurChunking par défaut coupe les tableaux post-LlamaParseMarkdownNodeParser, garder les tables en bloc
Facture LLM ×10 inexpliquéeusage agrégé exploseresponse_mode="refine" → 1 appel/node ; ou Opus sur l'extraction KGcompact/tree_summarize, Haiku sur extraction
Ingestion qui ne finit jamaisJob bloqué, throughput → 0LlamaParse séquentiel + rate limit Anthropic sur l'extraction KGnum_workers, backoff (max_retries), queue Redis
Updates incrémentaux dupliquésDoublons en base vectorielleSimpleDirectoryReader sans filename_as_id → nouveaux doc_idsfilename_as_id=True + refresh_ref_docs
KG plein de triples bidonPrécision graph faible, nœuds explosésSchemaLLMPathExtractor sans strict=True, schéma trop permissifstrict=True, schéma fermé, kg_validation_schema
OverloadedError en pic d'ingestion529 par rafalesParallélisme trop agressif vs quotaasyncio.Semaphore borné + OverloadedError retry SDK

Idempotence & reprise sur incident. Une ingestion de 50K docs DOIT être reprenable. Le pattern : un manifeste {doc_id: hash, status, parsed_at}. Au redémarrage, skip ce qui est déjà indexed. Sans ça, un crash à 80 % te coûte 11 jours de re-parsing. La gate (STORAGE_DIR / "built").exists() du code end-to-end est la version naïve ; en prod c'est un manifeste par document, pas un flag global.

Scale : le goulot n'est pas le vector store. Sur >10K docs, le coût et la latence d'ingestion sont dominés par (1) LlamaParse premium (I/O réseau + facture page) et (2) l'extraction KG (1 appel LLM/chunk). Le vector store ingère vite. Donc on parallélise le parsing (workers bornés), on batch les embeddings (embed_batch_size=64), et on rend l'extraction KG asynchrone et reprenable, pas le vector index.

🔄 Versions & écosystème 2026 ​

  • llama-index 0.12.x — Settings global (remplace ServiceContext), Workflows stables, PropertyGraphIndex remplace KnowledgeGraphIndex legacy.
  • llama-parse — versions premium, multimodal_premium, et balanced (économique). Tarif 2026 ~ $0.003-$0.045/page selon mode et volume.
  • LlamaCloud — managed ingestion pipelines + parsing + indexing. $99/mois starter, enterprise custom. RAG-as-a-service de fait.
  • llama-index-llms-anthropic — support Opus 4.8 / Sonnet 4.6 / Haiku 4.5, prompt caching, adaptive thinking (thinking={"type": "adaptive"} + output_config effort low/medium/high). ⚠️ L'ancienne forme budget_tokens renvoie HTTP 400 sur 4.7/4.8 — ne plus l'utiliser.
  • llama-index-llms-bedrock-converse — Anthropic via AWS Bedrock pour souveraineté FR (régions Paris).
  • llama-index-vector-stores- — connecteurs Qdrant, Weaviate, Pinecone, pgvector, ChromaDB, Milvus, Vespa, LanceDB, Elasticsearch, Mongo Atlas.
  • llama-index-graph-stores-neo4j — KG officiel.
  • JS / TS : llamaindex npm package, parité ≈ 70 % (en retard sur PropertyGraphIndex et Workflows complexes).

⚠️ Pitfalls ​

  1. LlamaParse async = parallèle, mais coûte — par défaut séquentiel. Passer num_workers=10 et surveiller la facture (premium $0.045/page × 50K pages = $2 250).
  2. PropertyGraphIndex ≠ KnowledgeGraphIndex — l'ancien est legacy, le nouveau utilise Pydantic schemas et est nettement plus précis. Migration manuelle requise.
  3. SchemaLLMPathExtractor lent et cher — appelle le LLM par chunk. Sur 50K docs, c'est 50K appels. Utiliser un modèle plus petit (Sonnet ou Haiku) avec strict=True pour réduire la facture.
  4. Tokens cachés — RouterQueryEngine fait un appel LLM JUSTE pour choisir la route, en plus de la query. Sur petits volumes négligeable, sur 1M req/jour ça compte.
  5. SimpleDirectoryReader non-idempotent — recrée des doc_ids différents à chaque run si pas de filename_as_id=True. Casse les updates incrémentaux.
  6. Settings global = singleton — pratique en script, dangereux en serveur multi-tenant. Préférer passer llm= et embed_model= explicites par contexte.
  7. Chunking par défaut massacre les tableaux — utiliser MarkdownNodeParser après LlamaParse markdown, sinon les tableaux sont coupés et illisibles.
  8. Neo4j-aura quotas free — vite saturé (200K nodes max free). Prévoir Aura Pro ou self-hosted dès la prod.
  9. PDF scannés sans OCR — sans premium_mode, LlamaParse renvoie du texte vide pour les scans. Activer explicitement.
  10. Reranking coûteux — Cohere rerank $1/1K req, sur 500 req/min = $720/jour. Cacher le résultat ou skipper si top_k déjà petit.

💰 Pricing / ROI client ​

LlamaIndex (open source) — gratuit.

LlamaParse (cloud) — tarifs 2026 indicatifs :

  • balanced : $0.003/page (économique, qualité OK)
  • premium : $0.025/page (OCR + tables + figures, RAG-ready)
  • multimodal_premium : $0.045/page (analyse images + reasoning)

LlamaCloud — $99/mois starter (10K pages/mois), $499/mois growth, enterprise custom.

ROI freelance FR :

  • Mission ingestion archive bancaire 50K docs : 35 j × 1300 € = 45 500 € HT. LlamaParse costs $2 200 (refacturable). Économise au client ~4 mois de dev custom.
  • KG Infogreffe/BODACC : build 45 j × 1400 € = 63 000 € HT. Maintenance 4 j/mois × 1300 € = 5 200 €/mois.
  • Audit "ingestion catastrophique" client existant : 5 j × 1200 € = 6 000 € HT. Découverte typique : chunking cassé, gain recall +30 % en 5 j.

Argumentaire commercial — "LlamaParse premium coûte 2K€ pour digérer 50K pages, vs 6 mois d'un junior à recoder. ROI évident, et la qualité parse est supérieure à AWS Textract sur PDF complexes français."

🧪 Testing / Eval ​

python
from llama_index.core.evaluation import (
    FaithfulnessEvaluator,
    RelevancyEvaluator,
    CorrectnessEvaluator,
    BatchEvalRunner,
)

evaluator_dict = {
    "faithfulness": FaithfulnessEvaluator(llm=Settings.llm),
    "relevancy": RelevancyEvaluator(llm=Settings.llm),
    "correctness": CorrectnessEvaluator(llm=Settings.llm),
}
runner = BatchEvalRunner(evaluator_dict, workers=4, show_progress=True)

# eval_questions = ["...", "..."]  + gold answers
results = await runner.aevaluate_queries(
    query_engine=engine,
    queries=eval_questions,
)
for metric, scores in results.items():
    mean = sum(s.score for s in scores if s.score is not None) / len(scores)
    print(f"{metric}: {mean:.3f}")

LlamaIndex ships avec :

  • Faithfulness : la réponse est-elle fidèle au contexte retrieved ?
  • Relevancy : le contexte est-il pertinent vs la question ?
  • Correctness : la réponse vs gold answer ?
  • Semantic Similarity : embedding-based eval.

Pour eval ingestion : créer un eval set "X devrait être trouvé dans le doc Y page Z" → mesurer recall@k.

🔁 Quand utiliser / éviter ​

Utiliser LlamaIndex quand :

  • Ingestion massive de documents complexes (PDF avec tables, scans, multi-format)
  • Besoin d'un Knowledge Graph (PropertyGraphIndex)
  • Mix vector + lexical + structured retrieval
  • Volumes importants (> 10K docs) — pipelines optimisés
  • Multi-format (PDF, DOCX, HTML, Notion, Confluence, Slack) — connecteurs prêts
  • LlamaParse pour qualité OCR

Éviter LlamaIndex quand :

  • Application simple LLM sans RAG → overhead inutile
  • Besoin d'orchestration multi-agent complexe → LangGraph ou Workflows
  • Stack JS uniquement et features avancées → parité incomplète, LangChain.js plus mature
  • Contrôle fin sur prompts et chains → trop d'abstraction parfois

Combo gagnant 2026 : LlamaIndex pour ingestion + indexes, LangGraph pour orchestration, raw SDK pour le hot path de génération.

🏋️ Exercices ​

Progressifs, du « ça marche » au « défends le chiffre en prod ». Fais-les sur un vrai corpus (10-20 PDF avec tableaux suffisent pour sentir la douleur).

Exo 1 — Le RAG qui ment sur les tableaux ​

Objectif : prouver que le chunking par défaut détruit le recall sur les données tabulaires, puis le réparer. Ingère 10 bilans PDF avec SimpleDirectoryReader chunking par défaut, puis avec LlamaParse markdown + MarkdownNodeParser. Pose 15 questions dont la réponse est dans une cellule de tableau. Mesure recall@5 dans les deux cas. Indice/Solution : le défaut splitte au milieu des lignes de table → le node retrieved contient une demi-ligne sans en-tête. MarkdownNodeParser garde la table en bloc. Attends-toi à +25-40 pts de recall. Le livrable est le chiffre, pas le code.

Exo 2 — Le routeur qui coûte un appel caché ​

Objectif : quantifier le coût réel d'un RouterQueryEngine et décider s'il vaut le coup. Branche Langfuse/Phoenix. Lance 100 requêtes à travers un router 3-voies. Compte le nombre exact d'appels LLM et de tokens par requête. Compare au coût d'un retrieval direct sans router. À partir de quel volume/jour le router devient-il le mauvais choix ? Indice/Solution : le router ajoute 1 appel de classification systématique. Sur Haiku c'est négligeable ; si tu l'as câblé sur Opus (erreur classique), c'est le poste de coût n°1. Réponse attendue : « route avec Haiku, ou remplace par un classifieur embeddings si le routing est stable ».

Exo 3 — Extraction KG à l'échelle sans exploser la facture ​

Objectif : indexer 1K docs en PropertyGraphIndex pour < X $ et un graph propre. Lance SchemaLLMPathExtractor sur 1K docs avec Opus, schéma ouvert. Mesure coût + nombre de triples bidon. Refais avec Haiku, strict=True, schéma fermé + kg_validation_schema. Compare coût, précision des triples, et latence totale. Indice/Solution : Opus ici = facture ×25 pour un gain marginal sur une tâche bornée par schéma. Le schéma fermé + strict=True coupe 60-80 % des triples parasites. Bonus : ajoute prompt caching sur le schéma/instruction → input ÷3-5.

Exo 4 — Rends l'ingestion reprenable (casse-la, puis répare-la) ​

Objectif : transformer le pipeline end-to-end en pipeline production-grade idempotent. Lance l'ingestion de 200 docs, tue le process à 50 %. Relance : combien de docs sont re-parsés ? Maintenant implémente un manifeste {doc_id, content_hash, status} qui skip les docs déjà indexed et re-parse seulement ceux dont le hash a changé. Re-tue, re-lance : zéro re-parsing inutile. Indice/Solution : la gate built.exists() du code de référence est tout-ou-rien. Remplace par un manifeste persistant (SQLite/Redis) + refresh_ref_docs. Le hash de contenu permet aussi les updates incrémentaux. C'est ce qui sépare un POC d'un pipeline qu'on ose relancer un lundi matin.

Exo 5 — Hybrid retrieval : prouve que la fusion bat chaque retriever seul ​

Objectif : démontrer chiffres à l'appui que BM25 + vector + rerank > chacun isolé, sur des requêtes adverses. Construis un eval set de 30 questions dont 10 « lexicales » (référence normative exacte type « DTU 13.2 ») et 10 « sémantiques » (paraphrases). Mesure recall pour : vector seul, BM25 seul, QueryFusionRetriever (reciprocal rerank), puis + CohereRerank. Trace où chaque méthode échoue. Indice/Solution : le vector rate les codes/références exactes (DTU 13.2 ≈ DTU 13.3 en embedding), BM25 rate les paraphrases. La fusion récupère les deux familles ; le rerank améliore la précision@k mais ajoute du coût ($1/1K req Cohere) — quantifie-le et décide.

Exo 6 — Défends le « build vs LlamaCloud » devant un CTO ​

Objectif : poser le calcul de make-vs-buy sur l'ingestion, pas l'intuition. Pour un corpus de 200K pages/mois, compare : (a) LlamaCloud growth, (b) self-hosted LlamaParse premium + Qdrant managé + ton orchestration. Inclus coût infra, coût LlamaParse à la page, coût LLM extraction, et coût humain de maintenance (jours/mois). Donne le seuil de bascule. Indice/Solution : LlamaCloud gagne sous un certain volume et quand tu n'as pas d'équipe data ; le self-host gagne au volume élevé et quand la souveraineté/le tuning du parsing comptent (régions FR via Bedrock). La bonne réponse est un seuil chiffré + les 2-3 facteurs non monétaires (souveraineté, lock-in, vitesse de delivery).

🎤 En entretien ​

Q : LangChain ou LlamaIndex pour un RAG d'entreprise ? Pourquoi pas les deux ? R : LlamaIndex pour la couche données (LlamaParse + indexes avancés + ingestion reprenable), LangGraph pour l'orchestration multi-agent stateful, raw SDK Anthropic sur le hot path de génération. Ce ne sont pas des concurrents mais des couches.

Q : Tu ingères 50K PDF, le recall est mauvais. Par où tu débugues ? R : D'abord le parsing (LlamaParse premium activé ? tables préservées ?), puis le chunking (MarkdownNodeParser, pas le splitter par défaut qui coupe les tables), puis le retrieval (top_k, hybrid BM25+vector, rerank). Je mesure recall@k à chaque étape — le problème est presque toujours en amont du retrieval, pas dans le LLM.

Q : KnowledgeGraphIndex vs PropertyGraphIndex ? R : L'ancien (legacy) extrait des triples libres, bruités et difficiles à requêter. PropertyGraphIndex (0.11+) combine embeddings ET triples typés par un schéma Pydantic + validation, et permet le retrieval hybride graph + vector. En 2026 on part sur PropertyGraphIndex, et on borne l'extraction avec SchemaLLMPathExtractor(strict=True) sur un petit modèle.

Q : Comment tu maîtrises le coût LLM d'un pipeline LlamaIndex ? R : Tiering par rôle (Haiku pour extraction/routing, Sonnet pour synthèse, Opus seulement pour l'eval offline), prompt caching sur le préfixe stable (schéma + instructions), response_mode compact plutôt que refine, et observabilité (usage agrégé par trace) pour voir les appels cachés du router/sub-question. Le piège classique : Opus partout « pour la qualité » → facture ×25 sur des tâches qu'un Haiku borné par schéma fait aussi bien.

🔗 Liens ​

Bibliothèque tech perso — Achref