640 lines
55 KiB
Markdown
640 lines
55 KiB
Markdown
# ГЛАВА 17. НАБЛЮДАЕМОСТЬ И ЭКСПЛУАТАЦИЯ LLM-ПРОДУКТА
|
||
|
||
---
|
||
|
||
В традиционных веб-сервисах наблюдаемость стоит на трёх столпах: логи, метрики, трейсы. Вы знаете, что сервер ответил за 120 мс, что HTTP-статус — 200, что база данных отработала два запроса. Этого достаточно, чтобы понять: сервис работает. Для LLM-систем — нет.
|
||
|
||
Представьте мониторинг больницы. Uptime — это электричество и водоснабжение: необходимые, но явно недостаточные условия. Пациенты могут умирать при 100%-м аптайме. Вам нужны исходы лечения — смертность, осложнения, повторные госпитализации. В LLM-системах роль «исхода» играет **качество ответа**: был ли он корректен, релевантен, безопасен. Традиционный APM скажет «сервер вернул 200 OK»; LLM observability скажет «ответ — галлюцинация».
|
||
|
||
Это и есть четвёртый столп: **content-level observability** — наблюдаемость на уровне содержания, а не инфраструктуры.
|
||
|
||
В [главе 13](13_anti_hallucination_loop.md) мы заложили фундамент: LDD (Log-Driven Development) с `LLMCallLog`, production-дашборд из семи панелей, anti-loop protocol и мониторинг-стек OpenTelemetry → Phoenix / Grafana / ClickHouse. Эта глава расширяет фундамент до полного операционного цикла: distributed tracing по стандарту GenAI, версионирование промптов, классификация ошибок, replay-дебаггинг, incident response и SLO для LLM.
|
||
|
||
---
|
||
|
||
## 17.1. OpenTelemetry GenAI: формирующийся стандарт
|
||
|
||
### Зачем нужен стандарт
|
||
|
||
Без стандарта каждый инструмент изобретает свою схему: LangSmith — свои трейсы, Phoenix — свои span-атрибуты, Datadog — свои метрики. Переключиться между ними — значит переписать инструментацию. OpenTelemetry GenAI semantic conventions решают эту проблему так же, как OTel решил её для HTTP и gRPC: единая схема span'ов и метрик, которую понимают все бэкенды.
|
||
|
||
### Статус: Development (v1.40.0, апрель 2026)
|
||
|
||
Конвенции ещё не stable — они находятся в фазе Development. Это значит: схема может меняться, но она уже используется в production крупными вендорами инструментации. Чтобы включить их:
|
||
|
||
```bash
|
||
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
|
||
```
|
||
|
||
### Ключевые типы span'ов
|
||
|
||
OpenTelemetry GenAI определяет операции через атрибут `gen_ai.operation.name`:
|
||
|
||
| Операция | Когда создаётся |
|
||
|----------|----------------|
|
||
| `chat` | Вызов chat completion API |
|
||
| `text_completion` | Вызов completion API |
|
||
| `embeddings` | Создание embedding'ов |
|
||
| `retrieval` | RAG-поиск по vector store |
|
||
| `execute_tool` | Выполнение tool call |
|
||
| `generate_content` | Мультимодальная генерация (Gemini и др.) |
|
||
| `create_agent` | Инициализация агента |
|
||
| `invoke_agent` | Вызов agent loop |
|
||
|
||
### Атрибуты span'а
|
||
|
||
Каждый span несёт стандартный набор атрибутов:
|
||
|
||
**Идентификация:**
|
||
- `gen_ai.operation.name` — тип операции
|
||
- `gen_ai.provider.name` — провайдер (`openai`, `anthropic`, `gcp.vertex_ai`)
|
||
- `gen_ai.request.model` — запрошенная модель (`claude-opus-4-6-20260401`)
|
||
- `gen_ai.response.model` — фактически использованная модель
|
||
|
||
**Потребление токенов:**
|
||
- `gen_ai.usage.input_tokens` — входные токены
|
||
- `gen_ai.usage.output_tokens` — выходные токены
|
||
- `gen_ai.usage.cache_creation.input_tokens` — токены, записанные в prompt cache
|
||
- `gen_ai.usage.cache_read.input_tokens` — токены, прочитанные из prompt cache
|
||
|
||
**Агентные span'ы:**
|
||
- `gen_ai.agent.id` — идентификатор агента
|
||
- `gen_ai.agent.name` — читаемое имя
|
||
- `gen_ai.conversation.id` — идентификатор сессии/диалога
|
||
|
||
### Пять метрических гистограмм
|
||
|
||
Конвенции определяют пять ключевых метрик:
|
||
|
||
| Метрика | Единица | Что измеряет |
|
||
|---------|---------|-------------|
|
||
| `gen_ai.client.token.usage` | tokens | Потребление токенов на вызов |
|
||
| `gen_ai.client.operation.duration` | seconds | Полная длительность операции |
|
||
| `gen_ai.server.time_to_first_token` | seconds | TTFT — время до первого токена |
|
||
| `gen_ai.server.time_per_output_token` | seconds | TPOT — время на каждый выходной токен |
|
||
| `gen_ai.server.request.duration` | seconds | Серверная длительность обработки запроса |
|
||
|
||
TTFT и TPOT — два числа, которые определяют UX streaming-интерфейсов. TTFT отвечает за ощущение «система думает» vs «система зависла». TPOT определяет скорость потока текста. Для chat-интерфейса TTFT < 1 с и TPOT < 50 мс обычно считаются комфортными; для agent loop, где пользователь не видит поток, важнее `operation.duration` целиком.
|
||
|
||
### Захват содержимого (opt-in)
|
||
|
||
По умолчанию OTel GenAI не записывает тексты промптов и ответов — только метаданные. Но для replay-дебаггинга нужен полный ввод-вывод. Три атрибута, включаемые явно:
|
||
|
||
- `gen_ai.input.messages` — входные сообщения (system, user, assistant history)
|
||
- `gen_ai.output.messages` — ответ модели
|
||
- `gen_ai.system_instructions` — системный промпт
|
||
|
||
Включение content capture — осознанное решение: это критически важно для отладки, но требует обработки PII (см. §17.5).
|
||
|
||
### MCP semantic conventions
|
||
|
||
Отдельный набор конвенций покрывает вызовы MCP-серверов (Model Context Protocol). Если ваш агент использует MCP-tools, span'ы вызовов тоже стандартизированы.
|
||
|
||
---
|
||
|
||
## 17.2. Инструменты: OpenLLMetry, Phoenix, LangSmith, W&B Weave
|
||
|
||
В [главе 13](13_anti_hallucination_loop.md) мы описали платформы наблюдаемости обзорно. Здесь — сравнение четырёх ключевых инструментов с точки зрения операционной эксплуатации.
|
||
|
||
| Инструмент | Тип | Ключевая сила |
|
||
|-----------|-----|---------------|
|
||
| **OpenLLMetry** (Traceloop) | OSS-инструментации | Авто-инструментация OpenAI, Anthropic, Gemini, vector DB, фреймворков. Экспорт в Datadog, Honeycomb, Grafana, New Relic, Splunk |
|
||
| **Arize Phoenix** | OSS-платформа | Полная observability + evaluation + datasets. Нативный OpenTelemetry. MCP-сервер |
|
||
| **LangSmith** | Managed-платформа | Framework-agnostic tracing + evaluation + prompt management + deployment |
|
||
| **W&B Weave** (Weights & Biases) | Managed-платформа | Tracing + evaluation + dataset versioning. Интеграция с W&B ML-платформой |
|
||
|
||
### OpenLLMetry
|
||
|
||
OpenLLMetry — набор OpenTelemetry-инструментаций для LLM-провайдеров и фреймворков. Не платформа, а библиотека: она генерирует span'ы по GenAI-конвенциям и отправляет их в любой OTel-совместимый бэкенд. Это значит, что вы не привязаны к конкретной платформе — сегодня экспортируете в Phoenix, завтра в Datadog, без изменения кода приложения.
|
||
|
||
Интеграция занимает три строки: инициализация Traceloop SDK с именем сервиса — и каждый LLM-вызов автоматически превращается в OTel span с токенами, latency, моделью и (опционально) полным содержимым.
|
||
|
||
> **Промпт для генерации кода:** «Напиши инициализацию OpenLLMetry (traceloop SDK) для Python-сервиса. Имя приложения — параметр. Все вызовы OpenAI, Anthropic, LangChain должны автоматически трассироваться. Покажи также настройку OTel Collector endpoint.»
|
||
|
||
### Arize Phoenix
|
||
|
||
Phoenix — open-source платформа, которая объединяет tracing, evaluation и dataset management. Запускается локально (`python -m phoenix.server.main serve`), не требует отправки данных наружу. Ключевое отличие от чистого трейсинга: Phoenix позволяет запускать LLM-as-Judge оценки прямо на трейсах, строить datasets из production-трафика и сравнивать prompt-варианты через Experiments. MCP-сервер Phoenix позволяет LLM-агенту запрашивать трейсы и метрики напрямую.
|
||
|
||
### LangSmith
|
||
|
||
LangSmith — managed-платформа от LangChain, но framework-agnostic: работает с любым LLM-приложением через SDK. Сильная сторона — полный цикл: tracing → evaluation → prompt management → annotation queues → deployment. Prompt Hub хранит версии промптов с привязкой к трейсам и eval-результатам. Annotation queues (см. §17.7) позволяют маршрутизировать трейсы на human review.
|
||
|
||
### W&B Weave
|
||
|
||
W&B Weave — платформа от Weights & Biases для трейсинга, оценки и версионирования данных LLM-приложений. Сильная сторона — глубокая интеграция с ML-платформой W&B: если команда уже использует W&B для обучения моделей, Weave добавляет observability для inference в ту же экосистему. Weave поддерживает автоматический трейсинг через декоратор `@weave.op()`, встроенные scorers для evaluation (hallucination, summarization, context relevance), versioned datasets и интеграцию с OpenAI, Anthropic, LangChain, LlamaIndex.
|
||
|
||
### Когда что выбирать
|
||
|
||
| Сценарий | Рекомендация |
|
||
|----------|-------------|
|
||
| «Хочу OTel-трейсы в существующий стек (Datadog/Grafana)» | OpenLLMetry |
|
||
| «Нужна полная платформа on-premise» | Phoenix |
|
||
| «Нужен managed-сервис с prompt management и annotation» | LangSmith |
|
||
| «Команда уже использует W&B для ML-экспериментов» | W&B Weave |
|
||
| «Максимальная гибкость, multi-vendor» | OpenLLMetry + Phoenix |
|
||
|
||
---
|
||
|
||
## 17.3. Prompt versioning и lineage
|
||
|
||
### Проблема
|
||
|
||
Пользователь сообщает: «вчера система отвечала правильно, сегодня — галлюцинация». Вопрос: что изменилось? Модель? Контекст RAG? Промпт? Без lineage — связи между конкретной версией промпта и конкретным трейсом — ответить невозможно. Это как расследовать ДТП без видеозаписи: есть результат, но нет причины.
|
||
|
||
### Паттерн: prompt-as-code
|
||
|
||
Промпты живут в Git, как код. Каждое изменение — коммит с описанием. Версия — SHA коммита или семантический тег.
|
||
|
||
```
|
||
prompts/
|
||
rag_answer/
|
||
system.md # системный промпт
|
||
user_template.md # шаблон пользовательского сообщения
|
||
config.yaml # model, temperature, max_tokens
|
||
```
|
||
|
||
При каждом LLM-вызове версия промпта (SHA или тег) записывается в span как custom-атрибуты: `prompt.version`, `prompt.sha`, `prompt.template`. Теперь, видя проблемный трейс, вы точно знаете: какой промпт, какой версии, с какими параметрами его породил.
|
||
|
||
> **Промпт для генерации кода:** «Напиши Python-функцию, которая при каждом LLM-вызове записывает в текущий OTel span версию промпта (semver-тег, SHA коммита, имя шаблона) через OpenTelemetry API. Используй `opentelemetry.trace`.»
|
||
|
||
### Платформенная поддержка
|
||
|
||
**Phoenix** имеет встроенный prompt management с версионированием и тегированием. Промпты хранятся в платформе, версии привязываются к трейсам автоматически.
|
||
|
||
**LangSmith Hub** хранит историю версий промптов. Каждая версия связана с трейсами и eval-результатами — можно увидеть, как изменение промпта повлияло на метрики.
|
||
|
||
### Анти-паттерн
|
||
|
||
Промпты как строки в коде без версионирования — когда system prompt захардкожен прямо в вызове `client.chat.completions.create()` без связи с Git-коммитом. Промпт изменили, задеплоили — и через неделю невозможно понять, какая версия промпта обслуживала конкретного пользователя. Регрессии становятся неотлаживаемыми.
|
||
|
||
---
|
||
|
||
|
||
### Belief-state logging: `reason / explore / reflect` вместо сырого CoT
|
||
|
||
В production нельзя рассчитывать на то, что hidden chain-of-thought модели будет доступен, стабилен или вообще пригоден для аудита. Поэтому полезнее логировать не «весь поток мыслей», а **типизированные переходы состояния**: почему мы считаем, что guard пройден (`reason`), где мы пошли в ветвление или fallback (`explore`), и чем мы сверили результат перед выходом (`reflect`).
|
||
|
||
Такой дизайн хорошо сочетается с двумя линиями исследований. Работы о belief states показывают, что модель действительно поддерживает внутреннее состояние убеждений в residual stream; это делает язык «belief state» осмысленным как инженерную абстракцию. Но эта литература не даёт production API к скрытым состояниям. Отдельно работа ByteDance **The Molecular Structure of Thought** описывает long-CoT через три типа взаимодействий — deep reasoning, self-reflection и self-exploration. Именно из неё естественно возникает схема логов `logger.reason() / logger.reflect() / logger.explore()`.
|
||
|
||
Отсюда практический паттерн `belief_scope(id)`: ограничить рискованный фрагмент кода именованным scope и писать в trace не приватное CoT, а события вида:
|
||
|
||
- `belief.scope = TransferFunds`
|
||
- `decision.id = AuthPattern.v3`
|
||
- `event = reason | explore | reflect`
|
||
- `fallback.retained = true/false`
|
||
- `rejected.path = "recompute-price-in-handler"`
|
||
|
||
OpenTelemetry GenAI уже задаёт каркас inference/tool spans; decision-layer поля можно добавлять как custom attributes и events. Это важное различие. `belief_scope` — не «рентген головы модели», а **observability contract** между агентом, кодом и человеком-оператором. Он полезен ровно потому, что превращает неявный reasoning в минимальный аудируемый след.
|
||
|
||
Связка с decision memory здесь критична: если `explore` привёл к retained workaround, это знание должно пережить и трейс, и перезапуск процесса. На уровне кода его лучше поднимать в локальный contract header (`@RATIONALE`, `@REJECTED`), а на уровне telemetry — прикреплять к span и checkpoint.
|
||
|
||
#### Практический пример: из неявного reasoning в аудируемый след
|
||
|
||
```python
|
||
# [DEF:TransferFunds:Function]
|
||
# @PURPOSE: Перевести деньги между счетами без потери аудируемости.
|
||
# @RATIONALE: Денежный путь должен быть replayable и идемпотентным.
|
||
# @REJECTED: Не пересчитывать баланс повторно в HTTP-handler.
|
||
# [/DEF:TransferFunds:Function]
|
||
|
||
def transfer_funds(from_id: str, to_id: str, amount: Decimal) -> str:
|
||
with belief_scope("TransferFunds"):
|
||
logger.reason("Validated transfer request", extra={"amount": str(amount)})
|
||
|
||
if not balance_service.has_enough(from_id, amount):
|
||
logger.explore("Insufficient funds", extra={"account_id": from_id})
|
||
raise InsufficientFunds()
|
||
|
||
tx_id = ledger.transfer(from_id, to_id, amount)
|
||
logger.reflect("Transfer committed", extra={"tx_id": tx_id})
|
||
return tx_id
|
||
```
|
||
|
||
А в trace это выглядит не как скрытый CoT, а как компактная последовательность проверяемых событий:
|
||
|
||
```text
|
||
INFO reason belief.scope=TransferFunds amount=125.00
|
||
WARN explore belief.scope=TransferFunds account_id=acc_17 fallback.retained=false
|
||
DEBUG reflect belief.scope=TransferFunds tx_id=tx_9912
|
||
```
|
||
|
||
Если ветка `explore` привела к retained workaround и этот workaround остался в коде, правило простое: обновите локальный header и добавьте `@RATIONALE`/`@REJECTED` до закрытия задачи. Тогда инцидент перестаёт быть только логом — он становится decision memory.
|
||
|
||
### Verification plan как observability-артефакт
|
||
|
||
Хорошая эксплуатация LLM-системы требует не только трейсинга факта вызова модели, но и отдельного ответа на вопрос: **как другой агент докажет, что модуль или поток всё ещё корректен?** В `grace-marketplace` это вынесено в самостоятельный артефакт `verification-plan.xml`.
|
||
|
||
Это полезная инженерная мысль и вне GRACE-экосистемы. В production почти всегда нужны три слоя доказательства:
|
||
|
||
1. **deterministic assertions** — точный результат, формат, инварианты;
|
||
2. **trace/log assertions** — прошли ли мы нужную ветку, сработал ли guard, не задели ли forbidden block;
|
||
3. **wave/phase checks** — не сломался ли более широкий интеграционный surface после мержа нескольких изменений.
|
||
|
||
На практике это означает: verification нужно проектировать как часть архитектуры, а не дописывать после багов. Если critical branch невозможно увидеть в trace, значит проблема не только в логировании, но и в design of evidence.
|
||
|
||
Отсюда полезный operational rule: у каждого важного пути должен быть хотя бы один **stable marker**, который связывает runtime-событие с исходным semantic block. Формат вроде `[Module][function][BLOCK_NAME]` хорош именно потому, что он годится и для поиска по логам, и для тестового assert, и для быстрого перехода назад в код.
|
||
|
||
### FailurePacket: минимальная форма передачи инцидента
|
||
|
||
Когда verification не прошла, агенту или инженеру нужен не «ещё один длинный лог», а компактный handoff-объект. В GRACE это оформлено как `FailurePacket`: короткая структура, где зафиксированы:
|
||
|
||
- сценарий, который сломался;
|
||
- expected evidence;
|
||
- observed evidence;
|
||
- first divergent module / function / block;
|
||
- suggested next action.
|
||
|
||
Это кажется мелочью, но на практике именно такой объект резко сокращает debugging-loop. Вместо пересказа всей истории вы передаёте следующему агенту уже отфильтрованную точку расхождения. Для durable agent systems это почти обязательная практика: инцидент должен быть переносим между людьми, моделями и сессиями без потери контекста.
|
||
|
||
```yaml
|
||
failure_packet:
|
||
scenario: refund_above_limit
|
||
expected_evidence:
|
||
- "[Billing][Refund][LIMIT_GUARD]"
|
||
observed_evidence:
|
||
- "guard_missing"
|
||
first_divergent_block: RefundHandler.LIMIT_GUARD
|
||
next_action: reopen execution packet and rerun targeted verification
|
||
```
|
||
|
||
Хороший FailurePacket должен отвечать на один вопрос быстрее любого длинного трейса: **где именно система перестала соответствовать ожиданию?**
|
||
|
||
## 17.4. Классификация ошибок
|
||
|
||
### Пять категорий
|
||
|
||
OTel `error.type` покрывает инфраструктурные ошибки: rate limit, timeout, 500. Но content-level ошибки — галлюцинации, off-topic, отказы — HTTP-статус не отражает. Нужна явная классификация.
|
||
|
||
| Категория | Примеры | Детекция |
|
||
|-----------|---------|----------|
|
||
| **Provider errors** | Rate limit (429), timeout, 500 | HTTP-статус, OTel `error.type` |
|
||
| **Format errors** | Невалидный JSON, нарушение schema | Schema validation (Pydantic, JSON Schema) |
|
||
| **Content errors** | Галлюцинация, off-topic, неуместный отказ | LLM-as-Judge, evals ([глава 14](14_llm_system_quality_evaluation.md)) |
|
||
| **Safety errors** | Нарушение content policy, обнаружен injection | Guardrails ([глава 15](15_llm_system_security.md)) |
|
||
| **Cost errors** | Переполнение контекста, превышение token-бюджета | Счётчик токенов, бюджетный лимит |
|
||
|
||
### Реализация
|
||
|
||
Каждый трейс классифицируется при завершении. Алгоритм простой: проверить `error.type` (provider error) → проверить `schema_valid` (format error) → проверить превышение token-бюджета (cost error) → проверить `guardrail_triggered` (safety error) → проверить `eval_score < 0.5` (content error). Первое срабатывание выигрывает; если ни одно условие не сработало — ошибки нет.
|
||
|
||
> **Промпт для генерации кода:** «Напиши Python-функцию `classify_error(span_data: dict) -> str | None`, которая классифицирует ошибку OTel GenAI span'а по 5 категориям: provider_error (есть error.type), format_error (невалидная schema), cost_error (превышение token budget), safety_error (сработал guardrail), content_error (eval_score < 0.5). Приоритет — первое совпадение. Возвращает None, если ошибки нет.»
|
||
|
||
Классифицированные ошибки агрегируются в дашборд: распределение по категориям, тренды за неделю, корреляция с версиями промптов. Всплеск content errors после деплоя — сигнал к rollback. Рост provider errors — сигнал проверить статус провайдера.
|
||
|
||
---
|
||
|
||
## 17.5. Replay и debugging
|
||
|
||
### Цикл отладки
|
||
|
||
Классический debugging-loop для LLM-систем:
|
||
|
||
```
|
||
Обнаружена ошибка → Найти трейс с полным I/O →
|
||
→ Воспроизвести в playground → Модифицировать промпт →
|
||
→ Проверить фикс на golden dataset → Задеплоить новую версию
|
||
```
|
||
|
||
Без полного ввода-вывода в трейсе первый шаг невозможен. Без версионирования промптов (§17.3) последний шаг непредсказуем.
|
||
|
||
### Content capture и replay
|
||
|
||
OTel opt-in content capture (§17.1) записывает полный контекст вызова: system prompt, user messages, assistant history, tool results. Это позволяет **точный replay**: извлечь из трейса `gen_ai.input.messages` и `gen_ai.request.model`, отправить те же сообщения в ту же модель с `temperature=0` — детерминистичный replay.
|
||
|
||
> **Промпт для генерации кода:** «Напиши Python-функцию `replay_from_trace(trace_data: dict, client) -> str`, которая извлекает из OTel-трейса поля `gen_ai.input.messages` и `gen_ai.request.model`, и воспроизводит LLM-вызов через OpenAI-compatible API с temperature=0 для детерминистичности.»
|
||
|
||
### Phoenix Playground
|
||
|
||
Phoenix предлагает визуальный replay: выбирается трейс, его inputs загружаются в Playground, где можно изменить промпт, модель, параметры и re-run прямо в UI. Это резко сокращает цикл отладки: от «нашёл проблему» до «протестировал фикс» — минуты, не часы.
|
||
|
||
### PII и content capture в production
|
||
|
||
Content capture записывает всё, что видит модель — включая персональные данные пользователей. Два подхода:
|
||
|
||
1. **Маскирование на уровне коллектора**: OTel Collector Processor, который заменяет PII-паттерны (email, телефоны, имена) на маски до отправки в бэкенд.
|
||
2. **Раздельное хранение**: метаданные трейсов — в общий бэкенд, content — в защищённое хранилище с ограниченным доступом и TTL.
|
||
|
||
Без решения PII-вопроса content capture в production — compliance-нарушение. С решением — самый мощный debugging-инструмент для LLM.
|
||
|
||
---
|
||
|
||
## 17.6. A/B-тестирование промптов
|
||
|
||
### Offline-first
|
||
|
||
В отличие от A/B-тестирования фич продукта, A/B промптов чаще всего офлайновое: несколько вариантов промпта запускаются на одном и том же golden dataset (построенном по методике из [главы 14](14_llm_system_quality_evaluation.md)), метрики сравниваются.
|
||
|
||
Почему offline-first? Потому что online A/B для LLM требует:
|
||
- достаточного трафика для статистической значимости,
|
||
- инфраструктуры traffic splitting,
|
||
- надёжного real-time eval (а не post-hoc).
|
||
|
||
Для большинства команд offline eval на golden dataset — быстрее, дешевле и надёжнее.
|
||
|
||
### Phoenix Experiments
|
||
|
||
Phoenix Experiments позволяют сравнивать изменения промптов, моделей и retrieval-стратегий на одних и тех же входных данных. Создаёте dataset, запускаете несколько вариантов (каждый со своим набором evaluators — faithfulness, relevance, answer_correctness), получаете метрики по каждому варианту.
|
||
|
||
> **Промпт для генерации кода:** «Напиши скрипт для A/B-тестирования промптов через Arize Phoenix Experiments. Используй `px.Client().get_dataset()` для загрузки golden dataset, `px.run_experiment()` для запуска двух вариантов RAG-пайплайна с evaluators (faithfulness, relevance, answer_correctness). Каждый эксперимент — именованный вариант промпта (например, prompt-v2.3 и prompt-v2.4).»
|
||
|
||
### LangSmith
|
||
|
||
LangSmith предлагает dataset-driven evaluation с variant comparison: загружаете dataset, запускаете несколько конфигураций, сравниваете eval-результаты в UI. Привязка к prompt versions из Hub позволяет отслеживать прогресс между итерациями.
|
||
|
||
### Online A/B
|
||
|
||
Для online A/B нужна инфраструктура: traffic splitter (процент трафика на каждый вариант), real-time eval (автоматическая оценка каждого ответа), мониторинг метрик по вариантам. Рекомендуемый подход — gradual rollout:
|
||
|
||
1. Новый промпт проходит offline eval на golden dataset.
|
||
2. Деплой на 5% трафика с мониторингом eval-метрик.
|
||
3. При отсутствии деградации — расширение до 50%, затем 100%.
|
||
4. При деградации — автоматический rollback.
|
||
|
||
Ключевой вопрос — не «какой промпт набрал больше баллов», а «статистически значима ли разница?» (подробнее — [глава 14, §14.9](14_llm_system_quality_evaluation.md)).
|
||
|
||
---
|
||
|
||
## 17.7. Human review queues
|
||
|
||
### Когда эскалировать
|
||
|
||
Автоматические evals не идеальны. Есть ответы, где automated eval неуверен или ошибается. Для таких случаев нужен human review — но ревьюировать всё невозможно и дорого. Эскалация по порогам:
|
||
|
||
| Триггер | Источник | Порог |
|
||
|---------|----------|-------|
|
||
| Низкий confidence score | LLM-as-Judge eval | Score < 0.6 |
|
||
| Обнаружена галлюцинация | Faithfulness eval | Faithfulness < 0.5 |
|
||
| Safety-флаг | Guardrails | Любое срабатывание |
|
||
| User thumbs-down | UI feedback | Любой негативный |
|
||
| Edge-case тема | Topic classifier | Тема не из обучающих данных |
|
||
|
||
### Архитектура
|
||
|
||
```
|
||
Auto-eval ──→ Threshold check ──→ Human review queue ──→
|
||
Reviewer UI ──→ Correction ──→ Golden dataset update ──→
|
||
Re-eval / Re-prompt cycle
|
||
```
|
||
|
||
Очередь реализуется через SQS, Redis Streams или встроенный механизм платформы. Reviewer видит: оригинальный вопрос, ответ модели, eval-оценки, полный трейс. Reviewer может: подтвердить ответ, исправить, отклонить, добавить в golden dataset.
|
||
|
||
### LangSmith Annotation Queues
|
||
|
||
LangSmith предоставляет встроенные annotation queues: трейсы маршрутизируются на human review по правилам (eval-порог, topic, error type). Reviewer в UI видит полный контекст, проставляет оценки и комментарии. Результаты ревью отправляются обратно в datasets — замыкая цикл feedback → golden dataset → eval improvement.
|
||
|
||
### Анти-паттерн: ревьюировать всё
|
||
|
||
Если 100% трейсов идут на human review — это не observability, а ручная работа. Стоимость растёт линейно с трафиком, а reviewers через неделю устанут и начнут пропускать ошибки. Auto-eval с порогами отсеивает 90–95% трейсов; human review обрабатывает оставшиеся 5–10%. Именно этот фильтр делает систему масштабируемой.
|
||
|
||
---
|
||
|
||
## 17.8. Incident response для LLM-систем
|
||
|
||
В классическом SRE есть runbooks — пошаговые инструкции на каждый тип инцидента. LLM-системы добавляют новые типы инцидентов, не существовавшие в традиционном бэкенде. Пять основных playbooks:
|
||
|
||
### Playbook 1: Всплеск галлюцинаций
|
||
|
||
**Триггер:** faithfulness-метрика упала ниже SLO (см. §17.9) за скользящее окно.
|
||
|
||
**Расследование:**
|
||
1. Проверить: был ли деплой промпта? → Сравнить версии промптов по трейсам.
|
||
2. Проверить: обновилась ли модель? → Сравнить `gen_ai.response.model` в span'ах до и после.
|
||
3. Проверить: изменился ли RAG-индекс? → Сравнить retrieved chunks в трейсах.
|
||
|
||
**Митигация:**
|
||
- Rollback промпта к предыдущей стабильной версии.
|
||
- Rebuild RAG-индекса, если обнаружена коррупция данных.
|
||
- Добавить golden examples для проблемных кейсов.
|
||
|
||
### Playbook 2: Взрыв стоимости
|
||
|
||
**Триггер:** `gen_ai.client.token.usage` вырос в 2× и более за 30 минут.
|
||
|
||
**Расследование:**
|
||
1. Input inflation? → Проверить длину входных сообщений — возможно, RAG возвращает слишком много chunks.
|
||
2. Infinite loop? → Проверить количество шагов agent loop в трейсах.
|
||
3. Новый паттерн использования? → Проверить распределение `gen_ai.operation.name`.
|
||
|
||
**Митигация:**
|
||
- Активировать rate limit на уровне пользователя/сессии.
|
||
- Circuit breaker: остановить agent loop после N шагов (anti-loop protocol из [главы 13](13_anti_hallucination_loop.md)).
|
||
- При подтверждении loop — фикс в логике агента.
|
||
|
||
### Playbook 3: Деградация latency
|
||
|
||
**Триггер:** p95 `gen_ai.client.operation.duration` или TTFT превышает SLO.
|
||
|
||
**Расследование:**
|
||
1. Проблема у провайдера? → Проверить status page провайдера, сравнить latency по провайдерам.
|
||
2. Рост длины входа? → Проверить `gen_ai.usage.input_tokens` — возможно, контекст раздулся.
|
||
3. KV-кэш заполнен? → Для self-hosted моделей проверить cache hit rate.
|
||
|
||
**Митигация:**
|
||
- Fallback на резервную модель (паттерн из [главы 10](10_agent_not_chat.md)).
|
||
- Cache warming: предзаполнение KV-кэша для частых системных промптов.
|
||
- Уменьшение контекста: сократить число RAG-chunks, обрезать history.
|
||
|
||
### Playbook 4: Обнаружен prompt injection
|
||
|
||
**Триггер:** алерт от guardrail-системы ([глава 15](15_llm_system_security.md)).
|
||
|
||
**Расследование:**
|
||
1. Direct или indirect injection? Direct — от пользователя, indirect — из tool output или RAG-документа.
|
||
2. Какой источник? → Проверить трейс: откуда пришёл injected content.
|
||
|
||
**Митигация:**
|
||
- Заблокировать источник (пользователь / документ / tool).
|
||
- Обновить фильтры guardrails.
|
||
- Для indirect: аудит всех tool outputs и RAG-источников.
|
||
|
||
### Playbook 5: Outage провайдера
|
||
|
||
**Триггер:** error rate > 50% за 5 минут, `error.type` = provider error.
|
||
|
||
**Митигация:**
|
||
- Автоматический failover на вторичного провайдера (паттерн multi-provider из [главы 10](10_agent_not_chat.md)).
|
||
- Если единственный провайдер — graceful degradation: кэшированные ответы, fallback-сообщение пользователю.
|
||
- Post-mortem: добавить второго провайдера, если его нет.
|
||
|
||
### Общая схема реагирования
|
||
|
||
| Инцидент | Триггер-метрика | Расследование | Митигация |
|
||
|----------|----------------|---------------|-----------|
|
||
| Hallucination spike | Faithfulness < SLO | Промпт? Модель? RAG? | Rollback, rebuild index |
|
||
| Cost explosion | Token usage > 2× | Input? Loop? Pattern? | Rate limit, circuit breaker |
|
||
| Latency degradation | TTFT/duration p95 > SLO | Провайдер? Input length? | Fallback model, cache |
|
||
| Prompt injection | Guardrail alert | Direct? Indirect? | Block source, update filters |
|
||
| Provider outage | Error rate > 50% | Status page | Failover, degradation |
|
||
|
||
---
|
||
|
||
## 17.9. SLO для LLM-систем
|
||
|
||
### SLI: что измерять
|
||
|
||
Service Level Indicators для LLM строятся поверх OTel GenAI-метрик:
|
||
|
||
**Latency:** p95 по `gen_ai.client.operation.duration`.
|
||
|
||
**TTFT:** p95 по `gen_ai.server.time_to_first_token`.
|
||
|
||
**Error rate:** доля span'ов, у которых заполнен `error.type`.
|
||
|
||
**Correctness:** доля ответов, у которых `eval_score` выше выбранного порога.
|
||
|
||
**Cost:** средняя стоимость запроса, то есть входные токены по входному тарифу плюс выходные токены по выходному тарифу.
|
||
|
||
Здесь `p_in` и `p_out` — цена входного и выходного токена для конкретной модели.
|
||
|
||
### Пример SLO
|
||
|
||
| SLI | SLO | Error budget | Алерт |
|
||
|-----|-----|-------------|-------|
|
||
| Latency p95 | < 3 с (chat), < 10 с (agent) | 5% нарушений/месяц | > 5% за скользящий 1 ч |
|
||
| Error rate | < 1% | 1% ошибок/месяц | > 2% за 15 мин |
|
||
| Correctness | > 95% на hourly eval sample | 5% failures/месяц | > 10% failures за 1 ч |
|
||
| Cost per request | < \$0.05 mean | — | > 2× mean за 30 мин |
|
||
|
||
### Correctness SLO — определяющее отличие
|
||
|
||
В традиционных веб-сервисах SLI покрывают latency, error rate, throughput. SLO на correctness — «был ли ответ правильным?» — не существует: если API вернул данные из базы, они либо корректны (база не врёт), либо это баг в коде.
|
||
|
||
Для LLM-систем correctness — полноценный SLI, потому что модель может вернуть уверенный, грамматически безупречный, но фактически неверный ответ с HTTP 200. Это требует:
|
||
- автоматических eval'ов (выборка из production-трейсов → LLM-as-Judge → score),
|
||
- порогов на score (correctness SLO),
|
||
- алертов при деградации.
|
||
|
||
Именно correctness SLO отличает LLM observability от классического APM. Без неё вы мониторите инфраструктуру, но не продукт.
|
||
|
||
### Error budget и решения
|
||
|
||
Error budget работает так же, как в Google SRE: если бюджет исчерпан — freeze на новые фичи, фокус на reliability. Для LLM-систем это значит:
|
||
|
||
- **Latency budget исчерпан** → не деплоить более тяжёлые модели, оптимизировать контекст.
|
||
- **Correctness budget исчерпан** → не деплоить новые промпты, запустить ревью проблемных кейсов.
|
||
- **Cost budget исчерпан** → аудит token usage, переход на более дешёвую модель для low-risk запросов.
|
||
|
||
---
|
||
|
||
## 17.10. Resilience testing: устойчивость как дисциплина
|
||
|
||
Наблюдаемость показывает, что произошло. Incident response определяет, как реагировать. Но инженерная зрелость — это способность *заранее* проверить, как система ведёт себя при деградации. LLM-системы зависят от внешних провайдеров, сетевых вызовов и недетерминированных компонентов — resilience testing для них важнее, чем для обычных web-сервисов.
|
||
|
||
### Load testing LLM-систем
|
||
|
||
LLM-вызов — не типичный HTTP-запрос с латенси 50 мс. Один вызов может длиться 2–30 секунд, а стоимость пропорциональна длине контекста. Классические нагрузочные тесты «X rps за Y минут» нужно адаптировать.
|
||
|
||
Что тестировать:
|
||
- **Concurrent requests → TTFT degradation.** При каком уровне параллелизма TTFT пробивает SLO?
|
||
- **Throughput saturation.** При каком числе одновременных запросов p99 latency начинает расти нелинейно?
|
||
- **Token budget exhaustion.** Rate limits провайдера — при какой нагрузке вы их достигаете и как система реагирует на 429?
|
||
|
||
Важный нюанс: нагрузочный тест с промптом «hello» бесполезен. Длина промпта влияет на latency и cost — тестируйте с реалистичными промптами из production-трейсов.
|
||
|
||
### Provider outage drills
|
||
|
||
LLM-провайдер — single point of failure. Что произойдёт, если OpenAI API вернёт 503 в течение 30 минут? Большинство команд не знает ответа, пока это не случится в production.
|
||
|
||
Drill: в staging-среде подставьте mock, возвращающий 503 или таймаут. Проверьте четыре вещи:
|
||
1. Fallback на альтернативного провайдера срабатывает.
|
||
2. Circuit breaker открывается за ожидаемое время.
|
||
3. Пользователь получает degraded experience, а не HTTP 500.
|
||
4. Алерт приходит в ожидаемый канал за ожидаемое время.
|
||
|
||
Pre-requisite: архитектура с provider abstraction layer (см. [Главу 20, паттерн Router](20_llm_application_design_patterns.md)).
|
||
|
||
### Fault injection на tool layer
|
||
|
||
В агентных системах каждый tool — потенциальная точка отказа. Fault injection: tool возвращает ошибку, пустой результат, мусорные данные, таймаут.
|
||
|
||
Что проверяем:
|
||
- Агент корректно обрабатывает ошибку инструмента и сообщает пользователю, а не галлюцинирует ответ.
|
||
- Агент не зацикливается на retry'ях (anti-loop protocol из §13).
|
||
- MCP-серверы: disconnection и reconnection. Если MCP-сервер отвечает через 60 секунд, агент должен использовать timeout и fallback, а не висеть бесконечно.
|
||
|
||
### Replay testing с environment mocks
|
||
|
||
Production traces (из OpenTelemetry) можно переиграть в тестовом окружении с замоканными внешними системами. Сценарий: берём trace реального запроса, подставляем mock вместо LLM и tools, проверяем, что orchestrator корректно обрабатывает каждый шаг.
|
||
|
||
Применение:
|
||
- **Регрессионное тестирование** после изменения routing-логики, prompt version или набора инструментов.
|
||
- **Agent regression test:** replay + grader = автоматическая проверка, что агент по-прежнему решает задачу (связь с [Главой 14, секция 14.10](14_llm_system_quality_evaluation.md)).
|
||
|
||
### Деградация при изменении routing или caching
|
||
|
||
Изменение KV-кэша провайдера, prompt caching policy или routing между моделями может незаметно изменить поведение системы — без единого алерта по latency или error rate.
|
||
|
||
Тест: сравните качество ответов (через eval suite из [Главы 14](14_llm_system_quality_evaluation.md)) до и после изменения routing/caching configuration. Canary deployments для prompt changes: новая версия промпта → 5% трафика → eval → если качество не деградировало → полная раскатка.
|
||
|
||
Resilience testing — не разовое мероприятие. Включите provider outage drill в ежемесячный runbook, fault injection — в CI/CD для агентных pipeline'ов, load testing — в pre-launch checklist. Система, которую не тестировали на отказ, откажет непредсказуемо.
|
||
|
||
---
|
||
|
||
## Практический вывод
|
||
|
||
### Девять шагов операционной зрелости
|
||
|
||
| # | Действие | Что даёт |
|
||
|---|----------|----------|
|
||
| 1 | **Инструментируйте OTel GenAI**: OpenLLMetry или Phoenix SDK | Стандартные span'ы и метрики для каждого LLM-вызова |
|
||
| 2 | **Версионируйте промпты в Git**, привязывайте версию к span'ам | Lineage: трейс → промпт → коммит → автор |
|
||
| 3 | **Классифицируйте ошибки** по 5 категориям, стройте тренды | Понимание: инфраструктура или контент? |
|
||
| 4 | **Включите content capture** для debug replay, решите PII | Точное воспроизведение проблемных вызовов |
|
||
| 5 | **Задайте SLO**: latency, error rate, correctness, cost | Измеримые обязательства перед продуктом |
|
||
| 6 | **Постройте human review queue**: auto-eval → порог → человек → dataset | Масштабируемая обратная связь |
|
||
| 7 | **Подготовьте 5 incident playbooks** | От реакции к процедуре |
|
||
| 8 | **A/B-тестируйте промпты** на golden datasets перед деплоем | Данные вместо интуиции |
|
||
| 9 | **Resilience testing**: load tests, provider outage drills, fault injection на tool layer | Проверенная устойчивость, а не надежда на uptime |
|
||
|
||
### Минимальный стек
|
||
|
||
Для команды, начинающей с нуля:
|
||
|
||
```
|
||
Приложение → OpenLLMetry (авто-инструментация)
|
||
→ Arize Phoenix (трейсы + evals + datasets)
|
||
→ Grafana (метрики + алерты)
|
||
→ Git (промпты + версии)
|
||
```
|
||
|
||
Для команды с существующим APM:
|
||
|
||
```
|
||
Приложение → OpenLLMetry → OTel Collector
|
||
→ Datadog / New Relic / Splunk (метрики)
|
||
→ Phoenix или LangSmith (LLM-специфичные трейсы + evals)
|
||
```
|
||
|
||
### Задания
|
||
|
||
1. **Добавьте OTel GenAI-трейсинг к существующему LLM-сервису.** Интегрируйте OpenLLMetry в проект, отправьте span'ы в Phoenix или локальный OTel Collector. Проверьте, что токены, latency и модель записываются корректно. Ожидаемый результат: визуализированные трейсы с атрибутами `gen_ai.*` в дашборде.
|
||
|
||
2. **Настройте correctness SLO.** Определите порог correctness для вашего продукта, настройте hourly eval sample из production-трейсов с LLM-as-Judge оценкой, создайте алерт на деградацию (>10% failures за 1 час). Ожидаемый результат: работающий алерт в Grafana/PagerDuty, срабатывающий при падении качества.
|
||
|
||
3. **Проведите A/B-тест двух вариантов промпта.** Соберите golden dataset из 30+ примеров (методика — [глава 14](14_llm_system_quality_evaluation.md)), запустите два варианта промпта через Phoenix Experiments или LangSmith, сравните по метрикам faithfulness и relevance. Ожидаемый результат: таблица сравнения с числовыми метриками и обоснованным решением о выборе варианта.
|
||
|
||
4. **Подготовьте incident playbook.** Напишите runbook для сценария «всплеск галлюцинаций» по шаблону из §17.8 (триггер, расследование, митигация), адаптированный под ваш стек. Ожидаемый результат: документ в репозитории, который on-call инженер может использовать ночью без помощи коллег.
|
||
|
||
5. **Проведите provider outage drill.** В staging-среде подставьте mock вместо LLM API, возвращающий 503. Проверьте: (1) срабатывает ли fallback, (2) за какое время открывается circuit breaker, (3) получает ли пользователь graceful degradation. Запишите результаты и исправьте найденные проблемы. Ожидаемый результат: задокументированный drill report с выявленными и устранёнными уязвимостями.
|
||
|
||
---
|
||
|
||
## Источники
|
||
|
||
- OpenTelemetry. "Semantic Conventions for Generative AI." v1.40.0 (2026). https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
||
- Traceloop. "OpenLLMetry: Open-source observability for your LLM application." https://github.com/traceloop/openllmetry
|
||
- Arize AI. "Phoenix: Open-source AI observability platform." https://github.com/Arize-ai/phoenix
|
||
- LangChain. "LangSmith Documentation." https://docs.smith.langchain.com/
|
||
- Weights & Biases. "W&B Weave: LLM Monitoring and Evaluation." https://docs.wandb.ai/guides/weave (2025–2026).
|
||
- Beyer, B., et al. (2016). "Site Reliability Engineering." O'Reilly — главы о SLI/SLO/Error Budget.
|
||
- Shai, A. S., et al. (2024). "Transformers represent belief state geometry in their residual stream." arXiv:2405.15943. Основание для инженерной метафоры belief state.
|
||
- Chen, Q., et al. (2026). "The Molecular Structure of Thought: Mapping the Topology of Long Chain-of-Thought Reasoning." arXiv:2601.06002. Deep reasoning, self-reflection, self-exploration как полезная схема typed traces.
|
||
- Laban, P., et al. (2025). "LLMs Get Lost In Multi-Turn Conversation." arXiv:2505.06120 / ICLR 2026. Multi-turn unreliability и необходимость checkpoint summaries.
|
||
- Ivanov, V. `osovv/grace-marketplace`: `verification-driven-dev.md`, `grace-verification`, `grace-fix` (2026). Verification как отдельный артефакт, stable log markers и FailurePacket для handoff.
|
||
|
||
---
|
||
|
||
**Навигация:**
|
||
- Назад: [Глава 16. Архитектура кода, дружественная ИИ](16_code_architecture.md)
|
||
- Далее: [Глава 18. Мультимодальные системы](18_multimodal_systems.md)
|