AI-фичи в production
Сделать демо AI-фичи легко — довести её до прода сложно. Демо не падает при смене модели, не растёт в счёте за токены, не деградирует при недоступности провайдера и не ломается на реальных пользовательских запросах. Эта статья — про эксплуатационную сторону AI-фичей: как измерять качество (evals), держать стоимость и латентность под контролем, строить fallback и деградацию, наблюдаемость и версионирование. UI-паттерны разобраны в AI UI, безопасность рендера вывода — в Безопасности вывода LLM.
Содержание
- Архитектура AI-фичи
- Evals: новые unit-тесты
- Quality-gate в CI
- Online-мониторинг
- Стоимость: кэш, роутинг, бюджеты
- Латентность
- Надёжность и деградация
- Наблюдаемость
- Версионирование промптов и моделей
- Критерии приёмки и упрощение
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Архитектура AI-фичи
Схема обязательна и не меняется от фичи к фиче:
Клиент (React) ──POST /api/chat──► Сервер (route) ──► LLM API
│
└── ключ, логи, лимиты, промпты — здесь
Клиент никогда не обращается к LLM API напрямую. Причины те же, что и раньше, и в 2026 году они только усилились:
| Задача | Клиент | Сервер |
|---|---|---|
| API-ключ | ❌ Никогда | ✅ Только здесь |
| Сборка промпта и системных инструкций | ⚠️ Отправляет данные | ✅ Собирает и дополняет |
| Логирование, лимиты, бюджеты | — | ✅ |
| Выбор модели, роутинг, fallback | — | ✅ |
| Рендер ответа | ✅ | — |
Сервер — единственная точка контроля. Именно на нём живут все механизмы из этой статьи: evals подключаются к серверной части, стоимость и латентность снимаются здесь, fallback и роутинг выбирают модель здесь же. Фронтенд отвечает за интерфейс (см. AI UI) и — в современном fullstack-стеке — за сам этот серверный слой.
Что здесь делает фронтендер
Слово «сервер» в схеме выше не означает чужой бэкенд. В подавляющем большинстве AI-фич это серверный слой самого фронтенд-фреймворка: Route Handler в Next.js, API-route в Nitro для Nuxt, edge-функция для SPA. Ключ, сборка промпта, роутинг, бюджеты и evals живут в том же репозитории и той же команде, что и компоненты, — это fullstack-часть фронтенда, а не отдельная зона ответственности. Поэтому все механизмы статьи — работа фронтендера, просто не в браузере.
У каждого механизма есть интерфейсный двойник, которым владеет именно фронтенд:
| Механизм | Что делает фронтенд |
|---|---|
| Латентность и TTFT | Стриминг, skeleton, «Стоп», бюджет на первый токен |
| Fallback и деградация | Уровни UI: полный → кэш → частичные фичи → read-only → статика |
| Feature flags и kill switch | Выключить AI-UI мгновенно, без релиза |
| Routing по сложности | Разная форма и формат ответа у моделей → устойчивый к ним рендер |
| Prompt caching | Статичный префикс (system prompt, схема) задаёт фронтенд — порядок критичен |
| Schema enforcement | Типизированный tool-output → типизированный компонент (generative UI) |
| Наблюдаемость | gen_ai.*-атрибуты в трейсах и метаданных запроса |
Отдельная зона ответственности фронтенда — тестирование самого AI-интерфейса: стриминг, состояния tool-частей, human-in-the-loop и частичный ответ при ошибке проверяют в Playwright на записанных SSE-фикстурах (см. AI UI), а не unit-тестами с живой моделью. Как гонять evals по качеству модели — ниже.
Evals: новые unit-тесты
Обычные unit-тесты проверяют детерминированный код: дали вход — получили предсказуемый выход. Вывод модели недетерминирован: та же система даёт разный ответ при том же входе. Поэтому expect(response).toBe("...") бесполезен, и на смене модели или промпта регрессия обнаруживается через неделю по жалобе пользователя. Вместо этого применяют evals — тесты для вероятностных систем.
Golden set
Основа evals — golden set: курируемый набор входов и критериев, а не случайная выборка. Каждый кейс — контракт: «при таком входе вывод должен удовлетворять таким критериям». Минимальный кейс содержит вход, ожидаемый результат или эталон (если есть), тип проверки, порог и метаданные (id, теги, источник). Практический старт — 200–500 кейсов на задачу с покрытием краевых случаев и уже наблюдавшихся сбоев; ниже 100 разброс между прогонами маскирует реальные регрессии. Golden set хранят в репозитории рядом с кодом (JSONL удобно диффить), а каждый продакшн-инцидент превращают минимум в один новый кейс.
Недетерминизм учитывают иначе: на один вход генерируют несколько выводов (обычно 5–10), оценивают каждый отдельно и смотрят на агрегат — stability rate («прошло 8 из 10 → 80% стабильности»). Это точнее, чем один прогон, и совместимо с порогом готовности.
Rule-based vs LLM-as-a-judge
| Тип проверки | Что проверяет | Плюсы | Ограничения |
|---|---|---|---|
| Exact match | Структурные ответы, классификация, роутинг | Ноль стоимости, идеально воспроизводимо, мгновенно | Ломается на валидных перефразировках и пробелах |
| Regex | Формат (email, дата, номер) | Почти бесплатно, покрывает семейства форматов | Не оценивает смысл |
| JSON schema | Соответствие схеме tool-call / API | Ловит пропущенные поля и неверные типы | Проверяет валидность, но не правильность значений |
| Semantic similarity | Перефразированный эталон, короткие ответы | Терпит синонимы | Нужен embedding-вызов и подбор порога |
| LLM-as-a-judge | Свободная генерация, тон, качество рассуждения | Масштабируется, оценивает субъективное | Дороже, медленнее и сам вносит шум |
Правило простое: если критерий формализуем — используйте детерминированную проверку; judge — только там, где нельзя. Смешивать их в одном пайплайне — норма: формат JSON и валидность hex-цветов проверяет код, «мотто совпадает с брендом» — judge. Даже при поддержке structured output схему проверяют отдельно: валидный JSON всё ещё может содержать пустые значения, обрезанный по лимиту токенов ответ или невозможную дату.
Калибровка judge
Judge — это тоже модель, и «поставили и поверили» не работает. Оценка judge должна согласовываться с человеческой. Механика калибровки:
- Judge выставляет оценки на трейсах, люди проверяют выборку и правят несогласия.
- Правки превращают в few-shot примеры внутри промпта judge — они показывают границы критерия.
- Следят за долей согласия во времени и дообучают промпт по данным, а не по интуиции.
Ориентир: сильные judge достигают ~80% согласия с человеком — примерно столько же, сколько люди соглашаются между собой. Ниже 80% на калибровочном наборе — тревожный сигнал, доверять оценкам ещё рано.
Judge систематически искажает оценки. Основные bias’ы и их лечение:
| Bias | Что происходит | Как лечить |
|---|---|---|
| Position bias | В попарном сравнении побеждает вариант в определённой позиции | Рандомизировать порядок, прогонять оба порядка |
| Verbosity bias | Длинный ответ получает выше балл независимо от качества | Явно сказать в рубрике, что длина — не сигнал качества; примеры кратких хороших ответов |
| Self-enhancement | Модель завышает оценки собственным ответам | Судить другой моделью, не той, что генерировала |
| Sycophancy | Judge соглашается с навязанной ему рамкой | Калибровка на примерах, нейтральные формулировки |
Важно не путать надёжность и валидность. Крупное исследование 2026 года (21 judge, ~541 000 оценок) показало: judge может быть сверхстабильным (test–retest > 0.95) и при этом иметь выраженный position bias, а точное согласие (exact match) системно завышает способность judge различать ответы — коррекция к Cohen’s kappa снижает метрику на 33–41 п.п. Вывод: judge аудируют по нескольким протоколам (согласие, стабильность, bias), а не по одной цифре.
Наконец, рубрика должна быть низкоразрешённой: бинарные или малогранулярные оценки надёжнее, чем «1–5 с шагом 0.1» — LLM плохо воспроизводит мелкие градации.
Как это запускается на практике
Термины не взаимозаменяемы, и с них стоит начать: golden set — это данные (вход + критерии), eval — замер выхода против этих данных, judge — один из способов оценить выход внутри eval, а раннер — скрипт, который всё это исполняет. Evals — не unit-тесты: они ходят в живую модель, стоят денег и времени, поэтому живут отдельным процессом, а не в vitest run.
Раскладка в репозитории рядом с серверным кодом:
evals/
├── golden/describe-short.jsonl # 200–500 кейсов, диффится в git
├── judges/brand-tone.ts # LLM-judge с рубрикой
├── checks/rule-based.ts # схема, regex, exact match
├── calibration/human-labels.jsonl # человеческие метки для согласия
└── run.ts # раннер
Кейс golden set — строка JSONL: вход, критерии и порог.
{"id":"desc-001","input":{"name":"Кроссовки","tone":"playful"},"criteria":{"schema":"DescriptionSchema","must_include":["brand"]},"threshold":0.8,"tags":["tone"]}
Раннер прогоняет кейсы через тот же серверный код, что и прод (не через копию промпта), считает rule-based и judge и агрегирует pass-rate. Технический сбой — отдельный счётчик error, а не fail (см. Quality-gate).
import { readFileSync } from 'node:fs';
import { describeItem } from '../src/server/ai/describe';
import { ruleChecks } from './checks/rule-based';
import { brandToneJudge } from './judges/brand-tone';
async function run(cases, samples = 5) {
let pass = 0, fail = 0, error = 0;
for (const c of cases) {
for (let i = 0; i < samples; i++) {
try {
const out = await describeItem(c.input);
const rule = ruleChecks(c, out);
const judged = rule.ok ? await brandToneJudge(c, out) : { ok: false };
rule.ok && judged.ok ? pass++ : fail++;
} catch {
error++;
}
}
}
const denom = pass + fail;
return { rate: denom ? pass / denom : 0, pass, fail, error };
}
const cases = readFileSync('evals/golden/describe-short.jsonl', 'utf8')
.trim().split('\n').map((line) => JSON.parse(line));
const { rate, ...counts } = await run(cases);
console.log({ rate, ...counts });
if (rate < Number(process.env.EVAL_THRESHOLD ?? 0.85)) process.exit(1);
Judge — это тоже вызов модели, но со структурированным выводом и другой моделью, чем генератор (лечение self-enhancement bias):
import { generateObject } from 'ai';
import { z } from 'zod';
async function brandToneJudge(testCase, output) {
const res = await generateObject({
model: 'openai/gpt-5-mini',
schema: z.object({ score: z.number().min(0).max(1), reason: z.string() }),
prompt: [
'Рубрика: ответ соответствует tone из входа и упоминает brand.',
'Оценка низкоразрешённая: 0 — не соответствует, 1 — полностью.',
'Длина ответа не является признаком качества.',
`Input: ${JSON.stringify(testCase.input)}`,
`Output: ${output}`,
].join('\n'),
});
return { ok: res.object.score >= 0.7, reason: res.object.reason };
}
Калибровку judge ведут не по одной цифре: собирают человеческие метки и считают согласие с поправкой на случайность (Cohen’s kappa), а не только exact match — иначе согласие системно завышается на 30–40 п.п.
function agreement(judge, human) {
const n = judge.length;
const raw = judge.filter((s, i) => s === human[i]).length / n;
return { raw, kappa: cohensKappa(judge, human) };
}
Итого три места запуска: локально (npm run eval на своей машине во время разработки), в CI как quality-gate (раздел ниже) и в рантайме поверх прод-трейсов (раздел Online-мониторинг). Писать раннер с нуля не обязательно: rule-based и judge в CI закрывают Promptfoo и evalite, а рантайм-оценки поверх трейсов — Langfuse, LangSmith и Braintrust.
Quality-gate в CI
Evals бесполезны, если их запускают руками перед релизом. Их место — CI, как у обычных тестов.
- PR: быстрый прогон — все rule-based проверки и небольшая выборка judge. Цель — отловить грубую регрессию за минуты.
- Ночью / перед релизом: полный прогон golden set с несколькими итерациями и всеми judge-метриками.
- Quality-gate: фиксированный порог pass-rate; падение ниже блокирует merge. Выборку и порог настраивают под бюджет — judge-вызовы стоят денег, поэтому дешёвые проверки гоняют чаще.
- Разделяйте ERROR и FAIL. Если вызов judge упал по таймауту или лимиту — это
ERROR, ретраится и не считается провалом оценки: иначе техническая нестабильность искажает pass-rate. - Версионируйте judge. Промпт judge храните рядом с golden set с версией и хэшем — правка judge может сдвинуть распределение баллов на целый пункт, и старые бейзлайны станут несравнимы.
Технически это отдельный job (он не должен блокировать быстрый unit-прогон), у которого есть pull_request-триггер и ночное расписание:
on:
pull_request:
schedule:
- cron: '0 2 * * *'
jobs:
eval-smoke:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run eval -- --suite smoke --samples 1
env:
EVAL_THRESHOLD: "0.85"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
eval-full:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run eval -- --suite full --samples 5
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
smoke — это все rule-based проверки плюс маленькая выборка judge ради скорости; full — весь golden set с несколькими генерациями на кейс и всеми judge-метриками. Раннер и judge разобраны в разделе Evals.
Online-мониторинг
Офлайн-evals ловят то, что вы предусмотрели; прод показывает то, чего вы не предусмотрели. Поэтому evals работают и в рантайме:
- Production traces — реальные входы и выходы как источник новых кейсов. Падение из прода промоутится в golden set одним действием, превращаясь в регрессионный тест.
- Обратная связь пользователя — thumbs up/down, кнопка regenerate/edit. Это самый дешёвый сигнал качества на реальном трафике.
- Runtime evals — небольшая доля трафика оценивается judge-ем в реальном времени, чтобы поймать дрейф качества до жалоб.
- Дрейф при смене модели. Провайдер может молча обновить версию модели (см.
gen_ai.response.model), и распределение ответов изменится. Смена модели — это всегда повторный прогон golden set, а не косметическая правка строки.
Технически онлайн-evals вешают на трейсы в платформе наблюдаемости: та же записанная пара «вход → выход» становится строкой датасета, а judge прогоняется фоновым воркером (очередь или cron), чтобы не тормозить ответ пользователю. Здесь пригождаются Langfuse, LangSmith и Braintrust — у них dataset, скореры и трассировка в одном месте. Провалившиеся трейсы одним действием превращаются в кейс golden set и уходят в офлайн-раннер.
Стоимость: кэш, роутинг, бюджеты
Счёт за инференс растёт не от одного дорогого запроса, а от объёма и агентских циклов: одна ошибка в retry-логике или зацикленный агент сжигает бюджет за минуты. Управление стоимостью — это порядок слоёв, а не одна настройка.
| Техника | Уровень | Эффект | Когда применять |
|---|---|---|---|
| Prompt caching | Провайдер/gateway | Скидка до ~90% на чтение общего префикса | Стабильные system prompt и схемы инструментов |
| Semantic caching | Приложение + vector store | Hit-rate ~70%, экономия до ~73% | Высокочастотные повторяющиеся вопросы |
| Cost-aware routing | Gateway | Простые запросы — на дешёвую модель | Разная сложность запросов |
| Token-budget / cap на ключ | Gateway | Отклоняет запросы при превышении лимита | Защита от runaway-циклов |
| Batch / дешёвые модели | Провайдер | Ниже цена за токен | Неинтерактивные задачи |
Prompt caching
Провайдер хранит вычисленные KV-тензоры для байт-в-байт одинакового префикса и на следующем запросе читает их вместо повторного пересчёта. Модель всё равно генерирует свежий ответ — поэтому prompt caching не несёт риска правильности. У Anthropic чтение кэша стоит 10% от входной цены (дискаунт 90%), запись — 1.25× (5 минут) или 2× (1 час); у OpenAI скидка на чтение — до 90%, запись 1.25×. Точка окупаемости — примерно одно повторное чтение.
Ключевое правило: статичное — в начало промпта. Таймстемп, user id или свежие документы внутри кэшируемого префикса дают ноль попаданий и счёт за запись на каждом вызове. Для агентских циклов и многоходовых диалогов кэш особенно выгоден: каждый ход дочитывает префикс предыдущего, платя полную цену только за добавленное.
Semantic caching
Semantic cache хранит пары «промпт → ответ» и возвращает сохранённый ответ, когда новый запрос близок по смыслу (по расстоянию embedding’ов). Это уже не переиспользование префикса, а пропуск генерации: экономятся и входные, и выходные токены, hit-rate в отчётах доходит до ~70%, а экономия — до ~73%. Цена — три вещи:
- Embedding-вызов на каждом запросе (платите даже при промахе).
- Порог близости — самый важный параметр: выше порог → меньше попаданий, ниже → растёт риск вернуть неправильный ответ.
- Устаревание — ответ должен истекать по TTL, иначе кэш переживёт факты, которые в нём закодированы.
Из-за риска ложных попаданий semantic cache плохо подходит для агентского и персонализированного трафика (к 2026 году tool-call запросы несут большую часть токенов): там similarity между промптами мало говорит о том, заслуживают ли они одного ответа. Его ниша — повторяющиеся Q&A.
Роутинг и бюджеты
Cost-aware роутинг отправляет лёгкие запросы на дешёвую модель, а сложные — на frontier:
const TIERS = {
fast: "openai/gpt-5-mini",
frontier: "anthropic/claude-sonnet-5",
};
Классификатор сложности сначала использует дешёвые эвристики (длина входа, наличие кода, ключевые слова) и лишь на неоднозначных случаях делает маленький модельный вызов. Смена модели — правка одной строки в конфиге.
Budget cap дополняет роутинг: он не снижает счёт, а защищает потолок — запросы сверх лимита отклоняются, превращая убежавший цикл в отказ, а не в сюрприз-инвойс. Полезно заводить отдельный ключ с бюджетом на каждый tier, чтобы кап действительно применялся.
Латентность
Пользователь чувствует две разные метрики: TTFT (время до первого токена) и общее время генерации. UI-сторона стриминга разобрана в AI UI; здесь — инженерные рычаги на сервере:
- Выбор модели. Маленькая модель отвечает заметно быстрее; не посылайте всё на frontier, если задача этого не требует.
- Стриминг. Снижает воспринимаемую латентность, не ускоряя генерацию.
- Короткие ответы. Ограничивайте длину (
max_tokens), запрещайте лишние рассуждения там, где они не нужны. - Кэш. Prompt-кэш убирает пересчёт префикса, semantic-кэш — целую генерацию.
- Зацикливания.
gen_ai.response.finish_reasonsсо значениемtool_callsвместоstop— сигнал runaway tool-loop, который бьёт и по латентности, и по цене.
Метрика gen_ai.client.operation.duration (и time_to_first_chunk для стриминга) позволяет отслеживать деградацию по модели и провайдеру, а не «в среднем по приложению».
Надёжность и деградация
Провайдеры дают 429 при всплеске, 5xx при инциденте, отваливаются по таймауту или объявляют модель deprecated. Ничего из этого не лечится более умным промптом — это операционные отказы, и против них строят слои.
Retry ≠ fallback ≠ circuit breaker
Три механизма решают разные задачи:
| Механизм | Что делает | Против чего |
|---|---|---|
| Retry | Повторяет того же кандидата с backoff и jitter | Транзиентный 5xx, оборванное соединение |
| Fallback | Переходит к другому кандидату | Полный отказ провайдера, deprecation модели |
| Circuit breaker | Временно убирает сбойного кандидата из пула | Затяжная деградация и каскадные отказы |
Без circuit breaker деградировавший провайдер, отдающий ошибки в 60% случаев, всё равно получает свою долю трафика, и retry только глубже загоняет его в перегрузку. Breaker работает как автомат: closed (норма) → open (порог пробит, трафик не идёт, идёт cooldown) → half-open (пробные запросы); успех закрывает, неудача снова открывает.
Fallback-цепочка
Хорошая цепочка — не «просто второй провайдер», а набор ограничений:
- Retryable vs terminal. 429, 5xx, timeout — транзиентны, стоит перейти к следующему кандидату. 400, 401 или отказ по политике контента провалятся так же везде — цепочку надо остановить, а не жечь бюджет.
- Один дедлайн, а не сумма таймаутов. Если каждый кандидат получает полный таймаут, цепочка из трёх моделей может тянуться три таймаута. Каждой попытке — своя доля общего бюджета.
- Capability parity. Fallback должен уметь то же: то же контекстное окно, поддержку tool calls и строгого JSON. Иначе availability-проблема превращается в parsing-проблему.
- Валидация вывода на fallback-пути. Модель другой семьи может вернуть иной формат — проверяйте результат, а не доверяйте ему.
Schema enforcement как страховка качества
Самый дешёвый способ поймать галлюцинацию — заставить модель вернуть типизированный ответ и провалидировать его. Если схема нарушена, платформа повторяет запрос, передав ошибку валидации как контекст, до лимита попыток. В одном продакшн-кейсе такой retry-loop на схемах и классификации ошибок снизил долю галлюцинаций с 15% до 1.5% — без изменений самой модели. При этом важно различать классы сбоев: schema violation, сигнал галлюцинации и инфраструктурная ошибка требуют трёх разных реакций, а не одного generic-retry.
Feature flags и деградация
Управлять поведением в момент инцидента нужно без релиза. Для этого есть feature flags и kill switch:
- Kill switch мгновенно отключает зависимость или фичу (например, «живой» рекомендатель).
- Fallback-флаг переключает UI на упрощённый путь: кэш, статичный список, шаблонный ответ.
- Прогрессивный роллаут ограничивает blast radius новой модели: 0.1% → 1% → 5% → 100% с автоматическим откатом по метрикам.
Уровни деградации выстраивают по убыванию: полная функциональность → пониженное качество (кэш) → частичные фичи → read-only → статичный fallback. Главное правило: путь деградации, который ни разу не тестировали, не сработает — его проверяют chaos-тестами.
Наблюдаемость
Без телеметрии fallback и circuit breaker работают слепо: фича всё ещё отвечает, счёт растёт, а первичный провайдер падает неделю — никто не знает. Стандарт де-факто для LLM/агентов — OpenTelemetry GenAI semantic conventions.
Агентское исполнение моделируется как дерево спанов:
invoke_agent (верхнеуровневый цикл)
├── chat — вызов модели
├── execute_tool — вызов инструмента
└── chat / execute_tool — следующие шаги
Ключевые атрибуты и метрики:
| Что | Имя |
|---|---|
| Модель | gen_ai.request.model / gen_ai.response.model |
| Провайдер | gen_ai.provider.name |
| Операция | gen_ai.operation.name: chat, execute_tool, invoke_agent, embeddings |
| Токены | gen_ai.usage.input_tokens, gen_ai.usage.output_tokens |
| Причина остановки | gen_ai.response.finish_reasons |
| Метрики | gen_ai.client.operation.duration, gen_ai.client.token.usage |
Из токенов и цены за токен считается стоимость на запрос; finish_reasons ловит зацикливания (tool_calls вместо stop); распределение gen_ai.response.model показывает, когда провайдер молча сменил версию.
Важные ограничения, о которых спрашивают:
- Содержимое промптов по умолчанию не пишется. Захват
gen_ai.input.messagesиgen_ai.output.messages— opt-in, потому что это чувствительные данные (GDPR и т.п.). Включайте осознанно и не на всём трафике. - Спецификация ещё в Development. В июне 2026 всё
gen_ai.*вынесли из основного репозитория в отдельныйsemantic-conventions-genai, у которого нет versioned-релиза. Поэтому версии инструментации пинят, а во время перехода аналитика должна уметь обе генерации имён (gen_ai.systemиgen_ai.provider.name,prompt_tokens/completion_tokensиinput_tokens/output_tokens) — но не суммировать пары, потому что совместимые фреймворки пишут одно значение под двумя именами. - Свой внутренний формат. Внешняя Development-схема не должна быть контрактом вашей БД: держите собственное представление провайдеров, моделей, токенов и eval-оценок, маппьте фреймворки в него, а наружу экспортируйте текущую конвенцию.
Версионирование промптов и моделей
Промпт — это код, и относиться к нему надо так же. Метрика качества без версии промпта бесполезна: непонятно, что именно изменилось.
- Промпт как код: храните в репозитории, версия + хэш в результатах evals. Правка формулировки — это изменение, которое нужно прогонять через golden set.
- Judge тоже версионируется отдельно: обновление judge меняет шкалу, и старые результаты нельзя сравнивать с новыми.
- Модель пинится. Смена версии модели (в т.ч. «тихая» на стороне провайдера) — повод для повторного прогона evals, а не для правки строки в конфиге.
- Changelog. Каждое изменение промпта, конфига и модели фиксируется, чтобы деградация метрики связывалась с причиной.
Связка получается такой: версия промпта + версия judge + версия модели + стабильность прогона = воспроизводимая оценка. Ровно эту метаинформацию стоит включать в отчёт evals.
Критерии приёмки и упрощение
AI-фича принимается не «когда работает», а по измеримым критериям. Примеры соответствия «критерий → метрика → проверка»:
| Критерий | Метрика | Как проверить |
|---|---|---|
| Качество ответов не регрессировало | Pass-rate golden set ≥ порога | CI quality-gate на PR и ночью |
| Первый токен быстрый | TTFT < бюджета | gen_ai.client.operation.duration / замер |
| Ответ стабилен | Stability rate ≥ порога | 5–10 генераций на кейс |
| Стоимость под контролем | Токен-кост на запрос в пределах | Бюджеты на ключ + дашборд |
| Фича переживает отказ модели | Fallback/degradation срабатывает | Chaos-тест с моком 5xx |
| Вывод безопасен для рендера | Санитизация, ключ на сервере | См. Безопасность вывода LLM |
Не каждую AI-задачу нужно решать чатом с RAG. Правило то же, что и при проектировании: начните с простейшего решения, которое закрывает проблему пользователя.
| Задача | Сложное | Простое | Когда достаточно простого |
|---|---|---|---|
| Ответы по базе | RAG: эмбеддинги + retrieval | Вшить факты в system prompt | Контент влезает в контекстное окно |
| Генерация описаний | Чат с историей | Один вызов + кнопка «Сгенерировать» | Диалог не нужен |
| Автодополнение | Полный чат | Инлайн-подсказка | Нужен один результат |
| Классификация | LLM-judge на всё | Rule-based / маленькая модель | Критерий формализуем |
Эксплуатационная зрелость — это в первую очередь про измеримость и предсказуемость, а не про максимальный стек. Простая фича с evals и бюджетом надёжнее сложной без них.
Ключевые тезисы для интервью
- Вывод модели недетерминирован, поэтому его проверяют evals на golden set (200–500 кейсов), а не unit-тестами; недетерминизм гасят несколькими генерациями и stability rate.
- Rule-based проверки дёшевы и точны, LLM-as-a-judge — для субъективного; judge калибруют по человеческим правкам (~80% согласия, как люди между собой) и лечат position/verbosity/self-enhancement bias.
- Evals живут в CI как quality-gate: быстрый срез на PR, полный прогон ночью, ERROR не путают с FAIL, judge и промпты версионируются.
- Прод служит источником кейсов: падения и негативная обратная связь промоутятся в golden set, а смена модели всегда требует повторного прогона.
- Стоимость снижают слоями: prompt caching (до ~90% скидки на префикс, без риска) → semantic caching (hit-rate ~70%, риск ложных попаданий и TTL) → cost-aware routing → бюджеты на ключ против runaway-циклов.
- Латентность — это TTFT и общее время; рычаги — модель, стриминг, короткие ответы, кэш и защита от tool-loop.
- Надёжность строится из retry (тот же кандидат), fallback (другой кандидат) и circuit breaker (убрать сбойного), с retryable/terminal-классификацией, одним дедлайном и capability parity.
- Schema enforcement + retry по ошибке валидации — дешёвый способ снизить галлюцинации; feature flags и kill switch откатывают фичу без релиза.
- Наблюдаемость — по OTel GenAI: спаны
invoke_agent/chat/execute_tool, атрибутыgen_ai.*, метрики токенов и латентности; контент пишется только opt-in, спецификация пока Development — пините версии. - Версионируйте промпты, judge и модели — иначе деградацию метрики не связать с причиной; начинайте с простейшей фичи, которую можно измерить.
- Сервер AI-фичи — это серверный слой фронтенд-фреймворка (Route Handler, Nitro), поэтому evals, стоимость и fallback — работа фронтендера; evals гоняют отдельным раннером в CI (smoke на PR, full ночью) и поверх прод-трейсов, а не в обычных unit-тестах. Тестирование самого AI-интерфейса — отдельно, в Playwright на записанных SSE-фикстурах.
Заключение
Довести AI-фичу до прода — значит сделать её измеримой, предсказуемой и управляемой:
- Evals вместо интуиции — golden set, rule-based + judge, калибровка и прогон в CI.
- Стоимость по слоям — prompt caching, semantic caching, routing и бюджеты; простые запросы не уходят на frontier.
- Латентность — модель, стриминг, короткие ответы и кэш, плюс контроль tool-loop.
- Надёжность — fallback-цепочки с одним дедлайном, circuit breaker, schema validation и feature flags.
- Наблюдаемость — стандарт OTel GenAI, из которого считаются токены, стоимость и зацикливания.
- Версионирование — промпты, judge и модели как код, чтобы понимать причину каждого изменения метрики.
Начните с golden set и бюджета: даже простая фича с этими двумя вещами переживёт первую смену модели и первый всплеск трафика. Всё остальное — деградация, кэш, наблюдаемость — наслаивается поверх.
Полезные ссылки
- Chrome for Developers — AI Evals — серия про evals: mental model, rubric, pipeline, layered testing
- LangChain — How to Calibrate LLM-as-Judge with Human Corrections — калибровка judge, bias’ы, agree-метрики
- arXiv:2606.19544 — Reliability without Validity (LLM-as-a-Judge, 2026) — крупное исследование согласия, стабильности и bias judge
- OpenTelemetry — GenAI Observability with OpenTelemetry (2026) — спаны, атрибуты и метрики GenAI на практике
- OpenTelemetry GenAI semantic conventions — спецификация
gen_ai.* - Vercel — LLM cost management — кэш, routing, бюджеты и их измеримый эффект
- Vercel — Semantic caching for LLM apps — hit-rate, порог близости, риски и TTL
- Vercel — Prompt caching across providers — модели активации, write/read-цены и TTL
- Inferbase — LLM Fallback Chains — retryable vs terminal, дедлайны, capability parity
- InfoQ — Platform Engineering Playbook for Production LLMs — schema enforcement, retry и снижение галлюцинаций
- Promptfoo — Intro — CLI для evals в CI: rule-based и model-graded проверки, пороги, отчёт
- Langfuse — Model-based evaluations — LLM-as-a-judge поверх трассировки, онлайн- и офлайн-оценки
- evalite — раннер evals на Vitest для TypeScript: кейсы, скореры, watch и CI
- Playwright — Network — перехват и подмена стрима: записанные SSE-фикстуры для тестов AI-интерфейса