Навигация

AI-фичи в production

Сделать демо AI-фичи легко — довести её до прода сложно. Демо не падает при смене модели, не растёт в счёте за токены, не деградирует при недоступности провайдера и не ломается на реальных пользовательских запросах. Эта статья — про эксплуатационную сторону AI-фичей: как измерять качество (evals), держать стоимость и латентность под контролем, строить fallback и деградацию, наблюдаемость и версионирование. UI-паттерны разобраны в AI UI, безопасность рендера вывода — в Безопасности вывода LLM.

Содержание

  1. Архитектура AI-фичи
  2. Evals: новые unit-тесты
  3. Quality-gate в CI
  4. Online-мониторинг
  5. Стоимость: кэш, роутинг, бюджеты
  6. Латентность
  7. Надёжность и деградация
  8. Наблюдаемость
  9. Версионирование промптов и моделей
  10. Критерии приёмки и упрощение
  11. Ключевые тезисы для интервью
  12. Заключение
  13. Полезные ссылки

Архитектура 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 должна согласовываться с человеческой. Механика калибровки:

  1. Judge выставляет оценки на трейсах, люди проверяют выборку и правят несогласия.
  2. Правки превращают в few-shot примеры внутри промпта judge — они показывают границы критерия.
  3. Следят за долей согласия во времени и дообучают промпт по данным, а не по интуиции.

Ориентир: сильные judge достигают ~80% согласия с человеком — примерно столько же, сколько люди соглашаются между собой. Ниже 80% на калибровочном наборе — тревожный сигнал, доверять оценкам ещё рано.

Judge систематически искажает оценки. Основные bias’ы и их лечение:

BiasЧто происходитКак лечить
Position biasВ попарном сравнении побеждает вариант в определённой позицииРандомизировать порядок, прогонять оба порядка
Verbosity biasДлинный ответ получает выше балл независимо от качестваЯвно сказать в рубрике, что длина — не сигнал качества; примеры кратких хороших ответов
Self-enhancementМодель завышает оценки собственным ответамСудить другой моделью, не той, что генерировала
SycophancyJudge соглашается с навязанной ему рамкойКалибровка на примерах, нейтральные формулировки

Важно не путать надёжность и валидность. Крупное исследование 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 storeHit-rate ~70%, экономия до ~73%Высокочастотные повторяющиеся вопросы
Cost-aware routingGatewayПростые запросы — на дешёвую модельРазная сложность запросов
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%. Цена — три вещи:

  1. Embedding-вызов на каждом запросе (платите даже при промахе).
  2. Порог близости — самый важный параметр: выше порог → меньше попаданий, ниже → растёт риск вернуть неправильный ответ.
  3. Устаревание — ответ должен истекать по 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-фичу до прода — значит сделать её измеримой, предсказуемой и управляемой:

  1. Evals вместо интуиции — golden set, rule-based + judge, калибровка и прогон в CI.
  2. Стоимость по слоям — prompt caching, semantic caching, routing и бюджеты; простые запросы не уходят на frontier.
  3. Латентность — модель, стриминг, короткие ответы и кэш, плюс контроль tool-loop.
  4. Надёжность — fallback-цепочки с одним дедлайном, circuit breaker, schema validation и feature flags.
  5. Наблюдаемость — стандарт OTel GenAI, из которого считаются токены, стоимость и зацикливания.
  6. Версионирование — промпты, judge и модели как код, чтобы понимать причину каждого изменения метрики.

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


Полезные ссылки