Разработка ИИ-агентов · Модуль 4 · Урок 4.4

Наблюдаемость, логирование, стоимость, кеширование

Без наблюдаемости агент — чёрный ящик

Агент недетерминирован и многошагов: когда он «повёл себя странно», без следов вы не поймёте, почему. Поэтому наблюдаемость встраивают с самого начала, а не прикручивают после инцидента. Минимум, который стоит логировать на каждый запрос: входной вопрос, каждый ход модели (finish_reason), какие инструменты вызваны и с какими аргументами, что они вернули, число оборотов цикла, задержка и токены.

Удобно вести трассировку: один request_id, под которым видна вся цепочка шагов. Логи делайте структурированными (JSON-поля, а не свободный текст) — их проще фильтровать и агрегировать. Для распределённых систем пригодится OpenTelemetry, но начать можно с обычного slog в stdlib.

Стоимость: считать и показывать

Каждый оборот цикла — это оплаченные токены, и в агенте они складываются. Поэтому стоимость надо измерять: провайдер возвращает поле usage (prompt/completion токены, иногда — кэшированные); агрегируйте его по всем оборотам одного запроса и считайте приблизительную цену по карте цен модели. Это ровно то, что делает счётчик токенов в тьюторе этого курса.

Что отслеживать: токены и цену на запрос, среднее число оборотов, долю запросов с эскалацией/поиском, частоту срабатывания safety-cap. Эти метрики ловят регрессии («после правки промпта средняя стоимость выросла вдвое») и помогают выбирать модель: дешёвую — на простые шаги, дорогую — на сложные (см. идею модельных тиров).

Кеширование: платить за повтор один раз

Два уровня кеширования экономят заметно.

  • Prompt caching (на стороне провайдера): большой стабильный префикс — системный промпт, длинные инструкции, справочные данные — помечается как кэшируемый, и его не тарифицируют заново по полной при повторных запросах. Держите стабильную часть в начале и неизменной — так она кешируется. Поле cached_tokens в usage показывает, сколько токенов пришло из кеша.
  • Кеш результатов инструментов (на вашей стороне): если инструмент детерминирован и его вход повторяется (тот же запрос к справочнику), закешируйте результат по ключу-аргументам и не дёргайте внешний сервис снова. Для недетерминированных и изменяющих данные инструментов кеш, разумеется, неуместен.

Кеширование — это про деньги и задержку; наблюдаемость покажет, работает ли оно (растёт ли cached_tokens, падает ли число обращений к инструментам).

Структурированное логирование шага агента (slog): инструменты, токены, задержка
func logTurn(log *slog.Logger, reqID string, turn int, fr string,
    tools []string, usage Usage, dur time.Duration) {

    log.Info("agent_turn",
        "req_id", reqID,
        "turn", turn,
        "finish_reason", fr,
        "tools", tools,
        "prompt_tokens", usage.PromptTokens,
        "completion_tokens", usage.CompletionTokens,
        "cached_tokens", usage.CachedTokens,
        "latency_ms", dur.Milliseconds(),
    )
}
Кеш результатов детерминированного инструмента по ключу-аргументам
type toolCache struct {
    mu sync.Mutex
    m  map[string]string // ключ: name+args -> результат
}

func (c *toolCache) call(name, argsJSON string, run func() string) string {
    key := name + "|" + argsJSON
    c.mu.Lock()
    if v, ok := c.m[key]; ok {
        c.mu.Unlock()
        return v // попадание в кеш — внешний вызов не делаем
    }
    c.mu.Unlock()

    v := run() // промах: выполняем инструмент
    c.mu.Lock()
    c.m[key] = v
    c.mu.Unlock()
    return v
}
// ВНИМАНИЕ: кешируем только детерминированные инструменты без побочных эффектов.

Anti-patterns

Анти-паттернПочему плохоКак правильно
Запускать агента в прод без логов/трейсовИнциденты невозможно разобратьСтруктурные логи и трассировка с request_id с самого начала
Не считать токены/стоимостьСюрприз в счёте, незаметные регрессииАгрегировать usage по оборотам, считать цену, мониторить метрики
Логировать секреты/персональные данныеУтечка через логиМаскировать ключи и чувствительные поля в логах
Кешировать недетерминированные/изменяющие инструментыУстаревшие/неверные результатыКеш только для детерминированных read-only вызовов
Менять стабильный префикс ради мелочейСбивается prompt caching, растёт ценаДержать системный префикс неизменным и в начале

Практическое задание

  • Добавьте структурированное логирование (slog) каждого оборота: инструменты, finish_reason, токены, задержку, request_id.
  • Агрегируйте usage по всем оборотам запроса и выведите приблизительную стоимость по карте цен.
  • Включите prompt caching на стабильный системный префикс и проверьте рост cached_tokens.
  • Реализуйте кеш результатов для одного детерминированного инструмента и измерьте сокращение внешних вызовов.
  • Убедитесь, что в логи не попадают ключи API и персональные данные.

Проверка знаний

Что минимально полезно логировать на каждом обороте агента?

  • A Только финальный ответ
  • B request_id, finish_reason, вызванные инструменты и аргументы, результаты, токены и задержку
  • C Ключ API для отладки
  • D Ничего — логи замедляют агент

Какие инструменты безопасно кешировать по аргументам?

  • A Любые
  • B Только детерминированные read-only: одинаковый вход всегда даёт одинаковый результат и нет побочных эффектов
  • C Те, что меняют данные (запись, оплата)
  • D Никакие

Почему важно держать системный префикс стабильным и в начале запроса?

  • A Так короче промпт
  • B Чтобы работал prompt caching: неизменный префикс не тарифицируется заново по полной, что видно по cached_tokens
  • C Это требование Go
  • D Чтобы модель отвечала длиннее