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

Ошибки, ретраи, программные gate'ы, guardrails

Ошибки неизбежны — отличайте их виды

Агент ходит в сеть, дёргает внешние API и доверяет недетерминированной модели — сбои будут. Ключ к надёжности — различать виды ошибок и реагировать по-разному.

  • Временные (transient): таймауты, 429 (rate limit), 5xx, обрыв сети. Их имеет смысл повторить.
  • Постоянные (permanent): 400 (плохой запрос), 401/403 (доступ), невалидные аргументы. Повтор не поможет — нужно чинить запрос или сообщать об ошибке.
  • Ошибки инструментов: инструмент вернул ошибку (нет такого заказа). Часто это не сбой агента, а нормальный результат, который надо вернуть модели — пусть учтёт.

Отдельно: ошибку инструмента лучше вернуть модели текстом, а не «ронять» цикл — тогда агент сможет переспросить или попробовать иначе.

Ретраи с экспоненциальной задержкой

Временные ошибки повторяют с экспоненциальной задержкой (backoff) и небольшим случайным разбросом (jitter), чтобы не «долбить» сервис синхронно. Обязательно ограничьте число попыток и общий таймаут — бесконечные ретраи опаснее одной ошибки. Уважайте заголовок Retry-After, если провайдер его прислал.

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

Программные gate'ы и guardrails

Нельзя позволять модели делать что угодно. Между «модель решила» и «действие выполнено» ставят программные gate'ы — проверки в коде, которые модель не может обойти. Примеры: подтверждать необратимые действия (удаление, платёж), ограничивать суммы, проверять права доступа, белый список (whitelist) допустимых операций. Это детерминированная защита: что бы модель ни «захотела», код решает, можно ли.

Guardrails — шире: проверки входа и выхода. На входе — фильтрация недопустимых запросов и попыток prompt injection. На выходе — проверка, что ответ не содержит запретного, что структура верна, что агент не вышел за рамки. Принцип прост: критичные правила навязывайте программно, а не надейтесь, что модель «сама не станет». Доверяй, но проверяй — в коде.

Ретрай с экспоненциальной задержкой только для временных ошибок
func withRetry(ctx context.Context, op func() error) error {
    const maxAttempts = 4
    delay := 500 * time.Millisecond

    var err error
    for attempt := 1; attempt <= maxAttempts; attempt++ {
        err = op()
        if err == nil {
            return nil
        }
        if !isTransient(err) { // постоянные ошибки не повторяем
            return err
        }
        if attempt == maxAttempts {
            break
        }
        select {
        case <-time.After(delay + jitter()):
            delay *= 2 // экспоненциальный backoff
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return fmt.Errorf("операция не удалась после %d попыток: %w", maxAttempts, err)
}
Программный gate: необратимое действие требует подтверждения
// Модель может ЗАПРОСИТь удаление, но выполняем его только через gate.
func dispatchDelete(argsJSON string, confirmed bool) string {
    var a struct{ ID string }
    if err := json.Unmarshal([]byte(argsJSON), &a); err != nil {
        return "ошибка аргументов: " + err.Error()
    }
    if !confirmed {
        // не выполняем — возвращаем модели требование подтверждения
        return "ТРЕБУЕТСЯ ПОДТВЕРЖДЕНИЕ: удаление " + a.ID +
            " необратимо. Переспроси пользователя и вызови снова с confirm=true."
    }
    if err := store.Delete(a.ID); err != nil {
        return "ошибка удаления: " + err.Error()
    }
    return "удалено: " + a.ID
}

Anti-patterns

Анти-паттернПочему плохоКак правильно
Повторять любую ошибкуРетрай 400/403 бесполезен, маскирует багПовторять только временные (429/5xx/таймаут)
Бесконечные ретраи без задержкиDDoS своего же провайдера, зависаниеBackoff + jitter + лимит попыток и общий таймаут
Ронять цикл на ошибке инструментаАгент не может восстановитьсяВернуть ошибку модели текстом — пусть переспросит/обойдёт
Надеяться, что модель «сама не сделает плохого»Необратимые действия без защитыПрограммные gate'ы и guardrails в коде, а не в промпте
Слепой ретрай платежа/отправкиДвойное списание/дубликатыИдемпотентность для неидемпотентных операций

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

  • Классифицируйте ошибки вашего агента на временные и постоянные; напишите isTransient.
  • Оберните сетевые вызовы в ретрай с экспоненциальной задержкой, jitter и лимитом попыток.
  • Сделайте так, чтобы ошибка инструмента возвращалась модели текстом, а не роняла цикл.
  • Поставьте программный gate на необратимое действие (требование подтверждения перед выполнением).
  • Добавьте простой выходной guardrail: проверку, что ответ соответствует ожидаемому формату/рамкам.

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

Какие ошибки имеет смысл повторять с backoff?

  • A 400 Bad Request и 403 Forbidden
  • B Временные: таймауты, 429 (rate limit), 5xx, обрывы сети
  • C Любые подряд, пока не получится
  • D Только ошибки парсинга JSON

Где должны жить критичные правила (например, запрет необратимого действия без подтверждения)?

  • A Только в системном промпте — модель послушается
  • B В коде как программный gate, который модель не может обойти
  • C Нигде — пользователь сам разберётся
  • D В описании инструмента, и этого достаточно

Инструмент вернул «заказ не найден». Как лучше поступить?

  • A Аварийно завершить весь агент
  • B Вернуть этот результат модели как обычный tool-результат — пусть учтёт и переспросит/предложит варианты
  • C Повторять вызов до бесконечности
  • D Скрыть ошибку и ответить, что заказ найден