# ГЛАВА 17. НАБЛЮДАЕМОСТЬ И ЭКСПЛУАТАЦИЯ LLM-ПРОДУКТА

---

В традиционных веб-сервисах наблюдаемость стоит на трёх столпах: логи, метрики, трейсы. Вы знаете, что сервер ответил за 120 мс, что HTTP-статус — 200, что база данных отработала два запроса. Этого достаточно, чтобы понять: сервис работает. Для LLM-систем — нет.

Представьте мониторинг больницы. Uptime — это электричество и водоснабжение: необходимые, но явно недостаточные условия. Пациенты могут умирать при 100%-м аптайме. Вам нужны исходы лечения — смертность, осложнения, повторные госпитализации. В LLM-системах роль «исхода» играет **качество ответа**: был ли он корректен, релевантен, безопасен. Традиционный APM скажет «сервер вернул 200 OK»; LLM observability скажет «ответ — галлюцинация».

Это и есть четвёртый столп: **content-level observability** — наблюдаемость на уровне содержания, а не инфраструктуры.

В [главе 13](13_anti_hallucination_loop.md) мы заложили фундамент: LDD (Log-Driven Development) с `LLMCallLog`, production-дашборд из семи панелей, anti-loop protocol и мониторинг-стек OpenTelemetry → Phoenix / Grafana / ClickHouse. Эта глава расширяет фундамент до полного операционного цикла: distributed tracing по стандарту GenAI, версионирование промптов, классификация ошибок, replay-дебаггинг, incident response и SLO для LLM.

---

## 17.1. OpenTelemetry GenAI: формирующийся стандарт

### Зачем нужен стандарт

Без стандарта каждый инструмент изобретает свою схему: LangSmith — свои трейсы, Phoenix — свои span-атрибуты, Datadog — свои метрики. Переключиться между ними — значит переписать инструментацию. OpenTelemetry GenAI semantic conventions решают эту проблему так же, как OTel решил её для HTTP и gRPC: единая схема span'ов и метрик, которую понимают все бэкенды.

### Статус: Development (v1.40.0, апрель 2026)

Конвенции ещё не stable — они находятся в фазе Development. Это значит: схема может меняться, но она уже используется в production крупными вендорами инструментации. Чтобы включить их:

```bash
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
```

### Ключевые типы span'ов

OpenTelemetry GenAI определяет операции через атрибут `gen_ai.operation.name`:

| Операция | Когда создаётся |
|----------|----------------|
| `chat` | Вызов chat completion API |
| `text_completion` | Вызов completion API |
| `embeddings` | Создание embedding'ов |
| `retrieval` | RAG-поиск по vector store |
| `execute_tool` | Выполнение tool call |
| `generate_content` | Мультимодальная генерация (Gemini и др.) |
| `create_agent` | Инициализация агента |
| `invoke_agent` | Вызов agent loop |

### Атрибуты span'а

Каждый span несёт стандартный набор атрибутов:

**Идентификация:**
- `gen_ai.operation.name` — тип операции
- `gen_ai.provider.name` — провайдер (`openai`, `anthropic`, `gcp.vertex_ai`)
- `gen_ai.request.model` — запрошенная модель (`claude-opus-4-6-20260401`)
- `gen_ai.response.model` — фактически использованная модель

**Потребление токенов:**
- `gen_ai.usage.input_tokens` — входные токены
- `gen_ai.usage.output_tokens` — выходные токены
- `gen_ai.usage.cache_creation.input_tokens` — токены, записанные в prompt cache
- `gen_ai.usage.cache_read.input_tokens` — токены, прочитанные из prompt cache

**Агентные span'ы:**
- `gen_ai.agent.id` — идентификатор агента
- `gen_ai.agent.name` — читаемое имя
- `gen_ai.conversation.id` — идентификатор сессии/диалога

### Пять метрических гистограмм

Конвенции определяют пять ключевых метрик:

| Метрика | Единица | Что измеряет |
|---------|---------|-------------|
| `gen_ai.client.token.usage` | tokens | Потребление токенов на вызов |
| `gen_ai.client.operation.duration` | seconds | Полная длительность операции |
| `gen_ai.server.time_to_first_token` | seconds | TTFT — время до первого токена |
| `gen_ai.server.time_per_output_token` | seconds | TPOT — время на каждый выходной токен |
| `gen_ai.server.request.duration` | seconds | Серверная длительность обработки запроса |

TTFT и TPOT — два числа, которые определяют UX streaming-интерфейсов. TTFT отвечает за ощущение «система думает» vs «система зависла». TPOT определяет скорость потока текста. Для chat-интерфейса TTFT < 1 с и TPOT < 50 мс обычно считаются комфортными; для agent loop, где пользователь не видит поток, важнее `operation.duration` целиком.

### Захват содержимого (opt-in)

По умолчанию OTel GenAI не записывает тексты промптов и ответов — только метаданные. Но для replay-дебаггинга нужен полный ввод-вывод. Три атрибута, включаемые явно:

- `gen_ai.input.messages` — входные сообщения (system, user, assistant history)
- `gen_ai.output.messages` — ответ модели
- `gen_ai.system_instructions` — системный промпт

Включение content capture — осознанное решение: это критически важно для отладки, но требует обработки PII (см. §17.5).

### MCP semantic conventions

Отдельный набор конвенций покрывает вызовы MCP-серверов (Model Context Protocol). Если ваш агент использует MCP-tools, span'ы вызовов тоже стандартизированы.

---

## 17.2. Инструменты: OpenLLMetry, Phoenix, LangSmith, W&B Weave

В [главе 13](13_anti_hallucination_loop.md) мы описали платформы наблюдаемости обзорно. Здесь — сравнение четырёх ключевых инструментов с точки зрения операционной эксплуатации.

| Инструмент | Тип | Ключевая сила |
|-----------|-----|---------------|
| **OpenLLMetry** (Traceloop) | OSS-инструментации | Авто-инструментация OpenAI, Anthropic, Gemini, vector DB, фреймворков. Экспорт в Datadog, Honeycomb, Grafana, New Relic, Splunk |
| **Arize Phoenix** | OSS-платформа | Полная observability + evaluation + datasets. Нативный OpenTelemetry. MCP-сервер |
| **LangSmith** | Managed-платформа | Framework-agnostic tracing + evaluation + prompt management + deployment |
| **W&B Weave** (Weights & Biases) | Managed-платформа | Tracing + evaluation + dataset versioning. Интеграция с W&B ML-платформой |

### OpenLLMetry

OpenLLMetry — набор OpenTelemetry-инструментаций для LLM-провайдеров и фреймворков. Не платформа, а библиотека: она генерирует span'ы по GenAI-конвенциям и отправляет их в любой OTel-совместимый бэкенд. Это значит, что вы не привязаны к конкретной платформе — сегодня экспортируете в Phoenix, завтра в Datadog, без изменения кода приложения.

Интеграция занимает три строки: инициализация Traceloop SDK с именем сервиса — и каждый LLM-вызов автоматически превращается в OTel span с токенами, latency, моделью и (опционально) полным содержимым.

> **Промпт для генерации кода:** «Напиши инициализацию OpenLLMetry (traceloop SDK) для Python-сервиса. Имя приложения — параметр. Все вызовы OpenAI, Anthropic, LangChain должны автоматически трассироваться. Покажи также настройку OTel Collector endpoint.»

### Arize Phoenix

Phoenix — open-source платформа, которая объединяет tracing, evaluation и dataset management. Запускается локально (`python -m phoenix.server.main serve`), не требует отправки данных наружу. Ключевое отличие от чистого трейсинга: Phoenix позволяет запускать LLM-as-Judge оценки прямо на трейсах, строить datasets из production-трафика и сравнивать prompt-варианты через Experiments. MCP-сервер Phoenix позволяет LLM-агенту запрашивать трейсы и метрики напрямую.

### LangSmith

LangSmith — managed-платформа от LangChain, но framework-agnostic: работает с любым LLM-приложением через SDK. Сильная сторона — полный цикл: tracing → evaluation → prompt management → annotation queues → deployment. Prompt Hub хранит версии промптов с привязкой к трейсам и eval-результатам. Annotation queues (см. §17.7) позволяют маршрутизировать трейсы на human review.

### W&B Weave

W&B Weave — платформа от Weights & Biases для трейсинга, оценки и версионирования данных LLM-приложений. Сильная сторона — глубокая интеграция с ML-платформой W&B: если команда уже использует W&B для обучения моделей, Weave добавляет observability для inference в ту же экосистему. Weave поддерживает автоматический трейсинг через декоратор `@weave.op()`, встроенные scorers для evaluation (hallucination, summarization, context relevance), versioned datasets и интеграцию с OpenAI, Anthropic, LangChain, LlamaIndex.

### Когда что выбирать

| Сценарий | Рекомендация |
|----------|-------------|
| «Хочу OTel-трейсы в существующий стек (Datadog/Grafana)» | OpenLLMetry |
| «Нужна полная платформа on-premise» | Phoenix |
| «Нужен managed-сервис с prompt management и annotation» | LangSmith |
| «Команда уже использует W&B для ML-экспериментов» | W&B Weave |
| «Максимальная гибкость, multi-vendor» | OpenLLMetry + Phoenix |

---

## 17.3. Prompt versioning и lineage

### Проблема

Пользователь сообщает: «вчера система отвечала правильно, сегодня — галлюцинация». Вопрос: что изменилось? Модель? Контекст RAG? Промпт? Без lineage — связи между конкретной версией промпта и конкретным трейсом — ответить невозможно. Это как расследовать ДТП без видеозаписи: есть результат, но нет причины.

### Паттерн: prompt-as-code

Промпты живут в Git, как код. Каждое изменение — коммит с описанием. Версия — SHA коммита или семантический тег.

```
prompts/
  rag_answer/
    system.md          # системный промпт
    user_template.md   # шаблон пользовательского сообщения
    config.yaml        # model, temperature, max_tokens
```

При каждом LLM-вызове версия промпта (SHA или тег) записывается в span как custom-атрибуты: `prompt.version`, `prompt.sha`, `prompt.template`. Теперь, видя проблемный трейс, вы точно знаете: какой промпт, какой версии, с какими параметрами его породил.

> **Промпт для генерации кода:** «Напиши Python-функцию, которая при каждом LLM-вызове записывает в текущий OTel span версию промпта (semver-тег, SHA коммита, имя шаблона) через OpenTelemetry API. Используй `opentelemetry.trace`.»

### Платформенная поддержка

**Phoenix** имеет встроенный prompt management с версионированием и тегированием. Промпты хранятся в платформе, версии привязываются к трейсам автоматически.

**LangSmith Hub** хранит историю версий промптов. Каждая версия связана с трейсами и eval-результатами — можно увидеть, как изменение промпта повлияло на метрики.

### Анти-паттерн

Промпты как строки в коде без версионирования — когда system prompt захардкожен прямо в вызове `client.chat.completions.create()` без связи с Git-коммитом. Промпт изменили, задеплоили — и через неделю невозможно понять, какая версия промпта обслуживала конкретного пользователя. Регрессии становятся неотлаживаемыми.

---


### Belief-state logging: `reason / explore / reflect` вместо сырого CoT

В production нельзя рассчитывать на то, что hidden chain-of-thought модели будет доступен, стабилен или вообще пригоден для аудита. Поэтому полезнее логировать не «весь поток мыслей», а **типизированные переходы состояния**: почему мы считаем, что guard пройден (`reason`), где мы пошли в ветвление или fallback (`explore`), и чем мы сверили результат перед выходом (`reflect`).

Такой дизайн хорошо сочетается с двумя линиями исследований. Работы о belief states показывают, что модель действительно поддерживает внутреннее состояние убеждений в residual stream; это делает язык «belief state» осмысленным как инженерную абстракцию. Но эта литература не даёт production API к скрытым состояниям. Отдельно работа ByteDance **The Molecular Structure of Thought** описывает long-CoT через три типа взаимодействий — deep reasoning, self-reflection и self-exploration. Именно из неё естественно возникает схема логов `logger.reason() / logger.reflect() / logger.explore()`.

Отсюда практический паттерн `belief_scope(id)`: ограничить рискованный фрагмент кода именованным scope и писать в trace не приватное CoT, а события вида:

- `belief.scope = TransferFunds`
- `decision.id = AuthPattern.v3`
- `event = reason | explore | reflect`
- `fallback.retained = true/false`
- `rejected.path = "recompute-price-in-handler"`

OpenTelemetry GenAI уже задаёт каркас inference/tool spans; decision-layer поля можно добавлять как custom attributes и events. Это важное различие. `belief_scope` — не «рентген головы модели», а **observability contract** между агентом, кодом и человеком-оператором. Он полезен ровно потому, что превращает неявный reasoning в минимальный аудируемый след.

Связка с decision memory здесь критична: если `explore` привёл к retained workaround, это знание должно пережить и трейс, и перезапуск процесса. На уровне кода его лучше поднимать в локальный contract header (`@RATIONALE`, `@REJECTED`), а на уровне telemetry — прикреплять к span и checkpoint.

#### Практический пример: из неявного reasoning в аудируемый след

```python
# [DEF:TransferFunds:Function]
# @PURPOSE: Перевести деньги между счетами без потери аудируемости.
# @RATIONALE: Денежный путь должен быть replayable и идемпотентным.
# @REJECTED: Не пересчитывать баланс повторно в HTTP-handler.
# [/DEF:TransferFunds:Function]

def transfer_funds(from_id: str, to_id: str, amount: Decimal) -> str:
    with belief_scope("TransferFunds"):
        logger.reason("Validated transfer request", extra={"amount": str(amount)})

        if not balance_service.has_enough(from_id, amount):
            logger.explore("Insufficient funds", extra={"account_id": from_id})
            raise InsufficientFunds()

        tx_id = ledger.transfer(from_id, to_id, amount)
        logger.reflect("Transfer committed", extra={"tx_id": tx_id})
        return tx_id
```

А в trace это выглядит не как скрытый CoT, а как компактная последовательность проверяемых событий:

```text
INFO  reason  belief.scope=TransferFunds amount=125.00
WARN  explore belief.scope=TransferFunds account_id=acc_17 fallback.retained=false
DEBUG reflect belief.scope=TransferFunds tx_id=tx_9912
```

Если ветка `explore` привела к retained workaround и этот workaround остался в коде, правило простое: обновите локальный header и добавьте `@RATIONALE`/`@REJECTED` до закрытия задачи. Тогда инцидент перестаёт быть только логом — он становится decision memory.

### Verification plan как observability-артефакт

Хорошая эксплуатация LLM-системы требует не только трейсинга факта вызова модели, но и отдельного ответа на вопрос: **как другой агент докажет, что модуль или поток всё ещё корректен?** В `grace-marketplace` это вынесено в самостоятельный артефакт `verification-plan.xml`.

Это полезная инженерная мысль и вне GRACE-экосистемы. В production почти всегда нужны три слоя доказательства:

1. **deterministic assertions** — точный результат, формат, инварианты;
2. **trace/log assertions** — прошли ли мы нужную ветку, сработал ли guard, не задели ли forbidden block;
3. **wave/phase checks** — не сломался ли более широкий интеграционный surface после мержа нескольких изменений.

На практике это означает: verification нужно проектировать как часть архитектуры, а не дописывать после багов. Если critical branch невозможно увидеть в trace, значит проблема не только в логировании, но и в design of evidence.

Отсюда полезный operational rule: у каждого важного пути должен быть хотя бы один **stable marker**, который связывает runtime-событие с исходным semantic block. Формат вроде `[Module][function][BLOCK_NAME]` хорош именно потому, что он годится и для поиска по логам, и для тестового assert, и для быстрого перехода назад в код.

### FailurePacket: минимальная форма передачи инцидента

Когда verification не прошла, агенту или инженеру нужен не «ещё один длинный лог», а компактный handoff-объект. В GRACE это оформлено как `FailurePacket`: короткая структура, где зафиксированы:

- сценарий, который сломался;
- expected evidence;
- observed evidence;
- first divergent module / function / block;
- suggested next action.

Это кажется мелочью, но на практике именно такой объект резко сокращает debugging-loop. Вместо пересказа всей истории вы передаёте следующему агенту уже отфильтрованную точку расхождения. Для durable agent systems это почти обязательная практика: инцидент должен быть переносим между людьми, моделями и сессиями без потери контекста.

```yaml
failure_packet:
  scenario: refund_above_limit
  expected_evidence:
    - "[Billing][Refund][LIMIT_GUARD]"
  observed_evidence:
    - "guard_missing"
  first_divergent_block: RefundHandler.LIMIT_GUARD
  next_action: reopen execution packet and rerun targeted verification
```

Хороший FailurePacket должен отвечать на один вопрос быстрее любого длинного трейса: **где именно система перестала соответствовать ожиданию?**

## 17.4. Классификация ошибок

### Пять категорий

OTel `error.type` покрывает инфраструктурные ошибки: rate limit, timeout, 500. Но content-level ошибки — галлюцинации, off-topic, отказы — HTTP-статус не отражает. Нужна явная классификация.

| Категория | Примеры | Детекция |
|-----------|---------|----------|
| **Provider errors** | Rate limit (429), timeout, 500 | HTTP-статус, OTel `error.type` |
| **Format errors** | Невалидный JSON, нарушение schema | Schema validation (Pydantic, JSON Schema) |
| **Content errors** | Галлюцинация, off-topic, неуместный отказ | LLM-as-Judge, evals ([глава 14](14_llm_system_quality_evaluation.md)) |
| **Safety errors** | Нарушение content policy, обнаружен injection | Guardrails ([глава 15](15_llm_system_security.md)) |
| **Cost errors** | Переполнение контекста, превышение token-бюджета | Счётчик токенов, бюджетный лимит |

### Реализация

Каждый трейс классифицируется при завершении. Алгоритм простой: проверить `error.type` (provider error) → проверить `schema_valid` (format error) → проверить превышение token-бюджета (cost error) → проверить `guardrail_triggered` (safety error) → проверить `eval_score < 0.5` (content error). Первое срабатывание выигрывает; если ни одно условие не сработало — ошибки нет.

> **Промпт для генерации кода:** «Напиши Python-функцию `classify_error(span_data: dict) -> str | None`, которая классифицирует ошибку OTel GenAI span'а по 5 категориям: provider_error (есть error.type), format_error (невалидная schema), cost_error (превышение token budget), safety_error (сработал guardrail), content_error (eval_score < 0.5). Приоритет — первое совпадение. Возвращает None, если ошибки нет.»

Классифицированные ошибки агрегируются в дашборд: распределение по категориям, тренды за неделю, корреляция с версиями промптов. Всплеск content errors после деплоя — сигнал к rollback. Рост provider errors — сигнал проверить статус провайдера.

---

## 17.5. Replay и debugging

### Цикл отладки

Классический debugging-loop для LLM-систем:

```
Обнаружена ошибка → Найти трейс с полным I/O →
  → Воспроизвести в playground → Модифицировать промпт →
  → Проверить фикс на golden dataset → Задеплоить новую версию
```

Без полного ввода-вывода в трейсе первый шаг невозможен. Без версионирования промптов (§17.3) последний шаг непредсказуем.

### Content capture и replay

OTel opt-in content capture (§17.1) записывает полный контекст вызова: system prompt, user messages, assistant history, tool results. Это позволяет **точный replay**: извлечь из трейса `gen_ai.input.messages` и `gen_ai.request.model`, отправить те же сообщения в ту же модель с `temperature=0` — детерминистичный replay.

> **Промпт для генерации кода:** «Напиши Python-функцию `replay_from_trace(trace_data: dict, client) -> str`, которая извлекает из OTel-трейса поля `gen_ai.input.messages` и `gen_ai.request.model`, и воспроизводит LLM-вызов через OpenAI-compatible API с temperature=0 для детерминистичности.»

### Phoenix Playground

Phoenix предлагает визуальный replay: выбирается трейс, его inputs загружаются в Playground, где можно изменить промпт, модель, параметры и re-run прямо в UI. Это резко сокращает цикл отладки: от «нашёл проблему» до «протестировал фикс» — минуты, не часы.

### PII и content capture в production

Content capture записывает всё, что видит модель — включая персональные данные пользователей. Два подхода:

1. **Маскирование на уровне коллектора**: OTel Collector Processor, который заменяет PII-паттерны (email, телефоны, имена) на маски до отправки в бэкенд.
2. **Раздельное хранение**: метаданные трейсов — в общий бэкенд, content — в защищённое хранилище с ограниченным доступом и TTL.

Без решения PII-вопроса content capture в production — compliance-нарушение. С решением — самый мощный debugging-инструмент для LLM.

---

## 17.6. A/B-тестирование промптов

### Offline-first

В отличие от A/B-тестирования фич продукта, A/B промптов чаще всего офлайновое: несколько вариантов промпта запускаются на одном и том же golden dataset (построенном по методике из [главы 14](14_llm_system_quality_evaluation.md)), метрики сравниваются.

Почему offline-first? Потому что online A/B для LLM требует:
- достаточного трафика для статистической значимости,
- инфраструктуры traffic splitting,
- надёжного real-time eval (а не post-hoc).

Для большинства команд offline eval на golden dataset — быстрее, дешевле и надёжнее.

### Phoenix Experiments

Phoenix Experiments позволяют сравнивать изменения промптов, моделей и retrieval-стратегий на одних и тех же входных данных. Создаёте dataset, запускаете несколько вариантов (каждый со своим набором evaluators — faithfulness, relevance, answer_correctness), получаете метрики по каждому варианту.

> **Промпт для генерации кода:** «Напиши скрипт для A/B-тестирования промптов через Arize Phoenix Experiments. Используй `px.Client().get_dataset()` для загрузки golden dataset, `px.run_experiment()` для запуска двух вариантов RAG-пайплайна с evaluators (faithfulness, relevance, answer_correctness). Каждый эксперимент — именованный вариант промпта (например, prompt-v2.3 и prompt-v2.4).»

### LangSmith

LangSmith предлагает dataset-driven evaluation с variant comparison: загружаете dataset, запускаете несколько конфигураций, сравниваете eval-результаты в UI. Привязка к prompt versions из Hub позволяет отслеживать прогресс между итерациями.

### Online A/B

Для online A/B нужна инфраструктура: traffic splitter (процент трафика на каждый вариант), real-time eval (автоматическая оценка каждого ответа), мониторинг метрик по вариантам. Рекомендуемый подход — gradual rollout:

1. Новый промпт проходит offline eval на golden dataset.
2. Деплой на 5% трафика с мониторингом eval-метрик.
3. При отсутствии деградации — расширение до 50%, затем 100%.
4. При деградации — автоматический rollback.

Ключевой вопрос — не «какой промпт набрал больше баллов», а «статистически значима ли разница?» (подробнее — [глава 14, §14.9](14_llm_system_quality_evaluation.md)).

---

## 17.7. Human review queues

### Когда эскалировать

Автоматические evals не идеальны. Есть ответы, где automated eval неуверен или ошибается. Для таких случаев нужен human review — но ревьюировать всё невозможно и дорого. Эскалация по порогам:

| Триггер | Источник | Порог |
|---------|----------|-------|
| Низкий confidence score | LLM-as-Judge eval | Score < 0.6 |
| Обнаружена галлюцинация | Faithfulness eval | Faithfulness < 0.5 |
| Safety-флаг | Guardrails | Любое срабатывание |
| User thumbs-down | UI feedback | Любой негативный |
| Edge-case тема | Topic classifier | Тема не из обучающих данных |

### Архитектура

```
Auto-eval ──→ Threshold check ──→ Human review queue ──→ 
  Reviewer UI ──→ Correction ──→ Golden dataset update ──→  
  Re-eval / Re-prompt cycle
```

Очередь реализуется через SQS, Redis Streams или встроенный механизм платформы. Reviewer видит: оригинальный вопрос, ответ модели, eval-оценки, полный трейс. Reviewer может: подтвердить ответ, исправить, отклонить, добавить в golden dataset.

### LangSmith Annotation Queues

LangSmith предоставляет встроенные annotation queues: трейсы маршрутизируются на human review по правилам (eval-порог, topic, error type). Reviewer в UI видит полный контекст, проставляет оценки и комментарии. Результаты ревью отправляются обратно в datasets — замыкая цикл feedback → golden dataset → eval improvement.

### Анти-паттерн: ревьюировать всё

Если 100% трейсов идут на human review — это не observability, а ручная работа. Стоимость растёт линейно с трафиком, а reviewers через неделю устанут и начнут пропускать ошибки. Auto-eval с порогами отсеивает 90–95% трейсов; human review обрабатывает оставшиеся 5–10%. Именно этот фильтр делает систему масштабируемой.

---

## 17.8. Incident response для LLM-систем

В классическом SRE есть runbooks — пошаговые инструкции на каждый тип инцидента. LLM-системы добавляют новые типы инцидентов, не существовавшие в традиционном бэкенде. Пять основных playbooks:

### Playbook 1: Всплеск галлюцинаций

**Триггер:** faithfulness-метрика упала ниже SLO (см. §17.9) за скользящее окно.

**Расследование:**
1. Проверить: был ли деплой промпта? → Сравнить версии промптов по трейсам.
2. Проверить: обновилась ли модель? → Сравнить `gen_ai.response.model` в span'ах до и после.
3. Проверить: изменился ли RAG-индекс? → Сравнить retrieved chunks в трейсах.

**Митигация:**
- Rollback промпта к предыдущей стабильной версии.
- Rebuild RAG-индекса, если обнаружена коррупция данных.
- Добавить golden examples для проблемных кейсов.

### Playbook 2: Взрыв стоимости

**Триггер:** `gen_ai.client.token.usage` вырос в 2× и более за 30 минут.

**Расследование:**
1. Input inflation? → Проверить длину входных сообщений — возможно, RAG возвращает слишком много chunks.
2. Infinite loop? → Проверить количество шагов agent loop в трейсах.
3. Новый паттерн использования? → Проверить распределение `gen_ai.operation.name`.

**Митигация:**
- Активировать rate limit на уровне пользователя/сессии.
- Circuit breaker: остановить agent loop после N шагов (anti-loop protocol из [главы 13](13_anti_hallucination_loop.md)).
- При подтверждении loop — фикс в логике агента.

### Playbook 3: Деградация latency

**Триггер:** p95 `gen_ai.client.operation.duration` или TTFT превышает SLO.

**Расследование:**
1. Проблема у провайдера? → Проверить status page провайдера, сравнить latency по провайдерам.
2. Рост длины входа? → Проверить `gen_ai.usage.input_tokens` — возможно, контекст раздулся.
3. KV-кэш заполнен? → Для self-hosted моделей проверить cache hit rate.

**Митигация:**
- Fallback на резервную модель (паттерн из [главы 10](10_agent_not_chat.md)).
- Cache warming: предзаполнение KV-кэша для частых системных промптов.
- Уменьшение контекста: сократить число RAG-chunks, обрезать history.

### Playbook 4: Обнаружен prompt injection

**Триггер:** алерт от guardrail-системы ([глава 15](15_llm_system_security.md)).

**Расследование:**
1. Direct или indirect injection? Direct — от пользователя, indirect — из tool output или RAG-документа.
2. Какой источник? → Проверить трейс: откуда пришёл injected content.

**Митигация:**
- Заблокировать источник (пользователь / документ / tool).
- Обновить фильтры guardrails.
- Для indirect: аудит всех tool outputs и RAG-источников.

### Playbook 5: Outage провайдера

**Триггер:** error rate > 50% за 5 минут, `error.type` = provider error.

**Митигация:**
- Автоматический failover на вторичного провайдера (паттерн multi-provider из [главы 10](10_agent_not_chat.md)).
- Если единственный провайдер — graceful degradation: кэшированные ответы, fallback-сообщение пользователю.
- Post-mortem: добавить второго провайдера, если его нет.

### Общая схема реагирования

| Инцидент | Триггер-метрика | Расследование | Митигация |
|----------|----------------|---------------|-----------|
| Hallucination spike | Faithfulness < SLO | Промпт? Модель? RAG? | Rollback, rebuild index |
| Cost explosion | Token usage > 2× | Input? Loop? Pattern? | Rate limit, circuit breaker |
| Latency degradation | TTFT/duration p95 > SLO | Провайдер? Input length? | Fallback model, cache |
| Prompt injection | Guardrail alert | Direct? Indirect? | Block source, update filters |
| Provider outage | Error rate > 50% | Status page | Failover, degradation |

---

## 17.9. SLO для LLM-систем

### SLI: что измерять

Service Level Indicators для LLM строятся поверх OTel GenAI-метрик:

**Latency:** p95 по `gen_ai.client.operation.duration`.

**TTFT:** p95 по `gen_ai.server.time_to_first_token`.

**Error rate:** доля span'ов, у которых заполнен `error.type`.

**Correctness:** доля ответов, у которых `eval_score` выше выбранного порога.

**Cost:** средняя стоимость запроса, то есть входные токены по входному тарифу плюс выходные токены по выходному тарифу.

Здесь `p_in` и `p_out` — цена входного и выходного токена для конкретной модели.

### Пример SLO

| SLI | SLO | Error budget | Алерт |
|-----|-----|-------------|-------|
| Latency p95 | < 3 с (chat), < 10 с (agent) | 5% нарушений/месяц | > 5% за скользящий 1 ч |
| Error rate | < 1% | 1% ошибок/месяц | > 2% за 15 мин |
| Correctness | > 95% на hourly eval sample | 5% failures/месяц | > 10% failures за 1 ч |
| Cost per request | < \$0.05 mean | — | > 2× mean за 30 мин |

### Correctness SLO — определяющее отличие

В традиционных веб-сервисах SLI покрывают latency, error rate, throughput. SLO на correctness — «был ли ответ правильным?» — не существует: если API вернул данные из базы, они либо корректны (база не врёт), либо это баг в коде.

Для LLM-систем correctness — полноценный SLI, потому что модель может вернуть уверенный, грамматически безупречный, но фактически неверный ответ с HTTP 200. Это требует:
- автоматических eval'ов (выборка из production-трейсов → LLM-as-Judge → score),
- порогов на score (correctness SLO),
- алертов при деградации.

Именно correctness SLO отличает LLM observability от классического APM. Без неё вы мониторите инфраструктуру, но не продукт.

### Error budget и решения

Error budget работает так же, как в Google SRE: если бюджет исчерпан — freeze на новые фичи, фокус на reliability. Для LLM-систем это значит:

- **Latency budget исчерпан** → не деплоить более тяжёлые модели, оптимизировать контекст.
- **Correctness budget исчерпан** → не деплоить новые промпты, запустить ревью проблемных кейсов.
- **Cost budget исчерпан** → аудит token usage, переход на более дешёвую модель для low-risk запросов.

---

## 17.10. Resilience testing: устойчивость как дисциплина

Наблюдаемость показывает, что произошло. Incident response определяет, как реагировать. Но инженерная зрелость — это способность *заранее* проверить, как система ведёт себя при деградации. LLM-системы зависят от внешних провайдеров, сетевых вызовов и недетерминированных компонентов — resilience testing для них важнее, чем для обычных web-сервисов.

### Load testing LLM-систем

LLM-вызов — не типичный HTTP-запрос с латенси 50 мс. Один вызов может длиться 2–30 секунд, а стоимость пропорциональна длине контекста. Классические нагрузочные тесты «X rps за Y минут» нужно адаптировать.

Что тестировать:
- **Concurrent requests → TTFT degradation.** При каком уровне параллелизма TTFT пробивает SLO?
- **Throughput saturation.** При каком числе одновременных запросов p99 latency начинает расти нелинейно?
- **Token budget exhaustion.** Rate limits провайдера — при какой нагрузке вы их достигаете и как система реагирует на 429?

Важный нюанс: нагрузочный тест с промптом «hello» бесполезен. Длина промпта влияет на latency и cost — тестируйте с реалистичными промптами из production-трейсов.

### Provider outage drills

LLM-провайдер — single point of failure. Что произойдёт, если OpenAI API вернёт 503 в течение 30 минут? Большинство команд не знает ответа, пока это не случится в production.

Drill: в staging-среде подставьте mock, возвращающий 503 или таймаут. Проверьте четыре вещи:
1. Fallback на альтернативного провайдера срабатывает.
2. Circuit breaker открывается за ожидаемое время.
3. Пользователь получает degraded experience, а не HTTP 500.
4. Алерт приходит в ожидаемый канал за ожидаемое время.

Pre-requisite: архитектура с provider abstraction layer (см. [Главу 20, паттерн Router](20_llm_application_design_patterns.md)).

### Fault injection на tool layer

В агентных системах каждый tool — потенциальная точка отказа. Fault injection: tool возвращает ошибку, пустой результат, мусорные данные, таймаут.

Что проверяем:
- Агент корректно обрабатывает ошибку инструмента и сообщает пользователю, а не галлюцинирует ответ.
- Агент не зацикливается на retry'ях (anti-loop protocol из §13).
- MCP-серверы: disconnection и reconnection. Если MCP-сервер отвечает через 60 секунд, агент должен использовать timeout и fallback, а не висеть бесконечно.

### Replay testing с environment mocks

Production traces (из OpenTelemetry) можно переиграть в тестовом окружении с замоканными внешними системами. Сценарий: берём trace реального запроса, подставляем mock вместо LLM и tools, проверяем, что orchestrator корректно обрабатывает каждый шаг.

Применение:
- **Регрессионное тестирование** после изменения routing-логики, prompt version или набора инструментов.
- **Agent regression test:** replay + grader = автоматическая проверка, что агент по-прежнему решает задачу (связь с [Главой 14, секция 14.10](14_llm_system_quality_evaluation.md)).

### Деградация при изменении routing или caching

Изменение KV-кэша провайдера, prompt caching policy или routing между моделями может незаметно изменить поведение системы — без единого алерта по latency или error rate.

Тест: сравните качество ответов (через eval suite из [Главы 14](14_llm_system_quality_evaluation.md)) до и после изменения routing/caching configuration. Canary deployments для prompt changes: новая версия промпта → 5% трафика → eval → если качество не деградировало → полная раскатка.

Resilience testing — не разовое мероприятие. Включите provider outage drill в ежемесячный runbook, fault injection — в CI/CD для агентных pipeline'ов, load testing — в pre-launch checklist. Система, которую не тестировали на отказ, откажет непредсказуемо.

---

## Практический вывод

### Девять шагов операционной зрелости

| # | Действие | Что даёт |
|---|----------|----------|
| 1 | **Инструментируйте OTel GenAI**: OpenLLMetry или Phoenix SDK | Стандартные span'ы и метрики для каждого LLM-вызова |
| 2 | **Версионируйте промпты в Git**, привязывайте версию к span'ам | Lineage: трейс → промпт → коммит → автор |
| 3 | **Классифицируйте ошибки** по 5 категориям, стройте тренды | Понимание: инфраструктура или контент? |
| 4 | **Включите content capture** для debug replay, решите PII | Точное воспроизведение проблемных вызовов |
| 5 | **Задайте SLO**: latency, error rate, correctness, cost | Измеримые обязательства перед продуктом |
| 6 | **Постройте human review queue**: auto-eval → порог → человек → dataset | Масштабируемая обратная связь |
| 7 | **Подготовьте 5 incident playbooks** | От реакции к процедуре |
| 8 | **A/B-тестируйте промпты** на golden datasets перед деплоем | Данные вместо интуиции |
| 9 | **Resilience testing**: load tests, provider outage drills, fault injection на tool layer | Проверенная устойчивость, а не надежда на uptime |

### Минимальный стек

Для команды, начинающей с нуля:

```
Приложение → OpenLLMetry (авто-инструментация)
  → Arize Phoenix (трейсы + evals + datasets)
  → Grafana (метрики + алерты)
  → Git (промпты + версии)
```

Для команды с существующим APM:

```
Приложение → OpenLLMetry → OTel Collector
  → Datadog / New Relic / Splunk (метрики)
  → Phoenix или LangSmith (LLM-специфичные трейсы + evals)
```

### Задания

1. **Добавьте OTel GenAI-трейсинг к существующему LLM-сервису.** Интегрируйте OpenLLMetry в проект, отправьте span'ы в Phoenix или локальный OTel Collector. Проверьте, что токены, latency и модель записываются корректно. Ожидаемый результат: визуализированные трейсы с атрибутами `gen_ai.*` в дашборде.

2. **Настройте correctness SLO.** Определите порог correctness для вашего продукта, настройте hourly eval sample из production-трейсов с LLM-as-Judge оценкой, создайте алерт на деградацию (>10% failures за 1 час). Ожидаемый результат: работающий алерт в Grafana/PagerDuty, срабатывающий при падении качества.

3. **Проведите A/B-тест двух вариантов промпта.** Соберите golden dataset из 30+ примеров (методика — [глава 14](14_llm_system_quality_evaluation.md)), запустите два варианта промпта через Phoenix Experiments или LangSmith, сравните по метрикам faithfulness и relevance. Ожидаемый результат: таблица сравнения с числовыми метриками и обоснованным решением о выборе варианта.

4. **Подготовьте incident playbook.** Напишите runbook для сценария «всплеск галлюцинаций» по шаблону из §17.8 (триггер, расследование, митигация), адаптированный под ваш стек. Ожидаемый результат: документ в репозитории, который on-call инженер может использовать ночью без помощи коллег.

5. **Проведите provider outage drill.** В staging-среде подставьте mock вместо LLM API, возвращающий 503. Проверьте: (1) срабатывает ли fallback, (2) за какое время открывается circuit breaker, (3) получает ли пользователь graceful degradation. Запишите результаты и исправьте найденные проблемы. Ожидаемый результат: задокументированный drill report с выявленными и устранёнными уязвимостями.

---

## Источники

- OpenTelemetry. "Semantic Conventions for Generative AI." v1.40.0 (2026). https://opentelemetry.io/docs/specs/semconv/gen-ai/
- Traceloop. "OpenLLMetry: Open-source observability for your LLM application." https://github.com/traceloop/openllmetry
- Arize AI. "Phoenix: Open-source AI observability platform." https://github.com/Arize-ai/phoenix
- LangChain. "LangSmith Documentation." https://docs.smith.langchain.com/
- Weights & Biases. "W&B Weave: LLM Monitoring and Evaluation." https://docs.wandb.ai/guides/weave (2025–2026).
- Beyer, B., et al. (2016). "Site Reliability Engineering." O'Reilly — главы о SLI/SLO/Error Budget.
- Shai, A. S., et al. (2024). "Transformers represent belief state geometry in their residual stream." arXiv:2405.15943. Основание для инженерной метафоры belief state.
- Chen, Q., et al. (2026). "The Molecular Structure of Thought: Mapping the Topology of Long Chain-of-Thought Reasoning." arXiv:2601.06002. Deep reasoning, self-reflection, self-exploration как полезная схема typed traces.
- Laban, P., et al. (2025). "LLMs Get Lost In Multi-Turn Conversation." arXiv:2505.06120 / ICLR 2026. Multi-turn unreliability и необходимость checkpoint summaries.
- Ivanov, V. `osovv/grace-marketplace`: `verification-driven-dev.md`, `grace-verification`, `grace-fix` (2026). Verification как отдельный артефакт, stable log markers и FailurePacket для handoff.

---

**Навигация:**
- Назад: [Глава 16. Архитектура кода, дружественная ИИ](16_code_architecture.md)
- Далее: [Глава 18. Мультимодальные системы](18_multimodal_systems.md)
