Accueil / Articles

RAG-Anything sur 65 livres sur le vin : où la récupération par graphe réussit et échoue.

Une évaluation pratique du graphe de connaissances de RAG-Anything sur 65 PDFs sur le vin : choix du parseur, coûts, réponses graphiques versus vectorielles, et le point faible concernant les vins effervescents.

Éditeur : REACTAPP.TOP12 min de lecture2831 mots

Sources: I Tested RAG-Anything on 65 Wine Books. Here’s What a Knowledge Graph Gets Right — and What It Misses.

Le moyen le plus fiable pour évaluer un framework est de créer quelque chose de concret avec lui, de tester ses limites et d’observer ce qui se passe, plutôt que de se fier à des benchmarks ou à un README. C’est précisément ce traitement qui est réservé à RAG-Anything. Il s’agit d’un projet open source de l’HKU qui prétend générer automatiquement un graphe de connaissances à partir de documents multimodaux, et le matériel de test est une bibliothèque personnelle de livres sur le vin, rassemblée par quelqu’un qui a suivi les cours et les examens WSET et qui aime simplement ce sujet.

Pourquoi le vin constitue un bon test de stress

Le vin relève naturellement d’un domaine basé sur les relations. Les tanins influencent la capacité d’un vin à vieillir, l’acidité interagit avec les tanins, et les levures provoquent la fermentation qui produit des composés aromatiques. Les raisins sont liés aux régions, les régions aux climats, et les climats aux styles de vin. Si l’extraction structurée d’entités et de relations peut surpasser la recherche vectorielle classique partout ailleurs, elle devrait le faire ici également. La question à tester est la suivante : RAG-Anything parvient-il à créer un graphe de connaissances utile à partir de vrais livres PDF, et répond-il mieux aux questions sur le vin qu’une recherche vectorielle naïve ?

En réalité, RAG-Anything repose sur LightRAG. S’y ajoutent une analyse multimodale (PDF, DOCX, images), une extraction automatique d’entités et de relations pour former un graphe, ainsi que cinq modes de requête qui équilibrent la navigation dans le graphe et la récupération vectorielle. Les analyseurs utilisés incluent MinerU, Docling et PaddleOCR ; plusieurs backends d’LLM fonctionnent également, et les tableaux, les images et les équations sont traités au moment de l’ingestion. Avec plus de 15 000 étoiles sur GitHub, il bénéficie d’une véritable attention, bien que les étoiles ne soient certainement pas le premier indicateur à considérer. À suivre : des résultats mesurés, du code adaptable, et une indication sur les cas où le RAG graphique est utile et ceux où il ne l’est pas.

Architecture

Il existe deux phases : l’ingestion et la consultation. L’ingestion fait passer un PDF par ce pipeline, en commençant par un analyseur de documents, soit MinerU (basé sur un VLM), soit le plus léger et moins coûteux Docling :

PDF Document
    |
    v
[Document Parser]  ─── MinerU (VLM-based) or Docling (lighter/cheaper)
    |
    v
[Content Extraction]  ─── text, tables, images, equations
    |
    v
[Text Chunking]  ─── split into manageable pieces
    |
    v
[LLM Entity/Relation Extraction]  ─── LLM extracts entities + relationships
    |
    ├──> [Knowledge Graph]  ─── entities as nodes, relations as edges (GraphML)
    └──> [Vector Embeddings]  ─── chunks embedded for similarity search (JSON)

Un LLM lit chaque fragment et identifie des entités telles que « Cabernet Sauvignon », « fermentation malolactique » et « Bordeaux », ainsi que les liens entre elles : le Cabernet Sauvignon pousse à Bordeaux, et la fermentation malolactique adoucit l’acidité. Les résultats sont stockés sous forme de nœuds et d’arêtes dans un graphe GraphML persistant.

La consultation propose cinq modes, chacun ayant sa propre stratégie de recherche. Le tableau indique ce que chaque mode recherche, à quoi il convient et où il est faible :

+--------+------------------------------+-------------------------------+---------------------------------+
| Mode   | What It Searches             | Best For                      | Weakness                        |
+--------+------------------------------+-------------------------------+---------------------------------+
| naive  | Vector similarity only       | Factoid questions, robustness | Misses relational structure     |
| local  | Graph neighborhood traversal | Entity-specific deep dives    | Blind to entities not extracted |
| global | Community-level summaries    | Broad thematic questions      | Less specific, slower           |
| hybrid | local + global               | Balanced depth and breadth    | No vector fallback              |
| mix    | Graph + vector together      | General-purpose (recommended) | Slowest mode                    |
+--------+------------------------------+-------------------------------+---------------------------------+

Pour les modèles, GPT-4o-mini s’occupe de la génération de texte, ce qui permet de rester dans des frais abordables grâce aux centaines d’appels aux LLM lors de l’extraction d’entités. text-embedding-3-small génère des embeddings, et le traitement d’images est désactivé pour réduire les coûts :

from openai import AsyncOpenAI
from lightrag.utils import EmbeddingFunc
from raganything import RAGAnythingConfig

aclient = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# Text LLM - handles entity extraction and answer synthesis
async def llm_model_func(prompt, system_prompt=None, history_messages=None, **kwargs):
    messages = []
    if system_prompt:
        messages.append({"role": "system", "content": system_prompt})
    if history_messages:
        messages.extend(history_messages)
    messages.append({"role": "user", "content": prompt})
    response = await aclient.chat.completions.create(
        model="gpt-4o-mini", messages=messages, temperature=0.0,
    )
    return response.choices[0].message.content

# Embeddings - 1,536 dimensions, 8K token window
async def _embed_texts(texts, **kwargs):
    response = await aclient.embeddings.create(model="text-embedding-3-small", input=texts)
    return np.array([item.embedding for item in response.data])

embedding_func = EmbeddingFunc(embedding_dim=1536, max_token_size=8192, func=_embed_texts)

# Configuration - Docling parser, tables enabled, images disabled for cost
rag_config = RAGAnythingConfig(
    working_dir="./rag_storage",
    parser="docling",
    enable_image_processing=False,    # skipping images - valid for my use-case
    enable_table_processing=True,
    enable_equation_processing=True,
)

Le coût représente un véritable compromis. Toutes les pages de tous les livres sont analysées, divisées en parties et traitées par GPT-4o-mini pour l’extraction des informations, et l’ajout de la gestion des images entraînerait des appels supplémentaires à GPT-4o Vision. Pour cette expérience, le texte et les tableaux suffisaient.

Le corpus et les analyseurs

La collection comprend 65 PDF choisis pour couvrir différents aspects. Les livres WSET sont des matériaux pédagogiques structurés contenant des tableaux et un vocabulaire technique, tandis que les autres sont plus narratifs, composés de conseils ou d’histoires sur la vinification. Ce mélange permet de tester si le réseau peut synthétiser des informations provenant de styles d’écriture et de niveaux de complexité différents.

MinerU, le analyseur par défaut, s’appuie sur un modèle vision-langage pour lire la mise en page des pages. Il est approfondi mais lent. La première utilisation télécharge le VLM en environ 7 minutes, et chaque page nécessite ensuite environ 15 secondes sur le CPU ; ainsi, un livre de 50 pages a nécessité environ 20 minutes de traitement avant que l’extraction ne commence. Les livres de 300 pages le faisaient planter complètement, même lorsqu’ils étaient divisés en sections de 50 pages, en raison d’un temps d’attente insuffisant de l’OCR.

Docling, un analyseur plus léger ne nécessitant pas de VLM, a géré le même livre sans planter, et tout le processus d’extraction a été terminé, c’est pourquoi il a été utilisé pour les livres restants. La leçon pratique est que MinerU offre une compréhension plus fine de la mise en page, permettant par exemple de détecter les figures et de distinguer plus précisément les en-têtes du texte principal, tandis que Docling constitue le choix réaliste pour les configurations utilisant uniquement un CPU. Une nouvelle exécution sur une GPU locale est prévue.

Le code d’ingestion est court, avec une seule appel par document :

from raganything import RAGAnything

rag = RAGAnything(
    config=rag_config,
    llm_model_func=llm_model_func,
    embedding_func=embedding_func,
)

for pdf_path in sorted(Path("./data").glob("*.pdf")):
    file_start = time.time()
    await rag.process_document_complete(
        file_path=str(pdf_path),
        output_dir="./output",
    )
    print(f"Completed {pdf_path.name} in {time.time() - file_start:.1f}s")

await rag.finalize_storages()

Le framework gère en interne le parsing, le découpage en blocs, l’extraction d’entités, la construction de graphes et l’incorporation de vecteurs. L’API est bien conçue, au point presque de simplifier excessivement les choses.

Tous les livres ont été extraits en 6 heures, à un rythme moyen de 30 pages par minute. Le parsing n’est pas le goulot d’étranglement. Ce sont plutôt les centaines d’appels aux modèles LLM pour l’extraction d’entités, car chaque bloc est analysé par GPT-4o-mini afin d’identifier les entités et les relations. Néanmoins, la facture liée aux modèles LLM, sans compter les images, est restée faible, autour de 5 dollars pour toute l’expérience, y compris les tests d’évaluation. Le tableau suivant résume la taille du graphe obtenu :

+----------------------------+------------------------------+
| Metric                     | Value                        |
+----------------------------+------------------------------+
| Entities (graph nodes)     | 37,132                       |
| Relations (graph edges)    | 47,650                       |
| Text chunks                | 6,247                        |
| Storage on disk            | ~185 MB (all JSON + GraphML) |
| Indexed documents          | 65                           |
| Load time at query startup | ~10 seconds                  |
+----------------------------+------------------------------+

Aucune base de données externe n’est impliquée. Un fichier GraphML contient le graphe, tandis que des fichiers JSON sont utilisés pour les stockages vectoriels et les données clé-valeur. L’ensemble de la base de connaissances tient dans une image Docker, sans aucune infrastructure de soutien.

Graphique versus vectoriel

L’expérience principale a envoyé 6 requêtes via deux modes, local (seulement parcours du graphe) et naive (seulement similarité vectorielle), puis a comparé les réponses. Les requêtes ont été choisies pour tester le raisonnement relationnel, comparatif, causal et inter-documentaire.

async def compare_modes(rag, query):
    """Compare different retrieval modes on the same query."""
    for mode in ["local", "naive"]:
        result = await rag.aquery(query, mode=mode)
        print(f"[{mode}] {len(result)} chars, {elapsed:.1f}s")

Les résultats de chaque requête, accompagnés des temps d’exécution, sont présentés ci-dessous.

+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| Query                                             | Graph (local)        | Vector (naive)      | Winner                        |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| How does soil type influence wine character?      | 32.5s / 3,464 chars  | 28.2s / 2,531 chars | Tie                           |
| Relationship between tannins, acidity, and aging? | 24.8s / 2,266 chars  | 25.8s / 2,289 chars | Graph (structure)             |
| Compare red vs. white winemaking                  | 32.9s / 3,119 chars  | 42.0s / 3,423 chars | Graph (speed + structure).    |
| What role does yeast play in fermentation?        | 27.8s / 2,587 chars  | 26.8s / 2,403 chars | Tie                           |
| How do fortified wines differ from table wines?   | 28.5s / 2,635 chars  | 23.1s / 2,597 chars | Tie                           |
| Sparkling wine production methods?                | 10.7s / 48 chars     | 48.4s / 2,900 chars | Vector (graph fails)          |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+

Tannins, acidité et vieillissement. Une question relationnelle classique. Le graphe présente des arêtes explicites allant des tannins au potentiel de vieillissement, de l’acidité à la fraîcheur, et des deux vers la qualité. En suivant ces arêtes, on obtient une description structurée de l’interaction entre les tannins et l’acidité, ce qui évite de devoir combiner des éléments qui pourraient être éloignés l’un de l’autre dans le texte source.

Vinification des vins rouges versus vins blancs. Le graphe met en évidence ce contraste grâce à des liens entre entités : la vinification des vins rouges et celle des vins blancs sont chacune reliées à des entités de processus communes (fermentation, pressage, maturation) par des arêtes relationnelles différentes. Il a permis de trouver la réponse en 32,9 secondes, contre 42,0 secondes pour une recherche vectorielle, avec des résultats plus organisés.

Vins fortifiés versus vins de table. Cela nécessite une synthèse inter-documents à partir de plusieurs ouvrages, et le graphe a permis de relier les entités de manière fluide au-delà des limites des documents.

Pour des questions factuelles simples, comme la manière dont le sol influence les caractéristiques du vin ou le rôle des levures pendant la fermentation, les deux modes ont donné des réponses comparables. Les blocs contiennent ces informations directement, ce qui permet à la similarité vectorielle de retrouver les passages aussi efficacement que la recherche par parcours.

L’échec le plus important

Pour une question concernant les méthodes de production des vins mousseux, le mode graphique a indiqué qu’il ne disposait pas d’informations suffisantes. Cette réponse comptait 48 caractères et a nécessité 10,7 secondes. Le mode vectoriel, quant à lui, a fourni une réponse détaillée de 2 900 caractères couvrant les méthodes traditionnelle, Charmat, par transfert et ancestrale, en 48,4 secondes.

L’extraction d’entités avait manqué les concepts clés liés aux vins mousseux. Aucun nœud n’existait pour « méthode champenoise », « méthode en cuve » ou « riddling », de sorte qu’une requête graphique concernant la production de vins mousseux ne trouvait rien à explorer. Le graphe était donc aveugle à ce sujet. La recherche vectorielle se contentait de localiser les fragments de texte contenant ces termes et de les transmettre au LLM ; elle tolère les échecs d’extraction car elle fonctionne sur du texte brut plutôt que sur un ensemble d’entités soigneusement sélectionné.

La limite fondamentale est la suivante : lorsque l’extraction manque un concept, les requêtes graphiques ne peuvent pas le détecter, donc le graphe n’est complet que dans la mesure où le processus d’extraction l’est. La recherche vectorielle ne présente pas ce type de problème. C’est pourquoi le mode mix existe, et pourquoi choisir uniquement le mode local est une mauvaise option.

Le temps de traitement mérite également une attention particulière. Les requêtes graphiques ont passé de 10,7 secondes (en cas d’échec, lorsqu’aucun résultat n’était trouvé rapidement) à 32,9 secondes, tandis que les requêtes vectorielles sont passées de 23,1 à 48,4 secondes. La synthèse par modèles de langage grand public domine dans les deux cas plutôt que la récupération de données, et le cache rend les requêtes répétées sur des sujets similaires beaucoup plus rapides.

Examen du graphe

Pour examiner le graphe lui-même, une visualisation interactive créée à l’aide de NetworkX et pyvis a affiché les 120 entités les plus connectées :

import networkx as nx
from pyvis.network import Network

G = nx.read_graphml("rag_storage/graph_chunk_entity_relation.graphml")

# Focus on the most connected nodes (hubs)
degree_dict = dict(G.degree())
top_nodes = sorted(degree_dict, key=degree_dict.get, reverse=True)[:120]
subG = G.subgraph(top_nodes).copy()

# Categorize nodes by wine domain keywords
def categorize(name):
    lower = name.lower()
    if any(k in lower for k in ["cabernet", "merlot", "pinot", "riesling", ...]):
        return "grape"       # Red nodes
    if any(k in lower for k in ["bordeaux", "california", "champagne", ...]):
        return "region"      # Blue nodes
    if any(k in lower for k in ["fermentation", "aging", "maceration", ...]):
        return "process"     # Green nodes
    if any(k in lower for k in ["port", "sherry", "sparkling", ...]):
        return "wine_type"   # Orange nodes
    return "general"         # Purple nodes

Les couleurs des nœuds indiquent leur catégorie (rouge pour les raisins, bleu pour les régions, vert pour les processus, orange pour les types de vin, violet pour les concepts généraux) et la taille des nœuds est proportionnelle au nombre de connexions.

Les 15 entités les plus connectées montrent ce que le graphique a bien capturé. « Vin », une entité générale, arrive en tête avec 2 136 connexions :

+--------------------+-------------+-----------+
| Entity             | Connections | Category  |
+--------------------+-------------+-----------+
| Wine               | 2,136       | General   |
| Wine Production    | 1,344       | General   |
| Italian Wines      | 768         | General   |
| Bordeaux           | 442         | Region    |
| Cabernet Sauvignon | 420         | Grape     |
| Riesling           | 412         | Grape     |
| Champagne          | 348         | Region    |
| California         | 321         | Region    |
| Grapes             | 287         | Grape     |
| Port               | 243         | Wine Type |
+--------------------+-------------+-----------+

Les motifs visibles comprennent un groupe dense de processus de fermentation, des variétés de raisin qui relient ces processus aux régions concernées, ainsi qu’un sous-groupe d’entités de vins fortifiés (Port, Sherry, Madeira) disposant de leurs propres nœuds de production. Les zones peu densément peuplées sont tout aussi révélatrices. Les concepts liés aux vins mousseux sont sous-représentés, ce qui explique l’échec de la requête. La collection contient également peu de livres sur les vins mousseux, donc ce manque pourrait provenir en partie des données.

Une petite interface de démonstration

Pour les partages et les démonstrations non techniques, l’interface de requête peut être intégrée dans Gradio :

import gradio as gr


def query_wine(question, mode):
    t0 = time.time()
    result = _run_async(_query(question, mode))
    elapsed = time.time() - t0
    return result, f"**Mode:** {mode} | **Time:** {elapsed:.1f}s"

with gr.Blocks(title="Wine Knowledge RAG") as demo:
    question = gr.Textbox(label="Ask a wine question", lines=2)
    mode = gr.Radio(
        choices=["mix", "local", "global", "hybrid", "naive"],
        value="mix", label="Retrieval Mode",
    )
    submit_btn = gr.Button("Ask", variant="primary")
    stats = gr.Markdown("")
    answer = gr.Markdown(label="Answer")
    submit_btn.click(fn=query_wine, inputs=[question, mode], outputs=[answer, stats])

L’application charge le graphe prédéfini en environ 20 secondes et n’a pas besoin de GPU pour les requêtes, puisque tous les calculs s’effectuent via des appels API vers OpenAI.

Ce que montre l’expérience

Graph RAG améliore clairement les réponses pour trois types de requêtes :

  • Les questions relationnelles, qui demandent comment une chose influence une autre : les arêtes entre entités signifient que la réponse n’exige pas que celles-ci partagent un même fragment de texte. Le lien entre les tanins, l’acidité et le vieillissement a été un exemple clair de succès.
  • Les comparaisons entre deux éléments : les contrastes s’organisent automatiquement. La vinification des vins rouges et blancs a réutilisé des nœuds de processus à travers différentes arêtes relationnelles, ce qui a permis d’obtenir des réponses bien structurées côte à côte.
  • La synthèse à partir de plusieurs livres : la recherche vectorielle peut également s’en charger, mais le graphe met en évidence les connexions et ne dépend pas du fait que les embeddings se trouvent à proximité les uns des autres.

Trois faiblesses ressortent :

  • L’extraction d’entités constitue le maillon faible. Les méthodes relatives aux vins effervescents étaient présentes dans le texte brut mais n’ont jamais été transformées en entités graphiques, ce qui représente une limite de toute approche basée sur l’extraction.
  • Les requêtes factuelles n’ont pas besoin de graphe. Une question comme « À quelle température conserver le vin ? » peut être traitée efficacement par une recherche vectorielle, tandis que l’usage d’un graphe ajoute des coûts inutiles sans apporter de bénéfice.
  • Les coûts et les délais sont réels. L’ingestion des données nécessite des centaines d’appels aux modèles de langage grand public, et les délais de réponse de 25 à 30 secondes sont principalement dus à la synthèse par ces modèles.

Une matrice de décision associe les types de requêtes aux trois options : vectorielle (naïve), graphique (locale) et mixte, la solution mixte étant recommandée :

+----------------------------+----------------+---------------+-------------------+
| Query Type                 | Vector (naive) | Graph (local) | Mix (recommended) |
+----------------------------+----------------+---------------+-------------------+
| Factoid lookup             | Good           | Good          | Good              |
| Relational ("X affects Y") | OK             | Best          | Best              |
| Comparative ("A vs B")     | OK             | Best          | Best              |
| Cross-document synthesis   | OK             | Good          | Best              |
| Topic with extraction gaps | Best           | Fails         | Good              |
+----------------------------+----------------+---------------+-------------------+

Quatre conseils pratiques :

  1. Utilisez toujours le mode mix. On ne doit pas compter uniquement sur la recherche par graphe, et l’absence d’une couverture complète en est la raison.
  2. Préférez Docling à MinerU pour les déploiements utilisant uniquement le CPU, car le VLM de MinerU est approfondi mais fragile.
  3. Prévoyez du temps pour l’ingestion, surtout pour des corpus plus importants.
  4. Examinez le graphe. Vérifiez que les concepts clés du domaine apparaissent bien comme entités, et évaluez les métriques structurelles du graphe avant de vous y fier.

Verdict

RAG-Anything tient sa promesse principale. Il extrait les connaissances structurées des PDF pour les intégrer dans un graphe, ce qui améliore nettement les requêtes relationnelles et comparatives : l’analyse a permis d’obtenir des réponses plus organisées concernant le tanin, l’acidité et le vieillissement du vin, la comparaison entre vins rouges et blancs s’est affichée plus rapidement et de manière plus claire, et la synthèse d’informations provenant de livres édités par différents éditeurs a fonctionné. Il est prometteur mais encore jeune, la fiabilité du analyseur et la couverture de l’extraction étant les aspects à surveiller. Toute personne envisageant de l’utiliser en environnement de production devrait répéter cette expérience avec ses propres données, car un fichier README ne montre pas où il peut échouer.

Lectures complémentaires

  • Notes pratiques : J’ai créé un pipeline RAG hybride à partir des documents de la SEC — Voici les détails — Guide pas à pas des Notes pratiques : J’ai créé un pipeline RAG hybride à partir des documents de la SEC — Voici les détails : contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes souhaitant mettre en œuvre ce modèle.
  • Votre API est-elle prête pour les agents IA ? Une liste de contrôle pratique pour la préparation MCP — Ce qui transforme une API fonctionnelle en quelque chose que les agents peuvent utiliser de manière fiable : documents, schémas, authentification, choix des points de terminaison, descriptions, tests, hébergement et gestion des versions.
  • Migrer un serveur MCP vers la spécification 2026-07-28 : vingt minutes, étape par étape — Les sessions ont disparu sans que rien ne tombe en panne. Découvrez les changements apportés par MCP 2026-07-28, la migration vers le SDK Python 2.0 en cinq étapes chronométrées, la réécriture de MRTR et ce que signifie la dépréciation.
  • TypeScript 7 et la version pour Go : des compilations plus rapides, une utilisation moindre de la mémoire et des retours en temps réel depuis l’éditeur — TypeScript 7 remplace le compilateur par un moteur natif Go. Compilations en parallèle, consommation mémoire réduite, un nouveau serveur de langage, ainsi que les points à vérifier avant de procéder à la migration.
  • Comparaison de quatre modèles multi-agents : sous-agents, compétences, transferts et routeurs — Une comparaison pratique des quatre architectures multi-agents fondamentales, indiquant le coût de chacune, leur domaine d’application, ainsi que la manière de choisir celle qui convient sans surconstruire.