Подключили LLM к базе на 253 таблицы тремя способами. Больше всех ошибались не модели
253 таблицы, 3 550 колонок и три комментария к схеме на всю базу. К этой красоте у нас прикручен агент, который пишет SQL за аналитиков: про него была первая статья. Агент работает через MCP: модель сама вызывает инструменты — посмотреть список таблиц, прочитать схему, выполнить запрос.
И вот на ревью очередной доработки мы разругались. Половина отдела: MCP это правильно, модель должна исследовать базу как человек. Вторая половина, включая меня: дорого и медленно, надо вшить схему в промт и не гонять модель по десять раз за схемой, которая не меняется. Спорили аргументами уровня «мне кажется». Потом надоело, и мы сделали то, что надо было сделать сразу: собрали бенчмарк на своих данных и померили.
Спойлер: не подтвердилась ни одна из позиций спора. А главную ошибку эксперимента совершили не модели.
Сначала калибровка
Финтех, миллион с лишним пользователей, шесть стран. База: MSSQL, 253 таблицы, самая широкая — 274 колонки. Описаний в схеме нет: 3 записи MS_Description на всю базу, семантика полей живёт в C#-коде приложения в виде енамов. Рабочая модель: MiniMax-M2, температура 0. Все цифры ниже — с этой конфигурации; на моделях другого класса и цены выводы могут развернуться, я это покажу.
Дисклеймер тот же, что всегда: кейсы реальные, несущественные детали изменены, цифры компании округлены. Метрики эксперимента точные.
Три подхода
A. MCP. Боевой вариант: инструменты db_list_tables / db_describe / db_query, агент сам исследует схему и выполняет запросы. Позиция первой половины отдела.
B. Schema linking. Два вызова модели: сначала ей показывают каталог всех 253 таблиц (имя плюс число колонок, всего 2 234 токена), она выбирает до 10 релевантных; потом получает полный DDL выбранных таблиц с их енамами и пишет SQL. На вопрос уходит 0.6-2.8k токенов. Моя позиция.
C. Вся схема в промт. Наивный вариант, который мы добавили третьим, чтобы было от чего отталкиваться: весь DDL плюс все 346 енамов, 58 727 токенов в каждый запрос. Никто в отделе за него не топил.
Про C мы заранее посчитали деньги, потому что «так никто не делает, это дорого». Оказалось: весь прогон C, до трёх попыток на каждый вопрос по 58k токенов — примерно 110 рублей, и это ещё без промт-кеширования, с ним дешевле. На MiniMax input стоит $0.26 за миллион токенов, и аргумент про деньги на нашей модели не работает. На Claude или GPT-4 та же арифметика дала бы другой результат, держите это в голове.
Как мерили
Сравнивать сгенерированный SQL с эталонным по тексту бессмысленно: два разных запроса могут быть оба правильными. Поэтому execution accuracy, как в академических бенчмарках Spider и BIRD: выполняем запрос, сравниваем result set с эталонным. Порядок строк и имена колонок не важны, числа с округлением, NULL и пустая строка — разные вещи.
Золотой набор: 29 вопросов, отобранных вручную из реальной истории обращений аналитиков к агенту (дубли и слишком специфичные убраны). 12 простых (одна таблица), 11 средних (2-3 джойна), 6 сложных (агрегации, оконные функции). На вопрос даётся 3 попытки. EX@1 значит совпадение с первой попытки, EX@3 значит хотя бы с одной из трёх. После неудачной попытки модель получает обратную связь по фиксированному шаблону, одинаковому для всех веток: текст ошибки БД либо «результат неверен, ожидались примерно такие колонки», без данных. Это слабая, но всё же подсказка, поэтому EX@3 — метрика с самоисправлением, в лоб со Spider её сравнивать нельзя.
Дальше честность требует признания. Эталонные ответы должен подтверждать живой аналитик, а у нас его не было: роль судьи взял агент на Claude, тот же, что собирал испытательный стенд. Компенсировали двумя проверками на каждый эталон: второй независимый SQL, написанный другим путём, обязан дать тот же результат, плюс сверка с реальностью: цифра обязана быть правдоподобной для бизнеса. Судья и испытуемые — модели разных вендоров, это снижает риск одинаковых ошибок, но не убирает его. Почему я так подробно об этом — увидите в следующем разделе.
Судья ошибся трижды. Один раз его поправила испытуемая модель
Пока собирали золотой набор, судья совершил три ошибки, и все три — одного типа: семантика домена додумана по имени колонки, без чтения кода.
Ошибка первая. Вопрос «покажи склады с регионами» судья объявил неотвечаемым: в таблице Warehouses региона нет. На прогоне все три ветки дружно нашли другую таблицу, где регион есть, с реальными данными. Составитель бенчмарка ошибся ровно тем способом, который принято приписывать моделям: искал по имени вместо смысла.
Ошибка вторая, на два порядка. «Сколько активных партнёров» судья посчитал через колонку BusinessmanStatus = 1 и получил 432. Перекрёстный запрос ошибку пропустил: оба запроса честно считали одно и то же неверное множество. Поймал только sanity-чек на соседнем вопросе: по той же логике в одной из стран активных партнёров вышло ноль, при нескольких тысячах пользователей. Полез в код: BusinessmanStatus — это статус ИП для выплат, а роль партнёра лежит в таблице ролей. Правильный ответ: 33 434. Ошибка в 77 раз, со статусом «подтверждён» после перекрёстной проверки.
Ошибка третья, и её нашла испытуемая модель. На том же вопросе про склады ветка C выдала фильтр по типу склада, которого не было в эталоне. Проверка по енаму в коде показала: модель права, франчайзинговые склады действительно отбираются по типу, эталон судьи считал лишние шесть штук.
Вывод, который дороже всей таблицы результатов: перекрёстная проверка ловит ошибки SQL, но не ошибки понимания предметной области. Их ловят только инварианты бизнес-реальности («в стране с тысячами пользователей не может быть ноль активных партнёров» — вот это инвариант) и несогласие испытуемых с эталоном.
И следствие, которое надо проговорить до таблиц: три пойманные ошибки на 29 эталонов — это десять процентов известного шума разметки. Сколько ошибок не поймано, я не знаю. Поэтому дальше я не интерпретирую разницы между ветками меньше десятка процентных пунктов: они внутри шума эталонов.
Живая база и четыре дефекта, которые выглядели как результаты
Эксперимент шёл на боевой базе, доступ только на чтение. Пилот на 5 вопросах, три ветки, 15 прогонов — и 1 зачёт. При том что на простейшем вопросе все три ветки написали буквально эталонный SQL. Разбор показал: база принимает около 11 регистраций и 39 заказов в час, прогон вопроса тремя ветками идёт 2-3 минуты, а эталон считался один раз в начале. COUNT(*) успевал уехать. Сравнение мерило скорость регистраций, а не качество SQL.
Это был первый из четырёх дефектов механики, и у всех четырёх общая черта: ни один не упал с ошибкой, каждый выдавал правдоподобные цифры. Второй: прогон, запущенный не из той директории, молча взял дефолтные настройки и ушёл на другого провайдера с другой моделью — спас только чужой WAF с ответом 403, иначе мы бы измерили не ту модель и не узнали об этом. Третий жил во второй метрике (о ней ниже): из-за порядка операций она выходила строже основной, что логически невозможно — по этой невозможности его и нашли. Четвёртый: скрипт пересчёта импортировал модуль прогона и перезапустил эксперимент, потратив деньги на дубликаты.
Всё, что было прогнано до починки, забраковано с пометками в логах и перепрогнано; в зачётных таблицах только чистые данные. Практическое правило, которое мы из этого вынесли: у каждой метрики должен быть инвариант, и проверять надо его, а не правдоподобие чисел. «Цифры выглядят осмысленно» — это не проверка. И второе: эталон на живой базе надо считать вплотную к запросу испытуемого, окно в минуты даёт ложные негативы, которые прекрасно маскируются под «модели плохо справились».
Результаты
Основная метрика EX — полное совпадение result set с эталоном (EX@1 с первой попытки, EX@3 хотя бы с одной из трёх).
Ветка | EX@1 | EX@3 | trunc50@3 | медиана латентности | ₽/вопрос | tool-calls |
|---|---|---|---|---|---|---|
A (MCP) | 13.8% | 37.9% | 44.8% | 104 с | 4.76 | 24.5 |
B (schema linking) | 31.0% | 55.2% | 55.2% | 43 с | 0.39 | 0 |
C (вся схема) | 24.1% | 58.6% | 62.1% | 22 с | 3.30 | 0 |
Про вторую метрику. У 12 вопросов из 29 эталонный ответ длиннее 50 строк, а боевой промт ветки A предписывает узкие выборки с TOP N — на таких вопросах полное совпадение для неё недостижимо по построению, а не из-за качества SQL. EX@trunc50 сравнивает совпадение при усечении эталона до 50 строк (после канонизации порядка, одинаково для всех веток) и показывает подходы в прод-роли «ответь аналитику». На тех самых 12 вопросах она поднимает A с 8.3% до 25.0% по EX@3.
По категориям сложности:
Ветка | simple EX@3 | medium EX@3 | complex EX@3 |
|---|---|---|---|
A (MCP) | 66.7% | 18.2% | 16.7% |
B (schema linking) | 66.7% | 54.5% | 33.3% |
C (вся схема) | 75.0% | 36.4% | 66.7% |
Наивный подход, за который никто не топил, оказался не хуже лучшего — и быстрее всех. Формально C впереди B по EX@3 на один вопрос, 17 против 16, и называть это победой по точности я не буду: это шум. Устойчивое различие в другом: C вдвое быстрее B и в пять раз быстрее A. Причина контринтуитивна: B делает два последовательных вызова модели, C один, и контекст в 20 раз больше оказывается быстрее лишнего round-trip.
Моя позиция выиграла деньги и первый выстрел. B точнее всех с первой попытки: 9 вопросов против 7 у C. Возможно, короткий контекст меньше отвлекает, но на этой выборке утверждать не берусь. И B в 8 раз дешевле C — правда, в абсолюте это 11 рублей против 96 за весь прогон. На дешёвых моделях экономия схлопнулась до погрешности; на дорогих она бы решала.
На сложных вопросах schema linking споткнулся об собственную архитектуру. Complex: C взял 4 вопроса из 6, B только 2. Выборка крошечная, но паттерн совпадает с выводом работы «The Death of Schema Linking?» (Maamari et al., 2024): на сильных long-context моделях отбор таблиц перестаёт помогать, потому что ошибка отбора необратима. Если нужная таблица не попала в выборку первого этапа, второй этап её уже не вернёт. Точную частоту таких потерь (как часто нужная таблица не попадала в топ-10) мы по логам ещё не считали — первый кандидат на продолжение.
MCP в нашей прод-конфигурации проиграл обеим. Здесь важная оговорка: A — единственная ветка в боевой конфигурации (промт с TOP N и узкими выборками), B и C собраны лабораторно. Так что это результат про наш прод, а не приговор MCP как идее — чистое сравнение потребовало бы ветку A с лабораторным промтом, и это кандидат на следующий эксперимент. Что видно уже сейчас: в среднем 24.5 обращения к инструментам на вопрос, модель тратит попытки на разведку схемы через INFORMATION_SCHEMA вместо ответа. Кстати, наши 13.8% EX@1 на базе с 3 550 колонками хорошо ложатся на данные Spider 2.0 (Lei et al., 2024): на реалистичных enterprise-базах агентные системы, решающие старый Spider на 90%+, падают до 10-25%.
Вопросы, которые не взял никто
8 из 29. И это самый полезный столбец эксперимента, потому что провалы не случайные.
Три вопроса упираются в одно: семантика живёт в коде, а не в базе. Роль партнёра не нашла ни одна ветка (та самая, на которой ошибся и судья), тип франчайзингового склада — тоже. Колонки хранят int, смысл которого задан C#-енамом; в базе смысла нет физически. Ни один из трёх подходов не имеет доступа к коду приложения, и на этих вопросах они равны в своей беспомощности. Ровно из-за этой проблемы бенчмарк BIRD выдаёт моделям external knowledge к каждому вопросу: на реальных базах схема не несёт смысла значений.
Остальные пять провалов: один вопрос оказался двусмысленным («активный каталог» все три ветки поняли шире моего эталона: претензия к формулировке вопроса, не к моделям, и эталон я задним числом не правил), один провал — невалидный T-SQL от ветки B (ORDER BY внутри UNION ALL, честный незачёт), остальное — сложные джойны и грязные данные.
Отдельная полка — грязные данные, каждый случай генерирует синтаксически корректный неверный ответ:
поле страны доставки при одном и том же ID страны содержит «Германия» (17 794 записи), «Germany» (1 226) и пустую строку. Ветка, фильтровавшая текстом, молча потеряла 17 тысяч строк;
мёртвая колонка IsBusinessman: False у всех ~400 тысяч пользователей. Ветки, увидевшие говорящее имя, получили пустой результат;
у одного склада регион записан пустой строкой вместо NULL: IS NOT NULL завышает счёт на единицу;
колонки фискального кода товара существуют, но не заполнены ни у одного из ~3 тысяч товаров.
Последний случай мы вынесли в отдельный мини-тест: вопрос, на который данных в базе нет, хотя колонки есть. Правильное поведение — сказать «данных нет». Результат: 0 из 3, все ветки уверенно выдали исполнимый SQL по пустым колонкам. Один вопрос — это кейс, а не метрика, но кейс показательный: аналитик получил бы пустую таблицу и сам решал бы, что она означает — «совпадений нет» или «поле не заполняется». Подход A, у которого был инструмент заглянуть в данные, им не воспользовался.
Что мы поняли
Спор «MCP против схемы в промте» был не о том. Разница между подходами оказалась меньше, чем влияние вещей, о которых мы не спорили: качества данных, места, где живёт семантика, и способности системы честно сказать «не знаю».
Если собрать выводы в порядке убывания пользы:
Семантика в коде — потолок любого подхода «схема → SQL». Пока смысл значений живёт в C#-енамах, модель видит голые числа. Изолированной абляции «с енамами / без» мы не делали, но 3 из 8 общих провалов упираются именно в семантику из кода — больше, чем любая разница между архитектурами.
Наивное решение обязано быть в бенчмарке. Вариант C мы добавили как грушу для битья, а он оказался не хуже лучшего и быстрее всех. Без него мы бы выбрали B и считали себя молодцами.
На дешёвых моделях длинный контекст быстрее и не дороже лишнего round-trip. Быстрая прикидка для вашего случая: сравните «размер схемы в токенах × цена input × среднее число попыток» с ценой и латентностью второго вызова модели. Schema linking нужен, когда схема не влезает в контекст или input дорогой; иначе кладите всё.
Перекрёстная проверка эталонов не ловит ошибки понимания домена. Инварианты бизнес-реальности — ловят. Стройте проверку на них.
Бенчмарк на живой базе обязан считать эталон вплотную к запросу, иначе он меряет трафик, а не модель.
Хорошая новость для тех, кто захочет повторить: это вечер работы, если есть история вопросов. Три шага: взять 20-30 реальных вопросов аналитиков и построить к ним эталонные SQL с проверкой инвариантами; прогнать свои варианты подключения с фиксированными промтами, температурой 0 и одинаковым шаблоном обратной связи; сравнивать result set’ы, а не текст запросов. Подробности и готовые промты — в первом комментарии.
И отдельно: наши результаты не открытие, а производственное воспроизведение известных. Паритет полной схемы и schema linking на сильных моделях: The Death of Schema Linking?. Провал агентов на реальных enterprise-схемах: Spider 2.0. Необходимость внешних знаний к грязным базам: BIRD. Мы просто убедились, что всё это правда и на нашем проде, за 245 рублей.
Что меняем в проде: выбрали C. Вся схема с енамами уходит в системный промт, двухэтапный отбор таблиц не внедряем: точность не хуже, вдвое быстрее, и одна ступень вместо двух — меньше мест, где можно ошибиться необратимо. На нашей модели это решение практически бесплатное; если когда-нибудь переедем на дорогую модель или схема перерастёт контекст, вернёмся к прикидке из вывода 3.
Ограничения
Одна база (MSSQL), одна модель (MiniMax-M2 через официальный API, прогон 14.08.2026, температура 0), один прогон каждой ветки, 29 вопросов: всё на нашем наборе, без претензии на генерализацию. Повторных прогонов не было; при повторе цифры могут сдвинуться на 1-2 вопроса, а это сопоставимо со всеми разницами между B и C, поэтому их я и не интерпретирую. Устойчивые различия: латентность, деньги и профиль провалов. Эталоны строил и подтверждал агент на Claude, не человек, и трижды ошибся: дважды его поправили процедуры, один раз — сама испытуемая модель. Три пойманные ошибки из 29 эталонов дают оценку шума разметки снизу, непойманные не посчитаны. Правила зачёта фиксировались до прогонов. Эталоны, в которых доказана ошибка судьи, исправлялись, затронутые прогоны браковались целиком, и вопрос шёл в прогон заново; перезачётов задним числом не было. Сырые логи: 614 записей, включая все забракованные с причинами. Сами вопросы и схему выложить не можем, NDA.
Вердикты по-нашему: C 🟢 — берём в прод. B 🟡 — хорош первым выстрелом и деньгами, вернёмся к нему, когда схема перерастёт контекст или модель подорожает. A 🔴 — в текущей прод-конфигурации; это вердикт нашему конфигу, не MCP как идее, реабилитационный матч — в следующем эксперименте.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.