Генератор SQL на базе LLM — понятный продукт с понятной ценностью. Он может быть отдельной платформой или инструментом для агента, решающего более общую задачу. Генерировать код модели с попеременным успехом, к счастью, умеют. 

И что же? Берем API с моделью помощнее, даем ей доступ к БД, задаем вопрос, смотрим на результат, и всё — полноценная замена аналитику? Конечно, нет, ведь аналитик делает гораздо больше, чем просто пишет и исполняет SQL. 

Однако давайте остановимся на SQL и посмотрим, почему это тоже не так просто:

  1. Во внутренний контур никто API от OpenAI или Anthropic не встроит и уж тем более не будет туда отправлять чувствительные данные. 

  2. Если нет возможности обратиться к внешним API, то надо разворачивать модель локально и желательно без GPU, а это дорого и инфраструктура пока не готова.

  3. Открытые LLM нередко ошибаются, особенно когда контекста много и он плохо сформулирован или когда речь заходит об определенном диалекте SQL, например PostgreSQL 17.

  4. Процесс сбора данных и обучения LLM под конкретный диалект дорогостоящий во всех смыслах. 

  5. На локальных моделях можно использовать грамматики для guided decoding, однако их разработка и отладка — задачи непростые. 

По итогу проблемы есть. Посмотрим, как их можно решить.

Что позволяет модели хорошо генерировать SQL?

В нашей области практика — критерий истины, поэтому рассмотрим несколько существующих подходов, которые дают отличные результаты в бенчмарках.

CHASE-SQL

Авторы CHASE-SQL предположили, что использование различных техник генерации SQL по-отдельности нецелесообразно: каждая из них имеет уникальные особенности, преимущества и недостатки. 

Если нам важна именно точность на бенчмарке, а не доступность или скорость модели, почему бы не воспользоваться всем имеющимся арсеналом?

То есть решаем задачу по отдельности в нескольких обособленных пайплайнах, а затем результат каждого подхода отправляем в модель-селектор, выбирающую финальный ответ. 

В сущности, каждый пайплайн из себя представляет хитрый промптинг крупной модели, типа Gemini или Claude, и этап саморефлексии в случае синтаксической ошибки. 

SQLFuse

Веха в развитии систем для решения задачи Text-To-SQL, совмещающая в себе лучшие практики промпт-инжиниринга, эффективные многомодульные пайплайны, красивые архитектурные решения, технологии fine-tuning'а LLM и многое другое.

Модель состоит из 4 частей: 

  • Schema Mining — все про промптинг и создание контекста для модели. Здесь достаются таблицы, столбцы и констреинты типа primary key и foreign key.

  • Schema Linking — модуль, позволяющий выделить значимое подмножество столбцов и таблиц для генерации SQL. Выполняется тоже при помощи языковой модели, обученной конкретно этой задаче при помощи SFT (Supervised Fine-Tuning).

  • SQL Generation — основная модель, в которую направляется весь накопленный ранее контекст вместе с вопросом пользователя. Тоже обучается отдельно при помощи SFT на TRAIN-датасете из Spider-бенчмарка.

  • SQL Critic — стандартный прием, когда результат генератора валидируется еще одним этапом. Только на этот раз у модели есть примеры хороших и плохих практик, пары схожих вопросов и ответов и другая априорная информация.

SkyRL-SQL

Основан на RL-дообучении базовой модели так, чтобы она могла в несколько шагов решать поставленную задачу (Multi-Turn RL). По сути, этот подход наиболее близок тому, что будет описано в этой статье.

Соответственно, совокупность дообучения моделей, формирования контекста и выстраивание пайплайнов позволяют сделать генерацию SQL стабильным и перспективным мероприятием. 

Бенчмаркинг

Как оценивать SQL-генераторы? Можно на глаз на известной задаче, но это несерьезно. Обычно прибегают к помощи хорошо сконструированных, проверенных комьюнити и временем бенчмарков. Для задачи text-to-SQL есть 2 общепринятых бенчмарка — Spider и BIRD. Посмотрим, как выглядит каждый из них.

Spider

Классический кросс-доменный text-to-SQL-бенчмарк проверяет способность модели обобщаться на новые схемы БД и генерировать сложные SQL (joins, nested, group by/having и так далее). 

Главные метрики:

  • Exact Match (EM) — точное совпадение с эталонным SQL.

  • Execution Accuracy (EX) — совпадение результатов выполнения предсказанного и эталонного SQL.

Примеры из бенчмарка:

Вопрос

Ответ

How many heads of the departments are older than 56 ?

SELECT count(*) FROM head WHERE age > 56

List the name, born state and age of the heads of departments ordered by age.

SELECT name , born_state , age FROM head ORDER BY age

List the creation year, name and budget of each department.

SELECT creation , name , budget_in_billions FROM department

What is the maximum and minimum budget of the departments?

SELECT max(budget_in_billions) , min(budget_in_billions) FROM department

What is the average number of employees of the departments whose rank is between 10 and 15?

SELECT avg(num_employees) FROM department WHERE ranking BETWEEN 10 AND 15

BIRD

Большой бенчмарк для text-to-SQL, который ближе к реальному миру. Основной акцент сделан на корректном вычленении значений из таблиц. 

Метрики:

  • EX/EM.

  • Soft F1 по результатам выполнения (снижает чувствительность к порядку столбцов и пропускам).

Пример из бенчмарка:

Вопрос

Ответ

What is the ratio of customers who pay in EUR against customers who pay in CZK?

SELECT CAST(SUM(CASE WHEN Currency = 'EUR' THEN 1 ELSE 0 END) AS DOUBLE) / SUM(CASE WHEN Currency = 'CZK' THEN 1 ELSE 0 END) FROM customers

In 2012, who had the least consumption in LAM?

SELECT T1.CustomerID FROM customers AS T1 INNER JOIN yearmonth AS T2 ON T1.CustomerID = T2.CustomerID WHERE T1.Segment = 'LAM' AND SUBSTR(T2.Date, 1, 4) = '2012' GROUP BY T1.CustomerID ORDER BY SUM(T2.Consumption) ASC LIMIT 1

What was the average monthly consumption of customers in SME for the year 2013?

SELECT AVG(T2.Consumption) / 12 FROM customers AS T1 INNER JOIN yearmonth AS T2 ON T1.CustomerID = T2.CustomerID WHERE SUBSTR(T2.Date, 1, 4) = '2013' AND T1.Segment = 'SME'

What was the difference in gas consumption between CZK-paying customers and EUR-paying customers in 2012?

SELECT SUM(CASE WHEN T1.Currency = 'CZK' THEN T2.Consumption ELSE 0 END) - SUM(CASE WHEN T1.Currency = 'EUR' THEN T2.Consumption ELSE 0 END) FROM customers AS T1 INNER JOIN yearmonth AS T2 ON T1.CustomerID = T2.CustomerID WHERE SUBSTR(T2.Date, 1, 4) = '2012'

Which year recorded the most consumption of gas paid in CZK?

SELECT SUBSTR(T2.Date, 1, 4) FROM customers AS T1 INNER JOIN yearmonth AS T2 ON T1.CustomerID = T2.CustomerID WHERE T1.Currency = 'CZK' GROUP BY SUBSTR(T2.Date, 1, 4)

Как дообучать думающие модели?

Почему не работает классический SFT?

Под SFT (Supervised Fine-Tuning) обычно понимают обучение модели на размеченных парах (вход → желаемый выход) с минимизацией какого-либо функционала, например кросс-энтропии. То есть модель просто учится имитировать ответы. 

Для задач, где требуется длинное рассуждение (SQL, код, математика), такой подход быстро упирается в два фундаментальных ограничения.

Во-первых, нет готовых reasoning-датасетов. Открытые наборы либо вообще не содержат CoT (chain-of-thought), либо включают короткие, шумные и несогласованные пояснения.

Во-вторых, нет соответствия конечной цели. SFT оптимизирует правдоподобие целевых токенов, тогда как практические метрики — это правильность исполнения (EX для SQL), предпочтения пользователей и все такое.

Получается, что модель лучше пишет, но не обязательно лучше решает.

Как доработать датасет под SFT? Генерация reasoning-части

На практике эту дыру пытаются закрыть reverse-майнингом CoT: берут сильную модель, дают ей исходную задачу с правильным ответом или возможностью проверить себя и просят восстановить правдоподобную цепочку рассуждений. 

Дальше — многократная генерация вариантов, автоматическая проверка, выбор лучших траекторий, нормализация стиля и упаковка в формат (вход → reasoning + ответ). Получается датасет, на котором уже можно делать SFT так, чтобы ученик учился формату и логике рассуждения, а не только финальному выводу.

Этот путь работает, но у него есть три неприятные особенности:

  1. Дорого и долго. Сильная модель стоит денег и времени; верификация тоже не бесплатна (поднятие окружений, исполнение запросов, перегенерации при неудаче).

  2. Не всегда эффективно. Учитель иногда реконструирует красивую, но несвязную цепочку мыслей. В результ��те после фильтрации остается меньше качественных примеров, чем хотелось бы.

  3. Трудно исключить утечку априорной информации из промпта. Чтобы reverse-майнинг вообще сработал, в промпт обычно добавляют подсказки: ответ или структуру решения. Все это априорные знания, которые затем оказываются в данных для SFT, что в целом делает SFT бессмысленной затеей.

Что такое Reinforcement Learning?

Обучение с подкреплением (RL) — это идеология, в которой модель рассматривается как политика, выбирающая действия так, чтобы максимизировать ожидаемое вознаграждение. 

Есть три основные сущности: состояние (контекст для модели), действие (следующий токен или целая последовательность) и награда. В классическом виде агент взаимодействует со средой, пробует варианты, получает обратную связь и обновляет политику, чтобы повысить суммарную награду.

В контексте LLM наградой могут быть результаты исполнения (прошел ли тест, правильно ли выполняется SQL), предпочтения (какой ответ пользователи считают лучшим). Ключевое идеологическое отличие от SFT в том, что RL оптимизирует именно целевую метрику, а не правдоподобие ответов. 

Если нам важна EX, мы обучаемся так, чтобы ее повысить; если важны пользовательские предпочтения, награда формулируется из суррогатной модели предпочтений и направляет обновления политики.

GRPO

GRPO (Group Relative Policy Optimization) — это вариант PPO для RL-дообучения, где обучение ведется по группам из нескольких сэмплов на один и тот же запрос. 

Здесь важно то, что оптимизируется политика не по абсолютным наградам, а по относительным преимуществам внутри группы ответов. Это снижает дисперсию градиента и убирает необходимость в Value Model (подробнее про PPO).

Для каждого запросаq сэмплируемG ответов\{o_i\}_{i=1}^G из старой политики\pi_{\theta_{\mathrm{old}}}, считаем sequence-level награды\{R_i\} и превращаем их в относительные преимущества:


A_i = R_i - \frac{1}{G}\sum_{j=1}^{G} R_j

Отношение вероятностей берется на уровне токена:


r_{i,t}(\theta) =\frac{\pi_{\theta}\!\big(o_{i,t}\mid q,\,o_{i,<t}\big)}     {\pi_{\theta_{\mathrm{old}}}\!\big(o_{i,t}\mid q,\,o_{i,<t}\big)}.

Оптимизационный функционал в стиле PPO:


\mathcal{J}(\theta)=\mathbb{E}_{q\sim P(q),\,\{o_i\}\sim \pi_{\theta_{\mathrm{old}}}}\!\left[\frac{1}{G}\sum_{i=1}^{G}\frac{1}{|o_i|}\sum_{t=1}^{|o_i|}\min\!\Big(r_{i,t}(\theta)\,A_i,\;\operatorname{clip}\!\big(r_{i,t}(\theta),\,1-\varepsilon,\,1+\varepsilon\big)\,A_i\Big)\right]\!.

GSPO

GSPO (Group Sequence Policy Optimization) — относительно новый подход к RL-дообучению языковых моделей, который очень схож с GRPO. Отличается он тем, что градиент функционала зависит не от вероятностного распределения конкретных токенов, а от всей последовательности сразу. 

Достигается это следующим изменением в формуле:

\mathcal{J}_{\mathrm{GSPO}}(\theta) = \mathbb{E}_{q\sim P(q),\, \{o_i\}_{i=1}^{G}\sim \pi_{\theta_{\mathrm{old}}}(O\mid q)} \Bigg[   \frac{1}{G}\sum_{i=1}^{G} \min\!\Big(       s_i(\theta)A_i,\,       \operatorname{clip}\!\big(s_i(\theta),\,1-\varepsilon,\,1+\varepsilon\big)A_i   \Big) \Bigg]

s_i(\theta) = \left(     \frac{\pi_{\theta}(o_i\mid q)}{\pi_{\theta_{\mathrm{old}}}(o_i\mid q)}   \right)^{1/\lvert o_i\rvert} = \exp\!\left(     \frac{1}{\lvert o_i\rvert}     \sum_{t=1}^{\lvert o_i\rvert}     \log\frac{\pi_{\theta}(o_{i,t}\mid q,\,o_{i,<t})}{\pi_{\theta_{\mathrm{old}}}(o_{i,t}\mid q,\,o_{i,<t})}   \right)

Как оказалось, такое изменение приводит к более стабильному обучению и быстрой сходимости, особенно для длинных цепочек рассуждений и MoE-моделей.

Больше интересных подробностей про GRPO vs GSPO можно узнать здесь.

GGPO?

К параметрам сэмплирования в vLLM можно добавить и грамматику в формате GBNF, как я описывал в предыдущей статье. Она позволит избежать синтаксически неверных выражений в ходе генерации эквивалентных SQL, однако для этого необходимо иметь на руках соответствующую грамматику. 

Таким образом, мы можем в целом озаглавить этот фреймворком, как GGPO (Guided Grammar Policy Optimization).

Датасет

Для обучения было специально подготовлено 2 датасета:

  1. https://huggingface.co/datasets/Safreliy/BIRD-train-postgresql

  2. https://huggingface.co/datasets/Safreliy/synthetic_text_to_sql

Каждый из них отфильтрован так, чтобы сэмплы отрабатывали на СУБД PostgreSQL. 

Во-первых, необходимо, чтобы БД создавались и заполнялись без ошибок, как в случае BIRD, так и в случае synthetic. 

Во-вторых, необходимо, чтобы SQL-запросы корректно выполнялись и возвращали непустой результат.

Еще есть демодатасет с reasoning-контентом для предварительного SFT-дообучения, но его качество еще нужно верифицировать: 

https://huggingface.co/datasets/Safreliy/BIRD-reasoning-postgresql

Результаты

Техническая информация:

  1. Обучение производили на A100 80GB на протяжении трёх суток.

  2. Фреймворк дообучения — TRL (GRPO + sequence-level sampling = GSPO).

  3. Количество генераций в группе на один сэмпл равняется 10.

  4. LoRA:Rank=32,\alpha=64.

  5. Генерация rollouts при помощи vLLM.

  6. Параметры сэмплирования: temperature=0.7, top_p=0.9, top_k=20.

  7. Batchsize = 20.

На валидационном датасете мы получили прирост целевой метрики на ~11% относительно базовой модели, что достаточно неплохо для такой крохи, как Qwen3-0.6B. При этом эпоха завершилась лишь на 28%, очевидно, что потенциал дальнейшего улучшения метрики присутствует. 

Попробовать модель самому можно здесь.

Прогон на BIRD-бенчмарке

А как ведет себя модель на mini-dev-версии BIRD-бенчмарка?

Проведем эксперимент — 30 раз прогоним 500 примеров из бенчмарка на базовой модели Qwen3-0.6B (назовем ее BASE) и на нашей LLM, дообученной при помощи GSPO (назовем ее FT). И там, и там поставим temperature=0.3 и top_p=0.9.

Примеры из бенчмарка разбиты на 3 категории: simple, moderate и challenging. По каждой из них будем считать метрику EX. Также посчитаем суммарный total по всем категориям. Таким образом, будем мерить улучшение или ухудшение метрики по 4 разным показателям. 

Также стоит помнить, что по примерам распределение неравномерное. Так, в simple входит 148 примеров, в moderate входит 250, а в challenging — 102, в сумме total=500. 

Посмотрим на сводную табличку с подсчитанной статистикой:

simple (N=148)

24.055 ± 1.964

23.807 ± 2.021

−0.248

−1.03

[−1.239; 0.744]

0.63647

0.88056

−0.123

moderate (N=250)

6.627 ± 1.006

6.053 ± 0.865

−0.573

−8.65

[−1.040; −0.120]

0.02310

0.06930

−0.603

challenging (N=102)

2.744 ± 1.322

3.659 ± 1.285

+0.915

+33.33

[0.261; 1.568]

0.01120

0.04480

+0.692

total (N=500)

10.993 ± 0.797

10.820 ± 0.846

−0.173

−1.58

[−0.587; 0.234]

0.44028

0.88056

−0.208

Получилась интересная картина: BASE-модель уже неплохо генерирует SQL, и по total обогнать ее пока не удалось. Но если посмотреть по категориям, картинка становится яснее. Больше всего мы преуспели в challenging — самой малочисленной группе: относительный прирост EX составил +33%. 

Из-за малого вклада этой категории в общий счет это почти не сдвинуло total. При этом есть некоторая просадка в moderate (−8.65%) — самой многочисленной категории. Однако после учета множественных проверок (Holm-adj p) она не считается статистически значимой.

Мы используем пермутационный тест со статистикой Уэлча как основной. 

Идея такая: считаем t-статистику Уэлча (устойчивую к разным дисперсиям) для наблюдаемых данных, затем многократно перемешиваем метки групп (BASE/FT), каждый раз пересчитываем ту же статистику и смотрим долю значений статистики, не менее экстремальных, чем наблюдаемое. 

Такой подход не требует предположений о распределении и хорошо работает при умеренных объемах выборки.

Для оценки величины эффекта мы приводим Hedges’ g — это стандартизированная разница средних. Это значение удобно интерпретировать: ~0.2 (малый эффект), ~0.5 (средний эффект), ~0.8 (сильный эффект).

Итог: дообучение надежно усилило модель на самых сложных задачах, но немного ослабило на средних, что, тем не менее, незначимо с точки зрения статистики.

Дальше для улучшения метрик можно:

  1. Сделать предварительное обучение при помощи SFT на reasoning-датасете с постобучением при помощи GSPO.

  2. Сделать калибровку датасета так, чтобы получить равномерное распределение по всем категориям сложности задач.

  3. Увеличит�� ранг LoRA.

  4. Увеличить время обучения так, чтобы покрыть больше примеров из TRAIN.

  5. Добавить грамматику для guided decoding, покрывающую обучающий и валидационный датасеты.