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

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.
Показано 1 з 1Усі 1564 послуг
Text-to-SQL: автоматична генерація SQL з тексту
Середній
~5 днів
Часті запитання

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1361
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    957
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1189
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    646
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    929

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

Це рішення забезпечує автоматизацію запитів до БД через корпоративний месенджер, зокрема slack sql bot. Очікувана економія на аналітиках — до $5,000 на місяць. Вартість впровадження — $12,000, економія — $5,000 на місяць. Таким чином, окупність настає за 2-4 місяці.

Як передавати контекст схеми моделі?

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

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 з текстового питання (natural language to 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%. Нижче — реалізація self-correcting sql.

<details>
<summary>Деталі реалізації самокорекції</summary>

```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 <питання> (Slack SQL bot)
  • White-list дозволених таблиць для продактів (без особистих даних)
  • Кешування часто задаваних питань

Метрики:

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

Типові питання:

  • "Скільки замовлень скасовано за останні 7 днів по кожній категорії?"
  • "Топ-10 клієнтів за виручкою за поточний квартал"
  • "Середній чек по містах у порівнянні з минулим роком"

Порівняння продуктивності:

Метод Точність Середній час виконання
Одноразова генерація 89% 2 сек
З самокорекцією (2 ітерації) 97% 5 сек

Самокоригувальний Text-to-SQL краще за одноразову генерацію в 3 рази за точністю.

Як впровадити 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% на типових запитах.
Додаткові технічні деталі Система підтримує llm sql (Claude, GPT-4) для генерації. Використовується pgvector для векторного індексування схеми.

Які ризики використання Text-to-SQL?

Хоча система демонструє високу точність, слід враховувати можливі галюцинації на нестандартних запитах. Рекомендується тестувати на white-list таблиць та використовувати самокорекцію для зниження помилок.

Чи варто впроваджувати Text-to-SQL?

Впровадження Text-to-SQL окупається за 2-4 місяці за рахунок економії часу аналітиків. Наша система в 2 рази ефективніша за звичайні NLP-підходи завдяки самокорекції та контексту схеми.

Зв'яжіться з нами для демонстрації Text-to-SQL на вашій БД. Замовте пілотний проєкт — впровадження за 2 тижні. Отримайте консультацію з автоматизації запитів до БД та автоматизації аналітики.

Практичний розбір LLM: fine-tuning, RAG, агенти, деплой

Модель GPT‑4 або Claude 3.5 Sonnet через публічне API — не рішення, а просто інструмент. Коли приходить вимога «зробити як ChatGPT, але на наших даних», за нею стоїть реальна інженерна задача: від налаштування промптів до навчання 70B‑моделі на власній інфраструктурі. LLM розробка під ключ — це складний стек, і ми займаємося цим понад 5 років. За цей час реалізовано понад 20 проєктів у галузі генеративного AI: від RAG‑систем для юридичних департаментів до кастомних агентів для техпідтримки. Де саме знаходиться ваша задача — залежить від даних, latency‑вимог, бюджету та того, наскільки критична конфіденційність.

Типова ситуація: клієнт уже спробував ChatGPT, але результати нестабільні — то відповідає точно, то галюцинує. Або потрібна інтеграція в корпоративний портал з дотриманням політик безпеки. Розберемо кожен шар стеку в деталях — від RAG до production‑деплою.

Чому RAG‑системи ламаються і як це виправити?

RAG (Retrieval‑Augmented Generation) виглядає просто: знайшли релевантні документи, поклали в контекст, модель відповіла. На практиці збоїть у кількох місцях.

Chunking без перекриття. Класична помилка: chunk_size=512, overlap=0. Якщо відповідь лежить на межі двох чанків, retrieval не знайде жодного з достатньою впевненістю. Рішення: overlap 15–25% від chunk_size, а краще sentence‑aware splitting через spaCy або NLTK, а не наївне розбиття за символами.

Поганий embedder. Текст‑embedding‑ada‑002 — хороший для загального випадку, але на юридичних або медичних текстах програє спеціалізованим моделям: E5‑large‑v2, BGE‑M3 або fine‑tuned sentence‑transformers на доменних даних. Різниця в Recall@5 може становити 15–25%.

Відсутність re‑ranking. Векторний пошук оптимізований за швидкістю, не за релевантністю. Cross‑encoder re‑ranker (ms‑marco‑MiniLM‑L‑6‑v2, bge‑reranker‑large) після первинного retrieval піднімає точність топ‑3 при прийнятній затримці (+50–150 ms). Це часто важливіше за покращення embedding‑моделі.

Гібридний пошук. Тільки dense вектори погано працюють на точних запитах: імена, артикули, коди. BM25 (sparse) добре знаходить точні збіги, але не розуміє семантику. Гібрид через RRF (Reciprocal Rank Fusion) — оптимальний компроміс. Qdrant, Weaviate та pgvector 0.7+ підтримують гібридний пошук нативно.

Типова production‑архітектура корпоративного knowledge base
  1. Документи → preprocessing (PyMuPDF, Unstructured)
  2. Chunking → embedding (BGE‑M3)
  3. Qdrant (гібридний dense+sparse)
  4. Cross‑encoder re‑ranking
  5. Контекст → LLM (vLLM або OpenAI API)
  6. Відповідь з джерелами (RAGAS для оцінки якості)

Коли варто fine‑tune, а не промпт‑інжиніринг?

Промпт‑інжиніринг вирішує ~70% завдань адаптації LLM під домен. Решта 30% вимагають донавчання. Три ознаки: модель ігнорує специфічний формат виведення навіть при детальному описі в промпті; задача вимагає глибокого знання спеціалізованої лексики (медицина, право); потрібно значно знизити витрати на токени, замінивши велику модель меншою спеціалізованою.

LoRA та QLoRA — стандарт для SFT. LoRA додає trainable low‑rank матриці до attention‑шарів. Типова конфігурація для Llama‑3 8B: r=64, lora_alpha=128, target_modules=["q_proj","v_proj","k_proj","o_proj"] — параметрів, що навчаються, ~0.8%, навчання на одній A100 40GB. QLoRA додає 4‑бітну квантизацію (NF4) і дозволяє fine‑tune 70B модель на двох A100 40GB, хоча швидкість падає вдвічі порівняно з bf16.

DPO замість RLHF. Direct Preference Optimization вимагає лише пари (chosen, rejected), а не скалярні reward‑сигнали. DPOTrainer з бібліотеки trl (Hugging Face) реалізує це кількома десятками рядків.

Типова помилка. Датасет з 500 прикладів, 5 епох, validation loss 0.8 — здається норм. Але на тесті модель деградувала на загальних інструкціях. Причина: catastrophic forgetting. Рішення — додати 10–20% загальних instruction‑following прикладів (Alpaca, FLAN) у навчальну вибірку, щоб не зруйнувати вихідні здібності.

Як обрати базову модель: 8B чи 70B?

Модель Параметри Сильні сторони Контекст
Llama‑3.1 8B 8B Баланс якість/швидкість 128k
Llama‑3.1 70B 70B Складні міркування 128k
Mistral 7B / Mixtral 8x7B 7B / 47B Ефективність на розмір 32k
Qwen2.5 72B 72B Код, мультимовність 128k
Gemma 2 27B 27B Відкрита ліцензія 8k

Для більшості задач fine‑tuning 8B моделі достатньо. 70B потрібен, коли потрібне глибоке міркування або baseline 8B не досягає потрібної якості навіть після донавчання. Вартість інференсу Llama‑3 8B через vLLM на A100 значно нижча, ніж у GPT‑4, що робить його економічно вигідним.

Що дає PagedAttention в production?

vLLM — перший вибір для serving open‑source моделей. PagedAttention — ключове технічне рішення: KV‑cache керується як virtual memory в ОС, без фрагментації. Це дає throughput у 2–4 рази вище порівняно з наївним HuggingFace Transformers inference. Документація vLLM підтверджує: continuous batching та PagedAttention — стандарт для високонавантажених LLM‑сервісів.

Типові числа на A100 80GB для Llama‑3 8B (bf16): 400–600 req/s, P50 latency 200–400ms, P99 latency 600–900ms при concurrency 64. Для 70B на двох A100 з tensor parallelism: 80–120 req/s, P99 latency 1.5–2.5s. Квантизація AWQ або GPTQ знижує споживання пам'яті в 2 рази при втраті якості в межах 1–3%.

Мультиагентні системи

Агенти — LLM з доступом до інструментів: пошук, виконання коду, запити до API, робота з БД. Основні патерни:

  • ReAct (Reason + Act): модель розмірковує → обирає інструмент → спостерігає результат → знову розмірковує. LangChain та LlamaIndex реалізують з коробки.
  • Multi‑agent orchestration: кілька спеціалізованих агентів з координатором зверху. Приклад: coordinator → researcher (пошук + summarization) → coder (генерація та виконання коду) → critic (перевірка). Інструменти: AutoGen (Microsoft), CrewAI, кастомна реалізація на LangGraph.

В продакшені агентні системи недетерміновані. Обов'язкові guardrails, ліміти кроків, логування кожного кроку, human‑in‑the‑loop для критичних дій.

Як ми гарантуємо якість LLM рішення?

Ми використовуємо RAGAS для автоматичної оцінки відповідей: faithfulness, answer relevancy, context precision. Система трекінгу експериментів на базі MLflow фіксує всі метрики, датасети та конфіги. Це дозволяє порівнювати різні гіпотези та доводити покращення з цифрами. Гарантію стабільної роботи забезпечує continuous integration з тестами на специфічних сценаріях (prompt injection, edge‑cases).

Як почати LLM розробку: наступні кроки

Ми передаємо:

  • Технічну документацію (model card, конфіги, інструкції з розгортання)
  • Доступ до інфраструктури (репозиторій з кодом, навчені ваги)
  • 1 місяць підтримки після деплою (консультації, виправлення багів)
  • Навчання команди замовника (2–3 заняття з експлуатації системи)

Терміни: базовий RAG‑прототип — 1–2 тижні. Fine‑tuning з даними замовника — 3–6 тижнів (з урахуванням підготовки даних). Production‑система з моніторингом та перенавчанням — 2–4 місяці.

Етап Тривалість Що отримуєте
Аудит та збір даних 1–2 тиж. Eval‑датасет з 100+ прикладів, формалізація задачі
Baseline (промпт + RAG) 1–2 тиж. Робочий прототип, метрики якості
Fine‑tuning (якщо потрібно) 2–4 тиж. Навчена модель, LoRA‑ваги, model card
Деплой та моніторинг 1–2 тиж. vLLM сервер, Grafana + Prometheus
Документація та навчання 1 тиж. API‑документація, навчання команди

Вартість розраховується індивідуально і залежить від обсягу даних, складності моделі та вимог до інфраструктури. Хочете оцінити свій проєкт? Зв'яжіться з нами — ми підготуємо попереднє резюме за 1–2 робочі дні. Або замовте консультацію фахівця з вибору підходу: RAG, fine‑tuning або гібрид — розповімо, що підійде саме вам.