Главная / Статьи

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

Большая языковая модель читает каждый фрагмент текста и выделяет такие сущности, как «Каберне Совиньон», «малолактическая ферментация» и «Бордо», а также связи между ними: Каберне Совиньон растёт в Бордо, а малолактическая ферментация снижает кислотность. Результаты хранятся в виде узлов и рёбер в постоянной структуре 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 отвечает за генерацию текста, что позволяет сделать сотни вызовов больших языковых моделей во время извлечения сущностей доступными с точки зрения затрат. 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 Vision. Для этого эксперимента были достаточны текст и таблицы.

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

В коллекцию вошли 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 секунд. В обоих случаях доминирует синтез с помощью больших языковых моделей, а не поиск информации, причем кэширование значительно ускоряет повторные запросы по схожим темам.

Рассмотрение графа

Чтобы изучить сам граф, интерактивная визуализация, созданная с использованием 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.

Что показывает эксперимент

Метод Graph 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 к использованию ИИ-агентами? Практический чек-лист готовности MCP — Что превращает рабочую API в инструмент, который агенты могут надежно использовать: документация, схемы, механизмы аутентификации, выбор конечных точек, описания, тестирование, хостинг и версионирование.
  • Миграция сервера MCP на спецификацию 2026-07-28: двадцать минут по шагам — Сессии исчезли, но ничего не сломалось. Узнайте, какие изменения принесла спецификация MCP 2026-07-28, как осуществить миграцию на Python SDK 2.0 в пяти определённых этапах, что изменилось в MRTR и что означает устаревание.
  • TypeScript 7 и порт на Go: более быстрая компиляция, оптимизация использования памяти и улучшения в редакторе — TypeScript 7 заменяет компилятор на нативный движок Go. Возможность параллельной компиляции, снижение потребления памяти, новый языковой сервер, а также важные моменты, которые необходимо учесть при миграции.
  • Сравнение четырех паттернов многоподразделочных систем: субагенты, навыки, передача задач и маршрутизаторы — практическое сравнение четырех основных архитектур многоподразделочных систем, включающее информацию о затратах каждой из них, сферах применения и способах выбора подходящей без чрезмерной сложности.