ГлавнаяБлогКак ИИ строит графики: от запроса до PNG без единого пикселя от LLM
AI / Нейросети

Как ИИ строит графики: от запроса до PNG без единого пикселя от LLM

Узнайте, как мы научили ИИ строить графики: LLM пишет только спецификацию, числа проверяются, а рендер идет через Vega-Lite и vl-convert. Читайте и внедряйте!

Al
Редакция Algolitalgolit.ru
8 мин чтения16 августа 2026 г.

Почему эта статья — про то, как ИИ строит графики без галлюцинаций

Представьте: у вас есть данные, которые никто не читает. Не «данные», а дорогое кладбище метрик. Мы в LiveReview строим сервис, который ревьюит код и оценивает, насколько плохо будет, если изменение сломает прод. И накопили гору ревью-данных: кто, как часто, какие репозитории горят. Руководители спрашивали: «Растёт ли внедрение?» — и получали «ну, вроде да». Тогда мы сделали Livi — чат-бота, который отвечает на реальные вопросы с реальными графиками. Этот пост — о том, как Livi рисует эти графики. Главная идея: мы никогда не даём LLM касаться пикселей, а заставляем его описывать график декларативно. Читайте, как один и тот же спектакль превращается в интерактивный график в браузере и плоскую PNG в Slack, и почему выбор правильной формы графика — это глубокая кроличья нора.

Основное решение: не просите LLM рисовать, просите описать

Соблазнительная, но неправильная идея — «пусть LLM сгенерирует изображение». Не делайте так. Модели генерации изображений — это другая зверушка, и даже если вы заставите её нарисовать столбчатую диаграмму, вы не сможете проверить, реальные ли числа на ней. Вы доверитесь модели, которая галлюцинирует правдоподобные числа, и попросите её же отрисовать их в пикселях. Это не график, а фанфик в форме графика.

Правильная идея, к которой приходят все серьёзные интеграции, — пусть LLM пишет Vega-Lite, JSON-грамматику для декларативного описания графиков. Вы не говорите «нарисуй синий столбик вверх». Вы говорите:

{
  "mark": "bar",
  "encoding": {
    "x": {"field": "month", "type": "temporal"},
    "y": {"field": "review_count", "type": "quantitative"}
  }
}

Вот и весь график. Никаких пикселей, только описание того, что данные означают и как их сопоставить с картинкой. Рисованием занимается Vega-Lite. Задача LLM сужается до того, что он реально умеет: заполнить хорошо определённую схему. Модели гораздо лучше справляются с выбором mark: bar или mark: line, чем с галлюцинированием 600 пикселей правильной оси Y.

Критически важно: LLM никогда не видит реальные числа до рендера. Он пишет SQL, мы его выполняем, и настоящий результат вставляется в data.values нашим Go-кодом. Модель может быть сколь угодно креативной в оформлении, но ноль креатива в числах.

Что реально происходит между вопросом и графиком

Вот конвейер, примерно:

  1. Пользователь задаёт вопрос: «Сколько ревью было в прошлом месяце?»
  2. LLM получает узкий срез схемы БД (об этом ниже) и пишет «сырой» SQL-запрос для оценки размера результата.
  3. SQL-гард проверяет запрос: только чтение, запрещённые таблицы, защита от обхода тенант-изоляции.
  4. Выполняем запрос, получаем количество строк.
  5. LLM решает, нужен ли график (или лучше CSV), и пишет финальный SQL + спецификацию Vega-Lite.
  6. Снова SQL-гард, выполняем, получаем данные.
  7. Вставляем данные в спецификацию, рендерим (в браузере или через vl-convert для PNG).

Почему два шага написания SQL?

Потому что модели нужно знать, сколько строк будет в ответе, до того, как решить, имеет ли смысл график. Никому не нужен столбчатый график с 4000 столбцов. Первый шаг — «насколько это будет большим», второй — «окей, теперь реально получи данные и скажи, как их рисовать».

Зачем нужен SQL-гард?

Потому что LLM, пишущий сырой SQL против мультитенантной базы, — это один уверенный промпт от org_id = 1 OR 1 = 1. Мы прогоняем каждый сгенерированный запрос через гард, который отклоняет всё, что не read-only, проверяет каждую таблицу по денай-листу и ищет форму обхода изоляции тенанта (сравнения констант, голый OR TRUE и прочую классику). Это не гламурная работа, но это разница между «крутой AI-фичей» и «почему Org A видит данные Org B» в инцидент-канале.

Почему LLM видит только узкий срез схемы?

Потому что реальная схема — более 50 таблиц, и сваливать их все в каждый промпт — дорого и сбивает модель с толку: какой created_at к какой таблице относится. Об этом дальше отдельный раздел.

Учим модель, какие таблицы существуют: dbctx

Вот проблема, которая не видна, пока схема — игрушечная. У нас 58 таблиц: ревью, PR, AI-комментарии, обратная связь, биллинг, лицензии, очереди задач. Если вставлять полную схему в каждый промпт, мы будем жечь тысячи токенов на описание license_seat_assignments для вопроса «сколько ревью было в прошлом месяце».

Вместо «вот вся база, удачи» мы используем dbctx — Go-библиотеку, которая по естественно-языковому вопросу и живому Postgres-соединению возвращает только нужные таблицы, отформатированные как компактный LLM-дружественный текст, а не сырой дамп information_schema.

dbctx использует многослойный конвейер: лексическое и нечёткое сопоставление имён таблиц и колонок, полнотекстовый поиск, сопоставление с реальными значениями в данных, опциональный семантический эмбеддинг и курируемый слой терминологии (чтобы «LOC» резолвился в billable_loc, а «MR» и «PR» — в одно понятие). Всё, что набрало балл выше нуля, попадает в контекст, плюс всё, что достижимо через внешний ключ от отобранного. Выигрыш огромен: типичный вопрос сужает 58 таблиц до 25–30 — это вдвое меньше контекста до того, как модель написала хоть символ SQL. Это не оптимизация для галочки, это разница между промптом, в котором модель ясно мыслит, и промптом, где важные таблицы погребены под шумом биллинга.

Где наконец появляются пиксели: vl-convert

Итак, у нас есть спецификация Vega-Lite. Если пользователь в веб-дашборде — мы почти закончили. Но что насчёт Slack? Discord? Эти платформы не запускают JS-библиотеку внутри сообщения. Сообщение в Slack — не вкладка браузера. Нельзя попросить Slack интерпретировать спецификацию и отрисовать SVG.

Поэтому для всего, что не наш фронтенд, нужен реальный файл изображения. Здесь вступает vl-convert: Rust-бинарь (с Python и Node биндингами, но мы вызываем CLI), который принимает спецификацию Vega-Lite и растеризует её прямо в PNG, без headless-браузера. Это важно: старый способ — поднять headless Chrome, загрузить страницу с библиотекой графиков, сделать скриншот и молиться, чтобы Docker-образ не раздулся до двух гигабайт. vl-convert пропускает всё это: один бинарь, JSON на вход, PNG на выход, и так быстро, что никто не замечает.

Одна спецификация — две судьбы

Вот что я считаю действительно изящным. Мы генерируем одну спецификацию на график. Что с ней происходит дальше, зависит от того, куда она идёт.

В вебе фронтенд отдаёт сырую спецификацию в react-vega, и браузер делает всю работу: ховер-тултипы, ресайз, интерактив. Бэкенд для этого пути не рендерит изображения вообще — просто отдаёт JSON.

Для Slack и Discord та же спецификация уходит в vl-convert, превращается в плоскую PNG и прикрепляется к сообщению. Бот не знает и не заботится, что это «тот же» график, просто другой путь. С точки зрения конвейера, спецификация Vega-Lite — просто спецификация. Куда она попадёт, решает, станет ли она живым SVG или скриншотом в чате.

Это разделение позволяет дёшево добавлять новые места назначения: захотели еженедельный email-дайджест с графиками — та же спецификация, тот же vl-convert, новый механизм доставки. Сложная проблема (получить корректную, осмысленную спецификацию из LLM) решается ровно один раз.

Практический вывод: как это применить у себя

Смысл Livi был не «добавить чат-бота», а замкнуть петлю между данными, которые мы уже собираем, и человеком, которому нужно действовать. CTO, спрашивающий «инженеры вообще используют эту штуку?», должен получить календарный хитмэп ритма использования, а не таблицу и пожатие плечами.

Рецепт, если вы строите похожее:

  • Никогда не давайте модели касаться пикселей. Пусть пишет декларативную спецификацию.
  • Никогда не показывайте модели реальные числа до выполнения её запроса. Сначала выполните, потом вставляйте.
  • Гардите SQL как следует, а не «по ощущениям».
  • Выберите один рендер-конвейер (например, vl-convert) и пусть назначение решает, статично или интерактивно, а не модель.
  • Вложитесь в промпт-инжиниринг для «вкуса» к графикам. Это та часть, которая требует итераций.

Попробуйте сами: возьмите свою базу, подключите dbctx, заставьте LLM генерировать Vega-Lite, и вы увидите, как просто получить честные графики без галлюцинаций. А если понравится — загляните в наш репозиторий и поддержите звездой.

#ИИ-графики#Vega-Lite#SQL-гард#диаграммы#LLM
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →