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

Глава 11. Трассы, спаны и структурированные события

1. Начнем не с логов, а с расследования одного сбоя

Продолжим тот же кейс поддержки.

Пользователь пишет:

Я уже третий день жду активации доступа. Проверьте статус и создайте срочный тикет, если заявка застряла.

Агент отвечает пользователю, что тикет создан. Через десять минут оператор видит в helpdesk уже два одинаковых тикета на одну и ту же проблему.

Теперь у команды очень приземленный вопрос:

  • модель сама повторила вызов;
  • повтор сработал после тайм-аута;
  • инструмент вернул неоднозначный результат;
  • побочный эффект произошел до того, как рантайм увидел ошибку;
  • или тикеты создали два разных запуска.

Если у тебя есть только логи приложения и несколько метрик, ответ на этот вопрос обычно добывается долго и болезненно.

Именно поэтому наблюдаемость для агентных систем нужно строить не вокруг "логов вообще", а вокруг возможности восстановить историю одного запуска.

И в рамках этой главы это означает намеренно узкую вещь: трассировка — это слой захвата одного запуска, а не основа для обнаружения, корреляции и операций поверх множества запусков на масштабе всего estate.

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

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

2. Почему обычных логов почти всегда недостаточно

Когда система простая, действительно можно жить на плоских логах и паре метрик. Но агентная система почти всегда сложнее:

  • один пользовательский запрос превращается в многошаговый запуск;
  • внутри запуска есть планирование, извлечение, сборка prompt, вызовы инструментов и шлюзы политик;
  • часть шагов может уходить в фон;
  • ошибка может проявиться не там, где она возникла.

Если смотреть на все это только через плоские логи, ты быстро теряешь причинно-следственную связь. Видно шум, но не видно историю выполнения.

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

3. Трасса — это история одного запуска, спан — это осмысленный шаг

Здесь полезно закрепить простую модель:

  • trace описывает весь путь запроса или запуска;
  • span описывает отдельный значимый шаг внутри этого пути;
  • structured events добавляют точные факты, которые не стоит прятать в свободный текст.

Для того же кейса поддержки один запуск может включать:

  • оценку политики;
  • извлечение;
  • инференс модели;
  • выполнение инструмента;
  • ожидание подтверждения;
  • фоновое обновление памяти.

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

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

4. Как трасса должна выглядеть в нашем сценарии поддержки

Ниже важно не просто показать красивую схему, а увидеть, где именно может возникнуть сбой.

Зрелая трасса должна показывать не только модель, но и все ключевые контрольные точки

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_id, session_id, idempotency_key, решение политики, статус подтверждения и итог create_support_ticket. Тогда спор “модель повторила вызов или повтор сделал дубль” превращается из догадки в проверку одной цепочки событий.

Заметка о сквозных сценариях трассировки: структура трассы должна различать все три канонических сценария. Разбор обращений поддержки требует связать спаны инструментов, статус подтверждения, idempotency_key и итог create_support_ticket. Внутренний ассистент знаний требует спанов поиска с идентификаторами источников, отметками свежести, областью арендатора и событиями записи в память. Координация инцидентов требует спанов эскалации, исходов уведомлений, изменений роли реагирующего и событий состояния инцидента, чтобы разбор после инцидента видел не только финальный статус, но и цепочку решений.

5. Что стоит делать отдельными спанами

Не нужно делать отдельный span на каждую мелочь. Но и один гигантский span на весь запуск почти бесполезен.

Хорошее практическое правило такое:

  • отдельный span на шаг оркестрации;
  • отдельный span на извлечение;
  • отдельный span на вызов модели;
  • отдельный span на каждый вызов инструмента;
  • отдельный span на решение политики, если она влияет на поведение;
  • отдельный span на ожидание подтверждения человеком, если он есть.

Тогда trace остается читаемым и при этом показывает, где именно ушло время, деньги и надежность.

6. Структурированные события нужны там, где свободный текст только мешает

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

Структурированные события особенно полезны для:

  • решений политик;
  • исходов инструментов;
  • метаданных сборки prompt;
  • использования токенов;
  • атрибуции стоимости;
  • ключей идемпотентности;
  • контекста арендатора и principal;
  • записей памяти;
  • доказательств проверяющего о том, как запуск позже был оценен.

То есть событие должно отвечать не на вопрос "что бы написать в лог", а на вопрос "что потом понадобится анализировать машинно?"

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

7. Хорошая модель трассировки показывает контур управления, а не только задержку LLM

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

В реальности тот же запуск поддержки часто ломается в других местах:

  • извлечение начало возвращать шум;
  • движок политик слишком часто блокирует действия;
  • ожидание подтверждения растянулось;
  • адаптер инструмента деградировал;
  • фоновые обновления забили очередь;
  • сборка prompt раздула контекст;
  • инструмент записи вернул неоднозначный исход.

Поэтому хорошая модель трассировки должна покрывать весь поток управления, а не только шаг инференса.

Но при этом она все еще должна оставаться именно моделью трассировки. Она захватывает историю исполнения для последующего расследования. Более поздний слой наблюдаемости уже связывает множество трасс в доказательства на масштабе estate, логику обнаружения и операционную видимость.

Именно поэтому эта глава держится на границе захвата: что обязательно записывать, как это структурировать и что должно пережить последующий ревью. Более поздняя глава про наблюдаемость уже про доказательства на масштабе estate и обнаружение, а не про переопределение того, что такое trace.

7.1. Reasoning privacy против observability

У современных API появляется важный паттерн: модель может возвращать не только финальный model_output, но и краткий reasoning_summary, ссылку на reasoning artifact или encrypted reasoning item. Это полезно для расследований, но опасно, если команда начинает сохранять сырой chain-of-thought как обычный лог.

Практическое правило такое: trace должен быть расследуемым, но не обязан раскрывать полное внутреннее рассуждение модели. Для большинства production-разборов достаточно разделять четыре сущности:

  • model_output — текст или structured output, который реально увидел пользователь или следующий runtime step;
  • reasoning_summary — краткое объяснение высокого уровня, пригодное для triage и eval;
  • reasoning_reference или encrypted_reasoning_item — ссылка на приватный artifact, если provider/runtime поддерживает его безопасное хранение;
  • tool_evidence — фактические вызовы, результаты, approvals, retrieval sources и verifier evidence.

Такой дизайн сохраняет observability без превращения reasoning в бесконтрольный sensitive log. Оператор может увидеть, что модель опиралась на retrieval и policy evidence, но доступ к полному encrypted artifact должен идти через отдельный governance path: audit purpose, retention rule, access approval и redaction/DLP checks.

В маленьком reference runtime это выражается событием model_reasoning_evidence: оно может хранить summary/reference/encrypted item pointer и preview финального ответа, но не хранит raw reasoning. Для расследования это менее “любопытно”, зато намного безопаснее и устойчивее к будущим требованиям приватности.

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, если был шлюз

8.1. Production observability требует searchable spans, а не только красивый trace

Свежие материалы AWS AgentCore AgentOps и Microsoft Foundry показывают, что промышленный слой наблюдаемости быстро выходит за пределы “посмотреть один trace”.134 Команде нужны searchable spans, которые можно фильтровать по модели, инструменту, ошибке, задержке, стоимости, policy state и privacy state. Иначе trace полезен для ручного разбора, но слаб для regression review, дежурства и оценки стоимости.

Материал AWS про debugging production agents with AgentCore Observability добавляет к этому важную практическую планку: trace должен помогать расследовать не только явный exception, но и silent failure, infinite loop и tool invocation failure.2 Для этого в span недостаточно хранить общий status: нужен видимый reasoning step, выбранный tool selection, результат вызова, latency/retry context и точка, где произошел workflow break. Тогда debugging workflow идет от симптома к конкретному decision/tool span, а не к пересказу “модель почему-то зациклилась”.

Минимальный production span contract стоит расширять такими полями:

  • span_type: model_call, tool_call, retrieval, policy_gate, approval_wait, handoff, memory_write;
  • input_ref и output_ref: ссылки, хэши или redacted artifact pointers вместо сырых prompt/output тел;
  • latency_ms, retry_count, error_class и result_class;
  • token_input_count, token_output_count, token_cost и model_name;
  • tool_name, tool_principal, approval_state и policy_decision_id;
  • pii_redacted, redaction_policy_id и retention_class;
  • trace_search_tags: owner, scenario, release, eval dataset или incident id.

Такой контракт делает трассу пригодной не только для “что произошло?”, но и для “найди все похожие runs после релиза”, “почему вырос cost”, “какой tool начал деградировать”, “какие span можно передать verifier без PII” и “какие traces должны попасть в regression review”.

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

Если агент реализован как durable named instance, к этому минимуму нужно добавить поля, которые отличают долгоживущий actor от одного request handler:

  • agent_instance_id;
  • durable_state_version;
  • scheduled_wakeup_id, если запуск продолжился по расписанию;
  • resumable_stream_id или connection_id, если пользовательский поток пережил reconnect;
  • state_transition, если run загрузил, изменил, усыпил или разбудил экземпляр.

Иначе trace показывает только отдельный run и скрывает самый важный вопрос: это было продолжение того же named agent state или новая stateless реконструкция, которая просто выглядит похоже?

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

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

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

  1. Каждый запуск должен иметь один trace_id, который не теряется между span политик, модели и инструментов.
  2. Trace должен покрывать контур управления, а не только задержку модели.
  3. Все вызовы инструментов, ожидания подтверждения и решения политик должны оставлять машиночитаемые события.
  4. Неопределенность нужно логировать явно: side_effect_unknown полезнее, чем притворный success.
  5. Редактирование чувствительных данных и стабильность схемы должны проектироваться сразу, а не после первого разбора инцидента.
  6. Если оценка или раскатка зависят от суждений проверяющего, трассы должны сохранять явную связь с доказательствами проверяющего.

10. Пример структурированного события для выполнения инструмента

Ниже очень простой шаблон, который показывает правильный стиль мышления:

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 или другой идентификатор бизнес-объекта
  • result_class
  • verifier_id
  • evidence_refs

Они же помогают связывать операционную телеметрию с последующим оцениванием или разбором раскатки, вместо того чтобы потом заново собирать доказательства проверяющего вручную.

Именно они часто позволяют различить:

  • дублированный вызов инструмента;
  • поздний повтор;
  • чужая область арендатора;
  • неоднозначный внешний ответ.

11. Простой кодовый пример эмиссии span

Ниже каркас, который показывает самую идею: span должен не просто стартовать и завершаться, а фиксировать тип шага и исход в структуре, пригодной для анализа.

from time import monotonic

from agent_runtime_ref.models import ToolResult


def traced_step(name: str, fn):
    started = monotonic()
    status = "success"
    try:
        result = fn()
        if isinstance(result, ToolResult) and result.status != "success":
            status = "failure"
        return result
    except Exception:
        status = "failure"
        raise
    finally:
        duration_ms = int((monotonic() - started) * 1000)
        emit_span(name=name, status=status, duration_ms=duration_ms)


def emit_span(*, name: str, status: str, duration_ms: int) -> None:
    print(
        {
            "span_name": name,
            "status": status,
            "duration_ms": duration_ms,
        }
    )

Этот пример нарочно простой. Его задача не заменить SDK трассировки, а показать принцип: каждый важный шаг должен оставлять после себя структурированный след.

Статус спана и исход операции нельзя смешивать. Если обертка вызова завершилась без исключения, но инструмент вернул структурированный результат failed, спан не должен выглядеть как успешное действие. Либо его status отражает итог операции, либо событие дополнительно несет operation_status и result_class. Иначе расследование увидит зеленый спан внутри неуспешного запуска и потеряет причину отказа.

12. Что особенно важно не логировать как есть

Наблюдаемость не должна превращаться в утечку данных.

Поэтому с трассами и событиями нужно очень аккуратно обращаться с:

  • полными телами prompt;
  • сырыми извлеченными документами;
  • секретами и токенами;
  • PII;
  • содержимым чувствительных полезных нагрузок инструментов.

Практическое правило простое:

  • логируй метаданные и производные факты;
  • используй неключевые хэши только как несекретные контрольные суммы, а keyed HMAC — для корреляции чувствительных значений;
  • полные чувствительные полезные нагрузки не клади в общие телеметрические пайплайны без особой причины.

13. Что чаще всего ломается в наблюдаемости агентной системы

Проблемы здесь очень узнаваемы:

  • trace покрывает только вызов модели;
  • вызовы инструментов не связаны с исходным запуском;
  • решения политик видны в коде, но не видны в телеметрии;
  • события есть, но без контекста арендатора/principal;
  • span слишком крупные или слишком шумные;
  • схема событий меняется хаотично, и аналитика ломается.

Если это происходит, команда снова начинает жить на догадках и ручном чтении логов.

В этот момент у системы уже может быть телеметрический выхлоп, но надежной сырая история запуска у нее все еще нет.

14. Быстрый тест зрелости для наблюдаемости агентной системы

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

Более сильная планка такая:

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

Если этого нет, система может уже что-то телеметрировать, но операционной наблюдаемости у нее пока нет.

15. Практикум: расследование агентного запуска по trace

В этой главе уже есть базовый словарь наблюдаемости: trace, span, structured event, контекст арендатора, principal, решение политики, ключ идемпотентности и итог запуска. Но практический вопрос обычно звучит жестче: что именно делает дежурный инженер, когда агент создал не тот тикет, завис на подтверждении или вернулся с side_effect_unknown после записывающего инструмента?

Хорошая трассировка нужна не для того, чтобы любоваться красивым деревом span. Она нужна, чтобы за ограниченное время восстановить доказательную историю одного запуска и принять операционное решение: повторять, останавливать, сверять состояние вручную, блокировать раскатку или добавлять регрессионную оценку.

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

15.1. Начни с вопроса расследования

Не начинай расследование с поиска всех логов по словам ticket, error или имени пользователя. Это быстро приводит к шуму. Начни с одного проверяемого вопроса.

Например:

incident_question: "создал ли агент больше одного тикета после тайм-аута create_ticket"
expected_answer_shape:
  - исходный запрос
  - решение предварительной политики
  - решение политики инструмента
  - запись подтверждения, если она была
  - ключ идемпотентности
  - итог выполнения инструмента
  - итог запуска
  - доказательство отсутствия дубля или причина неопределенности

Такой вопрос сразу задает границы. Если в трассе нет trace_id, session_id, tenant_id, principal_id, agent_id, approval_id и idempotency_key, команда не расследует запуск, а пытается угадать его по фрагментам.

15.2. Собери минимальную оболочку трассы

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

trace_review:
  trace_id: trace-support-042
  session_id: session-support-017
  agent_id: support-triage-ref
  tenant_id: tenant-acme
  principal_id: user-42
  authorization_mode: user_delegated
  delegated_principal_id: user-42
  delegated_scope: support.ticket:create
  investigation_owner: oncall-agent-platform
  incident_question: "был ли создан дубль тикета после тайм-аута"

Эта карточка должна собираться из структурированных событий, а не из свободного текста. Если часть полей живет внутри payload, как в учебном agent_runtime_ref, это нормально для маленького рантайма. В промышленной схеме важно, чтобы они оставались стабильными и пригодными для выборок, экспорта и регрессионных проверок.

15.3. Построй линию времени событий

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

expected_sequence:
  - run_start
  - policy_precheck
  - retrieval
  - context_layers_built
  - span:model_step
  - tool_policy_decision
  - approval_requested
  - sandbox_profile_reviewed
  - tool_execution
  - span:tool:create_ticket
  - verification_result
  - run_failed
  - run_complete

Не каждый запуск пройдет все шаги. Например, если политика сразу запретила запрос, после policy_precheck может быть только run_complete со статусом denied. Если записывающая возможность требует человека, вместо прямого выполнения появится approval_requested, а tool_execution может иметь статус approval_required и tool_principal: pending_review. Если инструмент выполнился и вернул сбой, появится run_failed, а run_complete должен сохранить failure_reason.

Главное правило: отсутствие события тоже факт. Если tool_policy_decision отсутствует, но tool_execution есть, система потеряла доказательство допуска инструмента. Если есть approval_requested, но нет связи с idempotency_key, подтверждение уже не доказывает, какую именно запись оно защищало.

15.4. Проверь идентичность и границы доступа

После линии времени проверь инварианты идентичности. В агентной системе ошибка часто прячется не в модели, а в смешении контекста.

identity_invariants:
  same_trace_id_for_all_events: true
  same_session_id_for_run_events: true
  same_tenant_id_for_policy_and_approval: true
  same_principal_id_or_explicit_delegation: true
  delegated_scope_visible: true
  runtime_principal_recorded: true

Здесь важны две границы. Первая — пользовательская: кто попросил действие и от чьего имени оно выполняется. Вторая — сервисная: какой runtime principal реально вызывает инструмент. Для опасных записывающих действий эти границы нельзя восстанавливать из текста ответа модели. Они должны быть в событиях политики, подтверждения и выполнения.

Если authorization_mode равен user_delegated, трасса должна показывать delegated_principal_id и delegated_scope. Если действие выполняется платформенным сервисом, это тоже должно быть видно. Иначе постмортем не сможет ответить, это была ошибка прав пользователя, ошибка делегирования или ошибка самой платформы.

15.5. Разбери решение политики до вызова инструмента

Событие tool_policy_decision должно идти перед tool_execution, а не после него. В расследовании его нужно читать как контракт допуска.

Минимально проверь:

tool_policy_review:
  event_type: tool_policy_decision
  capability: create_ticket
  action: approval_required
  reason: approver:manager
  policy_id: support-write-policy-v3
  risk_tier: high
  tool_principal: svc-support-ticket-writer

В учебном рантайме часть полей может называться компактнее: capability, action, reason, policy_id. В промышленной схеме лучше явно различать decision, risk_tier, tool_principal и ссылку на версию policy bundle. Смысл один: команда должна понять, почему инструмент был разрешен, запрещен или отправлен на проверку человеком.

Плохой признак — когда политика видна только в коде. Тогда дежурный инженер после инцидента вынужден реконструировать состояние конфигурации на момент запуска. Хорошая трасса сохраняет решение как факт выполнения.

15.6. Свяжи подтверждение с записью

Для записывающих инструментов approval record должен быть связан с тем же trace_id, session_id, tenant_id, agent_id, capability и idempotency_key, что и последующий путь инструмента.

approval_review:
  event_type: approval_requested
  approval_id: appr_0017
  capability: create_ticket
  reviewer: manager
  status: pending
  idempotency_key: trace-support-042
  capability_session_status: pending
  authorization_mode: user_delegated
  delegated_principal_id: user-42
  delegated_scope: support.ticket:create

Проверь не только сам факт подтверждения. Проверь, какую полезную нагрузку оно защищало. В зрелой системе для этого полезны requested_fields_hash, версия контракта инструмента, срок действия подтверждения и ссылка на policy bundle. Если этих полей еще нет, хотя бы approval_id и idempotency_key должны позволять связать запрос проверки с дальнейшим tool_execution.

Отдельный анти-паттерн — подтверждение, которое живет отдельно от трассы. Тогда оно превращается в административную запись, а не в доказательство безопасного выполнения.

15.7. Раздели span-метрики и structured events

Span отвечает на вопрос "где запуск провел время". Structured event отвечает на вопрос "какое решение было принято и с какими доказательствами". Эти вопросы нельзя смешивать.

Например, span tool:create_ticket может показать, что вызов длился 1800 мс и завершился ошибкой. Но он сам по себе не обязан объяснять, почему инструмент был разрешен, какой reviewer был назначен, какой ключ идемпотентности защищал запись и что система сделала после неопределенности.

Практическое правило такое:

span_use:
  good_for:
    - latency
    - duration_ms
    - parent_child_timing
    - success_or_failure_at_step_level
structured_event_use:
  good_for:
    - policy_decision
    - approval_lifecycle
    - idempotency_evidence
    - side_effect_class
    - verifier_result
    - run_outcome

Если команда пытается засунуть все в span attributes, она быстро получает дорогую и неудобную кашу. Если команда пишет только события без span, она теряет временную структуру. Нужны оба слоя.

15.8. Не прячь side_effect_unknown

Самый опасный момент в кейсе поддержки — тайм-аут после записывающего инструмента. Внешняя система могла создать тикет, но рантайм мог не получить ответ. Если в этот момент трасса пишет success или просто error, она скрывает главное состояние.

Нужный класс исхода должен быть явным:

tool_result_review:
  event_type: tool_execution
  capability: create_ticket
  status: side_effect_unknown
  idempotency_key: trace-support-042
  retry_allowed: false
  recovery_required: manual_reconciliation_required
  external_lookup_key: trace-support-042

После этого запуск не должен молча повторять запись. Он должен перейти к сверке состояния, ручному восстановлению или остановке с явным run_failed. В итоговом run_complete полезно сохранить failure_reason, чтобы сессия, экспорт оценок и постмортем видели тот же факт.

Зрелая система не обещает, что неопределенности не будет. Она обещает, что неопределенность не будет замаскирована под успех.

15.9. Закрой расследование через проверку завершения

Событие verification_result нужно не только для тестов. В расследовании оно фиксирует, что именно команда проверила перед тем, как считать запуск завершенным или безопасно остановленным.

verification_result:
  stop_condition: "no duplicate ticket after create_ticket timeout"
  verification_command: "lookup ticket by idempotency_key trace-support-042"
  verification_result: pass
  verifier_actor: deterministic_gate
  evidence_refs:
    - trace:trace-support-042
    - external_lookup:support-ticket-api

Если проверка не прошла, это тоже результат. Тогда verification_result может быть fail, warning или blocked, а run_complete не должен притворяться штатным успехом. Для эксплуатационной книги это важнее красивого happy path: именно такие ветки потом становятся регрессионными сценариями.

15.10. Преврати находку в eval-регрессию

Последний шаг практикума — не написать постмортем и забыть. Если трасса показала опасный путь, преврати его в оценочный сценарий.

regression_candidate:
  scenario_id: duplicate_ticket_after_timeout
  source_trace_ids:
    - trace-support-042
  labels:
    - support_ticket
    - side_effect_unknown
    - idempotency
    - manual_reconciliation
  expected_outcomes:
    max_ticket_side_effects: 1
    idempotency_keys_present: true
    failed_run_traceable: true
  grading_rules:
    - type: max_side_effects
      expected: 1
      blocking: true
    - type: contains_event
      expected: verification_result
      blocking: true
    - type: contains_substring
      expected: side_effect_unknown
      blocking: true

Так трасса становится не только материалом разбора, но и частью защиты будущих релизов. Новая подсказка, новая модель, новый адаптер инструмента или новая политика подтверждений должны пройти тот же сценарий и доказать, что дубль не появляется снова.

15.11. Контрольный лист ревью trace

Когда расследование закончено, пройди по короткому контрольному листу:

  1. Есть ли один trace_id, который связывает вход, политику, контекст, инструмент, подтверждение и итог?
  2. Видны ли session_id, tenant_id, principal_id и agent_id на критических событиях?
  3. Есть ли policy_precheck до работы агента и tool_policy_decision до вызова инструмента?
  4. Связаны ли approval_requested, approval_id и idempotency_key с записывающим действием?
  5. Отличает ли трасса approval_required, validation_failure, side_effect_unknown, run_failed и run_complete?
  6. Есть ли span для дорогих и долгих шагов, но решения не спрятаны только в span attributes?
  7. Есть ли verification_result, который доказывает стоп-условие или объясняет блокировку?
  8. Можно ли из этой трассы сделать eval-сценарий без ручной археологии?

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

15.12. Что должно измениться в реализации после такого разбора

После первого полноценного trace review обычно появляются очень конкретные задачи:

  • сделать trace_id обязательным на границе входа;
  • протащить session_id, tenant_id, principal_id и agent_id во все критические события;
  • добавить событие tool_policy_decision перед каждым инструментом;
  • связать approval lifecycle с approval_id, capability, reviewer и idempotency_key;
  • фиксировать side_effect_unknown как отдельный класс исхода;
  • добавлять verification_result для опасных веток восстановления;
  • экспортировать трассу и сессию в форму, пригодную для eval dataset;
  • завести регрессионный сценарий для каждого тяжелого инцидента.

Это и есть главный смысл практикума. Наблюдаемость агентной системы считается зрелой не тогда, когда у нее есть много логов, а тогда, когда один спорный запуск можно превратить в доказательство, исправление и проверку будущего релиза.

16. Что делать сразу после этой главы

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

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

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

Шаблон завершения главы

Что запомнить: трасса — это доказательная история запуска, а не просто удобный журнал для отладки.

Типичные ошибки: писать в трассу свободный текст без структуры; терять связь с подтверждениями и политиками; хранить чувствительные данные без удаления или маскирования.

Что проверить в своей системе: какие события обязательны; где видны решения политики, инструментальные вызовы и итог; как трасса помогает расследовать дубль, утечку или неверную передачу управления.

Сопутствующие материалы: используй схему трассировки, запись подтверждения и практические кейсы как минимальный набор проверяемых событий.

Что читать дальше: переходи к Главе 12, чтобы превратить отдельные трассы в цели уровня сервиса и операционные сигналы.

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

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