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

55 KiB
Raw Blame History

ГЛАВА 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 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 мы описали платформы наблюдаемости обзорно. Здесь — сравнение четырёх ключевых инструментов с точки зрения операционной эксплуатации.

Инструмент Тип Ключевая сила
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 в аудируемый след

# [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 почти всегда нужны три слоя доказательства:

  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 это почти обязательная практика: инцидент должен быть переносим между людьми, моделями и сессиями без потери контекста.

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 записывает всё, что видит модель — включая персональные данные пользователей. Два подхода:

  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), метрики сравниваются.

Почему 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).


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).
  • При подтверждении 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).
  • Cache warming: предзаполнение KV-кэша для частых системных промптов.
  • Уменьшение контекста: сократить число RAG-chunks, обрезать history.

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

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

Расследование:

  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).
  • Если единственный провайдер — 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).

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)

Задания

  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), запустите два варианта промпта через 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.

Навигация: