Агент вернул код 200, а результат негодный, и по обычным логам не понять, на каком шаге он свернул не туда.
Наблюдаемость закладывают до релиза, и сводится она к четырём решениям, которые нам предстоит принять. Чем связать шаги одного запроса в общую цепочку, какими полями описывать каждый шаг, по каким признакам в телеметрии узнавать типовые отказы и в каких точках вырезать из неё персональные данные. Задним числом всё это обходится дороже и работает хуже.
Дневник курса, урок 19. Отсюда пригодится разобранное раньше: трассировка шагов агента и метрики качества (урок 7) — зачем вообще смотреть внутрь; режимы отказа команды агентов (урок 11) — что именно ищут в трейсе, когда мониторинг зелёный. Пост читается отдельно: все термины вводятся заново.
Содержание
- Наблюдаемость: инструмент отладки или слой архитектуры
- Сквозной идентификатор: как трейс переживает границу сервиса
- Чем описан шаг агента: перепутанный префикс молча ломает алерт
- Что искать в трейсе: режимы отказа под кодом 200
- Насколько верить оценке «80% сбоев приходится на три сигнатуры»
- Персональные данные в трейсах: резать приходится до записи
- Промпт в реестре против префиксного кэша
- Семантический кэш: экономия измерена, порог — нет
- Чек-лист перед выпуском: пять столпов
- Итог
- FAQ
- Источники
1. Наблюдаемость: инструмент отладки или слой архитектуры
Агента выкатили, подключили сбор ошибок, повесили дашборд с задержкой и кодами ответа. Выглядит закрытым вопросом ровно до того дня, когда приходит жалоба на неверный ответ, а на дашборде всё зелёное. Дальше начинается ручная археология по логам, и в конце её обычно рождается тикет «добавить нормальную трассировку», тем же порядком, каким бэкапы заводят после первой потери данных.
Рамок здесь две, и различаются они моментом входа. В уроке 7 наблюдаемость была инструментом отладки: трейс как детектор лжи, набор метрик качества, выбор бэкенда — всё то, что понадобится разработчику завтра, когда что-нибудь сломается. Здесь вопрос сдвигается на шаг раньше. Инструмент выбирают потом. Сначала в систему закладывают то, на что этим инструментом смотреть. Такая рамка ставит наблюдаемость в один ряд с отказоустойчивостью и безопасностью. Это слой, без которого систему не выпускают. Разговор о нём идёт языком спецификаций и обязательных полей, а удобство дашборда там вопрос десятый.
Вместе с рамкой меняется и предмет разговора. Вместо «какой инструмент поставим» появляются вопросы с нормативными ответами: каким заголовком передаётся контекст между сервисами, каким словарём описан шаг агента, где обрезаны персональные данные, что считать сигнатурой отказа. У каждого есть правильный ответ, либо зафиксированный в спецификации, либо принятый один раз на всю систему. Цена ошибки тут тоже другая. Потерянным на отладке днём она уже не измеряется. Платят собранной телеметрией, по которой потом ничего не найти.
Эксплуатацию таких сервисов давно описывают одной пропорцией. Успешный корпоративный сервис — это десять процентов магии языковых моделей и девяносто процентов классической распределённой инженерии. Пропорция эта не фигура речи, она показывает, где искать. Заголовок для сквозного идентификатора, словарь атрибутов, срок хранения записей, обрезка персональных данных, поведение кэша под нагрузкой — всё это распределённые системы образца двухтысячных. Языковая модель добавила к ним свой класс отказов и не отменила ни одного старого.
Есть и побочный эффект смены оптики, ради которого стоит переезжать. Системы, куда стекается телеметрия агента (приёмники трейсов), успели завести у себя реестр промптов. Текст лежит там версиями и правится без пересборки приложения, как настройки во внешнем конфиг-сервисе. Когда системный промпт переезжает туда, вытащить его из кода нельзя, потому что в коде его нет. Ни в репозитории, ни в собранном образе, ни в переменных окружения контейнера: приложение забирает текущую версию по идентификатору в рантайме. Наблюдаемость перестаёт быть только способом смотреть внутрь и заодно закрывает утечку системного промпта через образ и репозиторий.
2. Сквозной идентификатор: как трейс переживает границу сервиса
Пользователь пишет, что вчера вечером агент выдал ерунду. Идём в логи. У фронтенда есть запись про его запрос, у оркестратора — своя, у ретривера — своя, у внешнего API — четвёртая. Связать их можно только по времени, и на этом расследование заканчивается. В ту же секунду шли ещё десятки сессий, часы на узлах расходятся, а асинхронный шаг выполнился через минуту после того, как породивший его HTTP-запрос уже закрылся. Логи фиксируют, что произошло внутри процесса. Связи между процессами в них нет и быть не может, так что эту связь нам нужно принести с собой, внутри самого запроса.
Идея не новая, корреляционный идентификатор в логах живёт десятилетиями. Новое здесь то, что формат зафиксировали до последнего символа и его понимает чужой сервис, с которым вы ни о чём не договаривались. Склеивает куски единый идентификатор из стандарта W3C Trace Context. Едет он в HTTP-заголовке traceparent, и принять его, использовать и передать дальше обязан каждый участник цепочки. Стандарт не привязан ни к вендору, ни к языку.
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ │ │ └ trace-flags
│ │ └ parent-id, 16 hex
│ └ trace-id, 32 hex
└ версия
UI ──► FastAPI ──► оркестратор ──► ретривер ──► внешний API
один trace-id на всю цепочку, parent-id меняется на узле00: идентификатор трейса из 32 шестнадцатеричных символов, идентификатор вызывающего узла из 16 и байт флагов, младший бит которого — рекомендация sampled; trace-id остаётся неизменным на всём пути UI → FastAPI → оркестратор → ретривер → внешний API, parent-id переписывается на каждом переходе. Пример взят прямо из текста спецификации, настоящего прод-трейса в нём нет.Почему полей два, а не одно? Постоянный trace-id отвечает на вопрос «что вообще происходило в этом запросе». По нему собирается вся траектория, включая шаги в сервисах, о существовании которых вы вспоминаете только на разборе.
Меняющийся parent-id держит родство. Каждый отдельный шаг работы (вызов модели, поход в векторную базу, обращение к инструменту) пишется своей записью со временем начала и конца, статусом и набором атрибутов. Такая запись называется спаном, а трейс — это все спаны одного запроса, собранные вместе. Поле parent-id для каждого спана говорит, внутри какого другого он родился. Из этого и восстанавливается дерево вызовов, а плоский список событий на вопрос «кто кого дёрнул» не отвечает никогда. Ту же жалобу мы теперь разбираем одним запросом по идентификатору и попутно получаем сумму токенов по всем узлам, то есть цену конкретного разговора.
Дальше идут детали, которые теряются в пересказах. Третье поле спецификация называет parent-id и лишь оговаривает, что «в некоторых системах трассировки это известно как span-id». Интерфейсы подписывают его как Parent Span ID, однако искать в документации придётся по первому имени. Флаг sampled вызывающая сторона выставляет как рекомендацию, и принимающий сервис под своей нагрузкой вправе решить по-своему, писать трейс или отбросить. Версия ff запрещена, а заголовок со всеми нулями в любом из идентификаторов вендор обязан игнорировать, так что «трейс есть, но пустой» иногда описывает корректное поведение по стандарту, и бэкенд тут ни при чём. Рядом живёт заголовок tracestate для вендорских пар ключ-значение, которым жёсткий формат traceparent тесен.
Со зрелостью стандарта всё спокойно. Level 1 получил статус Recommendation 23 ноября 2021 года (редакционное обновление рекомендации 2020 года), то есть документ завершён. Level 2 добавляет флаг случайного trace-id и рекомендации по генерации идентификаторов, однако с 28 марта 2024 года висит в статусе Candidate Recommendation Draft и с тех пор не двигался. Базовый формат стабилен с 2021 года, и застрявший второй уровень ему не в упрёк.
Приёмная сторона восстанавливает контекст из заголовка, и дальше все порождённые спаны, включая асинхронные, сами цепляются к общему дереву:
carrier = {"traceparent": request.headers.get("traceparent", "")}
context = TraceContextTextMapPropagator().extract(carrier=carrier)
Без дополнительной работы это выходит только в синхронном HTTP. В событийной архитектуре заголовка нет, traceparent кладут в метаданные сообщения и разбирают на стороне подписчика руками, в каждом обработчике отдельно, а забытый обработчик обрывает дерево ровно там, где он стоит. Про этот шов подробнее в разборе мультиагентных систем в продакшене.
3. Чем описан шаг агента: перепутанный префикс молча ломает алерт
Вы ставите алерт на рост числа токенов в промпте, берёте имя атрибута из статьи, llm.token_count.prompt, и алерт молчит. Не потому, что всё хорошо, а потому, что в ваших спанах такого поля нет. Инструментация пишет gen_ai.usage.input_tokens. Запрос по неверному имени не падает с ошибкой, он честно возвращает пустой результат, и отличить «ничего не происходило» от «спрашиваем не то» приходится глазами.
Транспорт спора не вызывает. OpenTelemetry в мае 2026 прошёл в CNCF высшую ступень зрелости, Graduated. Спаны едут по OTLP, общему протоколу передачи телеметрии. Спорят про словарь, то есть про то, какими атрибутами описан шаг агента. Картина знакома всем, кто застал вендорные CSS-префиксы: механизм один, имена у каждого свои. Вокруг этого вопроса кочует неточность, которую стоит показать целиком.
Атрибуты input.value, output.value, llm.token_count.prompt и ключ openinference.span.kind легко принять за конвенцию OTel GenAI — так их подписывают и в учебных материалах. На деле это словарь OpenInference, открытой спецификации Arize поверх OpenTelemetry, которая добавляет к общему транспорту понятия промпта, вызова инструмента и извлечённого документа. У официального стандарта префикс другой: gen_ai.input.messages, gen_ai.usage.input_tokens, gen_ai.operation.name. Ошибка не косметическая, ведь по неверному имени атрибута запрос к телеметрии просто не вернёт ничего.
| Словарь | Кто ведёт | Как выглядит | Статус |
|---|---|---|---|
gen_ai.* |
OpenTelemetry (CNCF) | gen_ai.input.messages, gen_ai.usage.input_tokens |
Development, с мая 2026 в отдельном репозитории |
openinference.* |
Arize | input.value, openinference.span.kind, llm.token_count.* |
Apache-2.0, 30+ инструментаций, активен |
traceloop.* |
OpenLLMetry | traceloop.entity.input, traceloop.span.kind |
живой третий вариант |
gen_ai.* ведёт OpenTelemetry под эгидой CNCF, и он до сих пор в статусе Development, с мая 2026 в отдельном репозитории; openinference.* ведёт Arize под лицензией Apache-2.0, с тридцатью с лишним инструментациями; traceloop.* ведёт проект OpenLLMetry, и это живой третий вариант, а не наследие.Почему словарей оказалось три? Разнобой возник не от небрежности. Модель атрибутов в OpenTelemetry намеренно универсальна. Она описывает вызов, длительность и статус, но ничего не знает ни про промпт, ни про извлечённый документ, ни про то, что у ответа бывает цена в токенах. Пока официальные конвенции для этих понятий дозревали, инструменты уже писали трейсы, и каждый завёл свой префикс.
Ни один не вытеснил остальные, и спор кончился ничьей. Приёмники трейсов научились понимать все три словаря сразу. LangSmith публикует таблицы соответствия для всех трёх плюс собственный неймспейс, Langfuse для одного поля перечисляет gen_ai.prompt, input.value и вариант MLflow. Официальные конвенции при этом растут: GenAI-часть выделена в репозиторий semantic-conventions-genai, появились спаны plan, retrieval и memory, есть отдельный документ для Model Context Protocol, стандарта подключения инструментов. Однако статус остаётся Development и на уровне документов, и на уровне каждого спана.
Косвенный голос за OpenInference подают исследователи. Бенчмарк TRAIL собирал трейсы «через OpenTelemetry, а конкретно — через openinference standard», то есть надстройку вендора взяли рабочим стандартом для агентских трейсов. Отсюда простое требование к проекту. Словарь фиксируют один раз на систему, потому что от него зависит каждый запрос, который мы потом зададим телеметрии, и алерт, и дашборд, и разбор инцидента. Переехать на другой словарь потом дороже, чем кажется, ведь переписывать придётся не инструментацию, а всё, что по этой инструментации спрашивает.
А вот приёмник перестал тянуть за собой инструментацию. Раньше к вендору подключались через его SDK, и смена приёмника означала переписать код, который пишет спаны. Сейчас LangSmith принимает OTLP от любого приложения на своём эндпоинте и поддерживает веерную отправку через OpenTelemetry Collector, когда один поток спанов уходит одновременно к нему и в Datadog, Honeycomb или Grafana; для собственной установки эндпоинт просто подменяется. Спаны уходят по OTLP, меняется адрес приёмника. Дороже остаётся то, что построено поверх, — алерты и дашборды. Ось выбора между LangSmith и Langfuse от этого не сдвинулась. Спорят по-прежнему о том, где физически живут данные и по какой лицензии.
4. Что искать в трейсе: режимы отказа под кодом 200
Классический мониторинг отвечает на вопрос «сервис жив?». У агента ответ почти всегда «да» и почти всегда бесполезен. HTTP 200, задержка в норме, результат негодный или разорительный. Поэтому режимы отказа держат отдельным списком, где у каждого прописана сигнатура в телеметрии. Типология провалов агентов в проде тут превращается в рабочий документ, потому что каждому пункту нужен признак, по которому его видно в данных.
Trace (root)
└── LLM Span ──────────► llm.token_count.prompt / .completion
├── Tool Span ─────► много TOOL в одном трейсе = петля
├── Retriever Span ► документов 0, ответ уверенный = голодание RAG
└── Guardrail Span ► фильтр сработал, наружу ушёл пустой ответopeninference.span.kind (значения LLM, RETRIEVER, TOOL, GUARDRAIL, EVALUATOR и ещё пять), и именно он превращает отладку в запрос к телеметрии — вместо чтения логов считаем спаны нужного типа внутри трейса.Держится тут всё на обязательном поле типа. Без него спан отличается от соседа только именем функции, и вопрос «сколько раз агент дёрнул инструмент» превращается в подбор регулярного выражения по именам, которые завтра поменяет рефакторинг. С типом тот же вопрос решается фильтром по значению TOOL и подсчётом строк внутри трейса. Разница между «поискать в логах» и «спросить у телеметрии» здесь именно такая. Во втором случае ответ воспроизводим, ложится в алерт и переживает переименование метода.
Сбои, у которых в мониторинге один и тот же вид:
- Семантический сбой — 200 OK за две секунды, а модель согласилась выдать закрытые данные: сработала инъекция.
- Голодание RAG — запрос к базе успешен, ретривер вернул пустой список, и ответ модель сочинила без единого документа.
- Бесконечная петля — 200 «в процессе», агент долбит один инструмент, расход токенов растёт с каждой итерацией.
- Отказ безопасности — 200 OK, проверка на выходе отсекла ответ, клиент получил пустоту вместо результата.
Как это выглядит на разборе, проще показать на втором пункте. Приходит жалоба: агент уверенно рассказал про условия тарифа, которого в компании нет. В мониторинге по этому запросу всё чисто, код 200, время ответа в пределах обычного, ни одного исключения.
Поднимаем трейс по идентификатору из тикета. Корневой спан на месте, внутри спан типа RETRIEVER со статусом «успешно» и списком документов длиной ноль, следом спан типа LLM с длинным связным ответом. Диагноз читается прямо из дерева. Поиск отработал штатно и ничего не нашёл, а модель, не получив контекста, ответила из того, что запомнила при обучении. Ни база, ни сервис поиска не сломаны, сломан контракт «нет документов, нет ответа», которого в системе не было. Чего в этой цепочке рассуждений нет? Чтения логов и сопоставления меток времени. Есть трейс, тип спана и размер результата.
Алерты обычно ставят на три сигнатуры. Бесконечные петли: условия выхода нет, агент уходит на десятки итераций одного инструмента. Ходовое значение — двадцать и больше, измеренной границы за ним нет. Калибровать придётся на своём сценарии, потому что честная длинная задача тоже даёт много вызовов. Держит петли пара «жёсткий max_iterations в оркестраторе плюс порог на число TOOL-спанов внутри трейса».
Порча памяти начинается с невалидного JSON, записанного в долговременный контекст, и дальше агент считает испорченное состояние своим. Тут выручают Pydantic-схемы на каждом узле, которые срабатывают до записи, пока испорченное значение не ушло дальше. Каскадный коллапс растёт из одного неверного шага, отравившего контекст, и вся генерация ниже идёт от ложной предпосылки; отсекается он проверкой обоснованности перед сборкой финального ответа.
5. Насколько верить оценке «80% сбоев приходится на три сигнатуры»
Оценку приводят в разборах и на презентациях: на бесконечные петли, порчу памяти и каскадный коллапс приходится около 80% сбоев агента. Звучит убедительно, только первоисточника у неё нет, и держать её стоит как эвристику приоритизации алертов, снятую с конкретного контура, а не как измерение. Разница тут не в словах. Эвристику перепроверяют на своих данных, а измерением закрывают спор.
Исследователи меряют то же самое иначе, и общей пропорции не даёт ни одна работа.
| Источник | Что мерил | Что получил |
|---|---|---|
| производственный разбор (один контур) | методику не публиковали | «около 80% на три сигнатуры» |
| MAST, каталог режимов отказа мультиагентных систем | 7 открытых фреймворков | 14 режимов отказа в трёх категориях, частота отказа 41–86,7% |
| TRAIL | 148 размеченных человеком трейсов, 1987 спанов | 575 спанов с ошибкой, 11% у лучшей модели на разборе |
MAST прямо отрицает универсальность, потому что профиль отказов отражает архитектуру конкретной сборки. Разброс частоты это и показывает: похожие по замыслу сборки падают то в четырёх случаях из десяти, то почти в девяти. У TRAIL рамка своя, ошибки в трейсах размечены по трём областям: рассуждение; планирование и координация; исполнение.
Зато у TRAIL есть результат, который ломает лёгкий вывод «соберите телеметрию, дальше разберётесь». Задача там поставлена ровно так, как её хотелось бы отдать модели. Вот трейс целиком, найди в нём ошибку и укажи, в каком спане она случилась. На ней лучшая модель с длинным контекстом и набирает те самые 11%. Телеметрия себя не читает. Каталог сигнатур, пороги и алерты проектируются руками и заранее, а человек из контура разбора инцидентов пока не уходит. Модели остаётся вспомогательная роль (сгруппировать похожие трейсы, подсветить аномалию), однако решение о том, что считать отказом, принимается до того, как первый трейс записан.
6. Персональные данные в трейсах: резать приходится до записи
Про то, что нельзя отправлять паспорта и балансы в облачную модель, помнят все. Про то, что тот же текст уходит в трейс, забывают регулярно, а последствия там тяжелее. Запрос к модели прошёл и закончился, спан же оседает в аналитическом хранилище на месяцы, попадает в реплики, в ночную выгрузку и в бэкапы. Вычистить персональные данные из прода несложно; вычистить их из квартального бэкапа — уже отдельный проект. Мест, где мы вообще можем обезличить данные, ровно три.
приложение ──► инструментация ──► приёмник трейсов ──► хранилище
│ │ │
DLP-слой OPENINFERENCE_HIDE_* серверное маскирование
Presidio, → "__REDACTED__" (в Langfuse — платный
10–20 мс EE-ключ)OPENINFERENCE_HIDE_* на уровне инструментации, подменяющие значение константой "__REDACTED__", и серверное маскирование на стороне приёмника, которое у Langfuse входит в платную редакцию.Первый рубеж ставят в самом приложении. Это собственный слой DLP (data loss prevention), который перехватывает чувствительный текст до того, как он уйдёт наружу. Из трёх рубежей только он прикрывает заодно и саму отправку данных в модель, и платить за это приходится задержкой: слой висит в критическом пути каждого запроса. Методы очистки разные, от регулярного выражения до модели, которая размечает в тексте имена, адреса и номера (named entity recognition, NER). Задержка между ними отличается на порядки.
| Метод | Задержка | Полнота, ориентир | Где ломается |
|---|---|---|---|
| Регулярные выражения | меньше 1 мс | около 60% | нестандартное написание: лишний пробел в номере, и совпадения нет |
| NER-модель | 15–30 мс | около 85% | ложные срабатывания на брендах |
| Гибридный Presidio | 10–20 мс | около 92% | под российские сущности вроде номера паспорта дописывают свой распознаватель |
| Локальный BERT | свыше 150 мс | около 98% | режет пропускную способность сервиса |
Полноты 100% не даёт ни один вариант. Часть персональных данных всё равно осядет в спане, и чем дольше спаны живут, тем больше таких данных накапливается — отсюда и разумный срок хранения трейсов.
Второй рубеж дешевле остальных, и о нём чаще всего не знают. Инструментация OpenInference умеет не писать чувствительное с самого начала: OPENINFERENCE_HIDE_INPUTS, OPENINFERENCE_HIDE_INPUT_TEXT, OPENINFERENCE_HIDE_EMBEDDINGS_TEXT и ещё несколько переключателей. Гранулярность здесь важнее, чем кажется. Скрыть можно текст сообщений, оставив метаданные, число токенов и структуру дерева, то есть отказаться от содержимого, не отказываясь от отладки. Важна и деталь замены. Скрытое поле не пустеет, а получает "__REDACTED__", и потребитель трейса отличает «скрыто намеренно» от «потерялось по дороге», не начиная искать несуществующий баг инструментации.
Третий рубеж — маскирование на стороне приёмника, и здесь бесплатная самостоятельная установка (self-hosting) подводит. Ядро Langfuse под MIT и без ограничений по масштабу, но Server-Side Data Masking, журнал аудита, ролевая модель на уровне проекта и политики хранения идут по корпоративному ключу, так что «бесплатно и без ограничений» верно для ядра и неверно ровно для того, что нужно регулируемому контуру.
Дальше всё зависит от режима, под которым живёт сервис. Под ФЗ-152 и GDPR с компании спрашивают, где лежат персональные данные, сколько они хранятся и кто их читал. И в бесплатной установке выход остаётся один. Резать персональные данные приходится раньше, на первых двух рубежах, до того как спан ушёл по сети.
7. Промпт в реестре против префиксного кэша
Промпт вынесли из репозитория в реестр на стороне приёмника трейсов. Получили горячую замену без пересборки образа и отдельное ревью на изменение текста. Через неделю время до первого токена выросло, причём у всех запросов разом.
Откуда прибавка? Первая её часть очевидна. За промптом теперь ходят по сети на каждый запрос, 50–150 мс. Лечится локальным кэшем со временем жизни порядка пяти минут. Реестр остаётся источником правды, но в каждом обращении уже не участвует.
Вторая часть приходит уровнем ниже, там, где работает кэширование промпта. Считая ответ, модель держит для каждого прочитанного токена промежуточные векторы механизма внимания, их и называют KV-кэшем (key-value: ключи и значения, по которым следующий токен «оглядывается» на предыдущие). Пересчитывать их для одного и того же начала контекста незачем, и инференс-движки этим пользуются: хэшируют статический префикс блоками и подставляют готовое, если хэш совпал. Экономится ровно то, что видит пользователь, то есть время до первого токена.
Блочность здесь и создаёт ловушку. Механика та же, что у слоёв Docker: поменял первую строку, пересобирается всё, что ниже. Любая динамика в начале промпта (обращение по имени клиента, сегодняшняя дата, идентификатор сессии) меняет хэш первого блока, а вместе с ним и всех последующих, и кэш обнуляется на каждом запросе. Отсюда правило компоновки. Статическая часть идёт первой и выравнивается по границе блока (в vLLM блок по умолчанию 16 токенов), а переменные уезжают в конец контекста.
Рядом лежит вторая ловушка того же слоя, и срабатывает она уже не на задержке одного запроса, а на всём сервисе. Внезапно подросший промпт (добавили пример на полторы тысячи токенов) выедает свободные блоки памяти под кэш на пике, когда движок держит в работе много запросов сразу. Дальше вступает механика компиляции. Движок не компилирует вычислительный граф под каждую длину запроса отдельно, а держит несколько заранее собранных вариантов под диапазоны длин (бакеты) и подбирает ближайший подходящий. Промпт вырос, запросы поехали в тот диапазон, под который готового графа нет, и его собирают на лету, прямо под нагрузкой. Отсюда всплески задержки в 5–10 секунд на ровном месте.
Правка выглядела безобидно и прошла ревью как текстовая. Поэтому замер размера токенизированного промпта ставят шагом в CI, рядом с прогоном по эталонному набору. Изменение длины должно быть видно на ревью так же, как изменение смысла. Обе ловушки этого слоя укладываются в одну картинку:
промпт хэшируется блоками (в vLLM блок = 16 токенов)
статика первой [ роль · правила · примеры ][ имя · дата ]
хэш совпал, KV из кэша считаем хвост
динамика первой [ имя · дата ][ роль · правила · примеры ]
хэш сменился ──► пересчёт всего промпта
промпт вырос на ~1500 токенов
длина ушла в диапазон, под который графа нет
──► компиляция под нагрузкой ──► всплеск 5–10 с8. Семантический кэш: экономия измерена, порог — нет
В поддержку день за днём приходит один и тот же вопрос, каждый раз сформулированный иначе, и каждая формулировка честно оплачивается как новый запрос к модели. Обычный кэш по ключу тут бесполезен, ведь строки не совпадают ни разу.
Семантический кэш сравнивает запросы по смыслу и отдаёт готовый ответ, когда новый достаточно близок к уже отвеченному. Буквального совпадения строк ему не нужно. Запрос превращается в вектор, и среди сохранённых ищется ближайший по косинусной близости. Экономия измерена: кэш поверх Redis срезает до 68,8% обращений к API при доле попаданий 61,6–68,8%. SCALM, схема, которая строит политику кэширования на смысловых связях в логах диалогов, поднимает долю попаданий примерно в полтора раза и экономит 77% токенов против базовой реализации. Порядок величины — «две трети повторов можно не отправлять в модель», а не «минус девяносто процентов расходов».
У того же замера по Redis-кэшу есть обратная сторона. Доля семантически верных попаданий превышает 97%, то есть около 3% ответов из кэша не о том. На бенчмарке это статистика, а на банковском трафике — ответ про кредитную карту на вопрос про дебетовую. Это и есть дрейф кэша, только с числом.
Порог 0.94, который ходит по презентациям, честнее подавать как рабочую константу конкретного контура. Только подбором числа задача не решается, и понимать это лучше до того, как порог начнут крутить. Одним значением здесь задаётся сразу двусторонний компромисс.
порог похожести
строже ◄──────────────── 0.94 ────────────────► мягче
повтор ушёл в модель ~3% ответов из кэша
и оплачен заново про другой продукт
под порогом: полоса, где решения нет0.94 балансирует две потери: строгий порог отправляет в модель повторы, которые можно было переиспользовать, мягкий даёт около 3% ответов из кэша про другой продукт, а прямо под порогом остаётся полоса запросов, по которым решения нет ни в ту, ни в другую сторону.Хуже того, значение, разделяющее «то же самое другими словами» и «похоже, но про другой продукт», у разных групп запросов разное, и общей границы, на которую можно настроиться, просто нет. Один порог на всё работает как один тайм-аут на все внешние вызовы сразу: он либо рубит честные долгие запросы, либо не спасает от зависших. Формально известно другое: оптимальная офлайн-политика NP-трудна, то есть даже задним числом, зная весь поток запросов, её не вычислить, не то что угадать заранее.
Современный ответ переносит работу с порога на границу. Пограничные попадания, те самые чуть ниже порога, верифицируют асинхронно. Судья вне критического пути решает, годится ли кэшированный ответ, а одобренные пары уезжают в динамический слой. Задержка запроса при этом не страдает, ведь проверка идёт вне пути, на котором ждёт пользователь. Заявленный эффект — рост числа проверенных судьёй попаданий вплоть до 3,9 раза против того же кэша с одним фиксированным порогом. Механика прироста простая. При жёстком пороге пограничные пары отбрасывались и в кэш не возвращались никогда, а асинхронная проверка их подбирает. Число 3,9 авторы дают верхней границей диапазона, и типичным приростом его считать нельзя.
Зрелость подхода видна по статусу управляемого сервиса Redis LangCache — preview. Есть и ещё один ходовой приём, внешнего подтверждения которому не нашлось. Секторы кэша сбрасывают по тегам при обновлении базы знаний, иначе кэш продолжает отдавать ответы по документам, которых уже нет.
9. Чек-лист перед выпуском: пять столпов
Разобранное выше складывается в чек-лист приёмки: чем связать шаги, каким словарём их описать, что искать в получившемся, насколько верить чужой статистике отказов, где резать персональные данные, во что обходится вынос промпта наружу и какие повторы можно не отправлять в модель. Пунктов в нём ровно пять. Три закрываются этим постом, два лежат в соседних — но приёмку проходят все пять сразу, иначе смысла в ней нет.
Наблюдаемость. Трассировка по OpenTelemetry с одним из трёх словарей подключена, traceparent принимается, используется и передаётся дальше каждым узлом, включая обработчики очереди. Проверяется одним действием: взять идентификатор из тикета и посмотреть, находится ли по нему вся траектория, вместе с шагами, которые уехали в очередь.
Данные. Слой обезличивания стоит до вызова модели, переменные OPENINFERENCE_HIDE_* выставлены под то, чего в трейсе быть не должно, а срок хранения спанов согласован с полнотой этого слоя. Полнота ни у одного метода не равна ста процентам, значит, вечное хранение трейсов — это отложенный инцидент. Что делают с самим входом агента и почему фильтр на входе не заменяет обрезку на выходе — отдельная модель угроз.
Отказоустойчивость. Состояние графа вынесено в базу и переживает перезапуск процесса, пул соединений и повторы настроены, на вызовы провайдера повешен предохранитель с запасным путём. Механику разбирали в архитектуре мультиагентных систем; здесь от неё остаётся один пункт приёмки. Связь с наблюдаемостью прямая: порог срабатывания предохранителя берут из той же телеметрии.
Деньги. Каскадная маршрутизация и семантический кэш включены и, что важнее, измерены — доля попаданий и доля неверных попаданий посчитаны по своим логам. Числа из чужой статьи приёмку не закрывают. Сравнивать при этом надо стоимость исхода, а не стоимость вызова: дешёвая модель, которой понадобилось четыре захода, обходится дороже дорогой, справившейся с первого.
Релизный контур. Реестр промптов версионирован, прогон по эталонному набору автоматизирован, а замер размера токенизированного промпта стоит шагом в CI рядом с ним. Сама выкатка идёт ступенями с порогом отката, и числа для этого порога берутся из телеметрии, которую мы здесь и настраивали. Замер длины промпта выглядит мелочью до первого всплеска задержки на правке, которая прошла ревью как текстовая.
Итог
- Сквозной идентификатор — обязательство каждого узла цепочки:
traceparentпринимают, используют и передают дальше, а в шине сообщений прокидывают руками. - Словарь атрибутов выбирают один раз на систему. Их три (
gen_ai.*,openinference.*,traceloop.*), официальный до сих пор в статусе Development, а перепутанный префикс превращает алерт в пустой запрос. - Режимы отказа живут под кодом 200 и ловятся по типам спанов: метрики транспорта их не видят.
- Долю «80% сбоев на три сигнатуры» проверяйте на своих трейсах: MAST даёт 14 режимов и частоту отказа 41–86,7% с архитектурной зависимостью профиля.
- Телеметрия себя не читает — 11% у лучшей модели на разборе трейсов. Каталог сигнатур и пороги проектируются заранее и руками.
- Персональные данные режут до записи в трейс: серверного маскирования в бесплатной самостоятельной установке Langfuse нет.
- Приёмка идёт по пяти столпам сразу — наблюдаемость, данные, отказоустойчивость, деньги, релизный контур, — и три из них к языковой модели отношения не имеют вовсе.
Посмотрите, из чего состоит этот список: заголовок, словарь атрибутов, тип спана, срок хранения, размер блока префиксного кэша, порог похожести. Ни одного пункта про саму модель. В этом и смысл пропорции «десять процентов магии и девяносто процентов распределённой инженерии». Наблюдаемость агента собирается тем же инструментом, каким её собирали для микросервисов, просто с другим словарём и другим списком того, что считать поломкой. Новое здесь только одно, зато существенное: поломка перестала быть видна снаружи, и код 200 больше ничего не означает.
FAQ
Что такое traceparent и зачем он агенту?
traceparent — HTTP-заголовок стандарта W3C Trace Context, который переносит идентификатор трейса и идентификатор вызывающего узла через границы сервисов. Агенту он даёт три вещи: поиск всей траектории по одному идентификатору при разборе жалобы, атрибуцию стоимости запроса по сумме токенов на узлах и локализацию сбоя до конкретного шага. Без него трейс рассыпается на несвязанные куски. Фронтенд знает свой запрос, оркестратор — свой, ретривер — свой.
Чем OpenInference отличается от OTel GenAI semantic conventions?
Это два разных словаря атрибутов поверх одного транспорта. OpenInference — спецификация Arize с префиксами вида input.value, output.value, llm.token_count.prompt и обязательным openinference.span.kind; официальные конвенции OpenTelemetry используют префикс gen_ai.* и на середину 2026 года остаются в статусе Development. Оба живы, ни один не вытеснил другой, и крупные приёмники трейсов принимают оба плюс третий словарь traceloop.*, сводя их к своим полям.
Как в трейсе увидеть, что агент зациклился?
По числу спанов типа TOOL внутри одного трейса. Рост их количества сверх обычного для сценария — сигнатура петли: агент повторно вызывает один инструмент без условия выхода, а расход токенов растёт с каждой итерацией при полностью зелёном мониторинге. Лечится парой «жёсткий лимит итераций в оркестраторе + алерт на превышение порога числа TOOL-спанов»; сам порог придётся откалибровать на своих трейсах, потому что легитимные длинные задачи тоже дают много вызовов.
Можно ли отдать разбор трейсов языковой модели?
Пока нет. На бенчмарке TRAIL, собранном из 148 реальных трейсов и 1987 спанов, лучшая модель с длинным контекстом набирает 11% в задаче поиска и локализации ошибок. Практический вывод простой. Модель годится как помощник по кластеризации и подсказкам, однако каталог режимов отказа, пороги алертов и разбор инцидента остаются за инженером.
Где маскировать персональные данные перед отправкой в трейс?
Раньше, чем кажется. Рубежа три. Собственный слой перехвата чувствительных данных перед вызовом модели (гибридный Presidio даёт около 92% полноты при 10–20 мс), переменные OPENINFERENCE_HIDE_* на уровне инструментации с заменой значения на "__REDACTED__" и серверное маскирование на стороне приёмника. Третий рубеж в Langfuse входит в платную редакцию вместе с журналом аудита, поэтому в бесплатной самостоятельной установке работают первые два.
Насколько можно доверять порогу похожести 0.94 в семантическом кэше?
Как стартовой точке для конкретного контура. Универсальной константы за этим числом нет. По измерениям кэш срезает до 68,8% обращений к API при доле попаданий 61,6–68,8% и примерно 3% семантически неверных ответов. Единый порог задаёт жёсткий компромисс между потерянными переиспользованиями и неверными ответами, а оптимальная офлайн-политика формально NP-трудна, поэтому пограничные попадания разумнее верифицировать асинхронно, вне критического пути запроса.
Источники
- W3C — Trace Context (Level 1) — нормативный формат
traceparent, имя поляparent-id, статус рекомендации и оговорка про флагsampled. - W3C — Trace Context Level 2 — что добавляет второй уровень и почему он с 2024 года в статусе кандидата.
- OpenTelemetry — semantic-conventions-genai — официальный словарь
gen_ai.*, спаныplan/retrieval/memory, статус Development. - OpenInference — спецификация · настройки скрытия данных — обязательный
openinference.span.kind, атрибуты токенов, переменныеOPENINFERENCE_HIDE_*и константа"__REDACTED__". - LangSmith — Trace with OpenTelemetry — приём OTLP от любого приложения, веерная отправка через Collector, таблицы соответствия для трёх внешних словарей.
- Langfuse — License Key (EE) — что именно не входит в бесплатную установку: серверное маскирование данных и журнал аудита.
- TRAIL — arXiv:2505.08638 — 148 трейсов и 1987 спанов в формате OpenInference, таксономия из трёх областей и 11% у лучшей модели на разборе трейсов.
- Why Do Multi-Agent LLM Systems Fail? (MAST) — arXiv:2503.13657 — 14 режимов отказа в трёх категориях и частота отказа 41–86,7%.
- GPT Semantic Cache — arXiv:2411.05276 · SCALM — arXiv:2406.00025 — измеренная экономия на семантическом кэше и обратная сторона доли попаданий.
- Krites — arXiv:2602.13165 · From Exact Hits to Close Enough — arXiv:2603.03301 — проблема единого порога, асинхронная верификация и NP-трудность оптимальной политики кэша.
- Redis LangCache · vLLM — Automatic Prefix Caching — статус управляемого семантического кэша и размер блока префиксного кэширования.
Числа в тексте держатся на разном основании. Доли попаданий кэша, частота отказа у MAST и результат на TRAIL измерены и опубликованы с методикой. Полнота и задержка методов очистки, сетевая надбавка за поход в реестр промптов и всплеск при пересборке графа взяты из отраслевых ориентиров и на публичных методиках не проверялись, а пороги алертов и порог похожести остаются рабочими константами отдельных контуров. Калибруйте их на своих трейсах.