55 KiB
ГЛАВА 17. НАБЛЮДАЕМОСТЬ И ЭКСПЛУАТАЦИЯ LLM-ПРОДУКТА
В традиционных веб-сервисах наблюдаемость стоит на трёх столпах: логи, метрики, трейсы. Вы знаете, что сервер ответил за 120 мс, что HTTP-статус — 200, что база данных отработала два запроса. Этого достаточно, чтобы понять: сервис работает. Для LLM-систем — нет.
Представьте мониторинг больницы. Uptime — это электричество и водоснабжение: необходимые, но явно недостаточные условия. Пациенты могут умирать при 100%-м аптайме. Вам нужны исходы лечения — смертность, осложнения, повторные госпитализации. В LLM-системах роль «исхода» играет качество ответа: был ли он корректен, релевантен, безопасен. Традиционный APM скажет «сервер вернул 200 OK»; LLM observability скажет «ответ — галлюцинация».
Это и есть четвёртый столп: content-level observability — наблюдаемость на уровне содержания, а не инфраструктуры.
В главе 13 мы заложили фундамент: 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 крупными вендорами инструментации. Чтобы включить их:
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 cachegen_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 мы описали платформы наблюдаемости обзорно. Здесь — сравнение четырёх ключевых инструментов с точки зрения операционной эксплуатации.
| Инструмент | Тип | Ключевая сила |
|---|---|---|
| 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 = TransferFundsdecision.id = AuthPattern.v3event = reason | explore | reflectfallback.retained = true/falserejected.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 в аудируемый след
# [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, а как компактная последовательность проверяемых событий:
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 почти всегда нужны три слоя доказательства:
- deterministic assertions — точный результат, формат, инварианты;
- trace/log assertions — прошли ли мы нужную ветку, сработал ли guard, не задели ли forbidden block;
- 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 это почти обязательная практика: инцидент должен быть переносим между людьми, моделями и сессиями без потери контекста.
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) |
| Safety errors | Нарушение content policy, обнаружен injection | Guardrails (глава 15) |
| 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 записывает всё, что видит модель — включая персональные данные пользователей. Два подхода:
- Маскирование на уровне коллектора: OTel Collector Processor, который заменяет PII-паттерны (email, телефоны, имена) на маски до отправки в бэкенд.
- Раздельное хранение: метаданные трейсов — в общий бэкенд, content — в защищённое хранилище с ограниченным доступом и TTL.
Без решения PII-вопроса content capture в production — compliance-нарушение. С решением — самый мощный debugging-инструмент для LLM.
17.6. A/B-тестирование промптов
Offline-first
В отличие от A/B-тестирования фич продукта, A/B промптов чаще всего офлайновое: несколько вариантов промпта запускаются на одном и том же golden dataset (построенном по методике из главы 14), метрики сравниваются.
Почему 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:
- Новый промпт проходит offline eval на golden dataset.
- Деплой на 5% трафика с мониторингом eval-метрик.
- При отсутствии деградации — расширение до 50%, затем 100%.
- При деградации — автоматический rollback.
Ключевой вопрос — не «какой промпт набрал больше баллов», а «статистически значима ли разница?» (подробнее — глава 14, §14.9).
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) за скользящее окно.
Расследование:
- Проверить: был ли деплой промпта? → Сравнить версии промптов по трейсам.
- Проверить: обновилась ли модель? → Сравнить
gen_ai.response.modelв span'ах до и после. - Проверить: изменился ли RAG-индекс? → Сравнить retrieved chunks в трейсах.
Митигация:
- Rollback промпта к предыдущей стабильной версии.
- Rebuild RAG-индекса, если обнаружена коррупция данных.
- Добавить golden examples для проблемных кейсов.
Playbook 2: Взрыв стоимости
Триггер: gen_ai.client.token.usage вырос в 2× и более за 30 минут.
Расследование:
- Input inflation? → Проверить длину входных сообщений — возможно, RAG возвращает слишком много chunks.
- Infinite loop? → Проверить количество шагов agent loop в трейсах.
- Новый паттерн использования? → Проверить распределение
gen_ai.operation.name.
Митигация:
- Активировать rate limit на уровне пользователя/сессии.
- Circuit breaker: остановить agent loop после N шагов (anti-loop protocol из главы 13).
- При подтверждении loop — фикс в логике агента.
Playbook 3: Деградация latency
Триггер: p95 gen_ai.client.operation.duration или TTFT превышает SLO.
Расследование:
- Проблема у провайдера? → Проверить status page провайдера, сравнить latency по провайдерам.
- Рост длины входа? → Проверить
gen_ai.usage.input_tokens— возможно, контекст раздулся. - KV-кэш заполнен? → Для self-hosted моделей проверить cache hit rate.
Митигация:
- Fallback на резервную модель (паттерн из главы 10).
- Cache warming: предзаполнение KV-кэша для частых системных промптов.
- Уменьшение контекста: сократить число RAG-chunks, обрезать history.
Playbook 4: Обнаружен prompt injection
Триггер: алерт от guardrail-системы (глава 15).
Расследование:
- Direct или indirect injection? Direct — от пользователя, indirect — из tool output или RAG-документа.
- Какой источник? → Проверить трейс: откуда пришёл injected content.
Митигация:
- Заблокировать источник (пользователь / документ / tool).
- Обновить фильтры guardrails.
- Для indirect: аудит всех tool outputs и RAG-источников.
Playbook 5: Outage провайдера
Триггер: error rate > 50% за 5 минут, error.type = provider error.
Митигация:
- Автоматический failover на вторичного провайдера (паттерн multi-provider из главы 10).
- Если единственный провайдер — 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 или таймаут. Проверьте четыре вещи:
- Fallback на альтернативного провайдера срабатывает.
- Circuit breaker открывается за ожидаемое время.
- Пользователь получает degraded experience, а не HTTP 500.
- Алерт приходит в ожидаемый канал за ожидаемое время.
Pre-requisite: архитектура с provider abstraction layer (см. Главу 20, паттерн Router).
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).
Деградация при изменении routing или caching
Изменение KV-кэша провайдера, prompt caching policy или routing между моделями может незаметно изменить поведение системы — без единого алерта по latency или error rate.
Тест: сравните качество ответов (через eval suite из Главы 14) до и после изменения 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)
Задания
-
Добавьте OTel GenAI-трейсинг к существующему LLM-сервису. Интегрируйте OpenLLMetry в проект, отправьте span'ы в Phoenix или локальный OTel Collector. Проверьте, что токены, latency и модель записываются корректно. Ожидаемый результат: визуализированные трейсы с атрибутами
gen_ai.*в дашборде. -
Настройте correctness SLO. Определите порог correctness для вашего продукта, настройте hourly eval sample из production-трейсов с LLM-as-Judge оценкой, создайте алерт на деградацию (>10% failures за 1 час). Ожидаемый результат: работающий алерт в Grafana/PagerDuty, срабатывающий при падении качества.
-
Проведите A/B-тест двух вариантов промпта. Соберите golden dataset из 30+ примеров (методика — глава 14), запустите два варианта промпта через Phoenix Experiments или LangSmith, сравните по метрикам faithfulness и relevance. Ожидаемый результат: таблица сравнения с числовыми метриками и обоснованным решением о выборе варианта.
-
Подготовьте incident playbook. Напишите runbook для сценария «всплеск галлюцинаций» по шаблону из §17.8 (триггер, расследование, митигация), адаптированный под ваш стек. Ожидаемый результат: документ в репозитории, который on-call инженер может использовать ночью без помощи коллег.
-
Проведите 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.
Навигация: