Заметки из релизного цикла — часть 8 (финал)
Вы выкатываете регрессионный набор из поста 7. Он работает. Гейты с разрезами по срезам ловят настоящие баги. Откалиброванный судья держится.
Затем ваш инженерный лид спрашивает, сколько стоит запускать его на каждом PR. Вы перемножаете цифры: ~12 минут инференса судьи на PR, 60 PR в день, четыре измерения × семнадцать срезов — и счёт оказывается серьёзным. Хуже того: каждый разработчик теперь ждёт 12 минут зелёной галочки ради однострочной опечатки в промпте. Скорость падает[1], команда ворчит, кто-то предлагает «просто гонять гейты по ночам» — а это ровно тот способ потерять всё, ради чего гейты создавались.
Решение — не меньше тестирования. Решение — тестировать слоями, причём основная часть сигнала должна приходить за первые девяносто секунд. Этот пост — про то, что работает под слоем гейтов: субсекундные контрактные тесты, плотный smoke-слой, экономичный флот и двухнедельное теневое окно, прежде чем новый гейт начнёт кого-либо блокировать.
Это пост 8, последний в этой серии. К концу у вас будет полная картина — от четырёхэтапного пайплайна до контрактно-тестовой фикстуры, запускающейся на каждом коммите.
Что значит CI для кастомной языковой модели?
CI для кастомной LLM — это та работа, которую набору гейтов не приходится повторять. Гейт оценивает семантическое качество; CI ловит всё, что сделало бы оценку гейта бессмысленной, ещё до того как гейт потратит хоть один токен судьи.
Контрактные тесты выполняются за миллисекунды и проверяют, что шаблоны промптов всё ещё рендерятся, что схемы tool-call всё ещё парсятся, что индексы поиска всё ещё отвечают, что манифест всё ещё ссылается на реально существующие хэши. Они детерминированы, бесплатны и являются единственной причиной, по которой остальная часть пайплайна вообще может существовать. Pull request, ломающий шаблон промпта, должен падать за 200 мс, а не после 12 минут инференса судьи, оценивающего бессмыслицу.
Контрактный слой — это разница между счётом CI, который растёт линейно с количеством PR, и тем, который не растёт. CI-раннер Divinci тратит > 90% бюджета судьи на реальную семантическую оценку, а не на PR, которые провалили бы проверку схемы. Это и есть главное число.
Почему традиционный CI ломается для LLM — через призму стоимости
Посты 1 и 7 рассказали, почему детерминированный CI не работает для генеративной модели. Версия той же истории, о которой этот пост, — про стоимость этих четырёх свойств, а не про сам факт их существования.
| Свойство LLM | Сбой традиционного CI | Форма стоимости |
|---|---|---|
| Недетерминированные выходы | Утверждения на точное совпадение флакают | Перезапуски усиливают стоимость линейно от уровня флакания |
| Многомерное качество | Один булев флаг неинформативен | Каждое измерение — отдельный (платный) вызов судьи |
| Дрейф провайдера | Зафиксированный gpt-4-2024-01-01 тихо уходит на пенсию | Всплеск рекалибровки, когда провайдер отключает чекпоинт |
| Нелокальные эффекты промптов | Локальный юнит-тест не может уловить эффект | Изменения формы распределения между PR, а не внутри них — нужен полный перезапуск набора, а не дельта |
CI-архитектура должна сделать каждое из этих свойств экономически приемлемым. Контрактные тесты дёшево обрабатывают свойства 1 и 3. Smoke-тесты частично обрабатывают свойство 4. Только полный набор закрывает свойство 2 — и только на тех PR, которые этого реально требуют.
CI-«слоёный пирог» — от субсекунды до двадцати пяти минут
Архитектура, которую мы выкатываем, состоит из четырёх слоёв; каждый зарабатывает свои вычисления, ловя то, что не способны поймать более дешёвые нижние слои. Подход «осознание срезов» на каждом слое следует тому же уроку, который явно сформулирован в разборе Tianpan Semver Lie[4]: агрегированные сигналы лгут; посрезовые сигналы ловят то, что агрегаты скрывают.
Форма стоимости — это и есть архитектура. ~74% PR никогда не тратят ни одного токена судьи — контракта или smoke достаточно. До полного набора доходят те PR, что затронули промпт, конфиг модели, индекс поиска или код evaluation — то есть именно те изменения, где набор гейтов — единственный сигнал, которому стоит доверять. Релиз-кандидаты — небольшая доля, доходящая до слоя 4.
Контрактные тесты — несправедливое преимущество
Контрактные тесты — это первая линия, самая дешёвая линия и линия, которую большинство команд пропускают, потому что она кажется «недостойной» «пайплайна AI-оценки». Это также то место, где в наборах наших клиентов реально падает 30–40% потенциальных регрессий — ещё до единого вызова судьи.
Контрактный слой утверждает пять вещей и больше ничего:
- Рендер шаблона промпта. Каждый шаблон рендерится против канонической фикстуры без несвязанных переменных, разнесённых циклов или сломанных Jinja-инклудов.
- Схема tool-call. Схема аргументов каждого объявленного инструмента парсится, JSONSchema валиден, а отрендеренный промпт реально ссылается на все обязательные слоты.
- Целостность манифеста. Каждый SHA в релизном манифесте — модель, промпт, индекс поиска, судья, датасет — соответствует артефакту, который существует в реестре. Висячие ссылки падают здесь, а не тремя слоями глубже.
- Доступность индекса. Индекс поиска отвечает на известный запрос в пределах бюджета. Перестроенный индекс, который тихо сломал retrieval, всплывает здесь, а не в продакшене.
- Denylist и бюджет токенов. Любой шаблон промпта, в который попал запрещённый токен, превысил бюджет токенов на вызов или вышел за окно контекста, падает здесь. Эвристическая оценка семантического сходства[6] тоже достаточно дешева, чтобы запускать её на контрактном слое для нечёткого покрытия denylist там, где буквального сравнения строк недостаточно.
# Репрезентативный вызов контрактного теста — выполняется примерно за 600 мс
divinci ci contract \
--manifest release/staging.yaml \
--check schema,template,manifest,index,denylist \
--fail-fast \
--json-out /tmp/contract-report.jsonНи один из этих вызовов не обращается к судье. Ни один не недетерминирован. Ни один не стоит измеримых денег. И каждый из них исключает целый класс алертов вида «гейт сказал, что медицинский срез регрессировал», которые иначе потратили бы полные 12 минут инференса судьи на оценку выхода, который модель в принципе не могла бы выдать корректно.
Smoke-слой — 90 секунд, ~$0,05 на PR
Если контрактный слой — это дешёвое несправедливое преимущество, то smoke-слой — тот, что реально ловит регрессии меньше чем за цену кофе. От двадцати до тридцати кейсов, взятых из срезов с самым большим объёмом, оцениваемых только по выполнению задачи и безопасности, без faithfulness, без latency, без проверок grounded-retrieval. Каждый PR запускает это. Это занимает около 90 секунд, потому что кейсы батчатся в один вызов судьи со схемой структурированного вывода, и потому что судья — дешёвый откалиброванный, а не полнокачественный, используемый для релиз-кандидатов.
Мы отслеживаем, какой слой поймал каждое выкаченное исправление, в журнале регрессий — и гистограмма стабильна последние шесть месяцев в клиентских развёртываниях:
3%, которые просачиваются, — причина, по которой существует мгновенный откат из поста 5. Гейты не обещают нулевых пропусков; они обещают плотную верхнюю границу и быстрое восстановление для того, что прошло.
Масштабирование CI-флота — как 12-минутный набор остаётся дешёвым
Слой полного набора — там, где математика должна сходиться. Наивная реализация вызывает судью один раз на кейс-на-измерение, гоняет их последовательно, и счёт растёт линейно с числом кейсов. Три оптимизации делают большую часть работы, чтобы удержать его в работоспособном состоянии:
Кэш эмбеддингов. Отпечаток контекста retrieval для каждого кейса золотого датасета хэшируется; если кейс не менялся и индекс retrieval не менялся, кэшированный эмбеддинг остаётся в силе, и шаг retrieval пропускается. Hit-rate после первой стабильной недели стабильно выше 90% в наших клиентских развёртываниях.
Батчинг судьи. Откалиброванный судья вызывается со структурированным выводом, батча по 8–16 кейсов за вызов. Стоимость судьи за токен остаётся той же; накладные расходы на кейс падают, потому что системный промпт амортизируется по батчу. Порог безопасного батчинга задаётся собственным откалиброванным согласием судьи при таком размере батча[2] — мы измеряем это во время еженедельного прохода калибровки судьи (пост 7).
Переиспользование KV-кэша между кейсами. Для моделей, где один и тот же системный промпт и определения инструментов идут в начале каждого вызова, KV-кэш для этого префикса вычисляется один раз на запуск набора, а не один раз на кейс[3]. В open-weights-развёртываниях это прямолинейно; на закрытых API-моделях это зависит от поддержки prefix-caching у провайдера.
Совокупный эффект приводит полный набор примерно к тем цифрам стоимости, что показаны на диаграмме слоёного пирога выше. Точные значения — внутренние, но соотношение — публичное утверждение: ~74% PR тратят ноль долларов судьи; ~22% тратят копейки; оставшиеся 4% тратят пару долларов на самый высокоуверенный пред-выкатный сигнал, который мы умеем производить.
Теневой CI — включаем, не сломав команду
Единственная ошибка, которую мы чаще всего наблюдаем у команд, — переключение нового гейта из «off» в «blocking» в первый же день. Пороги были подобраны на вчерашних данных, уровень ложных срабатываний неизвестен, и когда гейт срабатывает впервые, у команды нет калибровки на тему «это реально или ложная тревога». Дежурного eval-инженера пагают, гейт отключают, доверие потеряно, проект мёртв.
Решение — теневой CI: запустить новый гейт неблокирующим на две недели, постить результат бот-комментарием на каждом PR и еженедельно ревьюить уровень ложных срабатываний, прежде чем переводить в блокирующий. У CI-раннера Divinci ровно для этого есть флаг --shadow. Комментарий PR выглядит так же, как и итоговая блокирующая версия — тот же diff-дисплей, та же разбивка по срезам — за тем исключением, что он не блокирует мердж.
divinci ci run --layer=full --shadow --duration=14d --report-as=bot-commentКогда уровень ложных срабатываний стабильно ниже 5% по окну — переключаем. Когда нет — ужимаем посрезовые пороги, перекалибровываем судью и снова идём в shadow. В любом случае команду не подстерёг новый гейт, срабатывающий в первый день.
GitHub Actions workflow, который реально композируется
Кусок, привязывающий слоёный пирог к вашему существующему CI, лежит в .github/workflows/llm-ci.yaml. Слои разведены так, что дешёвые падают быстро, а дорогие запускаются только когда это нужно — цепочки needs: и триггеры с фильтрацией по путям делают всю работу[5].
name: LLM CI
on:
pull_request:
paths:
- 'prompts/**'
- 'config/models.yaml'
- 'eval/**'
- 'retrieval/**'
- 'manifests/**'
jobs:
contract:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v4
- run: divinci ci contract --manifest manifests/staging.yaml --fail-fast
smoke:
needs: contract
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: divinci ci run --layer=smoke --post-pr-comment
env:
DIVINCI_API_KEY: ${{ secrets.DIVINCI_API_KEY }}
full:
needs: smoke
if: contains(steps.changes.outputs.paths, 'prompts/') || contains(steps.changes.outputs.paths, 'config/models.yaml')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- run: divinci ci run --layer=full --post-pr-comment --gate
env:
DIVINCI_API_KEY: ${{ secrets.DIVINCI_API_KEY }}Три вещи, на которые стоит обратить внимание. Слои сцеплены через needs:, так что smoke не запускается на сломанном контракте, а full — на сломанном smoke. Job full отфильтрован по путям до изменений, реально оправдывающих 12-минутный запуск — исправление опечатки в README не триггерит набор гейтов. Флаг --post-pr-comment — то, что делает посрезовый diff видимым, не покидая GitHub.
Цикл отладки упавшего PR
Вторая половина «гейт сработал» — это «покажи мне почему». Вывод регрессионного набора medical slice task-completion dropped 0.04 бесполезен без кейсов, его вызвавших. Мы выводим пять худших посрезовых diff-ов в комментарии PR — с исходным входом, бейзлайн-выходом, кандидатным выходом и трассой рассуждений судьи. Цикл отладки должен занимать секунды, а не минуты:
# Подтянуть 5 худших кейсов, которые подняли медицинский гейт на этом PR
divinci ci diffs --pr 1247 --slice medical --dimension task_completion --top 5Это та же диагностическая поверхность, что и в семишаговом дереве из поста 6, встроенная в обратную связь CI. Инженер, открывший PR, видит свидетельства уровня кейсов прямо на самом PR; ему не приходится идти в отдельный eval-дашборд.
Дисциплина версионирования — промпты, датасеты, судьи как код
Шаблоны промптов, золотые датасеты и промпты судьи живут в репозитории, зафиксированные по хэшу в релизном манифесте. Манифест — единственный объект, связывающий набор с конкретным воспроизводимым состоянием:
# manifests/staging.yaml — каждый CI-запуск хэширует это
release_id: rel-staging
model: { sha: 0c1f9…, weights: r2://models/custom-v7.2, open_weights: true }
prompt: { sha: c4a8e…, template: prompts/support/v3.4.j2 }
retrieval: { sha: b21f0…, index: r2://indices/kb-2026-04 }
judge: { sha: d8e21…, rubric: eval/rubrics/v7.yaml }
dataset: { sha: a90b1…, file: eval/datasets/golden-2026-04.jsonl }Когда CI-запуск постит оценку, оценка тегается этим хэшем манифеста. Когда оценка сдвигается, у вопроса «какой вход сдвинулся» есть прямой ответ: diff манифеста, а сработавший слой подсказывает, какое измерение смотреть в первую очередь. Это тот цикл, который замыкают четырёхэтапный пайплайн из поста 1 и vindex-квитанция из поста 4: манифест — это аудит-примитив, к которому, в разных формулировках, шли все восемь этих постов.
Чего это не решает
Те же три честных ограничения, которые мы выписывали в каждый пост этой серии.
- CI не тестирует то, чего нет в наборе. Каким бы изящным ни был слоёный пирог, единственные регрессии, которые он ловит, — те, что отметил бы какой-то кейс золотого датасета. Слой replay смягчает это для дрейфа поведения, но новые запросы, которые никогда не встречались, всё равно просачиваются, пока не появятся в продакшене. Систему нужно объединять с продакшен-мониторингом.
- Цифры стоимости меняются с ценообразованием моделей. Каждая цифра стоимости в этом посте зависит от тарифов на токены судьи, эмбеддинги и инференс, которые дрейфуют поквартально. Соотношения — 74% / 22% / 4%, 31% / 27% / 28% / 11% / 3% — несущие утверждения; долларовые цифры иллюстративны для определённого момента.
- Изменения чекпоинтов на стороне провайдера всё ещё сложны. Когда провайдер закрытого API тихо обновляет модель за стабильным именем, контрактный слой этого не поймает; только набор гейтов — и только постфактум. Мы смягчаем это, фиксируя явные идентификаторы чекпоинтов везде, где провайдер это поддерживает, и трактуя день анонса чекпоинта как триггер для полного перебазирования набора. Базовую проблему мы устранить не можем.
Закрываем серию
Это пост 8 из 8. Полная арка:
- Как построить LLM CI/CD-пайплайн с Divinci AI — четырёхэтапный пайплайн (Register / Gate / Roll / Observe), внутри которого с тех пор живёт всё.
- 10 сбоев релиза CI/CD в кастомных языковых моделях — именованные режимы сбоев 2026 года, каждый сопоставлен с этапом, который должен был его поймать.
- 12 возможностей QA и управления релизами для LLM — матрица возможностей и диаграмма Венна «трёх лагерей», располагающая Divinci относительно альтернатив.
- Валидация и релиз кастомных языковых моделей в регулируемых сферах — глубокое погружение в комплаенс, маппинг «регулятор → этап», vindex-квитанции.
- Автоматизированные LLM CI/CD-пайплайны с мгновенным откатом — операционный слой, спектр автоматизации, квитанция авто-отката.
- Как диагностировать сбои QA кастомных LLM за 7 шагов — диагностическое дерево решений; модель — правильный ответ примерно в одном алерте из семи.
- Автоматизированное регрессионное тестирование кастомных LLM в 2026 — посрезовые Spearman-гейты, откалиброванные судьи, замкнутый повтор продакшен-трасс.
- Этот пост. CI-инфраструктура, делающая всё вышеперечисленное запустимым на каждом PR.
Куски композируются: манифест — это аудит-примитив, гейты — слой безопасности, диагностическое дерево — цикл восстановления, vindex-квитанция — внешний якорь, а слоёный пирог — то, что делает всю систему доступной для запуска на каждом коммите. Если в вашем процессе релиза кастомных LLM этих пятёрки нет — пропуск именно там, о чём были эти восемь постов.
FAQ
Какой самый дешёвый тест я могу запускать на каждом коммите?
Проверка рендера шаблона промпта. Она выполняется за миллисекунды, не требует судьи, ловит удивительно большую долю поломок и никогда не стоит измеримого цента. Если вы её ещё не запускаете — это самый высокий ROI среди частей CI, который мы умеем рекомендовать.
Сколько мне ожидать, что будет стоить CI-пайплайн кастомной LLM?
Центы за типичный PR, единицы долларов за PR-релиз-кандидат. Соотношение зависит от ценообразования судьи и от того, какая доля ваших PR затрагивает промпты или конфиг модели. 4%-ная доля релиз-кандидатов, указанная выше, типична; для продуктов с частой итерацией промптов эта доля растёт, и средняя соответственно поднимается.
Стоит ли запускать полный набор на каждом коммите?
Нет. Фильтруйте по путям до PR, которые затрагивают промпты, конфиг модели, retrieval или eval-код. Для всех остальных изменений контракт + smoke достаточны, а 12-минутное ожидание из-за опечатки в README за один спринт потеряет доверие команды. Полный набор бесценен; тратьте его там, где изменение реально может сдвинуть измерение качества.
Как ввести новый гейт, не сломав всех?
Двухнедельное теневое окно, неблокирующее. Подстраивайте пороги по уровню ложных срабатываний, наблюдаемому во время shadow. Переключайте в блокирующий, только когда устойчивый уровень ложных срабатываний ниже вашей толерантности (мы используем 5%). Всё остальное — путь к гейту, который все научились игнорировать.
Какой единственный показатель отслеживать, если можно следить только за одним?
Доля подтверждённых регрессий, пойманных до продакшена. Гистограмма в этом посте даёт ~97% в зрелых развёртываниях Divinci. 3%, которые просачиваются, — причина, по которой существует мгновенный откат. 97% — то, ради чего набор существует.
Источники
- DORA / Google Cloud. «Accelerate State of DevOps — CI velocity, change-failure-rate and time-to-restore-service.» Межотраслевые бейзлайны, делающие утверждение «12 минут на PR — это слишком медленно» защитимым, а не мнением.
- Zheng et al. «Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena.» arXiv:2306.05685. Эмпирическое доказательство того, что батчированные вызовы LLM-as-judge могут сохранять калибровку при размерах батча, используемых в smoke и полном слое — причина, по которой цифры стоимости в этом посте достижимы.
- Pope et al. «Efficiently Scaling Transformer Inference.» arXiv:2211.05102. Техники переиспользования KV-кэша и шеринга префиксов, упомянутые в секции о масштабировании CI-флота.
- Pan, Tianpan. «The Semver Lie: how a minor LLM update broke production.» 29 апреля 2026 года. Именованный режим сбоя 2026 года для регрессионных наборов «только агрегаты»; причина, по которой CI-слоёный пирог осознаёт срезы насквозь.
- GitHub. «GitHub Actions — chaining jobs with `needs:` and conditional execution.» Примитив, на который опирается .yaml из этого поста.
- Zhang et al. «BERTScore: Evaluating Text Generation with BERT.» arXiv:1904.09675. Эвристическая метрика семантического сходства, упомянутая как альтернатива LLM-as-judge для более дешёвых слоёв; не то, что мы гоняем на гейтах, но полезное на контрактном слое для масштабного обнаружения запрещённых фраз.
Ready to Build Your Custom AI Solution?
Discover how Divinci AI can help you implement RAG systems, automate quality assurance, and streamline your AI development process.
Get Started Today