# Production LLM Stack: роутинг, evals и надежность

> Практическая архитектура production LLM: контракты, роутинг моделей, инструменты, валидация, наблюдаемость, evals, бюджеты и безопасные релизы.
> Author: Roman Belov · Published: 2026-08-21 · Source: https://futurecraft.pro/ru/blog/production-llm-stack/

Production LLM stack — это контур управления вокруг компонента, который может вернуть
убедительный неправильный ответ вместе с `200 OK`.

Вызов модели — только один блок:

```text
клиент
  → входной слой и политика
  → оркестратор задачи
  → контекст, retrieval, memory и инструменты
  → model gateway и провайдер
  → валидация ответа
  → продуктовое действие

каждый шаг → trace → evaluation → решение о релизе
```

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

Ниже — рабочий каркас этой системы. Ссылки ведут в отдельные практические руководства,
чтобы страница не превратилась в документацию одного вендора.

## Начните с контракта задачи

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

Для ответа службы поддержки контракт может выглядеть так:

```yaml
task: support.answer
input_schema: support_answer_request.v3
output_schema: support_answer_response.v2
required_capabilities:
  - structured_output
  - tool_calling
allowed_data_regions: [eu]
max_total_latency_ms: 8000
max_provider_attempts: 2
requires_citations: true
requires_human_review_when:
  - refund_amount > 500
  - source_conflict == true
```

Это не промпт, а обещание приложения. По нему роутер отсекает неподходящие модели,
оркестратор решает, когда остановиться, валидатор отклоняет ответ, а eval suite измеряет
результат.

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

| Уровень | Вопрос | Пример сигнала |
| --- | --- | --- |
| Контракт | Ответ соблюдает интерфейс? | Схема валидна, citation ID существуют |
| Качество задачи | Результат достаточно хорош? | Метка верна, ответ опирается на источник |
| Продуктовый исход | Пользователю помогло? | Тикет закрыт, правка принята, бронь завершена |

Валидный JSON может быть неправильным. Красивый ответ может не дать полезного исхода.
Нужны все три уровня.

## Разделите data plane и control plane

**Data plane** обслуживает запрос: собирает контекст, вызывает инструменты и модели,
валидирует результат и возвращает его. Этот путь должен быть коротким и ограниченным.

**Control plane** решает, что именно исполнит data plane:

- версии промптов и политик;
- допустимые модели и провайдеры;
- правила роутинга и fallback;
- evaluation datasets и пороги релиза;
- бюджеты, квоты и kill switch;
- release labels и точки отката.

Не позволяйте правке в dashboard менять все production-запросы без истории. Каждое
изменение control plane должно создавать неизменяемый release ID. Записывайте его в
trace.

Практичный release manifest:

```json
{
  "release": "support-answer-2026-08-21.3",
  "prompt": "support-answer@41",
  "policy": "support-policy@12",
  "router": "support-router@7",
  "retrieval": "help-center-index@2026-08-20",
  "toolset": "support-tools@5",
  "outputSchema": "support_answer_response.v2"
}
```

После этого фраза «AI стал хуже» превращается в diff, который можно проверить.

## Применяйте политику до роутинга

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

Полезные входы политики:

- задача и tenant;
- класс данных и разрешенные регионы обработки;
- обязательные возможности: vision, structured output, tool calling;
- разрешения на инструменты;
- класс задержки и оставшийся дедлайн;
- бюджет пользователя, tenant и задачи;
- назначенный релиз или эксперимент.

Результатом должен быть явный execution envelope:

```json
{
  "task": "support.answer",
  "tenantId": "tenant_internal_42",
  "dataClass": "confidential",
  "allowedProviders": ["provider-eu-a"],
  "allowedTools": ["search_help_center", "read_subscription"],
  "deadlineMs": 8000,
  "release": "support-answer-2026-08-21.3"
}
```

Не отправляйте credentials провайдера или чувствительные детали классификации в
браузер. Ими владеет серверный execution layer.

## Введите внутренний контракт модели

Типы provider SDK быстро протекают в бизнес-логику: finish reasons, формы tool call,
usage и коды ошибок оказываются по всему проекту. Поставьте узкий адаптер между
приложением и каждым провайдером или gateway.

Как минимум нормализуйте:

- сообщения запроса и типы контента;
- structured output и определения инструментов;
- timeout и cancellation;
- контент ответа и tool calls;
- usage и данные для расчета стоимости;
- request ID провайдера;
- класс ошибки и возможность повтора;
- фактически использованные модель и провайдера.

Не скрывайте важные возможности за ложной универсальностью. Интерфейс с escape hatch
для каждого вендора только маскирует lock-in. Оставьте общее ядро и явные capabilities,
а несовместимый маршрут отклоняйте до вызова.

LLM gateway может централизовать этот контракт, credentials, квоты и телеметрию. Но с
него не обязательно начинать. Сначала закройте одного провайдера своим адаптером.
Gateway становится полезен, когда одинаковые операционные контролы нужны нескольким
сервисам. Детали — в руководстве по
[multi-provider LLM architecture](/ru/blog/multi-provider-llm-architecture/).

## Роутите задачи, а не абстрактный «интеллект»

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

```text
обязательная модальность и инструменты
→ политика данных и регион
→ совместимость контракта ответа
→ оставшийся бюджет задержки
→ порог качества на этой задаче
→ лимиты скорости и расходов
→ предпочтительный кандидат
```

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

Начните с таблицы, а не с еще одного LLM-вызова:

| Класс задачи | Обязательное поведение | Основной путь | Допустимый fallback |
| --- | --- | --- | --- |
| Intent classification | Фиксированный enum, малая задержка | Проверенная малая модель | Правила или модель того же класса |
| Ответ по базе знаний | Citations и retrieval | Модель, проверенная на support set | Эквивалентная grounded-модель |
| Извлечение из документов | Строгая схема | Structured-output модель | Асинхронная очередь review |
| Рискованное действие | Tool call и approval | Достаточная модель | Без скрытого fallback |

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

## Задайте бюджет контекста

Контекст — ограниченный вход, а не мешок, который надо заполнить. Выделите отдельные
бюджеты и provenance для инструкций, текущего состояния, retrieval, memory, tool
results и истории разговора.

```text
system и policy          фиксированы, высший приоритет
вход задачи              обязателен
текущее состояние        точное и scoped
retrieved evidence       ранжировано, с источниками и freshness
memory                   отфильтрована по субъекту и lifecycle
tool results             типизированы и ограничены по размеру
история разговора        суммаризируется только при необходимости
```

Сборка и приоритеты разобраны в
[руководстве по context engineering](/ru/blog/context-engineering-guide/), а долговременное
состояние — в статье про [память AI-агента](/ru/blog/ai-agent-memory/). Держите оба слоя
вне model adapter, чтобы тестировать их независимо.

Для retrieval записывайте document IDs, версию индекса, фильтры, scores и финальные
chunks, отправленные модели. Тогда неправильный ответ можно разложить на «источника не
было», «retrieval его пропустил» или «модель его проигнорировала».

## Считайте инструменты привилегированным кодом

Tool call — предложение действия, а не авторизация. Проверяйте его в коде приложения с
учетом подтвержденного principal и текущего состояния.

Каждому инструменту нужны:

- узкая схема и ограниченный ответ;
- серверная авторизация;
- timeout короче дедлайна задачи;
- idempotency key для повторяемых записей;
- audit event без секретов и лишних персональных данных;
- approval boundary для значимых действий;
- типизированная ошибка, понятная оркестратору.

Не давайте пользовательскому агенту универсальные `run_sql`, `http_request` или shell.
Разделяйте возможности по бизнес-действиям. Для удаленных MCP-инструментов действуют те
же правила из [руководства по безопасности MCP](/ru/blog/mcp-security-guide/).

Лимит цикла контролирует оркестратор. Модель не должна бесконечно тратить tool calls,
потому что продолжает «исследовать» задачу.

## Валидируйте до побочного эффекта

Structured output переносит сбой из разбора прозы в проверку контракта, но не делает
значения истинными.

Проверяйте слоями:

1. **Синтаксис:** JSON разбирается, content type ожидаемый.
2. **Структура:** schema, enum, required fields, длина и cardinality.
3. **Ссылочная целостность:** ID существуют и принадлежат текущему tenant.
4. **Grounding:** citations указывают на evidence, которое модель действительно видела.
5. **Политика:** действие разрешено этому principal в текущем состоянии.
6. **Бизнес-правила:** суммы сходятся, даты допустимы, переход состояния разрешен.

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

## Стройте надежность вокруг одного дедлайна

У всей задачи должен быть сквозной дедлайн. Retrieval, tools, model calls, retry и
валидация тратят его вместе. Пять независимых таймаутов по 30 секунд превращают один
запрос в минуты ожидания.

Сначала классифицируйте сбой:

| Сбой | Повторить тот же route? | Fallback? | Обычное действие |
| --- | --- | --- | --- |
| Timeout до ответа | Один раз, если есть бюджет | Да, на совместимый route | Backoff с jitter |
| Provider 429/5xx | Ограниченно | Да | Учесть retry hint, открыть circuit при серии сбоев |
| Невалидный запрос или schema | Нет | Обычно нет | Исправить caller или контракт |
| Auth/permission | Нет | Нет | Остановить и поднять alert |
| Ответ нарушил контракт | Иногда один repair | Только на проверенный эквивалент | Повторить валидацию |
| Оборванный stream | Не replay прозрачно | Только явный restart | Не дублировать side effect |

Retry умножает нагрузку во время инцидента. Задайте малый attempt budget, exponential
backoff с jitter и [circuit breaker](/ru/blog/circuit-breaker-deno-edge-functions/),
который остановит трафик к падающей зависимости.

Cloudflare AI Gateway — один из вариантов реализации. В актуальной
[документации request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/)
есть per-request timeouts и ограниченные retry; сам gateway поддерживает роутинг,
fallback, rate limiting, caching и budget controls. Но эти переключатели не заменяют
общий дедлайн и политику совместимости на уровне приложения.

Fallback — не «попробовать следующую модель». До релиза докажите, что каждый кандидат:

- поддерживает обязательные инструменты и тот же output contract;
- имеет право обрабатывать этот класс данных в нужном регионе;
- укладывается в оставшийся latency и cost budget;
- проходит task dataset с согласованным порогом;
- не повторит уже совершенный side effect.

Если подходящего кандидата нет, деградируйте честно: верните частичный read-only ответ,
поставьте задачу в очередь, запросите review или завершите стабильной ошибкой.

## Один trace на пользовательскую задачу

Создавайте один root trace на один продуктовый результат. Retrieval, tool calls, model
generations, parsing, validation, retry и fallback должны быть дочерними шагами.

Используйте стабильные имена операций:

```text
support.answer
├── load-policy
├── retrieve-help-center
├── generate-answer
├── validate-citations
└── record-outcome
```

Записывайте release IDs и ограниченный набор диагностических измерений:

- task, environment, app release и experiment;
- версии prompt, policy, router, retrieval и toolset;
- выбранные model/provider и причина fallback;
- token usage, оценочная стоимость и задержка шагов;
- результат контракта, quality scores и product outcome;
- внутренние request IDs для корреляции и удаления.

По умолчанию не пишите в telemetry текст пользователя, секреты, access tokens и сырые
чувствительные tool results. Успешный трафик можно сэмплировать, но ошибок и canary
должно хватать для расследования регрессий.

Актуальные рекомендации Langfuse также советуют стабильные имена trace и observations,
осмысленные root input/output, model и cost metadata на generations и связь prompt
versions с traces. [Руководство по LLM observability](/ru/blog/llm-observability-langfuse/)
превращает это в telemetry contract, а
[пошаговая настройка Langfuse](/ru/blog/langfuse-step-by-step/) показывает реализацию.

## Постройте evaluation loop до смены моделей

Eval — это release test, а не демонстрационный leaderboard. Он строится из задач,
которые обязан выполнять продукт.

Комбинируйте разные способы проверки:

1. детерминированные проверки schemas, citations, permissions, totals и известных labels;
2. сравнение с reference, когда существует стабильный ожидаемый ответ;
3. human review для рискованных или неоднозначных решений;
4. откалиброванный LLM judge для узких rubric-based сигналов в масштабе;
5. реальные продуктовые outcomes, присоединенные после их появления.

[Руководство по LLM-as-a-judge](/ru/blog/llm-as-judge-automated-quality-gate/)
показывает, где judge помогает, а где врет. В статье про
[human-in-the-loop](/ru/blog/human-in-the-loop/) разобраны очереди review и escalation.

Храните фиксированный regression set с ID, входом задачи, policy context, ожидаемыми
инвариантами и labels для срезов. Добавляйте граничные и adversarial cases, а не только
happy path. Сравнивайте текущий и новый релизы на одном наборе и читайте ошибки, а не
один средний score.

Затем замкните цикл:

```text
production failure или проверенная жалоба
→ удалить чувствительные данные и воспроизвести
→ добавить или обновить dataset case
→ написать самый дешевый надежный evaluator
→ прогнать текущий и новый релизы
→ canary
→ наблюдать тот же сигнал online
```

Langfuse описывает тот же offline-to-online цикл в
[evaluation concepts](https://langfuse.com/docs/evaluation/core-concepts): изменение
проверяется на фиксированном dataset, live traces наблюдаются, а новые edge cases
возвращаются в набор. Инструмент вторичен; актив — сам feedback loop.

## Считайте стоимость успешной задачи

Токены — входная метрика. Полезная единица выглядит так:

```text
model + retrieval + tool + evaluation cost
-------------------------------------------
число задач, прошедших продуктовый критерий
```

Сегментируйте ее по task и release. Дешевая модель с дополнительными retries, repairs
и human escalation может увеличить реальную стоимость.

Нужны четыре guardrail:

- предел токенов, tool steps и attempts на запрос;
- квоты пользователя, tenant, task и environment;
- spend alerts плюс hard stop или согласованный degraded route;
- асинхронный режим для работы без требования интерактивной задержки.

Кэшируйте только там, где повторное использование семантически безопасно. Включайте в
ключ prompt, model, policy, retrieval version, locale и нужный user scope. Никогда не
делите персонализированные или permission-dependent ответы между principals.
[Руководство по оптимизации LLM-затрат](/ru/blog/ai-cost-optimization/) подробно
разбирает prompt caching, semantic caching, batching и model downsizing.

## Выпускайте prompt, model и retrieval как релиз

Считайте их программными релизами, даже если они живут в dashboard.

Безопасная последовательность:

1. создать неизменяемые версии prompt, policy, router и retrieval;
2. запустить deterministic tests и фиксированный evaluation set;
3. сравнить quality, contract failures, latency и cost по важным срезам;
4. направить canary только на трафик, подходящий кандидату;
5. наблюдать заранее заданные guardrails и product outcomes;
6. передвинуть label или вернуть его на предыдущую версию;
7. сохранить release manifest на каждом trace.

Langfuse Prompt Management использует неизменяемые версии и перемещаемые labels вроде
`production`; актуальная
[документация data model](https://langfuse.com/docs/prompt-management/data-model)
описывает promotion и rollback через labels. Кэшируйте удаленную конфигурацию prompt и
держите локальный known-good fallback, чтобы сбой control plane не остановил data plane.

Не меняйте одновременно prompt, model, retriever и tool schema, если релиз не обязан
быть атомарным. Иначе вы потеряете причинность и усложните rollback.

## Alerts должны вести к действию

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

- contract failures выше baseline релиза;
- end-to-end latency или timeout rate выше task SLO;
- доля fallback и открытых circuits;
- стоимость успешной задачи или скорость расходов;
- retrieval-empty и unresolved-citation rate;
- падение quality score на стабильной выборке;
- возраст human-review backlog для рискованных задач.

Каждому alert нужны owner, окно, runbook и release dimension.
[Руководство по автоматическим алертам](/ru/blog/automated-metric-alerts/) объясняет
thresholds и burn rate. Названия событий лучше один раз закрепить через
[event taxonomy для AI-продукта](/ru/blog/event-taxonomy-ai/), чтобы dashboard не стал
набором несовместимых счетчиков.

## Защитите данные и credentials заранее

До production-трафика ответьте:

- Какие классы данных разрешено отправлять каждому провайдеру и региону?
- Какие поля удаляются или токенизируются перед inference и telemetry?
- Где хранятся, как ограничиваются, ротируются и аудируются provider keys?
- Может ли tenant экспортировать или удалить prompts, traces, memory и cached outputs?
- Какие tools могут читать и менять конкретные ресурсы?
- Каков retention для raw inputs, derived scores и backups?

Используйте environment variables или secret manager, а не исходный код и client
bundle. Разделяйте staging и production credentials и quotas. В актуальном
[production guide OpenAI](https://developers.openai.com/api/docs/guides/production-best-practices)
также рекомендованы server-side хранение секретов, отдельные environments и явные
spend controls.

Redaction — не regex, который добавляют к логам после запуска. Определите разрешенные
поля на границе задачи, несите классификацию через trace и тестируйте, что error paths
не пишут raw payload.

## Последовательность внедрения без лишней сложности

### Этап 1: одна задача, один провайдер

- типизированные input и output contracts;
- один server-side provider adapter;
- end-to-end deadline и validation;
- root trace с release IDs;
- фиксированный regression dataset;
- явные spend и tool-step limits.

### Этап 2: операционные контролы

- versioning prompt и policy;
- dashboards и полезные alerts;
- bounded retry и circuit breaker;
- второй провайдер, проверенный как fallback;
- canary и процедура rollback.

### Этап 3: роутинг и стоимость

- deterministic task routing;
- пороги quality и cost для каждой задачи;
- безопасный cache и async/batch paths;
- online evaluation на выборке;
- перенос production failures в regression cases.

### Этап 4: агенты и сложная оркестрация

- несколько tools и bounded loops;
- durable memory с lifecycle controls;
- human approval для значимых действий;
- evaluation траекторий и tool behavior;
- trace propagation между сервисами.

Не начинайте с четвертого этапа только потому, что agent framework быстро собрал demo.
Каждый этап добавляет состояния, которые придется тестировать, наблюдать и
восстанавливать.

## Checklist готовности к production

**Контракт и политика**

- [ ] У каждой задачи есть typed input/output, критерий успеха и общий дедлайн.
- [ ] Authentication, data policy и tool permissions определяются до роутинга.
- [ ] Модель не авторизует собственные side effects.

**Исполнение и надежность**

- [ ] Специфика провайдера закрыта capability-aware адаптером.
- [ ] Retry ограничены, идемпотентны где нужно и делят дедлайн задачи.
- [ ] Каждый fallback сохраняет capabilities, policy, schema и проверенное quality.
- [ ] Для partial stream и committed side effects задано recovery behavior.

**Качество и релизы**

- [ ] Текущий и новый релизы запускаются на одном репрезентативном dataset.
- [ ] Contract checks, human review и calibrated judges закрывают разные сбои.
- [ ] Prompt, model, policy, tools и retrieval versions присутствуют в traces.
- [ ] Canary, guardrails, rollback и kill switch проверены на практике.

**Операции и стоимость**

- [ ] Один root trace соответствует одной пользовательской задаче.
- [ ] Dashboard показывает task success, latency, fallback и cost per successful task.
- [ ] Бюджеты заданы на запрос и нужный уровень user, tenant или task.
- [ ] Сбой telemetry не ломает продуктовый путь.

**Безопасность и данные**

- [ ] Keys остаются на сервере и разделены по environment.
- [ ] Чувствительные inputs и tool results минимизируются до inference и telemetry.
- [ ] Retention, access, correction, export и deletion описаны и протестированы.

Stack готов, когда релиз может сломаться известным способом, оставить достаточно
сигналов для расследования и откатиться без гадания. Больше вендоров и агентов этого не
дают. Это дают контракты, ограниченное исполнение, traces, evals и дисциплина релизов.
