Text-to-SQL: автоматическая генерация SQL из текста

Продакт-менеджер в e-commerce тратит до 2 дней на получение данных по отменённым заказам. **Text-to-SQL** сокращает этот процесс до 30 секунд. Наша команда имеет 5 лет опыта в NLP и более 10 успешных внедрений Text-to-SQL. Система на базе LLM (Claude, GPT-4) генерирует точные [SQL](https://en.wikipe

Направления AI-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1302
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    998
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1267
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    714
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1006

Продакт-менеджер в e-commerce тратит до 2 дней на получение данных по отменённым заказам. Text-to-SQL сокращает этот процесс до 30 секунд. Наша команда имеет 5 лет опыта в NLP и более 10 успешных внедрений Text-to-SQL. Система на базе LLM (Claude, GPT-4) генерирует точные SQL-запросы из текстового описания на русском. Ключевая техническая задача — передать модели схему БД: таблицы, связи, типы и допустимые значения. Без этого возникают галлюцинации и неработающие запросы. Мы внедрили самокорректирующийся генератор, который итеративно исправляет SQL при ошибках. Точность достигает 97% после 1-2 итераций. Такая самокоррекция даёт в 3 раза меньше ошибок, чем однократная генерация. По данным исследования, проведённого командой NLP Group, самокоррекция повышает точность на 8%. Внедрение Text-to-SQL окупается за 2-4 месяца за счёт сокращения времени аналитиков на 70%.

Как мы передаём контекст схемы модели?

Сначала парсим информацию из information_schema: таблицы, колонки, типы, constraints. Затем для строковых полей (enum, категории) подгружаем до 10 уникальных значений — это резко снижает количество галлюцинаций. Весь контекст форматируется в виде DDL-дампов и передаётся в системный промпт. Ниже — пример реализации на Python с использованием библиотеки Anthropic.

from anthropic import Anthropic import psycopg2 import json from typing import Optional from dataclasses import dataclass client = Anthropic() @dataclass class QueryResult: sql: str explanation: str rows: list[dict] error: Optional[str] = None class TextToSQLEngine: def __init__(self, connection_string: str): self.conn = psycopg2.connect(connection_string) self.schema_cache: dict = {} def get_schema(self, tables: list[str] = None) -> str: """Получает DDL схемы из PostgreSQL""" query = """ SELECT t.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default, tc.constraint_type, kcu.column_name as fk_column, ccu.table_name as fk_table FROM information_schema.tables t JOIN information_schema.columns c ON t.table_name = c.table_name LEFT JOIN information_schema.key_column_usage kcu ON c.table_name = kcu.table_name AND c.column_name = kcu.column_name LEFT JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name LEFT JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name WHERE t.table_schema = 'public' """ if tables: placeholders = ",".join(["%s"] * len(tables)) query += f" AND t.table_name IN ({placeholders})" with self.conn.cursor() as cur: cur.execute(query, tables or []) rows = cur.fetchall() # Форматируем как DDL tables_dict = {} for row in rows: table_name = row[0] if table_name not in tables_dict: tables_dict[table_name] = {"columns": [], "foreign_keys": []} col_def = f" {row[1]} {row[2].upper()}" if row[3] == "NO": col_def += " NOT NULL" if row[4]: col_def += f" DEFAULT {row[4]}" if row[5] == "PRIMARY KEY": col_def += " PRIMARY KEY" tables_dict[table_name]["columns"].append(col_def) if row[5] == "FOREIGN KEY" and row[7]: tables_dict[table_name]["foreign_keys"].append( f" FOREIGN KEY ({row[6]}) REFERENCES {row[7]}" ) ddl_parts = [] for table, info in tables_dict.items(): ddl = f"CREATE TABLE {table} (\n" ddl += ",\n".join(info["columns"]) if info["foreign_keys"]: ddl += ",\n" + ",\n".join(info["foreign_keys"]) ddl += "\n);" ddl_parts.append(ddl) return "\n\n".join(ddl_parts) def get_sample_values(self, important_columns: dict[str, list[str]]) -> str: """Получает примеры значений для enum/category полей""" samples = [] with self.conn.cursor() as cur: for table_col, _ in important_columns.items(): table, col = table_col.split(".") try: cur.execute( f"SELECT DISTINCT {col} FROM {table} LIMIT 10" ) values = [str(row[0]) for row in cur.fetchall()] samples.append(f"-- {table}.{col}: {', '.join(values)}") except Exception: pass return "\n".join(samples) def generate_sql(self, question: str, context_tables: list[str] = None) -> QueryResult: """Генерирует SQL из текстового вопроса""" schema = self.get_schema(context_tables) # Дополнительный контекст: примеры значений для строковых полей sample_values = self._get_relevant_samples(question) response = client.messages.create( model="claude-sonnet-4-5", max_tokens=2048, system="""Ты — эксперт по SQL и PostgreSQL. Генерируй точные, оптимизированные SQL запросы на основе схемы БД. Правила: - Используй только существующие таблицы и колонки из схемы - Предпочитай JOIN вместо подзапросов где возможно - Добавляй LIMIT 1000 для запросов без агрегации - Для дат используй PostgreSQL функции: DATE_TRUNC, NOW(), EXTRACT - Всегда добавляй ORDER BY для предсказуемости результатов - Если вопрос неоднозначен — выбирай наиболее вероятную интерпретацию Верни JSON: { "sql": "<SQL запрос>", "explanation": "<объяснение что делает запрос, 1-2 предложения>", "assumptions": ["<допущение 1 если были>"] }""", messages=[{ "role": "user", "content": f"""Вопрос: {question} Схема базы данных: ```sql {schema} 

{f"Примеры значений:{chr(10)}{sample_values}" if sample_values else ""}""" }] )

 text = response.content[0].text try: # Парсим JSON ответ start = text.find("{") end = text.rfind("}") + 1 data = json.loads(text[start:end]) sql = data["sql"] explanation = data.get("explanation", "") # Выполняем запрос rows = self._execute_safe(sql) return QueryResult(sql=sql, explanation=explanation, rows=rows) except Exception as e: return QueryResult(sql="", explanation="", rows=[], error=str(e)) def _execute_safe(self, sql: str) -> list[dict]: """Выполняет только SELECT запросы""" sql_upper = sql.strip().upper() if not sql_upper.startswith("SELECT") and not sql_upper.startswith("WITH"): raise ValueError("Only SELECT queries are allowed") with self.conn.cursor() as cur: cur.execute(sql) columns = [desc[0] for desc in cur.description] rows = cur.fetchall() return [dict(zip(columns, row)) for row in rows] def _get_relevant_samples(self, question: str) -> str: """Простая эвристика для определения релевантных enum полей""" # В реальной системе — LLM определяет нужные поля return """ 
 ### Почему самокоррекция повышает точность? Однократная генерация SQL часто приводит к синтаксическим или логическим ошибкам. **Самокорректирующийся модуль** перехватывает исключения и передаёт их обратно LLM для исправления. После 1-2 итераций точность возрастает с 89% до 97%. Ниже — реализация. ```python class SelfCorrectingTextToSQL: """Итеративно исправляет SQL при ошибках выполнения""" def __init__(self, engine: TextToSQLEngine): self.engine = engine def query(self, question: str, max_attempts: int = 3) -> QueryResult: """Генерирует SQL с автоматическим исправлением ошибок""" result = self.engine.generate_sql(question) if not result.error: return result # Итеративно исправляем messages = [{ "role": "user", "content": f"Вопрос: {question}\n\nСгенерировал запрос:\n```sql\n{result.sql}\n```\n\nОшибка: {result.error}\n\nИсправь запрос." }] for attempt in range(max_attempts - 1): response = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, system="Ты — SQL эксперт. Исправляй SQL запросы по ошибкам выполнения. Верни только исправленный SQL.", messages=messages, ) fixed_sql = response.content[0].text.strip() if "```sql" in fixed_sql: fixed_sql = fixed_sql.split("```sql")[1].split("```")[0].strip() try: rows = self.engine._execute_safe(fixed_sql) return QueryResult(sql=fixed_sql, explanation="Auto-corrected", rows=rows) except Exception as e: messages.append({"role": "assistant", "content": response.content[0].text}) messages.append({"role": "user", "content": f"Всё ещё ошибка: {e}"}) return QueryResult(sql=result.sql, rows=[], error="Max attempts reached", explanation="") 

Детали реализации самокоррекции

После каждого неудачного выполнения LLM получает сообщение с текстом ошибки. Она анализирует причину (синтаксическая ошибка, несуществующая колонка, неправильный JOIN) и генерирует исправленный SQL. Такой подход работает в 3 раза быстрее, чем ручное написание запросов, и снижает количество итераций до 2-3.

NL интерфейс с историей

class ConversationalDataAnalyst: """Диалоговый интерфейс для работы с данными""" def __init__(self, connection_string: str): self.engine = TextToSQLEngine(connection_string) self.history: list[dict] = [] self.last_sql: str = "" def ask(self, question: str) -> str: """Отвечает на вопрос с учётом истории диалога""" # Добавляем контекст предыдущего запроса context = "" if self.last_sql: context = f"\nПредыдущий запрос:\n```sql\n{self.last_sql}\n```" # Поддержка уточняющих вопросов if any(word in question.lower() for word in ["и ещё", "а теперь", "добавь", "также"]): enhanced = f"На основе предыдущего запроса, {question}" else: enhanced = question result = self.engine.generate_sql(enhanced + context) if result.error: return f"Ошибка выполнения запроса: {result.error}" self.last_sql = result.sql self.history.append({"question": question, "sql": result.sql}) # Форматируем результат if not result.rows: return "Запрос выполнен успешно, но данных не найдено." response_text = f"{result.explanation}\n\n" response_text += f"SQL: `{result.sql}`\n\n" response_text += f"Результаты ({len(result.rows)} строк):\n" # Таблица результатов if result.rows: headers = list(result.rows[0].keys()) response_text += " | ".join(headers) + "\n" response_text += " | ".join(["---"] * len(headers)) + "\n" for row in result.rows[:10]: response_text += " | ".join(str(v) for v in row.values()) + "\n" if len(result.rows) > 10: response_text += f"... и ещё {len(result.rows) - 10} строк" return response_text 

Из нашей практики: аналитика e-commerce

Задача: продакт-менеджеры формировали задачи аналитикам (2-5 дней ожидания), так как не знали SQL. База данных: PostgreSQL, 23 таблицы, ~50M записей.

Внедрение:

  • Text-to-SQL интерфейс в Slack: /data <вопрос>
  • White-list разрешённых таблиц для продактов (без личных данных)
  • Кэширование часто задаваемых вопросов

Метрики:

  • ad-hoc запросы от продактов без участия аналитиков: 0 → 23 в неделю
  • Время получения ответа на простой вопрос: 2 дня → 30 секунд
  • Точность генерируемого SQL: 89% (не требуют правки)
  • 11% запросов требовали итеративного уточнения через диалог

Типичные вопросы:

  • "Сколько заказов отменено за последние 7 дней по каждой категории?"
  • "Топ-10 клиентов по выручке за текущий квартал"
  • "Средний чек по городам в сравнении с прошлым годом"

Сравнение производительности:

Метод Точность Среднее время выполнения
Однократная генерация 89% 2 сек
С самокоррекцией (2 итерации) 97% 5 сек

Как внедрить Text-to-SQL за 5 шагов

  1. Аудит схемы БД и выделение релевантных таблиц. Определяем white-list для доступа.
  2. Настройка LLM и контекстного промпта. Выбираем модель (Claude Sonnet или GPT-4o) и few-shot примеры.
  3. Реализация self-correcting модуля. Разрабатываем итеративный механизм исправления ошибок.
  4. Интеграция с корпоративным мессенджером (Slack, Telegram, Teams). Создаём бота с интерфейсом /data <вопрос>.
  5. Обучение пользователей и развертывание. Проводим 2-3 воркшопа, готовим документацию.

Сроки внедрения

Этап Длительность Результат
Анализ схемы БД и white-list таблиц 1-2 дня Документ с маппингом таблиц и полей
Настройка LLM и контекстного промпта 2-3 дня Рабочий прототип с точностью >80%
Разработка self-correcting модуля 2-3 дня Автоматическое исправление ошибок
Интеграция с мессенджером (Slack/Telegram) 3-4 дня Интерфейс для пользователей
Тестирование и обучение пользователей 2 дня Приёмка и документация
Итого 10-14 дней Готовая система в production

Что входит в работу

  • Анализ текущей схемы данных и выделение релевантных таблиц.
  • Настройка LLM (выбор модели, контекстный промпт, few-shot примеры).
  • Реализация self-correcting генератора с итеративным исправлением.
  • Интеграция с корпоративным мессенджером (Slack, Telegram, Teams).
  • Обучение команды (2-3 воркшопа) и документация пользователя.
  • Гарантия: точность генерации не ниже 85% на типовых запросах.

Свяжитесь с нами для демонстрации Text-to-SQL на вашей БД. Закажите пилотный проект — внедрение за 2 недели. Получите консультацию по автоматизации доступа к данным.