Я столкнулся с реальной задачей: для проекта по аналитике госзакупок Казахстана нужно было сгенерировать длинный markdown-отчёт по каждой из ~50 000 организаций-заказчиков — с метриками, трендами, рейтингом и рекомендациями. Если каждую гонять через GPT-4o или Claude API — улетит несколько тысяч долларов и часы ожидания. И главное — нестабильно: одна и та же организация в разные дни даст разный текст.
Решение, которое я нашёл, до боли скучное и до боли эффективное — Data-to-Text прямо в SQL. Шаблоны с CASE WHEN, конкатенация строк, форматирование цифр в «млрд / млн / тыс ₸», интерпретация корреляций как «сильный рост». Результат — полноценный markdown-отчёт, посчитанный за миллисекунды, бесплатно, детерминированно, на 100% точно.
Пока все играются с RAG, агентами и MCP-серверами — я пишу CASE WHEN и не плачу за токены. Это не вместо LLM — это до LLM. И в 90% задач описаний по структурированным данным этого хватает с головой.
Проблема: 100k описаний и LLM-счёт на $1 250
Прикинем на пальцах. У меня было ~50 000 организаций. Для каждой нужен отчёт на ~5 000 токенов вывода (markdown-таблицы, интерпретация трендов, топ-10 закупок, скоринг). Берём актуальные цены Anthropic и OpenAI.
И это только output-токены. Если ещё забивать в input сами данные по организации — добавляем ещё 1-2 тыс. input-токенов на запрос. По реальной нагрузке — даже rate limits Tier 4 у OpenAI (1M TPM) не позволят пройти 50k запросов быстрее, чем за 5-10 часов. Ну и батч-обработка через Batch API режет цену вдвое, но всё равно — это не «бесплатно за 30 секунд».
Ещё одна фишка LLM, о которой все молчат: числовые галлюцинации. Я тестировал — модель легко превращает «total_volume = 1 234 567 890» в «1.23 миллиона тенге» вместо «1.23 млрд». На сотне отчётов этого не заметишь. На 50 000 — это репутационная катастрофа.
Что такое Data-to-Text (и почему это не AI-новинка)
Data-to-Text generation (D2T) — подзадача Natural Language Generation (NLG): берём структурированные данные (таблицу, JSON, граф) и на выходе получаем человеческий текст. Задаче больше 30 лет — академический интерес начался ещё в 1990-х (Reiter & Dale, «Building Natural Language Generation Systems», 2000).
Подходов исторически три:
Огромный пласт ресёрча — например RotoWire dataset (NBA-отчёты) и Wiseman et al. (2017) — посвящены попыткам обучить нейронки писать про спорт по таблице со статистикой. Спойлер: они до сих пор делают фактологические ошибки. Шаблоны — нет.
Индустриальные D2T-движки — Arria NLG, Yseop, Automated Insights — десятилетиями зарабатывают деньги на финансовых отчётах и автомате новостей. Bloomberg, Forbes, Yahoo Finance долгое время использовали Wordsmith от Automated Insights для генерации квартальных отчётов. Никаких LLM — чистые правила и шаблоны.
Самый дешёвый и надёжный способ сгенерировать текст — это не сгенерировать его в рантайме. Сгенерируйте его один раз заранее в SQL и кэшируйте.
LLM против SQL-шаблонов: честное сравнение
Я не говорю «D2T лучше LLM везде». Я говорю — у каждого свой кейс. Вот разрез, после которого выбор очевиден.
| Параметр | LLM (Claude / GPT) | SQL Templates |
|---|---|---|
| Скорость на 1 описание | 5–30 секунд | <1 миллисекунда |
| Стоимость на 100k записей | $200 – $2 000 | $0 (только compute) |
| Детерминизм | Нет (даже при temperature=0) | 100% |
| Точность чисел | Возможны галлюцинации | Точно по БД |
| Креативность / вариативность | Высокая | Ограничена шаблоном |
| Изменение текста | Поправить промпт | Переписать SQL + пересчёт |
| Требует интернета | Да | Нет |
| Privacy / on-prem | Сложно | Легко (всё в БД) |
| Работает на 1B+ записей | Невозможно | Стандартный workload |
Профиль: LLM vs SQL D2T (0–100, выше = лучше)
Субъективная оценка автора по 5 параметрам на основе годового опыта обоих подходов в проде.
Реальный кейс: 50 000 отчётов по госзакупкам Казахстана
Контекст: я работаю с открытыми данными Госзакупок РК — там есть API и около миллиона лотов в год. Дата-стек: ClickHouse — потому что это самая быстрая колоночная БД для агрегаций и текстовых функций. Цель: для каждой организации-заказчика сделать markdown-отчёт с разделами:
- Главные показатели (объём, лотов, контрагентов)
- Характер закупок (категории, типичные коды)
- Динамика за 2 года + сезонность
- Топ-10 закупок по сумме
- Финансовые метрики (средний чек, экономия от стартовой цены)
- Скоринг 0–100 с интерпретацией
На выходе — ~5 000 символов markdown на организацию × 50 000 организаций = ~250 МБ текста, который генерится за 30 секунд одним SQL-запросом из таблицы метрик org_scoring_base в таблицу org_scoring_raw. Дальше — индексация в Postgres + поиск, и текст просто читается из таблицы. Никаких LLM-вызовов в рантайме.
Пример 1: Форматирование чисел человекочитаемо
Самая частая микрозадача в D2T — 1234567890 превратить в 1.23 млрд ₸. В ClickHouse есть multiIf — компактнее каскада CASE WHEN.
-- ClickHouse: форматирование объёма закупок
multiIf(
org_total_volume >= 1000000000,
concat(toString(ROUND(org_total_volume / 1000000000.0, 2)), ' млрд ₸'),
org_total_volume >= 1000000,
concat(toString(ROUND(org_total_volume / 1000000.0, 1)), ' млн ₸'),
org_total_volume >= 1000,
concat(toString(ROUND(org_total_volume / 1000.0, 0)), 'к ₸'),
concat(toString(toUInt64(org_total_volume)), ' ₸')
) AS volume_humanОдин проход по миллиарду строк — секунды. Без AI. Без галлюцинаций.
Пример 2: Интерпретация трендов через CASE WHEN
У меня посчитана Pearson-корреляция квартальных объёмов закупок с временем — число от -1 до 1. Хочу превратить его в человеческий текст.
-- Перевод корреляции в текстовое описание тренда
CASE
WHEN org_volume_trend > 0.7 THEN
concat('Сильный рост (корреляция: +',
toString(ROUND(org_volume_trend, 2)), ')')
WHEN org_volume_trend > 0.3 THEN 'Умеренный рост'
WHEN org_volume_trend > -0.3 THEN 'Стабильный объём'
WHEN org_volume_trend > -0.7 THEN 'Умеренное снижение'
ELSE
concat('Сильное снижение (корреляция: ',
toString(ROUND(org_volume_trend, 2)), ')')
END AS trend_labelСтатистика → текст за один JOIN. Никакого «model.generate()».
Пример 3: Markdown-таблица генерится прямо в SQL
Самое красивое — markdown собирается concat из колонок. Пользователь увидит готовую таблицу без preprocessing на бэкенде.
-- Генерация markdown-таблицы прямо в строку
concat(
'## Главные показатели\n\n',
'| Метрика | Значение |\n',
'|---------------|---------------------|\n',
'| Объём закупок | ', volume_human, ' |\n',
'| Лотов | ', toString(lots_count), ' |\n',
'| Контрагентов | ', toString(suppliers), ' |\n',
'| Средний чек | ', avg_check_human, ' |\n',
'| Скоринг | ', toString(total_score), '/100 |\n\n'
) AS markdown_tableВ колонке БД у меня лежит готовый markdown-кусок. Никаких Jinja-шаблонов на бэкенде, никакой логики в API — отрендерил и отдал.
Пример 4: Числа → слова (месяц, день недели, регион)
Сезонность — топ-1 или топ-2 по числу закупок месяц. Для читаемости показываем словами.
-- Месяц-пик в виде слова
CASE peak_month
WHEN 1 THEN 'Январь' WHEN 2 THEN 'Февраль'
WHEN 3 THEN 'Март' WHEN 4 THEN 'Апрель'
WHEN 5 THEN 'Май' WHEN 6 THEN 'Июнь'
WHEN 7 THEN 'Июль' WHEN 8 THEN 'Август'
WHEN 9 THEN 'Сентябрь' WHEN 10 THEN 'Октябрь'
WHEN 11 THEN 'Ноябрь' WHEN 12 THEN 'Декабрь'
END AS peak_month_labelПример 5: Полный отчёт — сборка всего вместе
Финал — гигантский concat всех частей в одну колонку. Это и есть итоговое поле org_report_md в таблице org_scoring_raw.
INSERT INTO org_scoring_raw
SELECT
org_id,
org_name,
concat(
'# ', org_name, '\n\n',
'> Скоринг: **', toString(total_score), '/100**',
' • ', score_grade, '\n\n',
markdown_table,
'## Динамика закупок\n\n',
'Тренд за 2 года: **', trend_label, '**.\n',
'Пик активности — **', peak_month_label, '**.\n\n',
'## Рекомендации\n\n',
recommendations_md
) AS org_report_md
FROM org_scoring_base
WHERE org_total_volume > 0;Замер на моём кейсе: 50 000 организаций × ~5 000 символов = 250 МБ markdown. ClickHouse-кластер из 3 нод проходит за 32 секунды. На макбуке локально — около 2 минут. То же самое через Claude Sonnet API заняло бы по моим прикидкам ~10 часов и стоило бы $562 за один прогон.
Архитектура решения
Главная идея — текст хранится в БД как готовое поле. Никаких генераций на лету, никаких prompt-кэшей, никаких inference-серверов. Изменился шаблон — пересчитал таблицу за 30 секунд. Это в чистом виде ELT-подход (extract → load → transform), как его проповедуют dbt и современные дата-стеки.
Ключевой инсайт: при таком подходе у тебя на проде нет зависимости от LLM-провайдера. OpenAI упал, Anthropic поднял цены, Gemini поменял API — тебе всё равно. Текст уже в БД.
Стоимость: реальные цифры
Считаем стоимость генерации 100 000 описаний по 5 000 output-токенов = 500M output-токенов суммарно. Цены input я опускаю для простоты — они меньше output, но добавили бы ещё 10–30% к итоговому счёту.
Стоимость генерации 100k × 5k токенов = 500M output, $
Цены на май 2026 (Anthropic, OpenAI, Google AI). Лог-шкала.
Даже самая дешёвая модель — GPT-4o mini — обойдётся в ~$45 за прогон. SQL D2T — 0.01 цента (это даже не доллар). Разница в ~5 000–125 000 раз. И это без учёта rate limits, retry-логики, fallback на другую модель и стоимости разработки prompt-инфры.
Время на 100k описаний (секунды, лог-шкала)
Учитывая rate limits публичных API и concurrency. Источник — собственные замеры + публичные лимиты Anthropic.
Когда D2T работает идеально
Любая задача с фиксированной структурой и преобладанием цифр над эмоциями. Вот реальные индустриальные кейсы — большинство этих систем работают на template-based подходе годами.
Ещё один индустриальный пример — Yseop делает финотчёты для Société Générale и Crédit Agricole с 2007 года. Все эти годы — никаких LLM. Просто очень умные rule-based движки.
Когда D2T не подойдёт (и я честно говорю)
D2T — не серебряная пуля. Я бы не стал применять этот подход в задачах, где:
- Нужна креативность и вариативность — маркетинговые слоганы, тизеры, лонгриды
- Структура текста часто меняется — каждую неделю редактор переписывает форму
- Контекст из 100+ переменных с нелинейными связями — комбинаторный взрыв CASE WHEN
- Нужны диалоги, чат-боты с открытыми темами
- Перевод между языками с морфологическими нюансами (хотя для шаблонной части D2T переводится в i18n тривиально)
Простое правило: если ты можешь нарисовать дерево решений для текста на одной А4 — D2T справится. Если нет — нужен LLM.
Гибридный подход: лучшее из двух миров
В реальной жизни я не выбираю «или–или». Я делаю D2T для фактологии + LLM для оживления раз в N запросов. Схема:
Альтернативно — если нужен интерактивный чат «расскажи мне про эту организацию подробнее», я не гоню весь отчёт в контекст. Делаю RAG по pgvector / Qdrant — с эмбеддингами по тем же D2T-сгенерированным кускам. Получается быстро и дёшево: D2T-куски как factual ground truth, LLM как переводчик в диалоговый формат.
Чеклист: когда я выбираю D2T
Если у тебя >=5 пунктов из этого списка — даже не думай, бери template-based. LLM добавишь потом точечно.
- Источник — структурированные данные: БД, JSON, CSV. Не аудио, не видео, не свободный текст.
- Структура отчёта — фиксированная: «секция А, потом таблица B, потом график C» — одинаково для всех записей.
- Объём ≥ 1 000 записей: на маленьких объёмах LLM выгоднее по цене разработки. На больших — D2T побеждает экономически.
- Точность чисел критична: финансы, медицина, право, госзакупки — везде, где галлюцинация = жалоба или иск.
- Нужна детерминированность: юридические тексты, отчёты для регуляторов, пользователь должен видеть одно и то же.
- Низкая латентность важна: нужно отдавать текст за миллисекунды, без ожидания API.
- Privacy / on-prem: данные не должны уходить во внешний LLM-API. SQL крутится у тебя на сервере.
Я давно перестал подходить к таким задачам через «давайте натренируем модель» или «давайте промпт сделаем». Сначала всегда вопрос: «это можно решить SQL-ом?» — и в 9 случаях из 10 ответ да. LLM остаётся для оставшегося 1 случая, где нужна творческая работа с текстом. И это не отступление от AI-эпохи — это инженерная зрелость.
LLM — это не молоток, которым нужно бить по каждому гвоздю. Это очень дорогой и очень умный инструмент. Дёшевые гвозди забивайте дёшевым молотком — SQL'ом.