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

Под каждую задачу AI-аналитику нужна своя обвязка

Когда говорят про обвязку AI-аналитика, представляют одну машину: на входе вопрос, на выходе число. Но аналитик отдаёт не только вопросы. Я прогнал агента на четырёх типах задач — число, правка отчёта, выгрузка, A/B-тест — и увидел, что общая у обвязки примерно половина. Вторая половина меняется от типа к типу, и от неё зависит, можно ли задачу отдавать.

Что такое обвязка

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

Из чего собирается промпт и что в него не попадает, я разбирал в прошлой статье — «AI-аналитик видит не ваш проект, а промпт». Здесь другой вопрос: что в обвязке меняется, когда меняется сама задача.

Термины и файлы, на которые я дальше ссылаюсь:

терминчто этогде лежит
промежуточная таблицапочищенные сырые данныеstg_*
код сборки витринSQL, который строит витриныdbt/models/*.sql
витринарезультат кода сборкиfct_*, dim_*
описания витринтекст к таблицам и колонкам; до агента доезжает через манифест, после пересборкиschema.yml → dbt/target/manifest.json
файл определений метрикзаписанные решения: окна, статусы, гости, сравненияmetrics/metrics.yml
отбор контекстачасть обвязки: что положить в промптagent/retrieval.py
эталонный запросверный ответ, написанный по сырым таблицамtruth_sql/*.sql
модельтолько языковая модель—
агентмодель плюс обвязка—

Общее ядро и сменная часть

У всех типов задач одно ядро. Оно отвечает на вопрос «что агент знает о данных»: доступ к базе, список объектов, описания витрин и примеры значений, файл определений метрик metrics/metrics.yml, склейка промпта в agent/retrieval.py, лог с отпечатками блоков из agent/loop.py. От типа задачи ядро не зависит, его строят один раз.

Над ядром — сменная часть. Она отвечает на другой вопрос: что от агента хотят и как понять, что он справился.

Схема: пять сменных модулей по типам задач над общим ядром ВХОД ОТВЕТ ХОДЫ ПРИЁМКА число правка выгрузка A/B исследование вопрос3 исхода1 ходсверка старый запросодин запрос1 ходрегрессия заказ словамиодин запрос1–2 ходадве сверки сводка теста4 вердикта1 ходвердикт «почему»вывод / уточнениемного ходовустойчивость Ядро — одно на все типы доступ к базесписок объектовописания и примеры файл определений метриксклейка промпталог и отпечатки
Сменная часть — вход, контракт ответа, число ходов и приёмка. Исследование пунктиром: на стенде не измерено.

Та же картина таблицей, с тем, что измерено:

числоправка отчётавыгрузкаA/B-тестисследование
что даёмвопросдействующий запрос и что поменятьзаказ словамисводка по тестувопрос «почему»
что видитотбор контекста со слоем tables: на одном из вопросов — 3 объекта из 15вся схема, слоя tables нетвся схема, слоя tables неттолько сводку—
раннерgrid_subjects.py, two_arms.pyedit_run.pyextract_run.pyrun_verdicts.pyнет
что может вернутьответ, уточнение или отказодин запросодин запросодин из четырёх вердиктоввывод или уточнение
может спроситьданетнетнетда
сколько ходоводинодинодин, лучше дваодин на сводкемного
чем приниматьсверка с эталономрегрессия против прошлой версиивнутренние проверки и сверка ключей и значенийвердикт против заложенногоустойчивость вывода
измерено105 + 18 попыток18 попыток15 попыток15 в счёте, 20 всегоодна задача, три повтора

Правка отчёта: вход, который всё решает

Что даём. Текст действующего запроса и одну фразу правки: убрать фильтр, расширить период, добавить колонку. Действующий запрос — это эталонный запрос truth_sql/v1.sql…v5.sql, правка сверяется с v1_rev.sql…v6.sql. Раннер — experiments/edit_run.py.

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

Что вышло. Восемнадцать правок из восемнадцати верны. Во всех восемнадцати агент сам назвал в замечаниях решения, которые не трогал или принял сам. В семи поменял в запросе больше, чем просили — чаще всего удалил неиспользуемый кусок. На результат это не повлияло.

В правке «добавить колонку скидки» есть два пути: взять готовую колонку order_items.discount или пересчитать скидку через таблицу акций promotions. По числам они почти неотличимы — 11 заказов из 16 019 на копейку, из-за разного округления. Какой путь выбрал агент, видно только по тексту запроса.

«В заказе не было явного правила, как связывать discount с promotions — это моё решение, а не факт из данных».агент, правка «добавить колонку скидки», попытка 1

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

Два вида правки: снятие фильтра сохраняет старые строки, правка агрегата сохраняет ключи и меняет значения Сняли фильтр старые строки: 16 019 обязаны остаться как были новый результат: 17 003 Поменяли агрегат КЛЮЧБЫЛОСТАЛО 8 ключей те же · 8 из 8 значений другие
Одна регрессия на оба случая не годится. Слева правка обязана сохранить строки и не тронуть значения, справа — сохранить ключи и изменить все значения.

И сравнивать надо с допуском. Побайтовое сравнение дало бы 1 206 ложных «строка пропала» из 16 019 — это шум сложения дробных чисел, максимум 9·10−13.

Урок для обвязки

Подавайте агенту действующий запрос целиком — это и объяснение решений, и эталон для проверки. И смотрите не только на результат, но и на изменения в коде.

Выгрузка: проверка должна быть внутри

Что даём. Заказ словами и всю схему.

Вся схема оказалась важнее, чем я думал. По карте, составленной до прогона, из девяти решений, которые требовали пять заказов, восемь были записаны там, где агент их видит. Но одно из восьми — правило про повторные записи при оплате — лежит в описании промежуточной таблицы stg_order_items, в dbt/models/staging/schema.yml. Раннер выгрузки experiments/extract_run.py работает без слоя tables, поэтому описания stg_* доходят. При отборе контекста со слоем tables это описание отфильтровалось бы. Дойдёт ли решение до агента, зависит не от того, в каком файле оно лежит, а от того, как настроена обвязка.

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

Что вышло. Пятнадцать выгрузок, семь помечены неверными.

Пятнадцать выгрузок: 8 верно, 3 ложные тревоги проверки, 3 ошибки на незаписанном решении, названном до прогона, 1 на решении, найденном агентом На записанных и дошедших решениях ошибок: 0 8 верно 3 ложная тревога проверки: дата со временем, округление 3 не записано — названо до прогона 1 не записано — нашёл агент
Каждый квадрат — одна попытка. Красное — ошибки агента, и все они на решениях, которых нет там, где он может их прочитать.

Главный кадр. Заказ: выгрузить заказы гостей с каналом. Кого считать гостем, агенту нигде не записано. Верное решение — заказ без customer_id — живёт в коде сборки stg_orders.sql, а код сборки в промпт не попадает. В данных два способа: колонка is_guest_order в витрине fct_order_items и флаг is_guest в срезе клиентов customers_asof. Агент взял оба сразу, через «и».

Два определения гостя не пересекаются, поэтому их пересечение пусто: ноль строк вместо 11 475 is_guest_orderзаказ без клиента is_guestклиент-гость ∩ эталон 11 475 строк агент: оба условия через «и» 0 строк
Две популяции не пересекаются: у заказа без идентификатора клиента нет строки в таблице клиентов.

И пустая таблица прошла все внутренние проверки: ключ уникален, дублей нет, лишних колонок нет. Всё верно — потому что строк нет.

Запрос агента фильтровал двумя условиями сразу: WHERE foi.is_guest_order = TRUE AND c.is_guest = TRUE.

«Дополнительно проверено c.is_guest = TRUE, чтобы взять именно гостевой профиль».агент, выгрузка гостей — развилку назвал и закрыл неверно в одной фразе
Урок для обвязки

Внутренних проверок мало, нужна сверка с чем-то внешним. И проверка должна стоять в обвязке до того, как результат уйдёт заказчику, а не шагом «потом посмотрим».

Число: право спросить меняет поведение

Что даём. Вопрос. Агент может ответить, переспросить или отказаться.

Что вышло. Из 105 попыток верное число дали 20, в девяти агент ответил, но число не сошлось. Я разобрал все девять: все на решениях, которых агент не мог прочитать нигде — какие периоды сравнивать, в каком виде нужен ответ, делать ли поправку на категории.

Тогда я записал эти решения в новую ревизию metrics/metrics.yml — например, compare_periods: calendar_half для сравнения год к году и basis: new_registered для «базы клиентов» — и задал те же вопросы, по три раза каждый. Раннер — experiments/two_arms.py: два плеча с одинаковой подачей, разница только в блоке файла определений, а отпечаток layer_sha подтверждает, какой файл видела модель.

Без записи развилка взята 0 из 9, с записью 9 из 9; число сошлось 0 и 2 из 9 РАЗВИЛКА ВЗЯТАЧИСЛО СОШЛОСЬ без записис записью без записис записью 0 из 9 9 из 9 0 из 9 2 из 9 сошлось форма ответа: 4 другое определение «промо»: 3
Три вопроса, три повтора, одинаковая подача. Различие — только записанные решения в файле определений.

В четырёх случаях агент посчитал верно, но выдал два числа вместо процента — форму ответа я не записывал. В трёх разошлось определение «промо»: в витрине fct_order_items поле on_promo — это discount > 0, была скидка; в эталонном запросе truth_sql/s11a.sql — на товар действовала акция из promotions. Агент посчитал по полю витрины.

И обратный случай: без новой записи агент взял из metrics.yml окно window_days: 90 и честно посчитал на нём год к году. Решение было записано, но не для этого вопроса.

«Все развилки для метрики revenue (… compare_to=year_over_year с compare_periods=calendar_half) уже явно зафиксированы в metrics.yml, поэтому вариант один».агент, вопрос о росте выручки, с записью

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

Урок для обвязки

С правом спросить агент на одном из вопросов в двух попытках из трёх остановился и переспросил — про то самое определение, которое я потом записал. Без права он выбрал бы сам. Контракт ответа — не формальность: он решает, увидите ли вы развилку.

A/B-тест: вход решает, что вообще можно увидеть

Что даём. Сводку по тесту: дизайн — метрика, единица рандомизации, длительность, альфа, практический порог, охранные метрики — и таблицу результатов по группам. Кейсы взяты из синтетического корпуса ab-factory. У каждого три файла: contract.json — дизайн, data.csv — результаты по группам, truth.json — эталон, поле expected_decision. Сводку собирает build_case() из tools/build_trainer_pool.py.

У эталона три значения, а у агента четыре вердикта. ship — раскатывать, do_not_ship — не раскатывать, а investigate принимает и «не готов», и «тест сломан». Делить investigate самому значило бы принять за корпус незаписанное решение, поэтому главный счёт — по эталону корпуса.

Сначала пришлось решить, чего агенту не давать. В корпусе почти в каждом поле кейса лежит подсказка к ответу.

поле кейсачто в нёмчто сделали
notes — заметки авторапрямо называют проблему: «Underpowered — cannot conclude»убрали
title — заголовоку 21 кейса выдаёт вид ловушкиубрали
segments — разбивка по сегментаместь только у четырёх видов ловушек — само присутствие подсказываетне подаём никому
randomization.target_split — целевая доля группуказана только у кейсов со сломанным делениемподаём всем, по умолчанию 50/50
case_id — номер кейсакорпус публичный, номер — крючок для узнаванияубрали

Что может вернуть. Один из четырёх вердиктов: раскатывать, не раскатывать, не готов, тест сломан. «Не готов» — законный ответ, а не уход от ответа, и обвязка обязана его позволять.

Что вышло. Четырнадцать верных из пятнадцати. Правило, которое по умолчанию раскатывает всё значимое и в плюс (decision_agent.py), на тех же кейсах — девять. Ложных выкаток у агента ни одной.

Вердикты правила и агента по шести сценариям, три повтора агента ПРАВИЛОАГЕНТ · 1АГЕНТ · 2АГЕНТ · 3 чистый эффект мало данных эффект ниже порога сломанное деление групп множественные сравнения новизна — вне счёта раскатыватьраскатыватьраскатыватьраскатывать разбиратьсяне готовне готовне раскатывать не раскатыватьне раскатыватьне раскатыватьне раскатывать раскатыватьтест сломантест сломантест сломан разбиратьсяне раскатыватьне раскатыватьне раскатывать раскатыватьраскатыватьраскатыватьраскатывать 9 из 15 14 из 15
Красное — неверный вердикт. Правило считается один раз на кейс, в счёте — по трём повторам. Новизна вне счёта: её улика только в заметках, которые агент не видел.

Самое наглядное — тест со сломанным делением на группы. Правило его выкатило бы. Агент три раза из трёх сказал «тест сломан» и каждый раз сам посчитал проверку.

Объявлено 50 на 50, пришло 329 663 против 313 582 объявленопришло контроль 50%тест 50% контроль 329 663 · 51.2%тест 313 582 · 48.8% середина
Разница выглядит мелкой, но на трёхстах тысячах пользователей в плече она невозможна случайно. Правило смотрит только на эффект и p-значение — деление групп оно не проверяет.

Единственная ошибка. На тесте, где данных честно мало, агент два раза сказал «не готов», а в третий — «не раскатывать», по тем же самым числам. Решение «незначимый результат на малой выборке — это „эффекта нет“ или „данных мало“» в сводке не записано: я не подал ни минимальный детектируемый эффект mde_relative, ни целевую мощность power_target, хотя в contract.json они есть у всех 660 кейсов. Их просто не извлекает build_case(). Незаписанное решение модель закрывает каждый раз заново.

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

«DAU без данных, но это единственный неучтённый guardrail, и он не отменяет явного положительного результата».агент, тест с эффектом новизны

На тесте с множественными сравнениями вердикт верен три из трёх, но механизм не назван ни разу: лишних метрик в сводке нет.

Урок для обвязки

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

Исследование: одного хода мало

Вопрос «почему упала выручка» не решается одним запросом. Нужно выдвинуть гипотезу, посмотреть на результат, выдвинуть следующую.

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

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

Четыре вещи, которые обвязка решает за вас

решениегде проявилосьчто вышло
что показатьвыгрузка, A/Bслой tables в agent/retrieval.py включает и выключает сразу схему, описания и примеры; в сводке A/B любое лишнее поле может оказаться подсказкой
что агент может вернутьчисло, правка, выгрузка, A/Bу числа — answer, clarify, refuse; у правки и выгрузки инструкция «верни ОДИН SELECT»; где спросить нельзя, развилку агент закрывает сам
сколько раз посмотретьвыгрузка, исследованиеодин ход — агент не может посмотреть на данные: он спрашивает или выбирает молча; пустая таблица проходит молча
что останется в записивсепромпт не сохраняется, только отпечатки блоков layer_sha и context_sha; у 705 старых прогонов не было и отпечатков

Чего обвязка не может

Создать решение.

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

В A/B то же самое: агент сорвался ровно на том решении, которое я не подал в сводке.

Обвязка решает, дойдёт ли решение до агента. Придумать его она не может — это по-прежнему работа аналитика.

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

  1. Что вы даёте агенту на входДля правки — подаёте ли вы действующий запрос целиком. Для A/B — нет ли в сводке полей, которые подсказывают ответ, и есть ли поля, без которых ответ невозможен.
  2. Что он видитСоберите промпт, который реально уходит модели, и найдите в нём решения, которые вы считаете записанными. Описания из schema.yml доезжают через манифест — правка без пересборки до агента не дойдёт.
  3. Что он может вернутьМожет ли агент спросить или отказаться. Если нет — каждую развилку он закроет сам.
  4. Сколько ходов нужно задачеЕсли результату надо посмотреть на данные прежде, чем делать вывод, одного хода мало.
  5. Чем принимаете результат — и стоит ли проверка до выдачиПравку — против прошлой версии, выгрузку — изнутри и снаружи, A/B — по ширине интервала и делению на группы.

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

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

Число, правка и выгрузка — на синтетическом стенде retail-lab, где верный ответ известен по построению. A/B — на синтетическом корпусе ab-factory, шесть кейсов по три повтора. Объявления и отчёты по числу, правке и выгрузке: docs/81, docs/96–docs/101 в retail-lab. Отчёт по A/B-прогону пока не опубликован; корпус — ab-factory.

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

Эталон тоже проверялся. В корпусе A/B у одного кейса метка «мало данных» противоречила его собственным числам — интервал исключал практический порог. У другого агент нашёл сломанное деление на группы, которого не было в метке. Оба кейса выведены из счёта и заменены по правилу, объявленному до следующего вызова.

Оговорки

Один стенд и один корпус, оба синтетические. Одна модель. У правки, выгрузки и A/B агент не мог переспросить, у числа мог. Решения для второго прогона числа записаны при известном эталоне. В A/B по одному кейсу на сценарий, и подача — сводка, а не сырые данные. Все замеры — на одноходовом агенте.

Переносится устройство, а не числа.

Вся серия про 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-аналитик видит не ваш проект, а промпт2026.09 · 13 мин Модель отвечает не по вашему проекту, а по тексту, который для неё собрали. Что в него попадает, а что нет.
  9. Под каждую задачу AI-аналитику нужна своя обвязкаВы здесь
davydov.my · AI-аналитик · разбор восьмой