Перейти к содержанию

Глава 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_id
  • span_id
  • parent_span_id
  • run_id
  • tenant_id
  • principal_id
  • agent_id или id рабочего процесса
  • status
  • duration_ms
  • model_name, если был вызов модели
  • tool_name, если был вызов инструмента
  • policy_decision_id, если был шлюз

Для расследования инцидента поддержки этого уже достаточно, чтобы связать между собой рантайм, шлюз инструментов и конкретный внешний побочный эффект.

В более зрелой программе оценки полезно сохранять и достаточно связей для ревью с учетом verifier: не только что произошло в запуске, но и какие трассы и скриншоты потом легли в основу process_score, outcome_score или failure_attribution.

9. Практические правила для трассировки

Если нужен короткий операционный каркас, обычно достаточно таких правил:

  1. Каждый запуск должен иметь один trace_id, который не теряется между span политик, модели и инструментов.
  2. Trace должен покрывать контур управления, а не только задержку модели.
  3. Все вызовы инструментов, ожидания подтверждения и решения политик должны оставлять машиночитаемые события.
  4. Неопределенность нужно логировать явно: side_effect_unknown полезнее, чем притворный success.
  5. Редактирование чувствительных данных и стабильность схемы должны проектироваться сразу, а не после первого разбора инцидента.
  6. Если оценка или раскатка зависят от суждений 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_id
  • tool_principal
  • request_id или другой business object id
  • result_class
  • verifier_id
  • evidence_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. Что делать сразу после этой главы

Если хочешь быстро проверить свою модель наблюдаемости, пройди по короткому списку:

  1. Можно ли восстановить полный путь одного запуска по одному trace_id?
  2. Есть ли отдельные span для извлечения, вызовов модели, вызовов инструментов и шлюзов политик?
  3. Логируются ли ключи идемпотентности и идентификаторы решений политик?
  4. Есть ли контекст арендатора/principal в телеметрии?
  5. Можно ли увидеть, где запуск провел время и где выросла стоимость?
  6. Не утекают ли чувствительные полезные нагрузки в трассы?
  7. Стабильна ли схема structured events?

Если на несколько пунктов подряд ответ "нет", наблюдаемость у тебя пока декоративная, а не рабочая.

16. Что читать дальше

Теперь следующий шаг в этой же истории очень прямой: после того как команда научилась восстанавливать путь одного сбоя, нужно формализовать, что вообще считать "здоровой" системой каждый день. То есть перейти к SLO.