AI-агент повторяет действия: как не создавать дубли после таймаута

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

Где появляется повтор

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

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

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

Одна операция — один результат

На стороне сервиса, который создаёт заявку, храните ID операции вместе с результатом. При повторе с тем же ID и тем же содержанием верните старую запись. Если под тем же ID пришло другое содержимое, сообщите о конфликте: молчаливо возвращать прежний результат здесь опасно. Для конкурентных вызовов опирайтесь на уникальное ограничение в базе, а не только на проверку «сначала найти, потом вставить».

Следующий пример использует SQLite в памяти и показывает последовательный повтор. Он не обращается к внешнему сервису; в настоящей системе такую таблицу размещают в устойчивой базе сервиса, который выполняет действие.

import sqlite3

db = sqlite3.connect(':memory:')
db.execute('''CREATE TABLE tickets (
    id INTEGER PRIMARY KEY,
    operation_id TEXT NOT NULL UNIQUE,
    body TEXT NOT NULL
)''')


def create_ticket(operation_id, body):
    previous = db.execute(
        'SELECT id, body FROM tickets WHERE operation_id = ?',
        (operation_id,)
    ).fetchone()
    if previous:
        if previous[1] != body:
            raise ValueError('same operation_id with different body')
        return previous[0]
    with db:
        cursor = db.execute(
            'INSERT INTO tickets (operation_id, body) VALUES (?, ?)',
            (operation_id, body)
        )
    return cursor.lastrowid


first = create_ticket('op-42', 'Ошибка экспорта')
again = create_ticket('op-42', 'Ошибка экспорта')
new_request = create_ticket('op-43', 'Ошибка экспорта')
count = db.execute('SELECT COUNT(*) FROM tickets').fetchone()[0]
print(first, again, new_request, count)

Код вычислит 1 1 2 2: повтор вернул прежний ID, а новая операция создала вторую запись. Проверьте также конфликт аргументов с тем же ID и одновременные запросы. Последние требуют реальной базы и обработки её ошибки уникальности; пример в памяти доказывает только поведение последовательных вызовов.

Как связать это с агентом

В агентном приложении создайте operation_id до вызова инструмента и передайте его как служебный аргумент через оркестратор. При таймауте повторите тот же вызов с тем же ID. Инструмент должен вернуть прежний ticket_id или явную ошибку конфликта. Не берите новый ID из очередной генерации модели. Для шага, который только читает данные, такое хранилище обычно не нужно; оно важно для действий с внешним эффектом.

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

В платном курсе AI Agents PRO: LangGraph, AutoGen и LLMOps студент строит агента с графом состояний, инструментами и API, а затем отрабатывает повторы, очередь неуспешных задач, идемпотентность и наблюдаемость. На открытом уроке «FSM и DAG: оркестрация шагов» можно увидеть, как действия раскладываются по состояниям. Открыть программу курса; для самостоятельной проверки есть подборка задач по ответам агента.

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

Продолжить на практике

  • AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшне ↗

    Практический курс по AI-агентам. Соберёте продакшн RAG-сервис: от индекса и графа состояний до API, Docker, метрик качества и контроля стоимости. Плюс профессиональные блоки: LangGraph/AutoGen, Qdrant/FAISS+rerank, eval (RAGAS/custom), quality-гейты, OpenTelemetry+Prometheus/Grafana, CI/CD, ретраи/DLQ/идемпотентность, PII/RBAC.