AI-аналитик · разбор седьмойdavydov.my

Как AI-аналитик видит решения и почему этого мало

Модель отвечает по витринам. Витрина — не данные, а результат чьих-то решений, принятых задолго до вопроса. Я пошёл считать, сколько их и видит ли их модель.

Семь артефактов

Чтобы AI-аналитик посчитал число, человек до него пишет семь вещей. Вот они, с путями — чтобы не путать похожее.

артефактгде лежитчто этозачем
задачатрекертекстс этого всё начинается
вопросодна строка текстааналитик выводит его из задачи
код сборки витринdbt/models/*.sqlдевять файлов: три строят витрины, шесть — промежуточные таблицы под нихпревращают сырые таблицы в витрины, по расписанию
описания витринdbt/models/schema.ymlYAML: строка текста на таблицучто в таблице лежит, человеческими словами
тесты сборкиdbt/tests/тридцать пять SQL-проверокловят, если сборка сломалась; стоят и на витринах, и на промежуточных
файл определений метрикmetrics/metrics.ymlYAML: пятнадцать решений и комментарии к нимчто и как считать
эталонный запросtruth_sql/*.sqlSQL по сырым таблицам, не по витринамправда, с которой сверяют ответ; витрины не читает намеренно — иначе у правды и у проверяемого был бы общий родитель

Все семь написаны руками. На каждый потрачено время.

Два YAML, которые легко спутать. metrics/metrics.ymlфайл определений метрик: что считать метрикой и какие решения в ней приняты. dbt/models/schema.ymlописания витрин: что лежит в таблице. Первый про смысл метрики, второй про содержимое таблицы. В промпт попадают оба, но разными путями.

Как я называю вещи в этом тексте. Код сборки витрин — девять SQL-файлов. В dbt они называются моделями, но слово «модель» здесь занято языковой. Запускаются отдельно от агента: dbt run читает сырые таблицы и записывает результат. Витрина — готовая таблица fct_* или dim_*. Модель — только языковая. Агент — модель плюс обвязка вокруг неё. Отбор контекста — та часть обвязки, что собирает промпт.

Два слова, которые нельзя путать. Артефакт — то, что пишет человек: файл, текст, запрос. Их семь. Блок — часть собранного промпта. Их шесть. Соответствия между ними нет: три артефакта дают блоки, три блока человек не писал вовсе.

Откуда берётся промпт

Модель не получает ваш репозиторий. У неё окно, и весь проект в него не помещается — поэтому перед каждым вопросом отбор контекста собирает промпт: решает, что положить, и кладёт.

Я пересобрал этот промпт тем же кодом и сверил отпечатки блоков с логом. Совпали. Значит ниже — не реконструкция, а то, что реально ушло модели.

блок промптаисточникчто именнона нашем вопросе
инструкциякод обвязкиконстанта в agent/sql_gen.pyтри правила
файл определений метрикфайлmetrics/metrics.yml целиком, с комментариями4688 знаков
схема базыбазазапрос к information_schema3 объекта из 15
описания витринфайлdbt/target/manifest.json, только поля description2 строки
примеры значений из базыбазаSELECT DISTINCT … LIMIT 12 по поданным таблицам4 пары колонок
вопросснаружипришёл вместе с задачейодна строка

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

Файлов проекта отбор контекста открывает ровно два.

что за файлпутьоткрывается
файл определений метрикmetrics/metrics.ymlда
манифест сборкиdbt/target/manifest.jsonда
код сборки витринdbt/models/*.sqlнет
описания витринdbt/models/schema.ymlнет
тесты сборкиdbt/tests/*.sqlнет
эталонные запросыtruth_sql/*.sqlнет

Описания витрин в списке дважды и это не ошибка: файл schema.yml, куда вы их пишете, не открывается. Их содержимое попадает в промпт из манифеста, куда dbt перенёс его при сборке. Правка в schema.yml доедет до модели только после следующего dbt run.

Манифест — это JSON, который dbt пишет при компиляции: в нём описание каждой витрины, список колонок и — отдельным полем compiled_code — сам скомпилированный SQL. Отбор берёт оттуда только описания.

Теперь свяжем одно с другим: какой артефакт человека даёт какой блок промпта.

артефакт человекав промпткаким блоком и как
задачанет
вопросдаблок «вопрос», напрямую
код сборки витриннет
описания витриндаблок «описания» — но не из schema.yml, а из манифеста, куда их перенёс dbt
тесты сборкинет
файл определений метрикдаодноимённым блоком, целиком, напрямую
эталонный запроснет

Из семи артефактов человека блоки дают три. Остальные три блока промпта человек не писал: инструкция лежит в коде обвязки, схема и примеры значений спрашиваются у базы.

Витрины собраны заранее

Здесь важна последовательность во времени, и её легко упустить.

Витрины не собираются в момент вопроса. Команда dbt run выполняет девять файлов кода сборки, читает сырые таблицы и записывает результат — шесть промежуточных таблиц и три витрины. Запускается это по расписанию, раз в сутки или чаще, и к моменту, когда приходит вопрос, витрины уже лежат готовые.

Модель их не собирает и не пересобирает. Она читает результат.

Решения, закрытые в витрине, приняты заранее — другим человеком, в другой день, и заморожены в колонках.

Отсюда и вопрос, ради которого я полез в код сборки. Витрина — не данные, а чей-то выбор: кто-то решил, какое поле считать датой и кого считать гостем. Значит часть ответа определена до всякой модели и до всякого определения метрики.

Сколько этих решений — и видит ли их модель.

Сколько решений в коде сборки

Открываете код сборки. Видите SQL. Кажется, что он просто переносит данные из одного места в другое.

Отличить решение от реализации можно формально: решение — это место, где можно поставить другое и запрос останется рабочим.

строка в коде сборкиальтернативарешение?
customer_id is null as is_guest_orderфлаг is_guest в таблице клиентовда — обе исполнимы, результат разный
status = 'cancelled'status <> 'completed'да — при трёх статусах результат разный
received_at позицииreceived_at заказада — у отменённых оно часами позже
соединение по order_idнет — заменить нечем

Я прошёл по всем девяти файлам — шести промежуточным stg_*.sql и трём витринным fct_order_items.sql, fct_returns.sql, dim_customers.sql — и выписал такие места: что решалось, что выбрано, что было бы при другом выборе, и объявлено ли это где-нибудь, кроме самого SQL.

Получилось сорок четыре.

Сорок четыре — это всё подряд, включая соединения, где вряд ли кто-то сделает иначе. Поэтому я посчитал второй раз, по правилу, объявленному до счёта: альтернатива должна быть выполнима на данных и встречаться как чей-то реальный выбор в материалах проекта. Таких двадцать.

Что из них попадает в промпт

Теперь главный переход. Сорок четыре решения приняты. Модель читает два файла. Что из этих сорока четырёх до неё доезжает?

23описаны где-то — в комментарии кода сборки, в тесте, в описании таблицы
9попадают в промпт
14не попадают
17не описаны нигде — живут только в SQL сборки
4намерение не восстановлено — выбор сделан, но был ли он осознанным, из кода не видно

Разберём точнее, что именно из кода сборки доезжает.

где записано решениесущностьпутьв промптштук
в описании витриныописания витринschema.yml → манифестда9
в файле определенийфайл определений метрикmetrics/metrics.ymlда
в описании промежуточной таблицыописания витринschema.yml → манифесттолько при выключенном отборе таблиц2
в комментарии рядом с кодомкод сборки витринdbt/models/*.sqlнет12
в тестетесты сборкиdbt/tests/*.sqlнет
нигде, только в самом SQLкод сборки витринdbt/models/*.sqlнет17
намерение не восстановленонет4
всего944

Сам код сборки в промпт не попадает совсем — ни одной строки SQL, ни одного комментария. Решение доезжает до модели только тогда, когда кто-то отдельно, другими словами, в другом файле написал, что оно принято.

Тесты — обидный случай. В них записано ровно то, что нужно: например, что зерно таблицы позиций проверяется отдельно. Это объяснение существует, оно верное, и отбор его не открывает.

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

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

Это не «мы поленились». Это «мы написали не туда».

Общее: документация, которую не читает исполнитель, не работает, даже если она верная.
Частное: девять из двадцати трёх — доля моего проекта. Свою можно узнать за час: собрать промпт и поискать в нём свои объяснения.

Один вопрос, два пути

До сих пор речь шла о доставке: объяснение есть, но не доехало. Дальше выяснилось, что есть и вторая беда, и она другой природы.

Я взял вопрос «сколько активных клиентов сейчас» и задал его дважды. Первый раз — как обычно, по витринам. Второй — по сырым таблицам, с тем же файлом определений.

21 580по витринам · сошлось с эталоном
15 030по сырым таблицам · мимо

Модель в обоих случаях ответила с первой попытки, без уточнений и отказов. На сыром пути она сама написала запрос на пятьсот семьдесят шесть знаков: привела время к UTC, обрезала по курсору, сделала три соединения. Технически справилась.

Разрыв — 6550. Я разложил его исполнением, без обращения к модели: менял в запросе по одному условию и смотрел, как двигается число.

что менялидаёт
какое поле значит «гость»6549
забытое приведение времени к UTC1
граница окна: строгое сравнение или нет0

Одна развилка дала весь разрыв.

Словами и полем

В файле определений написано: гостевые заказы не считаем вовсе. Эта фраза доехала до модели — она в первых двух файлах, ничего не потерялось.

Но какое поле она значит, не написано. И на витрине вопрос не возникает: там поле одно, is_guest_order, посчитанное при сборке как «заказ без идентификатора клиента».

А в сырых таблицах способов два, и это две разные популяции:

способсколько
заказы без customer_id50 578
клиенты с флагом is_guest18 096

Модель взяла вторую.

Причём гостей она назвала вслух — перечислила эту развилку среди открытых. И всё равно выбрала не ту. А в конце написала, что все решения заданы слоем и потому вариант один. В сыром пути это неправда, и она этого не заметила.

Две беды, и лечатся они по-разному

Доставка. Объяснение написано, но в промпт не попало. Лежит в комментарии кода сборки или в тесте. Чинится переносом: тот же текст, но в другом месте.

Неоднозначность. Объяснение попало, модель его прочитала — и всё равно ошиблась, потому что текст допускает два исполнения. Переносом не чинится вообще: сколько ни переноси двусмысленную фразу, она останется двусмысленной.

Файл определений закрывает решение словами. Витрина закрывает его полем. Слова можно понять двояко — поле либо есть, либо нет.

Вот что на самом деле делает витрина. Не ускоряет запрос и не прячет сложность. Она делает решение неоткрываемым заново.

И оборотная сторона

Витрина принимает решение за всех. Если она закрыла развилку неверно, ни одна метрика её уже не переубедит — только пересборка.

Поэтому выбор между словами и полем — не «что лучше», а что чем платится. Слова оставляют метрике свободу и рискуют неоднозначностью. Поле убирает неоднозначность и забирает свободу.

В этом же стенде есть пример обратного решения: витрина возвратов держит обе даты — когда возврат случился и когда о нём узнали, — и в её описании прямо сказано, что выбор между ними принадлежит метрике. Развилка оставлена открытой сознательно.

Что проверить у себя

  1. Соберите промпт, который реально уходит вашей моделиИ посмотрите, что в нём лежит. Не «есть ли документация», а видит ли её модель.
  2. Поищите в нём свои объясненияТе, которыми вы гордитесь. У меня из двадцати трёх дошло девять.
  3. Возьмите три определения метрик и проверьте, исполнимы ли они одним способомЕсли написано «не считаем гостевые», а в данных два способа определить гостя — определение не работает. Оно выглядит закрытым, а решение всё равно принимает модель.
  4. По каждой неоднозначности решите, чем закрыватьДоуточнить словами — дёшево, но остаётся текстом. Закрыть полем в витрине — прочнее, но вы решаете за всех.
  5. Там, где развилку оставляете открытой, скажите это вслухВ описании таблицы. Иначе модель решит, что решение принято, и не станет спрашивать.

Путь целиком

Теперь всё вместе. Слева то, что существует, справа — доезжает ли оно до модели.

Семь артефактов человека
задача                    трекер ────────────────  нет
   ↓
вопрос                    ───────────────────────  ДА, напрямую

код сборки витрин         dbt/models/*.sql ──────  нет, ни строки
   ↓  44 решения                                   ├─  9 доезжают текстом
витрины ← собраны заранее, dbt run                 └─ 35 не доезжают никак

описания витрин           dbt/models/schema.yml ─  ДА, через манифест
тесты сборки              dbt/tests/ ────────────  нет
файл определений метрик   metrics/metrics.yml ───  ДА, целиком, напрямую
эталонный запрос          truth_sql/*.sql ───────  нет, его читает измеритель
                          по сырым таблицам
Три блока, которых человек не писал
инструкция                код обвязки ───────────  три правила
схема базы                запрос к базе ─────────  3 объекта из 15
примеры значений из базы  запрос к базе ─────────  12 значений

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

И сорок четыре решения кода сборки, из которых до модели добирается девять — не как код, а как текст описаний.

Как я это мерил

Промпт нигде не сохраняется, он собирается на лету. Но в лог пишутся отпечатки блоков. Я пересобрал промпт тем же кодом и сверил: отпечатки совпали с записью от восьмого сентября. Поэтому приведённый состав — факт, а не реконструкция.

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

Разрыв между путями разложен исполнением запросов, без обращения к модели: в SQL менялось по одному условию.

Оговорки

Один стенд, одна предметная область, двадцать одна таблица. Числа отсюда не переносятся, переносится устройство.

Проба двух путей сделана на одном вопросе. Она показывает механизм, а не долю: повторяется ли он, я не проверял.

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

Файл определений в обоих путях один и тот же, и он называет витрины, которых в сыром пути нет. Модель молча подставила сырые эквиваленты и не сообщила об этом.

Правило «не выбирай молча» записано в инструкции. То, что модель перечисляет развилки, — исполнение правила, а не её инициатива. Само поле для этого нигде не описано.

Все замеры сделаны на одноходовом агенте: один план, перезапуск только при падении. Следующий запрос по результату предыдущего он не пишет.

Вся серия про AI-аналитика

  1. Как я строил AI-аналитика2026.08 · 13 мин Учебная сборка: шесть этапов и семнадцать проверок
  2. Продакт задал двадцать вопросов AI-аналитику2026.08 · 12 мин Что он унесёт неверного и почему это не видно по числу
  3. Как получить от AI-аналитика ответ на задачу2026.09 · 10 мин Семь шагов между задачей и числом
  4. Как устроен слой метрик для AI-аналитика2026.09 · 10 мин Файл на сто семь строк и два его исполнителя
  5. AI-аналитику нужна не длинная задача, а точная2026.09 · 8 мин Семь сущностей задачи и три вида влияния шаблона
  6. Что бывает у AI-аналитика с вопросом по дороге к ответу2026.09 · 8 мин Шесть исходов одного вопроса
  7. Что бывает у AI-аналитика с вопросом по дороге к ответу (интерактив)2026.09 · карта Сто пять ячеек, каждая кликается
  8. Как AI-аналитик видит решения и почему этого малоВы здесь
davydov.my · AI-аналитик · разбор седьмой