Проблема: аналитики тонут в рутине, а данные слишком чувствительны для ChatGPT
В ecom.tech - российском маркетплейсе с оборотом в сотни миллионов - каждый день рождалось 50-70 одинаковых вопросов от отделов маркетинга, продаж и логистики. "Какая категория товаров показала самый высокий рост в Москве за последний месяц?" "Сколько клиентов из Санкт-Петербурга совершили повторные покупки?" "Какой средний чек у пользователей, которые пришли через TikTok?"
Аналитики тратили 60% рабочего времени на рутинные SQL-запросы. Пробовали ChatGPT - получили предупреждение от юридического отдела: данные о клиентах, закупочных ценах и логистических маршрутах не должны покидать периметр компании. Даже с анонимизацией оставался риск утечки структуры данных и бизнес-логики.
Ключевая ошибка, которую совершают 90% компаний: пытаются использовать публичные LLM для внутренних данных без понимания рисков. Даже если вы "забыли" передать реальные значения, сама структура запросов (JOIN между таблицами users и orders, фильтры по регионам, агрегации по категориям) раскрывает архитектуру вашего бизнеса.
Решение: локальная модель, изолированная база, строгие guardrails
Мы построили систему, которая работает по принципу "ничего не выходит за пределы серверной стойки". Три кита архитектуры:
- Локальная LLM - модель работает на наших GPU, без доступа в интернет
- Изолированная БД-реплика - только для чтения, с ограниченным набором таблиц
- SQL-валидатор - проверяет каждый запрос перед исполнением
Зачем такая паранойя? Представьте, что бот по ошибке сгенерирует "DELETE FROM users WHERE id > 0". Или начнет делать тяжелые JOIN на производственных таблицах в час пик. Или - что хуже всего - через подсказку в промпте передаст структуру вашей базы в ответ пользователю.
Выбор модели: почему не Llama 3.2 и не DeepSeek-V3
На февраль 2026 года в топе локальных моделей для SQL несколько кандидатов:
| Модель | Размер | Точность на Spider | Плюсы | Минусы для нашего кейса |
|---|---|---|---|---|
| SQLCoder-2 7B | 7B параметров | 86% | Специализирована на SQL, понимает диалекты | Требует 16GB VRAM |
| Llama 3.2 3B | 3B параметров | 72% | Быстрая, работает на CPU | Часто ошибается в сложных JOIN |
| DeepSeek-Coder-V3 1.3B | 1.3B параметров | 68% | Очень легкая, хороша для простых запросов | Плохо справляется с бизнес-логикой |
Мы выбрали SQLCoder-2 7B с квантованием до 4-бит. Почему? Потому что специализированные модели всегда бьют универсальные в своей нише. SQLCoder тренировали именно на генерации SQL, она понимает разницу между PostgreSQL, MySQL и даже умеет работать с оконными функциями.
А вот Llama 3.2, несмотря на всю ее популярность, слишком часто генерировала синтаксически правильный, но логически неверный SQL. Типичная ошибка: "SELECT COUNT(*) FROM orders WHERE user_id IN (SELECT id FROM users WHERE city = 'Москва')" вместо более оптимального JOIN.
Не верьте benchmark'ам на идеальных датасетах вроде Spider. В реальности ваша база содержит кастомные поля, нестандартные названия таблиц и бизнес-специфичную логику. Специализированная модель справится лучше.
1 Подготовка изолированной базы данных
Первое правило безопасности: бот не должен касаться production-базы. Создаем реплику только для чтения с ограниченным доступом:
-- Создаем пользователя только для чтения
CREATE USER bot_reader WITH PASSWORD 'complex_password_here';
GRANT CONNECT ON DATABASE ecom_db TO bot_reader;
-- Даем доступ только к определенным таблицам
GRANT SELECT ON users, orders, products, categories TO bot_reader;
-- Запрещаем доступ к служебным таблицам
REVOKE ALL ON payment_logs, admin_logs, internal_audit FROM bot_reader;
Затем создаем отдельную схему с вьюхами, которые скрывают чувствительные поля:
CREATE SCHEMA bot_schema;
CREATE VIEW bot_schema.safe_users AS
SELECT
id,
-- Хэшируем email вместо оригинала
MD5(email) as email_hash,
city,
registration_date,
-- Группируем возраст в диапазоны
CASE
WHEN age < 20 THEN 'under_20'
WHEN age BETWEEN 20 AND 30 THEN '20_30'
ELSE 'over_30'
END as age_group
FROM public.users;
Это не просто "безопасность", это защита от глупых ошибок. Если бот случайно сгенерирует запрос с email в WHERE-условии, он просто не найдет совпадений - потому что работает с хэшами.
2 Создание RAG-системы со схемой базы
Самая частая ошибка новичков: скормить модели "сырую" схему базы. 200 таблиц, 3000 колонок - модель запутается. Вместо этого создаем структурированное описание:
{
"tables": [
{
"name": "orders",
"description": "Таблица заказов. Содержит информацию о покупках клиентов.",
"columns": [
{"name": "id", "type": "integer", "description": "Уникальный идентификатор заказа"},
{"name": "user_id", "type": "integer", "description": "Ссылка на клиента в таблице users"},
{"name": "product_id", "type": "integer", "description": "Ссылка на товар в таблице products"},
{"name": "amount", "type": "decimal", "description": "Сумма заказа в рублях"},
{"name": "created_at", "type": "timestamp", "description": "Дата и время создания заказа"}
],
"relationships": [
{"foreign_table": "users", "foreign_key": "id", "local_key": "user_id"},
{"foreign_table": "products", "foreign_key": "id", "local_key": "product_id"}
],
"example_queries": [
"SELECT COUNT(*) FROM orders WHERE created_at >= '2025-01-01';",
"SELECT user_id, SUM(amount) FROM orders GROUP BY user_id ORDER BY SUM(amount) DESC LIMIT 10;"
]
}
]
}
Этот JSON становится контекстом для модели. Мы не просто даем имена таблиц - мы объясняем бизнес-смысл, приводим примеры запросов, показываем связи. Без такого структурирования точность падает на 30-40%.
Если вам интересно глубже погрузиться в архитектуру RAG для баз данных, посмотрите нашу статью "Когда SQL и векторный поиск дерутся за ваши данные", где мы разбираем гибридные подходы.
3 Настройка локальной LLM с Ollama
Ollama - не единственный вариант, но самый удобный для быстрого старта. Устанавливаем и загружаем модель:
# Устанавливаем Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Загружаем SQLCoder-2 с 4-битным квантованием
ollama pull sqlcoder2:7b-q4_K_M
# Запускаем модель с увеличенным контекстом
ollama run sqlcoder2:7b-q4_K_M \
--num_ctx 8192 \
--temperature 0.1 \
--seed 42
Температуру ставим 0.1 - нам нужна детерминированность, а не креативность. Контекст увеличиваем до 8192 токенов, чтобы поместилась схема базы и история диалога.
4 Промпт-инжиниринг: как заставить модель генерировать безопасный SQL
Вот промпт, который мы используем в ecom.tech. Обратите внимание на строгие инструкции:
Ты - SQL-ассистент для базы данных маркетплейса ecom.tech.
СХЕМА БАЗЫ ДАННЫХ:
{вставляем JSON со схемой}
ПРАВИЛА:
1. Генерируй только SQL-запросы на диалекте PostgreSQL 15
2. НИКОГДА не используй DROP, DELETE, UPDATE, INSERT, TRUNCATE
3. Используй только таблицы из приведенной схемы
4. Всегда добавляй LIMIT к запросам, если пользователь не указал иное
5. Если запрос требует JOIN нескольких таблиц - объясни логику в комментарии
6. Для дат используй формат 'YYYY-MM-DD'
7. Если не уверен в правильности запроса - верни NULL и объясни почему
ВОПРОС ПОЛЬЗОВАТЕЛЯ: {вопрос пользователя}
SQL-ЗАПРОС:
Ключевой момент: правило "верни NULL, если не уверен". Лучше не дать ответ, чем дать неправильный. В ранних версиях бот иногда генерировал SQL для несуществующих таблиц - потому что пользователь упомянул "отчеты", а модель решила, что есть таблица reports.
5 SQL-валидатор: последний рубеж защиты
Даже с лучшим промптом модель может ошибиться. Наш валидатор проверяет:
import sqlparse
from sqlparse.sql import Token
from sqlparse.tokens import Keyword, DML
def validate_sql(sql: str) -> tuple[bool, str]:
"""Проверяет SQL-запрос на безопасность"""
# 1. Парсим запрос
parsed = sqlparse.parse(sql)[0]
# 2. Проверяем, что это SELECT
first_token = parsed.token_first(skip_cm=True)
if not first_token or first_token.value.upper() != 'SELECT':
return False, "Только SELECT-запросы разрешены"
# 3. Ищем запрещенные операции
forbidden = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE',
'GRANT', 'REVOKE', 'ALTER', 'CREATE']
for token in parsed.flatten():
if token.ttype is Keyword and token.value.upper() in forbidden:
return False, f"Запрещенная операция: {token.value}"
# 4. Проверяем наличие LIMIT (если не агрегация)
has_limit = any(
token.ttype is Keyword and token.value.upper() == 'LIMIT'
for token in parsed.flatten()
)
# 5. Проверяем сложность запроса
estimated_cost = estimate_query_complexity(parsed)
if estimated_cost > 1000: # Условная метрика
return False, "Запрос слишком сложный для автоматического выполнения"
return True, "OK"
Валидатор не просто ищет ключевые слова - он оценивает сложность запроса. Слишком много JOIN? Слишком большие таблицы? Отклоняем и просим пользователя уточнить.
Интеграция с бизнес-процессами: как мы внедряли бота
Техническая часть - только половина успеха. Вторая половина - заставить людей использовать систему. Наша стратегия:
- Фаза альфа-тестирования - 5 аналитиков, только простые запросы
- Сбор обратной связи - фиксируем каждый случай, когда бот ошибся
- Доработка промпта - добавляем исключения для частых ошибок
- Постепенное расширение - добавляем новые таблицы и типы запросов
Через месяц бот обрабатывал 40% всех рутинных запросов. Аналитики экономили 2-3 часа в день. Но главное - мы создали "живую" систему, которая училась на ошибках.
Самый частый вопрос от руководства: "А если бот даст неправильные данные, и мы примем ошибочное решение?" Ответ: бот НЕ принимает решения. Он только генерирует SQL. Аналитик всегда проверяет результат, прежде чем использовать его в отчетах.
Типичные ошибки и как их избежать
За 3 месяца работы мы набили все возможные шишки:
| Ошибка | Последствия | Решение |
|---|---|---|
| Без LIMIT в агрегатных запросах | PostgreSQL пытался посчитать SUM по всей таблике из 50 млн строк | Добавили правило: всегда LIMIT, если нет GROUP BY |
| Неправильные алиасы таблиц | Запрос "SELECT u.name, o.amount FROM users o JOIN orders u" - полная путаница | Добавили в промпт требование использовать понятные алиасы |
| Игнорирование индексов | Модель генерировала запросы без учета индексов, что замедляло выполнение | Добавили информацию об индексах в схему базы |
Самая коварная ошибка: модель иногда "выдумывала" поля, которых нет в базе. Пользователь спрашивал "сколько клиентов из офлайн-магазинов", а модель искала поле offline_store в таблице users. Решение: добавили в валидатор проверку существования колонок через INFORMATION_SCHEMA.
Что дальше? Планы на 2026 год
Наш PoC доказал жизнеспособность подхода. Следующие шаги:
- Добавление большего контекста - не только схема, но и примеры реальных бизнес-запросов
- Интеграция с BI-системами - бот будет не только генерировать SQL, но и строить графики в Metabase
- Мультимодальность - пользователь загружает скриншот таблицы Excel, а бот предлагает SQL для аналогичных данных в базе
- Обучение на собственных данных - fine-tuning модели на исторических запросах аналитиков ecom.tech
Если вы только начинаете работать с базами данных и SQL, рекомендую курс "SQL для анализа данных". Понимание основ SQL критически важно даже при использовании ИИ-ассистентов - иначе вы не сможете проверить, правильный ли запрос сгенерировал бот.
Локальные LLM для бизнеса - это не будущее, это настоящее. Вопрос не в том, стоит ли внедрять такие системы, а в том, как сделать это безопасно и эффективно. Наш опыт в ecom.tech показывает: начинайте с малого, тестируйте на изолированных данных, и не ожидайте 100% точности с первого дня.
Главный урок: лучший бот-аналитик не тот, который никогда не ошибается (такого не существует), а тот, чьи ошибки безопасны и понятны. И который освобождает людей от рутины, а не создает новые проблемы.