Головна / Статті

RAG-Anything на 65 книгах про вино: де метод отримання інформації з графів є ефективним, а де — ні.

Практичний тест графа знань RAG-Anything на 65 PDF-файлах про вино: вибір парсера, витрати, відповіді у вигляді графів проти векторів та проблема з ігристим вином.

Видавець: REACTAPP.TOP12 хв читання2831 слів

Джерела: I Tested RAG-Anything on 65 Wine Books. Here’s What a Knowledge Graph Gets Right — and What It Misses.

Найнадійніший спосіб оцінити фреймворк — це створити з ним щось реальне, дійти до його меж та виміряти, що відбувається, замість того, щоб покладатися на тест-дані чи документацію. Саме так підходять до проекту RAG-Anything. Це проект з відкритим кодом від HKU, який стверджує, що може автоматично створювати граф знань з багатомодальних документів, а матеріалом для тестування є особиста бібліотека книг про вино, зібрана людиною, яка пройшла курси та іспити WSET та просто обожнює цю тему.

Чому вино — хороший інструмент для тестування на стрес

Вино — це сфера, яка за своєю природою ґрунтується на взаємозв’язках. Таніни впливають на те, наскільки добре вино старіє, кислотність взаємодіє з танінами, а дріжджі сприяють ферментації, яка створює ароматичні сполуки. Виноград пов’язує регіони, регіони — клімат, а клімат — стилі виробництва вина. Якщо структуроване вилучення сутностей та їхніх взаємозв’язків може перевершити звичайний векторний пошук у будь-якій ситуації, то це має статися й тут. Питання, яке перевіряється: чи може RAG-Anything створити корисну графіку знань з справжніх PDF-книг та чи він краще, ніж простий векторний пошук, відповідає на запитання про вино?

Під капотом RAG-Anything використовує LightRAG. До цього додається багатомодальний парсинг (PDF, DOCX, зображення), автоматична екстракція ентитетів та взаємозв’язків у вигляді графа, а також п’ять режимів запитів, які збалансовують просування по графу та пошук за векторами. Серед парсерів використовуються MinerU, Docling та PaddleOCR; працюють кілька бекендів на основі LLM, а таблиці, зображення та рівняння обробляються під час введення даних. З понад 15 000 зірок на GitHub цей проект привертає справжню увагу, хоча кількість зірок — це зовсім не перший показник для оцінки. Далі: числові результати, адаптований код та пояснення, коли використання графового RAG є ефективним, а коли — ні.

Архітектура

Існує два етапи: введення даних та запитування. На етапі введення PDF проходить через цей потік обробки, починаючи з парсера документів — або MinerU (на основі VLM), або простішого та дешевшого 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)

LLM читає кожен фрагмент та виділяє такі ентитети, як „Cabernet Sauvignon“, „малолактична ферментація“ та „Бордо“, а також зв’язки між ними: Cabernet Sauvignon росте в Бордо, а малолактична ферментація зменшує кислотність. Результати зберігаються у вигляді вузлів та ребер у постійній структурі GraphML.

Для пошуку існує п’ять режимів, кожен з яких має власну стратегію отримання інформації. У таблиці наведено, що шукає кожен режим, до чого він підходить та в чому має слабкості:

+--------+------------------------------+-------------------------------+---------------------------------+
| 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                    |
+--------+------------------------------+-------------------------------+---------------------------------+

Щодо моделей, GPT-4o-mini відповідає за генерацію тексту, що дозволяє залишати сотні викликів LLM під час вилучення елементів доступними з точки зору витрат. text-embedding-3-small створює ембеддинги, а обробка зображень вимкнена для зниження витрат:

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,
)

Витрати є справжньою проблемою. Усі сторінки всіх книг аналізуються, розділяються на частини та обробляються за допомогою GPT-4o-mini для отримання інформації, а додавання можливості обробки зображень призведе до додаткових викликів GPT-4o для розпізнавання зображень. Для цього експерименту вистачило тексту та таблиць.

Корпус та парсери

Колекція складалась з 65 PDF-файлів, обраних для охоплення різних аспектів. Книги WSET — це структуровані навчальні матеріали з таблицями та технічною термінологією, тоді як інші книги мають більш діалогічний стиль, складаючись із порад у форматі списків чи історій про виробництво вина. Саме таке поєднання дозволяє перевірити, чи може граф синтезувати інформацію з різних стилів письма та рівнів складності.

MinerU, стандартний парсер, використовує модель зору та мови для аналізу структури сторінки. Він є ретельним, але повільним. Під час першого використання завантаження моделі займає близько 7 хвилин, а обробка кожної сторінки на процесорі триває приблизно 15 секунд, тож обробка книги з 50 сторінок займала близько 20 хвилин перед початком вилучення даних. Книги з 300 сторінками призводили до повного зупинення роботи програми, навіть після розділення на частини по 50 сторінок, через тайм-аут OCR.

Docling – це легший парсер, який не вимагає моделі зору та мови, і він успішно обробив ту саму книгу без збоїв, завершивши весь процес вилучення даних, тому його використовували для решти книг. Практичний висновок полягає у тому, що MinerU забезпечує краще розуміння структури сторінки, наприклад, точніше виявляє ілюстрації та розрізняє заголовки від основного тексту, тоді як Docling є більш підходящим варіантом для систем з процесором без додаткових пристроїв. Планується повторна обробка на локальній відеокарті.

Код для обробки документів є коротким – достатньо одного виклику на кожен документ:

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()

Ця платформа внутрішньо обробляє парсинг, розділення на частини, вилучення ентитетів, створення графа та векторне закодування. API добре спроєктовано, майже до рівня надмірної спрощеності.

Усі книги були вилучені протягом 6 годин зі середньою швидкістю 30 сторінок на хвилину. Парсинг не є вузьким місцем. Проблемою є сотні викликів ШІ для вилучення ентитетів, адже кожна частина проходить обробку через GPT-4o-mini для визначення ентитетів та зв’язків. Проте рахунок за використання ШІ без зображень був невеликим — близько 5 доларів за весь експеримент, включаючи тести. У таблиці після обробки наведено розмір графа:

+----------------------------+------------------------------+
| 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                  |
+----------------------------+------------------------------+

Не використовується жодна зовнішня база даних. Граф зберігається у файлі GraphML, а дані для векторних сховищ та ключ-значення — у JSON-файлах. Повна база знань поміщається у образ Docker без необхідності додаткової інфраструктури.

Граф проти векторів

У центральному експерименті було надіслано 6 запитів через два режими: local (лише просування по графу) та naive (лише порівняння векторів), після чого порівнювалися відповіді. Запити були обрані для перевірки реляційного, порівняльного, причинно-наслідкового та міждокументного міркування.

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")

Результати кожного запиту разом із часом виконання наведені нижче.

+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| 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)          |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+

Таніни, кислотність та процес старіння. Класичне питання про взаємозв’язки. У графі є прямі зв’язки від танінів до потенціалу старіння, від кислотності до свіжості, а також від обох до якості. Пройшовши ці зв’язки, можна отримати структурований опис того, як таніни та кислотність взаємодіють, що інакше означало б поєднання фрагментів, які можуть знаходитися далеко один від одного у вихідному тексті.

Виробництво червоного та білого вина. Граф відображає цей контраст за допомогою зв’язків між елементами: виробництво червоного та білого вина кожне пов’язане зі спільними елементами процесу (ферментація, пресування, дозрівання) через різні зв’язки. Він дав відповідь за 32,9 секунди, тоді як векторний пошук зайняв 42,0 секунди, причому результат був більш організованим.

Укріплене та звичайне вино. Для цього потрібна синтеза з кількох документів, і граф плавно пов’язував елементи між межами документів.

Щодо простих запитань на факти, таких як те, як ґрунт впливає на характер вина чи що робить дріжджі під час ферментації, обидва режими давали схожі відповіді. Фрагменти містять цю інформацію безпосередньо, тому метод векторної схожості знаходить відповідні уривки так само ефективно, як і метод перебору.

Найбільш значуща невдача

На запитання про методи виробництва ігристого вина режим графа відповів, що у нього недостатньо інформації. Ця відповідь складалася з 48 символів та була сформована за 10,7 секунд. Режим векторів надав детальну відповідь довжиною 2 900 символів, яка охоплювала традиційний, Шармат, трансферний та анцестральний методи, протягом 48,4 секунд.

Екстракція ентитетів пропустила ключові поняття, пов’язані з ігристими винами. Не існувало вузлів для термінів „метод Шампенуа“, „метод у резервуарі“ чи „ріддлінг“, тому запит до графа щодо виробництва ігристих вин не знайшов нічого для аналізу. Граф був необізнаний щодо цієї теми. Векторний пошук просто знаходив фрагменти тексту, що містили ці терміни, та передавав їх ШІ, яке толерує помилки екстракції, оскільки працює з необробленим текстом, а не з підготовленим набором ентитетів.

Основне обмеження полягає у наступному: коли екстракція пропускає певне поняття, запити до графа не можуть його побачити, тож граф є настільки ж повним, наскільки повним є процес екстракції. У векторного пошуку немає такої проблеми. Саме тому існує режим mix, і саме тому використання лише режиму local є поганим вибором.

Також варто звернути увагу на час виконання. Час виконання запитів до графа зріс від 10,7 секунд (у разі невдачі, коли нічого не знаходилося швидко), до 32,9 секунд, а запитів до векторів — від 23,1 до 48,4 секунд. У обох випадках домінує синтез за допомогою LLM, а не пошук інформації, а кешування значно прискорює повторні запити на схожі теми.

Огляд графа

Щоб переглянути сам граф, інтерактивна візуалізація, створена за допомогою NetworkX та pyvis, показала 120 найбільш пов’язаних між собою елементів графа:

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

Кольори елементів на зображенні відповідають категоріям (червоний — для винограду, синій — для регіонів, зелений — для процесів, помаранчевий — для типів вина, фіолетовий — для загальних понять), а розмір елементів змінюється залежно від кількості зв’язків.

15 найбільш пов’язаних між собою елементів демонструють те, що граф ефективно зафіксував. „Вино“, як загальний елемент, лідирує з 2 136 зв’язками:

+--------------------+-------------+-----------+
| 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 |
+--------------------+-------------+-----------+

Серед помітних закономірностей — щільний скупчення процесів ферментації, сорти винограду, які пов’язують регіони між собою, а також підскупчення виробників фортецьких вин (Порт, Шеррі, Мадейра) із власними вузлами виробництва. Розріджені ділянки також є показовими. Концепції ігристих вин представлені недостатньо, що пояснює невдалий пошук. У колекції також мало книг про ігристі вина, тож ця прогалина може частково походити від самих даних.

Невеликий фронтенд для демонстрації

Для спільного використання та нетехнічних демонстрацій інтерфейс пошуку можна обгорнути за допомогою 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])

Додаток завантажує заздалегідь створену графіку приблизно за 20 секунд та не потребує GPU для виконання запитів, оскільки весь обчислювальний процес відбувається через виклики API до OpenAI.

Що показує експеримент

Графічний підхід RAG значно покращує відповіді на три типи запитів:

  • Відносні запити, у яких ставиться питання про вплив однієї речі на іншу: зв’язки між елементами означають, що для відповіді не обов’язково, щоб вони ділили один і той самий фрагмент тексту. Чітким прикладом є зв’язок між танінами, кислотністю та процесом витримки вина.
  • Порівняння двох речей: контрасти самі себе організовують. У виробництві червоного та білого вина використовувалися однакові етапи через різні зв’язки, що дозволило отримати чіткі паралельні відповіді.
  • Синтез інформації з кількох книг: векторний пошук також може це зробити, проте графік чітко відображає зв’язки та не залежить від того, чи знаходяться ембеддинги поблизу один від одного.

Виділяються три слабкості:

  • Вилучення ентитетів є слабкою ланкою. Методи, призначені для аналізу ігристих вин, були присутні у необробленому тексті, але так і не стали ентитетами графа — це обмеження будь-якого підходу, заснованого на вилученні.
  • Запити на факти не потребують графа. Запит на те, при якій температурі зберігати вино, чудово обробляється за допомогою векторного пошуку, а граф лише додає зайву навантаження без жодної користі.
  • Витрати та затримки є реальними проблемами. Для обробки даних потрібні сотні викликів ШІ, а затримка у виконанні запитів у 25–30 секунд зумовлена саме синтезом даних за допомогою ШІ.

Матриця прийняття рішень прив’язує типи запитів до трьох варіантів: векторний (наївний), графовий (локальний) та змішаний, причому рекомендується змішаний підхід:

+----------------------------+----------------+---------------+-------------------+
| 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              |
+----------------------------+----------------+---------------+-------------------+

Чотири практичні поради:

  1. Завжди використовуйте режим mix. Не варто покладатися лише на пошук за допомогою графа, і відсутність повного охоплення пояснює це.
  2. Для роботи лише на CPU обирайте Docling замість MinerU, адже VLM у MinerU є ретельним, але крихким.
  3. Враховуйте час на обробку даних, особливо для великих корпусів.
  4. Перевірте граф. Убедіться, що ключові концепції домену дійсно з’являються як ентитети, та оцініть метрики структури графа перед тим, як покладатися на нього.

Висновок

RAG-Anything дотримується своєї основної обіцянки. Він запозичує структуровані знання з PDF-файлів у граф, що значно покращує результати реляційних та порівняльних запитів: аналіз дав краще структуровану відповідь щодо танінів, кислотності та процесу старіння, порівняння червоних та білих вин виконувалося швидше та було більш організованим, а синтез інформації з книг різних видавців також працював ефективно. Цей інструмент має великий потенціал, але ще є молодим, тож варто стежити за надійністю парсера та обсягом вилученої інформації. Ті, хто розглядає його для використання в продакшені, повинні повторити цей експеримент на власних даних, адже інструкція README не покаже, де він може зламатися.

Пов’язана література

  • Практичні нотатки: Я створив гібридну систему RAG на основі документів SEC — ось що це дає — Детальний огляд Практичних нотаток: Я створив гібридну систему RAG на основі документів SEC — ось що це дає: контракти, перевірки та шаблони коду для команд, які впроваджують цю модель.
  • Чи готовий ваш API до використання AI-агентами? Практичний чек-лист готовності MCP — Що перетворює функціональний API на інструмент, який агенти можуть надійно використовувати: документація, схеми, механізми автентифікації, вибір кінцевих точок, описи, тестування, хостинг та версіонування.
  • Міграція сервера MCP до специфікації 2026-07-28: двадцять хвилин, крок за кроком — Сесії зникли, але нічого не зламалося. Дізнайтеся про зміни в MCP 2026-07-28, процес міграції на Python SDK 2.0 у п’яти кроках, перепис MRTR та що означає видалення з використання.
  • TypeScript 7 та версія для Go: швидша компіляція, економія пам’яті та покращення функцій редактора — TypeScript 7 замінює компілятор на нативний двигун Go. Паралельна компіляція, менше використання пам’яті, новий мовний сервер та важливі моменти, які потрібно перевірити перед міграцією.
  • Порівняння чотирьох патернів багатоагентних систем: субагенти, навички, передача обов’язків та маршрутизатори — практичне порівняння чотирьох основних архітектур багатоагентних систем, з описом витрат кожної з них, сфер їх застосування та способів вибору однієї без надмірної складності.