Files
BlackboxBook/book/17_observability_and_operations.md
2026-05-20 20:55:03 +03:00

640 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ГЛАВА 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 с порогами отсеивает 9095% трейсов; human review обрабатывает оставшиеся 510%. Именно этот фильтр делает систему масштабируемой.
---
## 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 мс. Один вызов может длиться 230 секунд, а стоимость пропорциональна длине контекста. Классические нагрузочные тесты «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 (20252026).
- 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)