Agentic RAG for Dummies: LangGraph, parent-child чанки и локальный Ollama

Большинство RAG-туториалов останавливаются на схеме «нарезал PDF, положил эмбеддинги, спросил чат». Этого хватает для демо на одном файле и ломается, как только появляются уточняющие вопросы, несколько тем в одном запросе и модель, которая отвечает «из головы», не открыв индекс. Репозиторий agentic-rag-for-dummies как раз про этот разрыв: агентный RAG на LangGraph, с иерархическими чанками, гибридным поиском и паузой, если запрос слишком мутный.

Автор: Giovanni Pasqualino (GiovanniPasq). Лицензия MIT, копирайт 2025. Репозиторий создан 13 октября 2025 года. Последний пуш на момент материала: 25 июля 2026, коммит обновления зависимостей. 21 июня 2026 туда же ушли память диалога, многошаговое уточнение, трассировка и оценка через RAGAS. На 21 августа 2026 GitHub показывает 3937 звёзд и 509 форков. Это учебный стенд с расширяемой раскладкой модулей, не коробочный SaaS.

Чем агентный RAG отличается от «эмбеддинги плюс prompt»

Обычный RAG один раз достаёт k ближайших кусков и скармливает их модели. Агентный RAG решает, нужно ли искать, что именно искать, достаточно ли найденного и не стоит ли спросить человека. В этом проекте цепочка такая:

запрос → сводка диалога → переписывание → уточнение →
параллельные агенты → агрегация ответа

На практике это закрывает три бытовых сценария. «Как обновить это?» после вопроса про SQL: память должна понять, что «это» = SQL. «Расскажи про ту штуку»: граф обязан остановиться и спросить, а не галлюцинировать. «Что такое JavaScript? Что такое Python?»: запрос режется на подвопросы и уходит в параллельные подграфы (map-reduce).

Официальный учебник LangGraph тоже строит retrieval-агента, который сам решает, звать векторное хранилище или ответить сразу. См. Build a custom RAG agent with LangGraph. Разница репозитория GiovanniPasq: готовый конвейер PDF → Markdown → parent/child → Qdrant, Gradio-интерфейс и ноутбуки, а не только каркас графа.

Как устроен поиск: маленькие чанки ищут, большие отвечают

Документы режутся дважды. Parent: крупные куски по Markdown-заголовкам H1–H3, с целевым размером 2000–4000 символов. Child: мелкие куски по 500 символов с overlap 100, связанные с родителем. В индекс Qdrant кладут детей (точное попадание), а в ответ подтягивают родителя (контекст). Это стандартный parent-child трюк, который в «наивном» RAG часто пропускают: маленький чанк хорошо матчится, но в нём нет соседних абзацев.

Поиск гибридный: плотные эмбеддинги Qwen/Qwen3-Embedding-0.6B плюс разреженный BM25 через Qdrant/bm25. Порог релевантности по умолчанию 0.4, число дочерних чанков DEFAULT_RETRIEVAL_K = 7. Родители лежат не в векторе, а в файловом store (JSON). Векторная база: локальный Qdrant по пути qdrant_db, не облако.

Инструменты агента в коде названы прямо: search_child_chunks и retrieve_parent_chunks. Если детей мало или они слабые, агент может перезапросить (self-correction). Чтобы граф не зациклился, стоят жёсткие потолки: максимум 8 вызовов инструментов, 10 итераций, recursion limit 50.

Два входа: ноутбук для учёбы, project/ для сборки

В README это разделено явно.

Учебный путь: Jupyter/Colab, notebooks/agentic_rag.ipynb. Рядом лежат pdf_to_markdown.ipynb, observability.ipynb и evaluation.ipynb (метрики RAGAS по фактическому контексту, который агент взял из инструментов). На обычном hosted Colab нет Ollama: ячейку с локальной моделью нужно заменить на облачный провайдер из документации.

Рабочий путь: каталог project/. Точка входа project/app.py, настройки в project/config.py, граф в project/rag_agent/, Gradio в project/ui/. Приложение по умолчанию Ollama-first. В README есть примеры подмены на OpenAI, Anthropic и Google Gemini через LangChain. Имена облачных моделей в примерах быстро устаревают: это нужно сверять с документацией провайдера, а не копировать строку навсегда.

Как поднять стенд локально

Требования из project/README: Python 3.11+ и либо локальный Ollama, либо ключи облачного провайдера. В requirements.txt на коммите от 25 июля 2026 зафиксированы, среди прочего, LangGraph 1.2.9, Gradio 6.20.0, langchain-qdrant 1.1.0, ragas 0.4.3, pymupdf4llm 1.28.0. Бейдж README говорит LangGraph 1.2+.

Локальная модель по умолчанию:

ollama pull granite4.1:8b

Клон и зависимости (как в корневом README):

git clone https://github.com/GiovanniPasq/agentic-rag-for-dummies
cd agentic-rag-for-dummies
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python project/app.py

Интерфейс Gradio по умолчанию слушает http://localhost:7860 (в README также указан пример http://127.0.0.1:7860). Документы загружаются в UI. Автор прямо пишет: in-memory демо рассчитано на одного локального пользователя. Если поднимать это на нескольких сессиях, каждой нужен свой LangGraph thread ID. Без этого память и уточнения поедут между людьми.

Альтернатива через uv:

uv venv .venv
source .venv/bin/activate
uv pip install -r requirements.txt

Docker есть в project/Dockerfile. Сборка из корня репозитория:

docker build -t agentic-rag -f project/Dockerfile .
docker run --name rag-assistant -p 7860:7860 agentic-rag

Официальная оговорка по памяти: минимум 8 ГБ RAM, выделенных Docker, 12 ГБ рекомендуют, если одновременно крутятся эмбеддинги Qwen и локальная Ollama. На Windows и macOS Docker идёт через Linux VM, индексация PDF может быть заметно медленнее, чем на голом Linux. GPU: флаг --gpus all, только NVIDIA, если драйвер и runtime это умеют.

Что крутить в config.py, не расползаясь по всему коду

Центральный файл: project/config.py. Менять провайдера «навсегда» недостаточно одной строки: для облака ещё правят инициализацию LLM в project/core/rag_system.py и ставят SDK. Для эмбеддингов достаточно сменить DENSE_MODEL, но коллекцию Qdrant нужно пересобрать. Старый индекс с другой размерностью вектора не совпадёт с новой моделью.

Значения по умолчанию из текущего config.py:

DENSE_MODEL = "Qwen/Qwen3-Embedding-0.6B"
SPARSE_MODEL = "Qdrant/bm25"
LLM_MODEL = "granite4.1:8b"
JUDGE_MODEL = "ministral-3:3b-instruct-2512-q8_0"
LLM_TEMPERATURE = 0
RETRIEVAL_SCORE_THRESHOLD = 0.4
DEFAULT_RETRIEVAL_K = 7
MAX_TOOL_CALLS = 8
MAX_ITERATIONS = 10
CHILD_CHUNK_SIZE = 500
CHILD_CHUNK_OVERLAP = 100
MIN_PARENT_SIZE = 2000
MAX_PARENT_SIZE = 4000

Судья JUDGE_MODEL нужен оценке RAGAS, не чату. Температура 0: детерминированнее, меньше «творчества» в цитатах из документации. После смены эмбеддингов в учебном UI: Clear All, при необходимости перезапуск, затем заново залить PDF. Уже проиндексированные файлы специально пропускаются, чтобы не плодить дубли.

Где обычно ломается

Это не список из головы, а сводка из troubleshooting корневого README и project/README.

  • Модель меньше 8B и без нормального tool calling: игнорирует инструкцию «иди в поиск», врёт агрегацию, не вызывает retrieval. Автор прямо советует 8B+.
  • Ollama не запущена или имя модели не совпало с LLM_MODEL: ошибка «model not found».
  • Сменили эмбеддинги, не пересобрали коллекцию: mismatch размерности вектора.
  • Пустой retrieval: PDF не прошли в Markdown, другое имя CHILD_COLLECTION, порог 0.4 отсёк слабые совпадения.
  • Ответ «из модели», без документов: системный промпт недостаточно жёстко требует поиск. Правится в project/rag_agent/prompts.py.
  • Агент крутится слишком долго или сдаётся рано: MAX_TOOL_CALLS / MAX_ITERATIONS.
  • После сжатия контекста пропали детали: поднять BASE_TOKEN_THRESHOLD или ослабить сжатие (TOKEN_GROWTH_FACTOR).
  • Несколько пользователей на одном thread: память диалога и HITL-уточнения смешаются. Для продакшена это не «включить HTTPS», а завести идентификатор сессии на каждого.

Отдельно: PDF → Markdown идёт через pymupdf4llm. Сканы без текстового слоя, кривые таблицы и колонтитулы дадут плохой Markdown, а дальше уже любой RAG будет искать мусор. Для разбора нарезки автор предлагает соседний инструмент Chunky.

Как понять, что стенд живой

После python project/app.py в браузере должна открыться Gradio на порту 7860. Загрузите PDF, дождитесь индексации, задайте вопрос, ответ на который есть только в файле, не в параметрах модели. Затем задайте follow-up с местоимением («а как это обновить?»). Если агент потерял тему, смотрите память и rewrite, не «ещё раз увеличьте k».

Опционально включают Langfuse (LANGFUSE_ENABLED=true плюс ключи). В трассе видны узлы summarize_history, rewrite_query, orchestrator, compress_context, aggregate_answers и вызовы двух retrieval-инструментов. Без ключей приложение стартует так же: трассировка выключена по умолчанию.

Оценку качества не путайте с «мне понравилось». Ноутбук evaluation.ipynb считает RAGAS по тем child/parent кускам, которые агент реально прочитал. Для судьи в config прописана отдельная локальная модель, не тот же granite, что отвечает в чате.

Кому это имеет смысл, а кому нет

Имеет смысл, если нужно понять агентный RAG руками: документация, внутренние PDF, база знаний, черновик бота по регламентам. Стек знакомый админам и питонистам: venv или Docker, Ollama на той же машине, Qdrant файлом на диске, порт 7860. Для сайта на WordPress это не замена поиска по постам и не плагин. Это отдельный Python-контур рядом, если вы отвечаете по своим PDF, а не по wp_posts.

Не стоит ждать из коробки мультитенантности, ACL на документы, очереди индексации и SLA. Parent store на JSON и in-memory thread годятся для учёбы и стенда. На проде те же идеи (parent-child, гибридный поиск, HITL, лимиты инструментов) обычно переносят в свой сервис, а не выставляют учебный Gradio в интернет.

Итог короткий. Наивный RAG заканчивается на «нашли 5 чанков». Этот репозиторий показывает следующий слой: граф, уточнение, параллельные подвопросы и контроль, чтобы агент не крутился вечно. Поднять можно за вечер на Python 3.11 и Ollama. Превращать в публичный ассистент без сессий, лимитов и нормального хранения родителей рано.

Источники


Комментарии загружаются…