Трендовые github проекты в нашем телеграм канале. Подпишись → AI-сервису нужен полный маршрут каждого запроса
У небольшого AI-приложения всё часто начинается с простой схемы: пользователь отправляет сообщение, backend вызывает модель, интерфейс показывает ответ. На этом этапе хватает обычных логов, пары метрик и ручной проверки промптов. Ситуация меняется, когда сервис получает память, RAG, несколько агентов, внешние API, фильтры безопасности и постобработку.
Итоговый ответ начинает зависеть от цепочки промежуточных решений. Запрос проходит safety-фильтр, классификатор намерения, поиск по базе знаний, выбор контекста, генерацию, проверку фактов, запись в память и финальный guard. Если результат странный, медленный или дорогой, инженеру нужно увидеть весь путь выполнения. Одной строки provider timeout или request failed уже недостаточно.
Практичный подход — считать каждый пользовательский запрос трассой, а каждую фазу обработки отдельным span. Тогда AI-пайплайн становится наблюдаемой системой: видно порядок шагов, задержки, ошибки, вложенность операций и контекст, который привёл к результату.
Логи фиксируют события, трассы показывают маршрут
Логирование остаётся полезным: оно записывает ошибки, предупреждения, входные параметры и важные бизнес-события. Но лог обычно отвечает на вопрос «что произошло в конкретной точке». Трассировка отвечает на вопрос «каким маршрутом запрос прошёл через систему».
Для AI-сервиса это критично. Один ответ может собрать данные из векторного индекса, пользовательской памяти, knowledge graph и внешнего API. Ошибка в финальном тексте иногда появляется из-за раннего этапа: неверной классификации намерения, пустой выдачи RAG, деградации провайдера или слишком жёсткого фильтра безопасности.
Модель Trace/Span раскладывает выполнение на понятную структуру:
Trace: user_message_42
├── safety_check
├── intent_detection
├── rag_retrieve
│ ├── vector_search
│ └── rerank
├── semantic_memory
├── generate_answer
├── truth_check
└── response_guard
У каждого span полезно хранить имя операции, тип, время старта и завершения, статус, родительский span, входные параметры без секретов, ключевые метаданные и ошибку при сбое. В итоге появляется дерево выполнения, которое можно открыть позже и восстановить картину запроса.
Что стоит сохранять в spans
Минимальный набор данных зависит от проекта, но для большинства backend- и homelab-сервисов полезны такие поля: trace_id, span_id, parent_span_id, имя операции, тип span, время старта и завершения, статус, ошибка, параметры LLM-вызова, признаки retrieval-этапа, cache hit, retry count и rate limit.
Контент промптов и пользовательских сообщений лучше сохранять осознанно. Для внутреннего стенда может хватить полного текста, для production чаще нужны маскирование PII, усечение больших payload и отдельная политика retention. Наблюдаемость должна помогать расследованию, сохраняя приватность и секреты.
Propagation связывает сервисы в одну историю
Как только AI-приложение выходит за пределы одного процесса, важна передача контекста трассировки. Backend может вызвать сервис поиска, worker очереди, gateway к LLM-провайдерам и API истории диалогов. Без propagation каждый компонент видит собственный локальный запрос, а инженер получает набор разрозненных логов.
Простой вариант — передавать идентификаторы трассы через HTTP-заголовки или сообщения очереди:
X-Trace-Id: 7d9f...
X-Span-Id: b12a...
X-Parent-Span-Id: 4a31...
X-Causal-Depth: 3
При входящем запросе сервис извлекает контекст, создаёт дочерний span и передаёт обновлённые значения дальше. Так цепочка остаётся связной даже при нескольких микросервисах. Если сервис не получил внешний trace id, он создаёт новую трассу и становится корнем дерева.
Для FastAPI это удобно оформить middleware: на входе разобрать заголовки, положить контекст в request state или context variables, на выходе вернуть X-Trace-Id клиенту. Затем wrapper для внешних вызовов и LLM-провайдеров автоматически создаёт spans вокруг каждой операции.
Persistence нужен для расследований
In-memory трассы хороши для live-отладки, но после перезапуска они пропадают. Для реальной эксплуатации нужна запись завершённых трасс на диск или в базу, восстановление индекса при старте и аккуратная обработка незавершённых операций.
Для маленького сервиса достаточно файловой структуры:
trace_store/
├── traces/{trace_id}.json
├── spans/{trace_id}/span_{span_id}.json
├── index.json
├── archive/
└── quarantine/
Завершённая трасса сохраняется как JSON, spans можно писать отдельно для постепенного flush. При рестарте активные трассы помечаются как interrupted, повреждённые записи отправляются в quarantine. Такой подход даёт простой retention: старые трассы архивируются, последние N дней остаются доступными для поиска и аудита.
Если нагрузка выше, хранилище можно заменить на PostgreSQL, ClickHouse, OpenSearch или OpenTelemetry-инфраструктуру. Главное — сохранить модель данных и дисциплину записи: каждый этап должен завершаться явно, ошибки должны попадать в span, пропущенные связи должны выявляться автоматической проверкой.
Аудит целостности ловит скрытые сбои
По сохранённым данным можно запускать аудит целостности. Он проверяет, что у каждого дочернего span существует родитель, интервалы времени согласованы, трасса имеет финальный статус, критические этапы присутствуют, причинная глубина остаётся в допустимых пределах.
Для AI-пайплайна такие проверки быстро находят неприятные классы багов:
- генерация стартовала без успешного retrieval, хотя сценарий требует контекст;
- ответ прошёл наружу после ошибки response guard;
- retry создал несколько несвязанных веток;
- worker завершился, оставив активный span;
- внешний провайдер вернул timeout, но итоговая трасса помечена успешной;
- после рестарта часть истории потерялась или записалась повреждённым JSON.
Аудит лучше запускать автоматически: по endpoint для конкретной трассы, регулярной фоновой задачей и в тестах для типовых сценариев. Тогда трассировка становится частью качества системы и помогает до ручного разбора инцидента.
Live-режим удобен при отладке агентов
Для приложений с агентами и RAG полезен live-режим. WebSocket или Server-Sent Events могут отправлять события trace_event на фронтенд: этап начался, завершился или упал. Интерфейс показывает активную стадию, завершённые шаги, ошибки и общую длительность запроса.
Такой экран быстро показывает, где запрос застрял: на retrieval, rerank, LLM-вызове, проверке фактов или сохранении памяти. Режимы работы лучше разделить заранее: live записывает и транслирует события, shadow только собирает данные, readonly открывает историю, disabled отключает инструмент.
План внедрения
Начинать стоит с узкого слоя:
- Ввести
trace_idдля каждого входящего запроса и возвращать его клиенту. - Обернуть основные фазы пайплайна в spans: safety, intent, retrieval, generate, validate, persist.
- Добавить wrapper для внешних API и LLM-вызовов с таймингами, retry и статусами.
- Передавать trace context через HTTP-заголовки и сообщения очередей.
- Сохранять завершённые трассы в JSON или базу с понятным retention.
- Сделать endpoint для просмотра трассы и endpoint аудита целостности.
- Очистить метаданные от секретов, токенов авторизации и лишних пользовательских данных.
- Добавить алерты на рост latency, частые ошибки отдельных spans и незавершённые трассы.
После этого расследование странного ответа меняется качественно. Вместо поиска по разрозненным логам инженер открывает trace id и видит весь маршрут: какие компоненты участвовали, сколько времени занял каждый шаг, где менялось состояние и какая операция привела к сбою.
Наблюдаемость AI-систем — часть эксплуатационной дисциплины. Чем больше в пайплайне памяти, агентов, retrieval и внешних сервисов, тем важнее иметь полный технический маршрут каждого запроса. Trace/Span-модель даёт такой маршрут в форме, понятной backend-разработчику и удобной для поддержки.