RAG-Anything bei 65 Weinbüchern: Wo die Graphenabfrage erfolgreich ist und wo sie versagt.
Ein praktischer Test des Wissensgraphen von RAG-Anything anhand von 65 Wein-PDFs: Auswahl des Parsers, Kosten, Vergleich von Graphen- und Vektorantworten sowie das Problem bei Schaumweinen.
Der zuverlässigste Weg, ein Framework zu beurteilen, besteht darin, damit etwas Reales zu erstellen, seine Grenzen auszutesten und zu beobachten, was passiert – anstatt Benchmarks oder eine README-Datei zu vertrauen. Genau das wird bei RAG-Anything getan. Es handelt sich um ein Open-Source-Projekt der HKU, das behauptet, automatisch ein Wissensgraph aus multimodalen Dokumenten erstellen zu können. Das Testmaterial besteht aus einer persönlichen Bibliothek an Weinführerbüchern, die von jemandem zusammengestellt wurde, der den WSET-Kurs und die Prüfungen absolviert hat und einfach ein Interesse an diesem Thema hat.
Warum Wein ein guter Stresstest ist
Wein ist ein von Natur aus relationales Gebiet. Tannine bestimmen, wie gut ein Wein reift, Säure wirkt mit Tanninen zusammen, und Hefen treiben die Gärung an, die Aromastoffe erzeugt. Reben verbinden sich mit Regionen, Regionen mit Klimazonen und Klimazonen mit Stilrichtungen. Wenn eine strukturierte Extraktion von Entitäten-Beziehungen überall eine bessere Leistung als eine einfache Vektorsuche erbringen kann, sollte das auch hier der Fall sein. Die zu prüfende Frage lautet: Erstellt RAG-Anything einen nützlichen Wissensgraphen aus echten PDF-Büchern und beantwortet es Weinfragen besser als eine naive Vektorsuche?
Im Inneren umhüllt RAG-Anything LightRAG. Darauf aufbauend kommen multimodales Parsen (PDF, DOCX, Bilder), die automatische Extraktion von Entitäten und Beziehungen in einen Graphen sowie fünf Abfragemodi, die das Durchlaufen des Graphen mit der Vektorabrufung abwägen. Zu den Parsern gehören MinerU, Docling und PaddleOCR; mehrere LLM-Backends sind nutzbar, und Tabellen, Bilder sowie Gleichungen werden bereits bei der Eingabe verarbeitet. Mit über 15.000 GitHub-Stars genießt es tatsächlich Aufmerksamkeit – doch Sterne sind kaum der erste Indikator, den man prüfen sollte. Im Folgenden: gemessene Ergebnisse, anpassbarer Code sowie eine Übersicht darüber, wann graphbasiertes RAG sinnvoll ist und wann nicht.
Architektur
Es gibt zwei Phasen: Eingabe und Abfrage. Bei der Eingabe wird ein PDF durch diesen Pipeline-Prozess geleitet, beginnend mit einem Dokumentparker – entweder MinerU (basierend auf VLMs) oder der leichtere und günstigere 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)
Ein LLM liest jeden Abschnitt und extrahiert Entitäten wie „Cabernet Sauvignon“, „Malolaktische Gärung“ und „Bordeaux“ sowie die Verbindungen dazwischen: Cabernet Sauvignon wächst in Bordeaux, und die Malolaktische Gärung verringert die Säure. Die Ergebnisse werden als Knoten und Kanten in einem persistenten GraphML-Graphen gespeichert.
Die Abfragen bieten fünf Modi, von denen jeder seine eigene Suchstrategie hat. Die Tabelle listet auf, wonach jeder Modus sucht, wofür er geeignet ist und wo seine Schwächen liegen:
+--------+------------------------------+-------------------------------+---------------------------------+
| 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 |
+--------+------------------------------+-------------------------------+---------------------------------+
Was die Modelle betrifft, so übernimmt GPT-4o-mini die Textgenerierung, wodurch die hunderten Anrufe an große Sprachmodelle bei der Entitätsextraktion erschwinglich bleiben. text-embedding-3-small erzeugt Embeddings, und der Bildverarbeitung wird aus Kostengründen verzichtet:
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,
)
Der Kostenfaktor stellt eine echte Abwägung dar. Alle Seiten aller Bücher werden analysiert, in Blöcke aufgeteilt und an GPT-4o-mini übermittelt, um Informationen zu extrahieren; die Unterstützung von Bildverarbeitung würde zusätzliche Aufrufe von GPT-4o Vision hinzufügen. Für dieses Experiment reichten Texte und Tabellen aus.
Das Korpus und die Parser
Die Sammlung umfasste 65 PDF-Dateien, die ausgewählt wurden, um unterschiedliche Aspekte abzudecken. Die WSET-Bücher sind strukturiertes Lehrmaterial mit Tabellen und technischem Vokabular, während andere in Form von Tipps im Stil von Listicles oder Geschichten über den Weinanbau verfasst sind. Diese Mischung dient dazu, zu prüfen, ob das System in der Lage ist, unterschiedliche Schreibstile und Komplexitätsgrade zu verarbeiten.
MinerU, der Standard-Parser, nutzt ein Vision-Language-Modell, um die Seitenstruktur zu erkennen. Er ist gründlich, aber langsam. Beim ersten Einsatz wird das VLM in etwa 7 Minuten heruntergeladen, und jede Seite benötigt anschließend rund 15 Sekunden auf der CPU. Daher dauerte das Parsen eines 50-seitigen Buches etwa 20 Minuten, bevor mit der Extraktion begonnen werden konnte. 300-seitige Bücher führten sogar dann zum Absturz, wenn sie in 50-seitige Abschnitte aufgeteilt wurden, weil der OCR abbrach.
Docling, ein leichterer Parser ohne Anforderung an ein VLM, bewältigte dasselbe Buch ohne Absturz, und der gesamte Extraktionsprozess wurde abgeschlossen. Deshalb wurde er für die verbleibenden Bücher verwendet. Die praktische Erkenntnis ist, dass MinerU ein besseres Verständnis der Seitenstruktur bietet – beispielsweise durch die genaue Erkennung von Abbildungen sowie die bessere Unterscheidung zwischen Überschriften und Textinhalt – während Docling die realistische Wahl für Systeme mit nur CPU ist. Eine erneute Ausführung auf einer lokalen GPU ist geplant.
Der Einpflegungscode ist kurz, es erfolgt eine Aufruf pro Dokument:
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()
Das Framework kümmert sich intern um die Parsing, Chunking, Entity-Extraktion, Graphenerstellung sowie Vector-Embedding. Die API ist gut gestaltet – fast bis zum Punkt der Übervereinfachung.
Sämtliche Bücher wurden in 6 Stunden extrahiert, mit durchschnittlich 30 Seiten pro Minute. Parsing ist nicht das Engpassproblem. Die Hunderte von LLM-Aufrufen zur Entity-Extraktion schon, da jeder Chunk durch GPT-4o-mini zur Identifizierung von Entitäten und Beziehungen geleitet wird. Dennoch blieb die Rechnung für die LLMs ohne Bilder gering – etwa 5 Dollar für das gesamte Experiment einschließlich der Evaluierungsläufe. Die Tabelle nach der Verarbeitung fasst die Größe des Graphen zusammen:
+----------------------------+------------------------------+
| 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 |
+----------------------------+------------------------------+
Es werden keine externen Datenbanken verwendet. Eine GraphML-Datei enthält das Graph, wobei JSON-Dateien für die Vektorlagern sowie für Schlüssel-Wert-Daten dienen. Die gesamte Wissensbasis passt in ein Docker-Bild ohne zusätzliche Infrastruktur.
Graph gegen Vektor
Im zentralen Experiment wurden 6 Abfragen über zwei Modi durchgeführt – local (nur Graph-Exploration) und naive (nur Vektorähnlichkeit) – anschließend wurden die Antworten verglichen. Die Abfragen wurden so gewählt, dass relationales, vergleichendes, kausales sowie mehrdokumentenbasiertes Denken getestet werden konnte.
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")
Die Ergebnisse für jede Abfrage zusammen mit den Zeiten sind unten aufgeführt.
+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| 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) |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+
Tannine, Säure und Reifung. Eine klassische relationale Frage. Das Diagramm weist explizite Kanten von Tanninen zum Reifungspotenzial, von der Säure zur Frische sowie von beiden zur Qualität auf. Der Weg entlang dieser Kanten liefert eine strukturierte Darstellung davon, wie Tannine und Säure miteinander interagieren – ansonsten würde man Teile zusammenfassen müssen, die im Quelltext weit voneinander entfernt liegen.
Rote gegen weiße Weinausstellung. Das Diagramm drückt den Kontrast durch Entitätsverbindungen aus: Sowohl die rote als auch die weiße Weinausstellung sind über unterschiedliche Beziehungs-Kanten mit gemeinsamen Prozessentitäten (Gärung, Pressen, Reifung) verbunden. Die Antwort wurde in 32,9 Sekunden ermittelt, während die Vektorsuche 42,0 Sekunden benötigte – und das Ergebnis war besser strukturiert.
Stärkere gegen Tafelweine. Dafür ist eine Synthese mehrerer Bücher erforderlich, und das Diagramm verband die Entitäten nahtlos über Dokumentgrenzen hinweg.
Zu einfachen Faktensfragen, wie zum Beispiel wie der Boden den Charakter des Weins prägt oder welche Rolle Hefe bei der Gärung spielt, gaben die beiden Modi vergleichbare Antworten. Die Textabschnitte enthalten diese Informationen direkt, wodurch die vektorbasierte Ähnlichkeitsprüfung die Passagen genauso effektiv ausfindig macht wie die manuelle Durchsuchung.
Der wichtigste Fehler
Bei einer Frage zu den Herstellungsverfahren von Sekt antwortete der Graph-Modus, dass er nicht genügend Informationen habe. Diese Antwort bestand aus 48 Zeichen und dauerte 10,7 Sekunden. Der vektorbasierte Modus lieferte hingegen eine detaillierte Antwort mit 2.900 Zeichen, die die traditionellen, Charmat-, Transfer- und ancestralen Herstellungsverfahren erläuterte, und benötigte dafür 48,4 Sekunden.
Die Entitätenextraktion hatte die wichtigsten Begriffe bezüglich Schaumweinherstellung übersehen. Es existierten keine Knoten für „Methode Champenoise“, „Tankmethode“ oder „Riddling“, weshalb eine Graphenabfrage zur Erfassung der Schaumweinproduktion nichts zum Durchsuchen fand. Der Graph war somit für dieses Thema blind. Die Vektorabfrage fand lediglich Textabschnitte, die diese Begriffe enthielten, und leitete sie an das große Sprachmodell weiter; zudem toleriert sie Fehlschläge bei der Extraktion, da sie mit Rohtext statt mit einem sorgfältig zusammengestellten Entitätenkatalog arbeitet.
Die grundlegende Einschränkung besteht darin, dass Graphenabfragen ein Konzept nicht erkennen können, wenn die Extraktion es übersieht – der Graph ist daher nur so vollständig wie der Extraktionsprozess. Die Vektorabfrage weist keinen solchen Fehlermechanismus auf. Deshalb gibt es den mix-Modus, und deshalb ist local allein eine schlechte Wahl.
Auch die Laufzeiten verdienen eine Betrachtung. Graphabfragen dauerten von 10,7 Sekunden (bei einem fehlgeschlagenen Versuch, bei dem schnell nichts gefunden wurde) bis zu 32,9 Sekunden, und Vektorabfragen von 23,1 bis 48,4 Sekunden. Die Synthese mit großen Sprachmodellen dominiert in beiden Fällen, nicht die Informationsabrufung, und das Caching macht wiederholte Abfragen zu ähnlichen Themen deutlich schneller.
Betrachtung des Graphen
Zur Inspektion des Graphen selbst zeigte eine interaktive Visualisierung, die mit NetworkX und pyvis erstellt wurde, die 120 am stärksten vernetzten Knotenentitäten:
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
Die Bilder färben die Knoten nach Kategorie ein (rot für Trauben, blau für Regionen, grün für Prozesse, orange für Weintypen, lila für allgemeine Konzepte) und skalieren die Knotengröße nach der Anzahl der Verbindungen.
Die 15 am stärksten vernetzten Entitäten zeigen, was das Diagramm gut erfassen konnte. „Wein“, eine allgemeine Entität, führt mit 2.136 Verbindungen:
+--------------------+-------------+-----------+
| 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 |
+--------------------+-------------+-----------+
Sichtbare Muster umfassen eine dichte Ansammlung von Gärprozessen, Rebsorten, die auf Regionen verweisen, sowie eine Untergruppe von Herstellern von Starkweinen (Port, Sherry, Madeira) mit ihren eigenen Produktionsstandorten. Auch dünn besiedelte Bereiche liefern wertvolle Informationen. Konzepte zu Schaumweinen sind unterrepräsentiert, was die fehlgeschlagene Abfrage erklärt. Die Sammlung enthält außerdem nur wenige Bücher über Schaumweine, sodass der Mangel teilweise auf die Daten zurückzuführen sein könnte.
Eine kleine Demo-Oberfläche
Zur Weitergabe sowie für nicht-technische Demonstrationen kann die Abfrageschnittstelle in Gradio eingebettet werden:
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])
Die App lädt das im Voraus erstellte Graph in etwa 20 Sekunden und benötigt für Abfragen keine GPU, da alle Berechnungen über API-Aufrufe an OpenAI abgewickelt werden.
Was das Experiment zeigt
Graph RAG verbessert eindeutig die Antworten bei drei Arten von Abfragen:
- Relationale Fragen, bei denen gefragt wird, wie eine Sache eine andere beeinflusst: Kanten zwischen Entitäten bedeuten, dass die Antwort nicht erfordert, dass beide einen Textabschnitt teilen. Der Zusammenhang zwischen Tanninen, Säuregehalt und Reifung war ein klares Erfolgsbeispiel.
- Vergleiche zwischen zwei Dingen: Kontraste ordnen sich selbst. Bei der Rotwein- und Weißweinherstellung wurden Prozessknoten über verschiedene Beziehungsverbindungen wiederverwendet, was übersichtliche nebeneinanderstehende Antworten ermöglichte.
- Synthese aus mehreren Büchern: Auch die Vektorsuche kann damit umgehen, doch das Graph macht die Verbindungen deutlich und hängt nicht davon ab, dass Embeddings in der Nähe voneinander liegen.
Drei Schwächen fallen auf:
- Die Entitätenextraktion ist die Schwachstelle. Methoden für Sekt waren im Rohtext enthalten, wurden aber nie zu Graphen-Entitäten – das ist eine Beschränkung jeder auf Extraktion basierenden Herangehensweise.
- Factoid-Fragen benötigen keinen Graphen. Eine Frage wie „Bei welcher Temperatur sollte Wein aufbewahrt werden?“ lässt sich mit einer Vektorsuche gut beantworten; der Graph führt lediglich zu zusätzlichem Aufwand ohne Vorteil.
- Kosten und Latenz sind real. Die Eingabe erfordert Hunderte von Aufrufen an große Sprachmodelle, und die Abfragezeit von 25 bis 30 Sekunden wird hauptsächlich durch die Synthese durch große Sprachmodelle bestimmt.
Eine Entscheidungsmatrix ordnet die Anfragearten den drei Optionen zu: Vektor (naiv), Graph (lokal) und Mischung, wobei die Mischung empfohlen wird:
+----------------------------+----------------+---------------+-------------------+
| 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 |
+----------------------------+----------------+---------------+-------------------+
Vier praktische Erkenntnisse:
- Verwenden Sie immer den
mix-Modus. Auf die reine Graphenabfrage sollte man sich nicht verlassen, und die fehlende umfassende Abdeckung zeigt den Grund dafür. - Wählen Sie für Deployment-Systeme mit nur CPU Docling statt MinerU, da MinerUs VLM zwar gründlich, aber anfällig ist.
- Planen Sie Zeit für den Dateneingang ein, insbesondere bei größeren Korpora.
- Prüfen Sie den Graphen. Stellen Sie sicher, dass die Schlüsselkonzepte des Domänenbereichs tatsächlich als Entitäten auftauchen, und bewerten Sie vor der Nutzung die strukturellen Graphenmetriken.
Fazit
RAG-Anything hält sein Hauptversprechen. Es zieht strukturiertes Wissen aus PDFs in ein Graphenmodell, das relationale und vergleichende Abfragen deutlich verbessert: Die Durchsuchung lieferte eine besser strukturierte Antwort zu Tannin, Säuregehalt und Reifung, der Vergleich zwischen Rot- und Weißweinen erfolgte schneller und übersichtlicher, und die Synthese aus Büchern verschiedener Verleger funktionierte. Es ist vielversprechend, aber noch jung – bei der Zuverlässigkeit des Parsers sowie dem Umfang der Extraktion sind besonders die Aspekte zu beobachten. Jeder, der es für die Produktion in Betracht zieht, sollte dieses Experiment mit eigenen Daten wiederholen, da eine README-Datei nicht zeigt, an welchen Stellen es versagt.
Verwandte Artikel
- Vergleich von 16 RAG-Architekturen: Welches Muster passt zu welchem Problem — Eine Übersicht über sechzehn RAG-Muster in vier Kategorien, von standardmäßigen und hybriden Suchmethoden bis hin zu agierenden, grafbasierten und federierten Designs, inklusive eines monat für Monat detaillierten Aufbauplans.
- Observabilität für Unternehmens-KI-Agenten mit Langfuse und OCI — Warum Logs, Metriken und Spuren für GenAI-Agenten unzureichend sind und wie man Anfragen, Toolaufrufe, Tokens sowie Sitzungen mithilfe des Open-Source-Tools Langfuse auf OCI Agents nachverfolgen kann.