Глава 11. Трассы, спаны и структурированные события¶
1. Начнем не с логов, а с расследования одного сбоя¶
Продолжим тот же кейс поддержки.
Пользователь пишет:
Я уже третий день жду активации доступа. Проверьте статус и создайте срочный тикет, если заявка застряла.
Агент отвечает пользователю, что тикет создан. Через десять минут оператор видит в helpdesk уже два одинаковых тикета на одну и ту же проблему.
Теперь у команды очень приземленный вопрос:
- модель сама повторила вызов;
- повтор сработал после тайм-аута;
- инструмент вернул неоднозначный результат;
- побочный эффект произошел до того, как рантайм увидел ошибку;
- или тикеты создали два разных запуска.
Если у тебя есть только логи приложения и несколько метрик, ответ на этот вопрос обычно добывается долго и болезненно.
Именно поэтому наблюдаемость для агентных систем нужно строить не вокруг "логов вообще", а вокруг возможности восстановить историю одного запуска.
И в рамках этой главы это означает намеренно узкую вещь: трассировка — это слой захвата одного запуска, а не основа для обнаружения, корреляции и операций поверх множества запусков на масштабе всего estate.
Роль трассировки в этой книге уже, чем весь слой наблюдаемости целиком. Трассировка захватывает сырую историю исполнения. Дальше в книге отдельные главы покажут, как наблюдаемость превращает эту историю в доказательную основу, как оценки превращают ее в суждения и как assurance или governance используют эти результаты.
Если тебе нужно увидеть, как трассировка связывается с политиками, подтверждениями, оценками, инцидентами и решением о раскатке в одну эксплуатационную запись, смотри отдельную страницу Сквозная цепочка доказательств.
2. Почему обычных логов почти всегда недостаточно¶
Когда система простая, действительно можно жить на плоских логах и паре метрик. Но агентная система почти всегда сложнее:
- один пользовательский запрос превращается в многошаговый запуск;
- внутри запуска есть планирование, извлечение, сборка prompt, вызовы инструментов и шлюзы политик;
- часть шагов может уходить в фон;
- ошибка может проявиться не там, где она возникла.
Если смотреть на все это только через плоские логи, ты быстро теряешь причинно-следственную связь. Видно шум, но не видно историю выполнения.
Для нашего инцидента поддержки это означает простую вещь: без хорошей трассировки команда не поймет, кто именно создал дубль тикета и почему это произошло.
3. Trace — это история одного запуска, span — это осмысленный шаг¶
Здесь полезно закрепить простую модель:
traceописывает весь путь запроса или запуска;spanописывает отдельный значимый шаг внутри этого пути;structured eventsдобавляют точные факты, которые не стоит прятать в свободный текст.
Для того же кейса поддержки один запуск может включать:
- оценку политики;
- извлечение;
- инференс модели;
- выполнение инструмента;
- ожидание подтверждения;
- фоновое обновление памяти.
Когда эта структура есть, команда перестает смотреть на систему как на хаотичную череду вызовов и начинает видеть цепочку наблюдаемых решений.
Это различие важно, потому что эта глава не спрашивает, как агрегировать, коррелировать и алертить на масштабе всего estate. Она спрашивает, какая сырая история исполнения должна пережить систему, чтобы такие функции вообще стали возможны.
4. Как trace должен выглядеть в нашем сценарии поддержки¶
Ниже важно не просто показать красивую схему, а увидеть, где именно может возникнуть сбой.
Зрелый trace должен показывать не только модель, но и все ключевые контрольные точки
flowchart LR
A["Запрос пользователя"] --> B["Трасса запуска"]
B --> C["Span политики"]
B --> D["Span извлечения"]
B --> E["Span модели"]
B --> F["Span инструмента: проверка статуса"]
B --> G["Span инструмента: создание тикета"]
B --> H["Span подтверждения"]
B --> I["Span обновления памяти"] Если этот trace собран правильно, команда должна быстро увидеть:
- был ли второй вызов инструмента в том же запуске;
- был ли повтор;
- каким был
idempotency_key; - на каком шаге появился
side_effect_unknown; - было ли подтверждение;
- какой шлюз политики разрешил действие.
Сквозной кейс: trace как ответ на спор
В инциденте сортировки обращений поддержки trace должен не просто сказать “тикет создан”. Он должен показать связку trace_id, session_id, idempotency_key, решение политики, статус подтверждения и итог create_support_ticket. Тогда спор “модель повторила вызов или повтор сделал дубль” превращается из догадки в проверку одной цепочки событий.
5. Что стоит делать отдельными span¶
Не нужно делать отдельный span на каждую мелочь. Но и один гигантский span на весь запуск почти бесполезен.
Хорошее практическое правило такое:
- отдельный span на шаг оркестрации;
- отдельный span на извлечение;
- отдельный span на вызов модели;
- отдельный span на каждый вызов инструмента;
- отдельный span на решение политики, если она влияет на поведение;
- отдельный span на ожидание подтверждения человеком, если он есть.
Тогда trace остается читаемым и при этом показывает, где именно ушло время, деньги и надежность.
6. Structured events нужны там, где свободный текст только мешает¶
Частая ошибка: полезные операционные факты уходят в человекочитаемый лог, а потом по ним невозможно строить аналитику или расследование.
Структурированные события особенно полезны для:
- решений политик;
- исходов инструментов;
- метаданных сборки prompt;
- использования токенов;
- атрибуции стоимости;
- ключей идемпотентности;
- контекста арендатора и principal;
- записей памяти;
- доказательства verifier о том, как запуск позже был оценен.
То есть событие должно отвечать не на вопрос "что бы написать в лог", а на вопрос "что потом понадобится анализировать машинно?"
Это различие важно. Trace — это еще не вердикт, не решение политики и не действие реагирования на инцидент. Это сырой слой захвата, без которого все последующие функции просто не смогут работать надежно.
7. Хорошая модель трассировки показывает контур управления, а не только задержку LLM¶
Если наблюдаемость сводится только к времени ответа модели, команда получает очень искаженную картину.
В реальности тот же запуск поддержки часто ломается в других местах:
- извлечение начало возвращать шум;
- движок политик слишком часто блокирует действия;
- ожидание подтверждения растянулось;
- адаптер инструмента деградировал;
- фоновые обновления забили очередь;
- сборка prompt раздула контекст;
- инструмент записи вернул неоднозначный исход.
Поэтому хорошая модель трассировки должна покрывать весь поток управления, а не только шаг инференса.
Но при этом она все еще должна оставаться именно моделью трассировки. Она захватывает историю исполнения для последующего расследования. Более поздний слой наблюдаемости уже связывает множество трасс в доказательства на масштабе estate, логику обнаружения и операционную видимость.
Именно поэтому эта глава держится на границе захвата: что обязательно записывать, как это структурировать и что должно пережить последующий ревью. Более поздняя глава про наблюдаемость уже про доказательства на масштабе estate и обнаружение, а не про переопределение того, что такое trace.
8. Минимальный набор полей для trace и span¶
Чтобы система действительно была пригодна для расследований, полезно иметь как минимум:
trace_idspan_idparent_span_idrun_idtenant_idprincipal_idagent_idили id рабочего процессаstatusduration_msmodel_name, если был вызов моделиtool_name, если был вызов инструментаpolicy_decision_id, если был шлюз
Для расследования инцидента поддержки этого уже достаточно, чтобы связать между собой рантайм, шлюз инструментов и конкретный внешний побочный эффект.
В более зрелой программе оценки полезно сохранять и достаточно связей для ревью с учетом verifier: не только что произошло в запуске, но и какие трассы и скриншоты потом легли в основу process_score, outcome_score или failure_attribution.
9. Практические правила для трассировки¶
Если нужен короткий операционный каркас, обычно достаточно таких правил:
- Каждый запуск должен иметь один
trace_id, который не теряется между span политик, модели и инструментов. - Trace должен покрывать контур управления, а не только задержку модели.
- Все вызовы инструментов, ожидания подтверждения и решения политик должны оставлять машиночитаемые события.
- Неопределенность нужно логировать явно:
side_effect_unknownполезнее, чем притворныйsuccess. - Редактирование чувствительных данных и стабильность схемы должны проектироваться сразу, а не после первого разбора инцидента.
- Если оценка или раскатка зависят от суждений verifier, трассы должны сохранять явную связь с доказательствами verifier.
10. Пример structured event для выполнения инструмента¶
Ниже очень простой шаблон, который показывает правильный стиль мышления:
event_type: tool_execution
trace_id: trc_01HXYZ
span_id: spn_02ABC
run_id: run_9842
tenant_id: tenant_acme
tool_name: create_ticket
status: success
duration_ms: 842
idempotency_key: act_77f1
policy_decision_id: pol_441
side_effect: created
Такое событие намного полезнее, чем строка вроде "ticket tool ok".
10.1. Для того же кейса особенно важны еще четыре поля¶
Если цель не просто смотреть дашборды, а реально расследовать сбои, к этому шаблону обычно стоит добавить:
approval_idtool_principalrequest_idили другой business object idresult_classverifier_idevidence_refs
Они же помогают связывать операционную телеметрию с последующим оцениванием или ревью раскатки, вместо того чтобы потом заново собирать доказательства verifier вручную.
Именно они часто позволяют различить:
- дублированный вызов инструмента;
- поздний повтор;
- чужая область арендатора;
- неоднозначный внешний ответ.
11. Простой кодовый пример эмиссии span¶
Ниже каркас, который показывает самую идею: span должен не просто стартовать и завершаться, а фиксировать тип шага и исход в структуре, пригодной для анализа.
from dataclasses import dataclass
from time import monotonic
@dataclass
class SpanResult:
name: str
status: str
duration_ms: int
def traced_step(name: str, fn):
started = monotonic()
try:
fn()
status = "success"
except Exception:
status = "failure"
raise
finally:
duration_ms = int((monotonic() - started) * 1000)
emit_span(SpanResult(name=name, status=status, duration_ms=duration_ms))
def emit_span(result: SpanResult) -> None:
print({"span_name": result.name, "status": result.status, "duration_ms": result.duration_ms})
Этот пример нарочно простой. Его задача не заменить SDK трассировки, а показать принцип: каждый важный шаг должен оставлять после себя структурированный след.
12. Что особенно важно не логировать как есть¶
Наблюдаемость не должна превращаться в утечку данных.
Поэтому с трассами и событиями нужно очень аккуратно обращаться с:
- полными телами prompt;
- сырыми извлеченными документами;
- секретами и токенами;
- PII;
- содержимым чувствительных полезных нагрузок инструментов.
Практическое правило простое:
- логируй метаданные и производные факты;
- логируй идентификаторы и хэши там, где это помогает;
- полные чувствительные полезные нагрузки не клади в общие телеметрические пайплайны без особой причины.
13. Что чаще всего ломается в наблюдаемости агентной системы¶
Проблемы здесь очень узнаваемы:
- trace покрывает только вызов модели;
- вызовы инструментов не связаны с исходным запуском;
- решения политик видны в коде, но не видны в телеметрии;
- события есть, но без контекста арендатора/principal;
- span слишком крупные или слишком шумные;
- схема событий меняется хаотично, и аналитика ломается.
Если это происходит, команда снова начинает жить на догадках и ручном чтении логов.
В этот момент у системы уже может быть телеметрический выхлоп, но надежной сырая история запуска у нее все еще нет.
14. Быстрый тест зрелости для наблюдаемости агентной системы¶
Команде не стоит считать наблюдаемость зрелой только потому, что у нее есть дашборды, логи и графики задержки модели.
Более сильная планка такая:
- один запуск можно восстановить от начала до конца;
- слои политики, модели, инструментов и подтверждений действительно видны;
- неопределенность не сплющивается в фальшивый успех;
- телеметрия помогает и разбора инцидента, и решениям о релизе;
- работа с чувствительными данными спроектирована, а не импровизирована.
Если этого нет, система может уже что-то телеметрировать, но операционной наблюдаемости у нее пока нет.
15. Что делать сразу после этой главы¶
Если хочешь быстро проверить свою модель наблюдаемости, пройди по короткому списку:
- Можно ли восстановить полный путь одного запуска по одному
trace_id? - Есть ли отдельные span для извлечения, вызовов модели, вызовов инструментов и шлюзов политик?
- Логируются ли ключи идемпотентности и идентификаторы решений политик?
- Есть ли контекст арендатора/principal в телеметрии?
- Можно ли увидеть, где запуск провел время и где выросла стоимость?
- Не утекают ли чувствительные полезные нагрузки в трассы?
- Стабильна ли схема structured events?
Если на несколько пунктов подряд ответ "нет", наблюдаемость у тебя пока декоративная, а не рабочая.
16. Что читать дальше¶
Теперь следующий шаг в этой же истории очень прямой: после того как команда научилась восстанавливать путь одного сбоя, нужно формализовать, что вообще считать "здоровой" системой каждый день. То есть перейти к SLO.