Практика · Экономика и качество AI-систем

Снижаем стоимость агента с маршрутизацией моделей по реальным трассам

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

Уровень: продвинутый Время: 90 минут Результат: обученный маршрутизатор и проверяемый отчёт

Что получится

Мы соберём LLM-маршрутизатор для AI-агента с двумя маршрутами:

  • cheap — более дешёвая и обычно более быстрая модель;
  • strong — более дорогая модель для запросов, на которых дешёвый маршрут не проходит критерий качества.

Маршрутизатор не пытается угадать «интеллектуальную сложность» в общем виде. Он предсказывает конкретное событие: сможет ли дешёвый маршрут успешно завершить операцию этого приложения при заданной версии инструкций, инструментов и валидаторов.

Итог работы состоит из пяти артефактов:

  1. обезличенного набора трасс с наблюдаемыми исходами;
  2. контрфактической разметки: результат дешёвой и сильной модели на одном входе;
  3. обученного классификатора с порогом и безопасной зоной неопределённости;
  4. теневого и ограниченного рабочего запуска;
  5. отчёта о стоимости, качестве, задержке, ошибочных маршрутах, эскалациях и повторах.

Почему замена модели целиком не решает задачу

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

  • число неверных решений и ответов, отклонённых валидатором;
  • повторные запросы пользователя;
  • автоматические повторные попытки;
  • эскалации на сильную модель после уже оплаченного дешёвого вызова;
  • ручную проверку и стоимость незавершённых операций;
  • хвостовую задержку из-за цепочки cheap → repair → strong.

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

Конкретный воспроизводимый кейс

Рассмотрим условного агента поддержки внутреннего продукта. Это проектный пример, а не рассказ о существующем клиенте и не готовые результаты теста.

Агент выполняет четыре типа операций:

  1. отвечает на вопрос по базе знаний;
  2. уточняет недостающие сведения;
  3. классифицирует обращение и предлагает следующий шаг;
  4. вызывает разрешённый инструмент для чтения статуса заявки.

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

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

1. Определите операцию и критерий качества

До сбора данных опишите, что значит «дешёвая модель справилась». Оценка не должна сводиться к тому, что ответ получен без HTTP-ошибки.

operation: support_answer:v4
starts_when: user_message_accepted
succeeds_when:
  - output_schema_valid
  - policy_checks_passed
  - required_facts_supported
  - requested_action_completed_or_safely_declined
  - no_corrective_user_retry_within_window
fails_when:
  - deterministic_validator_failed
  - reviewer_rejected
  - retry_budget_exhausted
  - deadline_exceeded
quality_floor:
  critical_violation_rate: 0
  aggregate_threshold: PRODUCT_OWNER_VALUE
latency_budget_ms: PRODUCT_OWNER_VALUE
retry_window: PRODUCT_OWNER_VALUE

PRODUCT_OWNER_VALUE необходимо заменить порогом вашего процесса. Универсального допустимого процента ошибок нет: цена неверного рекламного черновика и неверного финансового действия различается.

Разделите проверки на три слоя:

  1. Детерминированные: схема, обязательные поля, допустимые действия, ссылки на предоставленные источники, отсутствие запрещённых значений.
  2. Экспертные: корректность вывода, достаточность объяснения, соблюдение политики в неоднозначном случае.
  3. Поведенческие: повтор пользователя, исправление ответа, эскалация оператору, фактическое завершение операции.

Если машинная проверка видит лишь JSON, метка не должна называться «качественный ответ». Назовите её точнее: schema_and_policy_pass.

2. Зафиксируйте базовые варианты

Нужны как минимум три сравнимые политики:

Политика Назначение Что показывает
strong_only Все запросы идут сильной модели Базовое качество, стоимость и задержку
cheap_only Все запросы идут дешёвой модели Максимально доступную экономию и цену ухудшения
trace_router Маршрут выбирается по признакам реального входа Компромисс после обучения и настройки порога

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

3. Спроектируйте трассу операции

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

{
  "trace_id": "generated_trace_id",
  "operation_id": "generated_operation_id",
  "attempt_id": "generated_attempt_id",
  "call_id": "generated_call_id",
  "parent_call_id": null,
  "operation_type": "support_answer:v4",
  "route_policy": "strong_only",
  "selected_route": "strong",
  "router_version": null,
  "prompt_version": "support-v4",
  "adapter_version": "adapter-v2",
  "input_features_version": "features-v1",
  "started_at": "timestamp",
  "completed_at": "timestamp",
  "status": "completed",
  "quality_status": "pending",
  "is_user_retry": false,
  "retry_of_operation_id": null
}

Для каждого модельного вызова отдельно сохраняйте:

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

Не используйте текст запроса, email, идентификатор клиента или полный URL как метку временного ряда. Такие значения создают высокую кардинальность и могут раскрыть данные. Сырые входы храните в защищённом хранилище с ограниченным сроком жизни, а в метриках оставляйте агрегируемые категории.

Минимальная схема таблиц

CREATE TABLE router_operations (
  operation_id        TEXT PRIMARY KEY,
  trace_id            TEXT NOT NULL,
  operation_type      TEXT NOT NULL,
  cohort              TEXT NOT NULL,
  policy_version      TEXT NOT NULL,
  selected_route      TEXT NOT NULL,
  router_score        DOUBLE PRECISION,
  started_at          TIMESTAMP NOT NULL,
  completed_at        TIMESTAMP,
  terminal_status     TEXT,
  quality_pass        BOOLEAN,
  critical_violation BOOLEAN,
  user_retry          BOOLEAN NOT NULL DEFAULT FALSE,
  retry_of            TEXT
);

CREATE TABLE router_calls (
  call_id             TEXT PRIMARY KEY,
  operation_id        TEXT NOT NULL,
  attempt_no          INTEGER NOT NULL,
  route               TEXT NOT NULL,
  model_id            TEXT NOT NULL,
  prompt_version      TEXT NOT NULL,
  input_tokens        INTEGER,
  output_tokens       INTEGER,
  charged_cost        NUMERIC,
  currency            TEXT,
  latency_ms          INTEGER,
  schema_valid        BOOLEAN,
  validator_pass      BOOLEAN,
  error_code          TEXT,
  escalated           BOOLEAN NOT NULL DEFAULT FALSE
);

4. Подготовьте трассы без утечки рабочих данных

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

  • тип операции и канал;
  • число сообщений и приблизительная длина входа;
  • наличие вложений, таблиц, кода и нескольких языков;
  • число доступных документов и объём найденного контекста;
  • необходимость инструмента и число допустимых инструментов;
  • наличие противоречивых или отсутствующих обязательных полей;
  • версия промпта, схемы и приложения;
  • сигналы риска, вычисленные детерминированным кодом.

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

Сформируйте снимок данных с неизменяемой версией:

dataset/
├── manifest.json
├── traces.parquet
├── labels.parquet
├── splits.json
└── data_dictionary.md
{
  "dataset_version": "router-traces-v1",
  "created_at": "timestamp",
  "source_window": {
    "from": "timestamp",
    "to": "timestamp"
  },
  "prompt_versions": ["support-v4"],
  "feature_schema": "features-v1",
  "label_schema": "route-label-v1",
  "redaction_policy": "redaction-v3",
  "raw_text_included": false
}

5. Отберите репрезентативные операции

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

Отбирайте данные по слоям:

  • тип операции;
  • диапазон длины контекста;
  • использование инструментов;
  • успех и ошибка текущего процесса;
  • повтор пользователя;
  • язык и формат входа;
  • редкий, но критичный класс риска;
  • период времени, чтобы учесть сезонные изменения.

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

6. Выполните контрфактический replay

По рабочей трассе обычно известен исход только фактически выбранной модели. Для обучения нужен ответ на другой вопрос: что произошло бы с тем же входом на альтернативном маршруте?

Запустите сохранённые входы через обе модели в изолированном бенчмарке:

python -m router_lab.replay \
  --dataset dataset/traces.parquet \
  --routes config/routes.yaml \
  --prompt-version support-v4 \
  --output runs/replay-v1.jsonl \
  --max-concurrency SAFE_VALUE \
  --resume

Это пример интерфейса локального раннера, а не команда существующего внешнего продукта. Значение SAFE_VALUE выберите с учётом лимитов провайдеров и бюджета.

Replay должен соблюдать следующие условия:

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

7. Постройте метку маршрута

Для каждой операции получите четыре наблюдаемых значения:

  • cheap_pass — дешёвая модель прошла критерии;
  • strong_pass — сильная модель прошла критерии;
  • cheap_total_cost и strong_total_cost;
  • cheap_latency_ms и strong_latency_ms.

После этого назначьте метку:

Дешёвая Сильная Учебная метка Интерпретация
Прошла Прошла cheap_safe Есть возможность экономии
Не прошла Прошла strong_required Критическая ошибка дешёвого маршрута
Прошла Не прошла cheap_safe с флагом анализа Сильная модель не является безусловным эталоном
Не прошла Не прошла unsupported Нужны отказ, уточнение, другой процесс или человек

Не присваивайте strong_required всем случаям, где ответы моделей просто различаются. Метка должна отражать контракт качества, а не предпочтение более длинного текста.

Как учитывать повторы

Повтор пользователя может быть сигналом плохого ответа, но не всегда: человек способен уточнить новую задачу или отправить сообщение дважды из-за интерфейса. Введите явный классификатор причин:

retry_reason:
  - correction_same_intent
  - missing_required_information
  - transient_system_error
  - duplicate_submission
  - new_intent
  - unknown

Только подтверждённый correction_same_intent должен напрямую ухудшать оценку качества предыдущей операции. Для unknown показывайте отдельную чувствительность отчёта: результат при включении и исключении спорных повторов.

8. Разделите данные без утечки

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

Группируйте связанные данные по доступному безопасному идентификатору:

  • шаблон или кластер запроса;
  • диалог или логическая операция;
  • обезличенная группа источника;
  • версия документа или задачи.

Финальную проверочную часть лучше выделить по времени: обучаться на более ранних трассах, а проверяться на последующем окне. Это ближе к рабочему запуску и обнаруживает изменение распределения запросов.

{
  "train": {
    "rule": "earlier_window_and_group_exclusive"
  },
  "validation": {
    "rule": "later_window_and_group_exclusive"
  },
  "test": {
    "rule": "latest_closed_window",
    "locked": true
  }
}

Закрытый test-набор не используйте для подбора признаков и порога. Иначе он перестанет быть независимой проверкой.

9. Соберите признаки, доступные до маршрутизации

Начните с интерпретируемого набора:

{
  "operation_type": "support_answer",
  "input_chars_bucket": "1000_4000",
  "message_count_bucket": "2_4",
  "retrieved_chunks_bucket": "4_8",
  "retrieval_score_min_bucket": "low",
  "tool_required": true,
  "allowed_tool_count": 2,
  "attachment_present": false,
  "code_present": false,
  "table_present": true,
  "language_count": 1,
  "missing_required_fields": 1,
  "policy_conflict_detected": false,
  "previous_attempt_failed": false
}

Текстовые эмбеддинги могут повысить качество, но усложняют приватность, стоимость, отладку и контроль дрейфа. Сначала проверьте категориальные и числовые признаки. Добавляйте представление текста только после доказанного недостатка простой модели.

Любой признак должен иметь:

  • определение и тип;
  • момент доступности;
  • правило обработки пропусков;
  • версию вычисляющего кода;
  • оценку риска утечки данных;
  • одинаковую реализацию для обучения и рабочего трафика.

10. Обучите простой вероятностный классификатор

Цель модели — оценить вероятность P(strong_required | features). Для первой версии подходят логистическая регрессия или неглубокие деревья. Их проще калибровать, объяснять и воспроизводить.

python -m router_lab.train \
  --features dataset/features-v1.parquet \
  --labels dataset/labels-v1.parquet \
  --split dataset/splits.json \
  --estimator logistic_regression \
  --class-weight cost_sensitive \
  --calibrate isotonic \
  --output artifacts/router-v1

Это проектный интерфейс. Реальная реализация должна сохранять:

  • модель и список признаков в фиксированном порядке;
  • схему преобразований и категории неизвестных значений;
  • версию обучающих данных;
  • метрики по каждому типу операции;
  • калибровочную кривую;
  • выбранный порог и его обоснование;
  • хеши артефактов для воспроизводимости.

Почему обычной accuracy недостаточно

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

  • false cheap: выбран cheap, хотя требовался strong;
  • false strong: выбран strong, хотя cheap прошёл бы проверку;
  • полноту обнаружения strong_required;
  • долю трафика на каждом маршруте;
  • взвешенную стоимость ошибки;
  • калибровку вероятностей.

false cheap обычно опаснее: он создаёт ошибку качества. false strong чаще означает упущенную экономию. Эти ошибки нельзя считать равноценными.

11. Выберите порог через стоимость и риск

Пусть классификатор возвращает p = P(strong_required). Простейшая политика:

if hard_safety_rule(input):
    route = "strong"
elif p >= ROUTE_TO_STRONG_THRESHOLD:
    route = "strong"
else:
    route = "cheap"

Порог выбирают не по максимальной accuracy, а по допустимому числу false cheap и полной стоимости.

expected_cost(threshold) =
    direct_model_cost
  + automatic_retry_cost
  + escalation_cost
  + failed_operation_cost
  + human_review_cost
  + configured_quality_penalty

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

Добавьте зону неопределённости

Одна граница может быть нестабильной при плохо откалиброванной вероятности. Практичнее задать три области:

if hard_safety_rule(input):
    return STRONG

if p <= CHEAP_SAFE_MAX:
    return CHEAP

if p >= STRONG_REQUIRED_MIN:
    return STRONG

return STRONG  # безопасная политика для неопределённой зоны

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

Жёсткие правила поверх классификатора

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

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

12. Реализуйте рабочую политику

router:
  version: trace-router-v1
  feature_schema: features-v1
  model_artifact: artifacts/router-v1/model.bin

  thresholds:
    cheap_safe_max: CONFIGURED_VALUE
    strong_required_min: CONFIGURED_VALUE

  unknown_feature_policy: strong
  unsupported_operation_policy: strong
  router_error_policy: strong
  timeout_policy: strong

  hard_routes:
    external_write: strong
    policy_conflict: strong
    missing_validator: strong

  cheap_failure:
    schema_error: escalate_once
    policy_error: escalate_once
    provider_timeout: retry_same_route_once
    other: escalate_once

  budgets:
    max_model_calls_per_operation: CONFIGURED_VALUE
    max_escalations: 1
    deadline_ms: CONFIGURED_VALUE

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

Пример управляющего кода:

def execute_operation(request):
    features = build_features(request)

    if violates_hard_rule(features):
        decision = RouteDecision("strong", reason="hard_rule")
    else:
        decision = router.predict(features)

    record_route_decision(
        operation_id=request.operation_id,
        router_version=router.version,
        feature_version=features.version,
        score=decision.score,
        route=decision.route,
        reason=decision.reason,
    )

    result = call_route(decision.route, request)

    if result.retryable_transport_error:
        result = retry_within_budget(decision.route, request)

    if decision.route == "cheap" and not validators_accept(result):
        result = escalate_once_to_strong(request)

    return finalize_operation(request, result)

Записывайте причину каждого выбора: model_score, hard_rule, unknown_input, router_failure или manual_override. Без этого ошибочный маршрут нельзя разобрать.

13. Начните с теневого режима

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

  • распределение оценок на текущем трафике;
  • долю неизвестных признаков;
  • потенциальное распределение маршрутов;
  • расхождения с текущей политикой;
  • время вычисления признаков и решения;
  • сбои загрузки артефакта и несовместимость схем.

Теневой запуск сам по себе не доказывает качество альтернативной модели, если она не выполнялась. Он проверяет интеграцию маршрутизатора и распределение решений. Контрфактическое качество берётся из replay или из ограниченного эксперимента.

Критерии выхода из тени

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

14. Проведите ограниченный рабочий запуск

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

cohort = stable_hash(operation_id) % 100

if kill_switch_enabled:
    policy = "strong_only"
elif operation_type not in enabled_operation_types:
    policy = "strong_only"
elif cohort < rollout_percentage:
    policy = "trace_router"
else:
    policy = "strong_only"

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

Заранее задайте условия остановки:

  • критическое нарушение контракта;
  • рост подтверждённых исправляющих повторов сверх разрешённой границы;
  • ухудшение качества относительно контрольной политики;
  • выход задержки за установленный бюджет;
  • аномальный рост эскалаций;
  • невозможность связать вызовы с операциями;
  • расхождение учтённой стоимости с биллингом сверх допустимой погрешности.

15. Рассчитайте обязательные метрики

Полная стоимость

direct_cost =
  sum(charged_cost for every model call)

operation_cost =
  direct_cost
  + tool_cost
  + allocated_infrastructure_cost
  + measured_human_review_cost

cost_per_success =
  sum(operation_cost for all terminal operations)
  / count(quality_pass = true)

Стоимость неудачных операций остаётся в числителе. Иначе ненадёжная политика выглядит искусственно дешёвой.

Качество

quality_pass_rate =
  accepted_operations / evaluated_terminal_operations

critical_violation_rate =
  critical_violations / evaluated_terminal_operations

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

Задержка

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

operation_latency =
  terminal_timestamp - first_request_timestamp

Отдельно показывайте задержку без эскалации и задержку цепочки cheap → strong.

Ошибочные маршруты

false_cheap_rate =
  count(selected_cheap AND strong_required)
  / count(strong_required)

false_strong_rate =
  count(selected_strong AND cheap_safe)
  / count(cheap_safe)

Первый показатель отражает риск качества, второй — упущенную экономию.

Повторы и эскалации

automatic_retry_rate =
  operations_with_automatic_retry / all_operations

user_correction_retry_rate =
  confirmed_correction_retries / eligible_operations

cheap_to_strong_escalation_rate =
  cheap_operations_escalated_to_strong / cheap_operations

escalation_success_rate =
  successful_escalations / all_escalations

Высокая доля успешных эскалаций не означает, что система эффективна: каждый такой случай уже оплатил дешёвый вызов и увеличил задержку.

16. Соберите отчёт без выдуманных результатов

Следующий шаблон заполняется только после запуска. Пустые значения нельзя заменять ожиданиями команды.

Метрика strong_only cheap_only trace_router
Число завершённых операций Заполнить Заполнить Заполнить
Доля прошедших проверку Заполнить Заполнить Заполнить
Критические нарушения Заполнить Заполнить Заполнить
Полная стоимость Заполнить Заполнить Заполнить
Стоимость успешной операции Заполнить Заполнить Заполнить
Медианная задержка операции Заполнить Заполнить Заполнить
Хвостовая задержка операции Заполнить Заполнить Заполнить
Ошибочные маршруты false cheap Не применимо Не применимо Заполнить
Упущенная экономия false strong Не применимо Не применимо Заполнить
Автоматические повторы Заполнить Заполнить Заполнить
Исправляющие повторы пользователя Заполнить Заполнить Заполнить
Эскалации cheap → strong Не применимо По правилам эксперимента Заполнить

Разрез ошибочных маршрутов

К агрегату приложите таблицу разбора:

Группа Число Доля Стоимость Причина Следующее действие
Неизвестный тип входа Заполнить Заполнить Заполнить Проверить вручную Добавить безопасное правило или данные
Недостаточный контекст Заполнить Заполнить Заполнить Проверить поиск Не маскировать ошибку поиска маршрутизацией
Сложный инструментальный шаг Заполнить Заполнить Заполнить Проверить схему инструмента Правило или новый признак
Дрейф входного потока Заполнить Заполнить Заполнить Сравнить окна Переобучить после разметки

17. Проверьте расчёты независимыми запросами

Пример агрегации полной стоимости по политике:

WITH call_cost AS (
  SELECT
    operation_id,
    SUM(charged_cost) AS model_cost,
    COUNT(*) AS model_calls,
    SUM(CASE WHEN escalated THEN 1 ELSE 0 END) AS escalations
  FROM router_calls
  GROUP BY operation_id
)
SELECT
  o.policy_version,
  COUNT(*) AS operations,
  SUM(CASE WHEN o.quality_pass THEN 1 ELSE 0 END) AS successful_operations,
  SUM(c.model_cost) AS total_model_cost,
  SUM(c.model_cost)
    / NULLIF(SUM(CASE WHEN o.quality_pass THEN 1 ELSE 0 END), 0)
    AS model_cost_per_success,
  AVG(c.model_calls) AS calls_per_operation,
  AVG(c.escalations) AS escalations_per_operation
FROM router_operations o
JOIN call_cost c USING (operation_id)
WHERE o.terminal_status IS NOT NULL
GROUP BY o.policy_version;

Пример отчёта об ошибочных маршрутах требует независимой контрфактической метки:

SELECT
  selected_route,
  counterfactual_label,
  COUNT(*) AS operations
FROM router_evaluation
WHERE evaluation_status = 'complete'
GROUP BY selected_route, counterfactual_label
ORDER BY selected_route, counterfactual_label;

Перед публикацией итогов выполните сверку:

  • сумма стоимости вызовов совпадает с доступным биллингом в пределах объяснённой погрешности;
  • каждый вызов принадлежит ровно одной операции;
  • нет завершённых операций без конечного статуса качества;
  • повторы связаны с исходной операцией;
  • валюты и версии цен не смешаны;
  • длительность операции не заменена длительностью последнего вызова;
  • число операций в отчёте совпадает с назначенными экспериментальными когортами.

18. Проверка перед увеличением трафика

  1. Воспроизводимость. Повторное обучение на закреплённых данных и конфигурации создаёт эквивалентный артефакт и сопоставимые метрики.
  2. Контракт признаков. Оффлайн- и рабочая реализации возвращают одинаковые признаки для одной фикстуры.
  3. Неизвестный вход. Новая категория, пропущенное поле и несовместимая версия безопасно уходят на сильный маршрут.
  4. Отказ маршрутизатора. Тайм-аут, повреждённый артефакт и исключение не блокируют операцию и не отправляют её на дешёвую модель по умолчанию.
  5. Бюджет попыток. Цепочка не может бесконечно повторять дешёвый и сильный вызовы.
  6. Качество. Порог проходит ограничения целиком и для критичных срезов.
  7. Стоимость. Экономия сохраняется после эскалаций, повторов и неуспешных операций.
  8. Задержка. Хвостовая длительность не скрыта средним значением.
  9. Приватность. В артефактах обучения и отчёте нет исходных персональных данных и секретов.
  10. Отключение. Проверен немедленный откат на strong_only.

Минимальный набор автоматических тестов

pytest -q \
  tests/test_feature_parity.py \
  tests/test_unknown_categories.py \
  tests/test_router_fallback.py \
  tests/test_retry_budget.py \
  tests/test_cost_aggregation.py \
  tests/test_cohort_stability.py \
  tests/test_report_reconciliation.py

Названия файлов иллюстрируют структуру проекта. Добавьте реальные тесты для собственных контрактов.

Что обычно ломается

Сильная модель объявлена источником истины

Она тоже ошибается. Если метка равна «совпало ли с сильной моделью», маршрутизатор наследует её ошибки и стиль. Проверяйте обе модели независимым контрактом.

В обучении есть признаки из будущего

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

Тест содержит дубликаты обучения

Похожие обращения и соседние сообщения одного диалога создают ложное ощущение обобщения. Делите по группам и времени.

Отчёт считает вызовы, а не операции

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

Эскалации скрывают плохую маршрутизацию

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

Повтор пользователя автоматически считается ошибкой

Новый вопрос и дублированная отправка не равны исправлению прежнего ответа. Размечайте причину повторов и публикуйте долю неизвестных.

Маршрутизатор оптимизировали на среднем значении

Редкие опасные операции растворяются в массовых лёгких запросах. Проверяйте критичные срезы отдельно и применяйте жёсткие правила.

Прайс-лист применён задним числом

Историческая стоимость меняется после обновления тарифа. Сохраняйте фактическое списание либо версию цены и валюту на момент вызова.

Модель переобучили, но версию признаков не обновили

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

Дешёвый маршрут включён при отказе классификатора

Это опасная стратегия отказа. Ошибка маршрутизатора, неизвестная категория и повреждённый артефакт должны вести к безопасному маршруту.

Ограничения подхода

  • Replay не полностью повторяет продакшен. Состояние внешних систем, кэши, нагрузка и поведение пользователя могут отличаться.
  • Контрфактические вызовы стоят денег. Репрезентативную выборку и частоту переоценки нужно ограничивать бюджетом.
  • Качество меток ограничивает качество маршрутизатора. Неполный валидатор превращает незамеченные ошибки в положительные примеры.
  • Недетерминированность усложняет разметку. Один запуск не всегда характеризует маршрут; для спорных классов нужны повторы и распределение исходов.
  • Распределение запросов меняется. Новые функции, документы, пользователи и промпты делают старые трассы менее репрезентативными.
  • Две модели не всегда достаточны. Некоторые операции требуют отдельной специализированной модели, детерминированного кода или человека.
  • Маршрутизация не исправляет плохую архитектуру. Некачественный поиск, неверная схема инструмента и противоречивая инструкция останутся источниками ошибок.
  • Экономия провайдера не равна бизнес-выгоде. Нужно учитывать ручной труд, последствия ошибок и ценность завершённой операции.
  • Порог зависит от домена. Его нельзя переносить между продуктами без нового измерения.

Как сопровождать маршрутизатор после запуска

Маршрутизатор становится частью рабочего процесса, поэтому требует собственного цикла наблюдения:

  1. ежедневно проверять ошибки загрузки модели, неизвестные категории и распределение маршрутов;
  2. сравнивать распределение признаков с обучающим окном;
  3. еженедельно разбирать false cheap, повторы и дорогие эскалации;
  4. периодически воспроизводить свежую стратифицированную выборку на обоих маршрутах;
  5. добавлять подтверждённые ошибки в регрессионный набор;
  6. переобучать кандидата отдельно от принятой версии;
  7. снова проходить replay, тень и ограниченный запуск;
  8. хранить прежний артефакт и конфигурацию для возврата.

Не запускайте автоматическое переобучение непосредственно на неразобранных рабочих повторах. Иначе интерфейсные дубли, атаки и временные сбои станут обучающими сигналами.

Финальный контрольный список

  • операция и успех определены до эксперимента;
  • трассы связывают запрос, попытки, вызовы, результат и повторы;
  • данные обезличены, а срок хранения ограничен;
  • обе модели воспроизведены на одном контракте;
  • сильная модель не считается автоматической истиной;
  • признаки доступны до выбора маршрута;
  • train, validation и test разделены по группам и времени;
  • измерены false cheap и false strong;
  • порог выбран по риску и полной стоимости;
  • неизвестный вход и отказ маршрутизатора ведут на безопасный маршрут;
  • теневой режим пройден до воздействия на пользователя;
  • рабочий запуск ограничен стабильной когортой;
  • стоимость включает неудачи, повторы и эскалации;
  • качество и задержка показаны по критичным срезам;
  • итоговый отчёт сверён с исходными событиями и биллингом;
  • проверен аварийный переход на strong_only.