Почему я перестал платить OpenAI и собрал локальный GraphRAG за выходные

Знаете это чувство, когда смотришь на счёт за API и думаешь: «Блин, я что, нефтяную вышку арендую?» Вот и я так подумал, когда увидел, сколько стоит обработка документов клиентов через OpenAI. Спойлер: это был момент, когда я решил собрать свой локальный GraphRAG.

С чего всё началось

Работаю с одной компанией, у них куча внутренних документов — договоры, регламенты, технические спецификации. Типичная задача: сотрудник задаёт вопрос, система ищет по базе и выдаёт ответ с цитатами. Классический RAG.

Сначала сделал как все: OpenAI embeddings + Pinecone. Работало нормально, пока не пришёл счёт за месяц. $400 только за эмбеддинги. При том, что документов всего-то 2000, и обновляются они раз в неделю. И это ещё повезло — знакомым из других проектов приходили счета и по $1000–1500 в месяц, когда документов было больше.

Но главное даже не сумма. Попробуйте оплатить OpenAI из России — это тот ещё квест. Зарубежные карты, криптокошельки, посредники. Каждый месяц танцы с бубном: «а не заблокируют ли аккаунт?», «а дойдёт ли платёж?». Один знакомый так потерял доступ к аккаунту вместе со всеми промптами и историей — просто прилетел бан без объяснений.

Плюс юридический вопрос: передача данных российских клиентов в американское облако. Для компании, которая работает с крупным бизнесом, это красная тряпка для юристов. Клиент начал задавать вопросы: «А куда уходят наши данные?» — и у меня не было ответа, который бы их устроил.

Почему именно GraphRAG, а не обычный RAG?

Обычный RAG работает так: разбиваем документ на чанки, делаем эмбеддинги, ищем похожие по косинусному расстоянию. Проблема в том, что он плохо работает со связанными данными.

Пример из жизни: в договоре написано «Компания А является партнёром Компании Б», а в другом документе — «Компания Б поставщик Компании В». Обычный RAG не поймёт связь между А и В, потому что они в разных чанках. А GraphRAG поймёт, потому что строит граф знаний.

Для задач клиента это было критично — там куча связей между сущностями: компании, люди, проекты, документы.

Почему локальный стек — это не блажь, а необходимость

Для российских реалий локальное решение закрывает сразу четыре боли:

  • Суверенитет данных — всё остаётся внутри компании или на российских серверах, никаких вопросов от юристов.
  • Нет зависимости от санкций — завтра OpenAI может заблокировать все российские аккаунты, и ты останешься без работающей системы. А тут — твой сервер, твои правила.
  • Предсказуемость расходов — один раз настроил железо, и никаких сюрпризов с курсом доллара и счетами за токены.
  • Приватность — документы с коммерческой тайной не покидают периметр компании.

Что я выбрал и почему

Стек:

Ollama — для локального запуска LLM (выбрал Llama 3.1 8B, хватает для моих задач)

Neo4j — графовая база данных (бесплатная Community Edition)

LlamaParse — для парсинга PDF (работает лучше, чем PyPDF2)

LangChain — для оркестрации всего этого безумия

Почему не взял готовое решение? Есть Microsoft GraphRAG, но он:

1. Завязан на Azure OpenAI (а нам нужна приватность и независимость от санкций)
2. Тяжёлый (требует кучу ресурсов)
3. Сложный в настройке

Мне нужен был минимальный рабочий прототип за выходные.

Так родился мой репозиторий github.com/kaziava/local-graphrag-starter — опенсорс-стек с базовой реализацией.

Грабли, которые я собрал

Сначала взял PyMuPDF. Думал, справится. Ага, конечно. Таблицы превращались в кашу, заголовки терялись, а на некоторых документах вообще выдавал кракозябры.

Попробовал LlamaParse от LlamaIndex. Работает на порядок лучше, особенно с таблицами. Минус — требует API-ключ (но есть бесплатный тариф на 1000 страниц в месяц, мне хватило).

Вывод: не экономьте на парсинге. Плохой парсинг = мусор на входе = мусор на выходе.

Грабли #2: Neo4j и Docker

Первый раз поднимал Neo4j в Docker и забыл про volumes. Перезапустил контейнер — все данные стёрлись. Классика.

Правильный docker-compose:

services: neo4j: image: neo4j:latest volumes: - ./neo4j_data:/data - ./neo4j_logs:/logs environment: - NEO4J_AUTH=neo4j/password ports: - "7474:7474" - "7687:7687"

Ключевое — volumes. Без них данные живут только пока контейнер работает.

Грабли #3: Извлечение сущностей из текста

Самая сложная часть — заставить LLM правильно извлекать сущности и связи. Сначала промпт был такой:

Извлеки сущности и связи из текста.

Пришлось сделать промпт максимально конкретным:

Проанализируй текст и извлеки: 1. Сущности типа "Компания" (название, ИНН) 2. Сущности типа "Человек" (ФИО, должность) 3. Связи между ними (работает_в, является_партнёром, подписал_документ) Верни результат в формате JSON: { "entities": [...], "relationships": [...] }

Стало работать стабильно. Но всё равно приходится проверять первые 10–20 документов вручную, чтобы убедиться, что промпт отрабатывает корректно.

Как это работает (упрощённо)

  1. Загрузка документа → парсим через LlamaParse
  2. Извлечение сущностей → отправляем текст в LLM, получаем JSON с сущностями и связями
  3. Запись в Neo4j → создаём узлы и рёбра графа
  4. Поиск → когда пользователь задаёт вопрос, переводим его в Cypher-запрос и ищем по графу.

Пример Cypher-запроса:

MATCH (c:Company {name: 'Компания А'})-[:PARTNER_WITH]->(partner) RETURN partner.name

Это вернёт всех партнёров Компании А.

Производительность

Тестировал на машине с 16 GB RAM:

  • Парсинг 100 PDF (≈500 страниц) — 15 минут
  • Извлечение сущностей — 30 минут (с локальной Llama 3.1)
  • Поиск по графу — меньше 100 мс на запрос

Для 2000 документов это более чем достаточно. И $0 за API после первоначальной настройки.

Что можно улучшить

Честно скажу — это MVP. Вот что я планирую добавить:

  • Инкрементальные обновления — чтобы не переиндексировать все документы при изменении одного
  • Гибридный поиск — комбинировать векторный поиск (для нечётких запросов) и графовый (для точных связей)
  • Веб-интерфейс — чтобы не лазить в терминал
  • Мониторинг качества — автоматически проверять, насколько ответы соответствуют документам

Выводы

Локальный GraphRAG — это не так страшно, как кажется. За выходные можно собрать рабочий прототип, который:

  • Не сливает данные в облако
  • Не требует ежемесячных платежей за API
  • Не зависит от санкций и курса доллара
  • Лучше работает со связанными данными, чем обычный RAG

Да, есть грабли. Да, нужно разбираться с Docker и Neo4j. Но это того стоит, если у вас есть требования к приватности или вы просто устали платить за API и воевать с оплатой из России.

Код и инструкции — в моём репозитории

Там всё максимально просто: клонируете, поднимаете Docker, запускаете скрипт.

Больше хардкорных разборов — в моём Telegram-канале @llmops_engineering. Там я делюсь конфигами, бенчмарками векторных БД и реальными кейсами из продакшена. Без воды и хайпа про «ИИ заменит программистов».

📥 Бонус для подписчиков: чек-лист «10 проверок RAG перед выкаткой в прод» — помогает избежать типовых ошибок, которые я собирал годами.

P.S. Если будут вопросы по коду или настройке — пишите в комментариях или в личку в Telegram (@kaziava5). Отвечаю всем, кто реально строит, а не просто интересуется.