RAG-Anything w 65 książkach o winie: sytuacje, w których odzyskiwanie informacji z grafu odnosi sukces, oraz te, w których ponosi porażkę.
Praktyczne sprawdzenie grafu wiedzy RAG-Anything na podstawie 65 plików PDF o winie: wybór parsera, koszty, odpowiedzi oparte na grafie w porównaniu z odpowiedziami wektorowymi oraz problem z winem musującym.
Najpewniejszym sposobem oceny frameworka jest stworzenie z jego pomocą czegoś rzeczywistego, sprawdzenie jego ograniczeń i zobaczenie, co się dzieje, zamiast polegać na testach porównawczych czy dokumencie README. W tym przypadku RAG-Anything jest właśnie tak testowany. Jest to projekt open-source z HKU, który twierdzi, że potrafi automatycznie tworzyć graf wiedzy z dokumentów multimodalnych, a materiałem testowym jest osobista biblioteka książek o winie zebrana przez osobę, która ukończyła kursy i egzaminy WSET oraz po prostu lubi ten temat.
Dlaczego wino jest dobrym testem obciążeniowym
Wino to dziedzina naturalnie oparta na relacjach. Taniny wpływają na to, jak dobrze wino się starzeje, kwasowość oddziałuje z taninami, a drożdże napędzają fermentację, która tworzy związki smakowe. Winogrona łączą się z regionami, regiony z klimatem, a klimat z stylami wina. Jeśli strukturalna ekstrakcja entytetów i relacji może przewyższyć zwykłe wyszukiwanie wektorowe w dowolnej sytuacji, powinna to zrobić również w tym przypadku. Pytanie, które musimy rozstrzygnąć: czy RAG-Anything potrafi stworzyć przydatną grafę wiedzy na podstawie rzeczywistych książek w formacie PDF i czy odpowiada na pytania dotyczące wina lepiej niż proste wyszukiwanie wektorowe?
W rzeczywistości RAG-Anything opiera się na LightRAG. Dodatkowo oferuje możliwości wielomodalnego parsowania (PDF, DOCX, obrazy), automatycznego wyodrębniania entytetów i relacji w postaci grafu oraz pięć trybów zapytań, które równoważą przeszukiwanie grafu z wyszukiwaniem wektorowym. Do narzędzi parsujących należą MinerU, Docling i PaddleOCR; działają również różne backendy oparte na LLM, a tabele, obrazy i równania są przetwarzane już w momencie wprowadzania danych. Dzięki ponad 15 000 gwiazdek na GitHubie cieszy się dużym zainteresowaniem, chociaż liczba gwiazdek to raczej nie pierwszy wskaźnik, na który należy zwrócić uwagę. Dalej: wyniki pomiarowe, kod dostosowywalny oraz informacje o tym, kiedy podejście grafowe RAG się opłaca, a kiedy nie.
Architektura
Istnieją dwie fazy: wprowadzanie danych i wyszukiwanie. W fazie wprowadzania danych plik PDF przechodzi przez ten proces, zaczynając od parsera dokumentów – albo MinerU (opartego na VLM), albo lżejszego i tańszego Doclinga:
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)
LLM czyta każdy fragment i wyodrębnia takie entity jak „Cabernet Sauvignon”, „fermentacja malolaktyczna” oraz „Bordeaux”, a także powiązania między nimi: Cabernet Sauvignon rośnie w Bordeaux, a fermentacja malolaktyczna zmniejsza kwasowość. Wyniki są przechowywane jako węzły i krawędzie w trwałym grafie GraphML.
Zapytania dostępne są w pięciu trybach, z których każdy ma własną strategię wyszukiwania. Tabela przedstawia, co każdy z nich wyszukuje, do czego nadaje się oraz w czym ma słabe strony:
+--------+------------------------------+-------------------------------+---------------------------------+
| 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 |
+--------+------------------------------+-------------------------------+---------------------------------+
Jeśli chodzi o modele, GPT-4o-mini zajmuje się generowaniem tekstu, co sprawia, że setki wywołań modeli językowych podczas wyodrębniania elementów pozostają przystępne cenowo. text-embedding-3-small wytwarza embeddingi, a przetwarzanie obrazów jest wyłączone, aby obniżyć koszty:
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,
)
Koszt stanowi rzeczywisty kompromis. Wszystkie strony wszystkich książek są analizowane, dzielone na fragmenty i przekazywane do GPT-4o-mini w celu wydobywania informacji, a umożliwienie obsługi obrazów doprowadziłoby do dodatkowych wywołań funkcji widzenia GPT-4o. W ramach tego eksperymentu wystarczyły teksty i tabele.
Korpus i parserzy
Kolekcja składała się z 65 plików PDF wybranych tak, aby obejmować różne aspekty tematu. Książki WSET to ustrukturyzowane materiały dydaktyczne zawierające tabele i specjalistyczną terminologię, natomiast pozostałe to bardziej swobodne teksty, składające się z porad w stylu listów lub historii o produkcji wina. Taka mieszanka służy do sprawdzenia, czy model potrafi syntetyzować informacje z różnych stylów pisania i na różnych poziomach złożoności.
MinerU, domyślny parser, polega na modelu wizji-języka do analizy układu strony. Jest dokładny, ale wolny. Podczas pierwszego użycia pobieranie modelu VLM trwa około 7 minut, a każda strona wymaga następnie około 15 sekund na procesorze, więc książka składająca się z 50 stron zajęła około 20 minut na analizę przed rozpoczęciem wydobywania danych. Książki o 300 stronach powodowały całkowite zawieszenie programu, nawet po podziale na fragmenty po 50 stron, z powodu wygaśnięcia czasu oczekiwania OCR.
Docling, lżejszy parser nie wymagający modelu VLM, poradził sobie z tą samą książką bez żadnych problemów, a cały proces wydobywania danych został zakończony, dlatego został użyty do przetwarzania pozostałych książek. Praktyczna lekcja polega na tym, że MinerU zapewnia lepsze zrozumienie układu strony, np. poprzez dokładniejsze wykrywanie rysunków i odróżnianie nagłówków od tekstu głównego, natomiast Docling jest bardziej odpowiedni dla systemów opartych wyłącznie na procesorze. Planuje się ponowne przetworzenie na lokalnej karcie graficznej.
Kod służący do przetwarzania danych jest krótki – wystarcza jedna wywołanie na 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()
Framework samodzielnie zajmuje się analizą, dzieleniem na fragmenty, wyodrębnianiem entytetów, budową grafu oraz wstawianiem wektorów. API jest dobrze zaprojektowane, niemal do tego stopnia, że staje się zbyt uproszczone.
Wszystkie książki zostały wyodrębnione w ciągu 6 godzin, przy średnich 30 stronach na minutę. Analiza nie stanowi wąskiego gardła – problemem są setki wywołań modeli LLM potrzebnych do wyodrębniania entytetów, ponieważ każdy fragment przechodzi przez GPT-4o-mini w celu identyfikacji entytetów i związków pomiędzy nimi. Mimo to rachunek za modele LLM bez obrazów był niewielki – około 5 dolarów za całe doświadczenie, wliczając testy.
Tabela po przetworzeniu danych podsumowuje rozmiar grafu:
+----------------------------+------------------------------+
| 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 |
+----------------------------+------------------------------+
Nie jest używana żadna zewnętrzna baza danych. Graf jest przechowywany w pliku GraphML, a dane wektorowe oraz dane typu klucz-wartość znajdują się w plikach JSON. Cała baza wiedzy mieści się w obrazie Docker bez konieczności używania dodatkowej infrastruktury.
Graf versus wektor
W centralnym eksperymencie wysłano 6 zapytań w dwóch trybach: local (tylko przeszukiwanie grafu) oraz naive (tylko porównywanie wektorów), a następnie porównano uzyskane odpowiedzi. Wybrane zapytania miały na celu sprawdzenie umiejętności rozumowania relacyjnego, porównawczego, przyczynowo-skutkowego oraz w kontekście różnych dokumentów.
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")
Wyniki każdej wyszukiwki wraz z czasem ich obliczenia przedstawiono poniżej.
+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| 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) |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+
Taniny, kwasowość i dojrzewanie. Klasyczne pytanie oparte na związkach między elementami. Graf zawiera wyraźne krawędzie od tanin do potencjału dojrzewania, od kwasowości do świeżości, a od obu do jakości. Przechodząc po tych krawędziach uzyskuje się uporządkowany opis tego, jak taniny i kwasowość na siebie wpływają, co w przeciwnym razie oznaczałoby łączenie fragmentów mogących znajdować się daleko od siebie w tekście źródłowym.
Wytwarzanie win czerwonych versus białych. Graf przedstawia ten kontrast za pomocą powiązań między elementami: wytwarzanie win czerwonych i białych łączy się z wspólnymi elementami procesu (fermentacja, wyciskanie, dojrzewanie) poprzez różne krawędzie relacyjne. Odpowiedź została uzyskana w 32,9 sekundy, podczas gdy wyszukiwanie wektorowe zajęło 42,0 sekundy, przy tym bardziej uporządkowanym wyniku.
Wina wzmocnione versus wina stołowe. W tym przypadku konieczna jest synteza danych z kilku książek, a graf umożliwił płynne łączenie elementów pomimo różnic między dokumentami.
W przypadku prostych pytań faktograficznych, takich jak to, w jaki sposób gleba wpływa na charakter wina lub jaką rolę pełni drożdże podczas fermentacji, oba tryby dostarczyły porównywalne odpowiedzi. Fragmenty zawierają te informacje bezpośrednio, więc podobieństwo wektorowe pozwala na znalezienie odpowiednich fragmentów równie skutecznie jak przeszukiwanie w sposób tradycyjny.
Najważniejsza nieudana próba
Na pytanie dotyczące metod produkcji wina musującego tryb grafowy odpowiedział, że nie ma wystarczających informacji. Ta odpowiedź składała się z 48 znaków i została udzielona w ciągu 10,7 sekundy. Tryb wektorowy z kolei zwrócił szczegółową odpowiedź o długości 2900 znaków, omawiającą metody tradycyjną, Charmat, transferową oraz ancestralną, w czasie 48,4 sekundy.
Ekstrakcja entytetów pominęła kluczowe pojęcia związane z winami musującymi. Nie istniały żadne węzły dla „méthode champenoise”, „tank method” ani „riddling”, więc zapytanie grafowe dotyczące produkcji win musujących nie znalazło nic do przeanalizowania. Graf był całkowicie nieświadomy tego tematu. Wyszukiwanie wektorowe jedynie lokalizowało fragmenty tekstu zawierające te terminy i przekazywało je modelowi językowemu, przy czym toleruje błędy w ekstrakcji, ponieważ pracuje z surowym tekstem, a nie z uporządkowanym zestawem entytetów.
Podstawowe ograniczenie polega na tym, że gdy ekstrakcja pominie jakieś pojęcie, zapytania grafowe nie będą mogły go dostrzec, więc graf jest tak kompletny, jak tylko proces ekstrakcji. Wyszukiwanie wektorowe nie ma takiego problemu. Dlatego istnieje tryb mix, a sam tryb local jest słabym wyborem.
Czas wykonywania zadań również zasługuje na uwagę. Zapytania grafowe zajmowały od 10,7 sekundy (w przypadku niepowodzenia, gdy szybko nie znaleziono żadnych wyników) do 32,9 sekundy, natomiast zapytania wektorowe od 23,1 do 48,4 sekundy. W obu przypadkach dominuje synteza za pomocą LLM, a nie wyszukiwanie, a cache znacznie przyspiesza powtarzane zapytania na podobne tematy.
Przeglądanie grafu
Aby przejrzeć sam graf, interaktywna wizualizacja stworzona przy użyciu NetworkX i pyvis pokazała 120 najbardziej połączonych elementów grafu:
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
Kolory obrazka odpowiadają kategoriom węzłów (czerwony dla winogron, niebieski dla regionów, zielony dla procesów, pomarańczowy dla rodzajów wina, fioletowy dla pojęć ogólnych), a rozmiar węzłów jest proporcjonalny do liczby połączeń.
15 najbardziej połączonych entytetów pokazuje, co dobrze udało się uchwycić w grafie. „Wino”, będące entytetem ogólnym, ma 2 136 połączeń i znajduje się na czele:
+--------------------+-------------+-----------+
| 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 |
+--------------------+-------------+-----------+
Widoczne wzorce obejmują gęsty zbiór procesów fermentacyjnych, odmiany winogron łączące się z poszczególnymi regionami oraz podzbiór produktów wina wzmacnianego (Port, Sherry, Madeira) z własnymi węzłami produkcji. Rzadko zaludnione obszary również są istotne. Koncepcje wina musującego są słabo reprezentowane, co tłumaczy nieudaną wyszukiwarkę. W zbiorze znajduje się również niewiele książek o winie musującym, więc brak może wynikać częściowo z samych danych.
Niewielki interfejs demonstracyjny
Dla udostępniania i demonstracji niebędących technicznymi, interfejs wyszukiwarki można umieścić w 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])
Aplikacja ładuje gotowy graf w około 20 sekund i nie wymaga karty graficznej do przetwarzania zapytań, ponieważ wszystkie obliczenia odbywają się poprzez wywołania API do OpenAI.
Co pokazuje eksperyment
Graph RAG wyraźnie poprawia odpowiedzi na trzy rodzaje zapytań:
- Pytania relacyjne, dotyczące wpływu jednej rzeczy na drugą: krawędzie między elementami oznaczają, że odpowiedź nie wymaga, aby obie dzieliły ten sam fragment tekstu. Łączność między taninami, kwasowością a procesem dojrzewania była wyraźnym sukcesem.
- Porównania dwóch rzeczy: kontrasty same się organizują. W produkcji czerwonego i białego wina ponawiano użycie węzłów procesowych poprzez różne krawędzie relacyjne, co zapewniało uporządkowane, obok siebie umieszczone odpowiedzi.
- Synteza z kilku książek: wyszukiwanie wektorowe również sobie z tym radzi, jednak graf wyraźnie pokazuje powiązania i nie zależy od tego, czy embeddingi znajdują się blisko siebie.
Wyróżniają się trzy słabości:
- Ekstrakcja entytetów jest słabym ogniwem. Metody dotyczące wina musującego znajdowały się w tekście surowym, ale nigdy nie stały się entytetami grafu – to ograniczenie każdego podejścia opartego na ekstrakcji.
- Zapytania typu factoid nie wymagają grafu. Pytanie takie jak „w jakiej temperaturze przechowywać wino” jest dobrze obsługiwane przez wyszukiwanie wektorowe, a graf dodaje tylko zbędny obciążenie bez żadnych korzyści.
- Koszty i opóźnienia są rzeczywiste. Przetwarzanie danych wymaga setek wywołań modeli LLM, a opóźnienie zapytań wynoszące 25 do 30 sekund jest spowodowane głównie syntezą przez modele LLM.
Macierz decyzyjna łączy rodzaje zapytań z trzema opcjami: wektorową (naiwną), grafową (lokalną) oraz mieszaną, przy czym zaleca się opcję mieszaną:
+----------------------------+----------------+---------------+-------------------+
| 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 |
+----------------------------+----------------+---------------+-------------------+
Cztery praktyczne wskazówki:
- Zawsze używaj trybu
mix. Nie należy polegać wyłącznie na wyszukiwaniu w oparciu o graf, a brak kompleksowej pokrycia jest tego dowodem. - Dla implementacji wykorzystujących tylko CPU wybierz Docling zamiast MinerU, ponieważ VLM w MinerU jest dokładny, ale kruchy.
- Przydziel czas na przetwarzanie danych, szczególnie przy większych korpusach.
- Zbadaj graf. Sprawdź, czy kluczowe koncepcje domeny rzeczywiście pojawiają się jako entity, oraz ocenij metryki struktury grafu przed poleganiem na nim.
Wynik
RAG-Anything dotrzymuje swojej głównej obietnicy. Wydobywa ustrukturyzowaną wiedzę z plików PDF do grafu, co wyraźnie poprawia zapytania relacyjne i porównawcze: analiza dała lepiej uporządkowaną odpowiedź dotyczącą tanin, kwasowości i procesu starzenia, porównanie win czerwonych z białymi przyniosło szybsze i bardziej uporządkowane wyniki, a synteza informacji z książek różnych wydawców funkcjonowała prawidłowo. Jest obiecujący, ale wciąż w młodym stadium rozwoju – na bieżąco należy obserwować niezawodność parsera oraz zakres wydobywanej informacji. Każdy, kto rozważa jego użycie w produkcji, powinien powtórzyć to doświadczenie na własnych danych, ponieważ dokument README nie pokazuje, w jakich przypadkach narzeka na błędy.
Literatura pokrewna
- Porównanie 16 architektur RAG: który wzorzec pasuje do jakiego problemu — Przegląd szesnastu wzorców RAG w czterech kategoriach, od standardowych i hybrydowych metod wyszukiwania po rozwiązania oparte na agentach, grafach i federacjach, wraz z planem budowy krok po kroku.
- Obserwowalność dla agentów AI w przedsiębiorstwach za pomocą Langfuse i OCI — Dlaczego logi, metryki i ślady nie wystarczają dla agentów GenAI oraz jak śledzić prompty, wywołania narzędzi, tokeny i sesje za pomocą otwartego oprogramowania Langfuse w agentach OCI.