Главная / Статьи

От «Приступить» до завершения: состояние, одобрение и идемпотентность для действующих агентов

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

Издатель: REACTAPP.TOP14 мин чтения3113 слов

Источники: The Architecture of a Multi-Agent System That Can Actually Take Action

Демонстрация с участием нескольких агентов обычно заканчивается убедительным ответом. Однако производственная система начинает свою самую сложную работу сразу после этого, когда пользователь пишет «Отлично, продолжайте», и программному обеспечению необходимо преобразовать предложение, сделанное в ходе диалога, в реальные изменения в другой системе. В этой статье следуется одна заявка на возврат средств через координатора, специализированного агента, сохраненные предложения, утверждение человеком и выполнение действий; также показано, какие обязанности лежат на модели, а какие должны оставаться в коде приложения. В конце вы получите конкретный чек-лист для определения того, готов ли рабочий процесс с участием агентов к действиям в реальном мире.

Что меняется, когда ассистенту необходимо действовать

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

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

Отслеживание одного запроса на возврат средств

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

  1. Выяснить, о чём просит клиент.
  2. Загрузить информацию о заказе.
  • Найдите действующую политику возврата средств.
  • Сравните заказ с этой политикой.
  • Разрешите все оставшиеся вопросы.
  • Рассчитайте сумму возврата.
  • Составьте предложение.
  • Попросите одобрения.
  • Осуществите возврат средств.
  • Подтвердите, что операция прошла успешно.
  • В программном обеспечении каждый из этих этапов требует ответственного лица и места для выполнения. Эффективная последовательность действий передает запрос от пользователя оркестратору, затем к специалисту, который собирает доказательства и составляет предложение, которое затем утверждается, реализуется и в конце проверяется.

    • Распределение запросов — задача оркестратора.
    • Специалист проводит анализ ситуации.
    • Инструменты соединяют систему с документацией и актуальными данными.
    • Явное описание состояния отражает предлагаемое действие.
    • Пользователь утверждает конкретное предложение.
  • Код приложения выполняет операцию.
  • Проверка определяет, произошел ли желаемый результат.
  • Все это связано одним принципом:

    Модель может определять, что должно произойти; приложение — что допускается к выполнению.

    Приведенные ниже фрагменты представляют собой архитектурные схемы на Python с использованием LlamaIndex. Конфигурация модели, интеграции с хранилищами и сервисами не показаны, а такие обозначения, как llm и policy_retriever, обозначают компоненты, которые должно предоставить ваше приложение. В некоторых фрагментах переносы строк сокращены (например, операторы объединены в одну строку), поэтому их следует рассматривать как краткие описания, а не как код для копирования и вставки.

    Разделение маршрутизации и специализированной работы

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

    Более четкое разделение задач позволяет выделить два аспекта: определение того, к какой группе относится запрос, и выполнение специализированной работы над ним. Для помощника по поддержке это означает наличие координатора, который отвечает на общие вопросы и передает запросы, связанные с возвратом средств, специалисту. Этот специалист создается с использованием FunctionAgent от LlamaIndex:

    from llama_index.core.agent.workflow import FunctionAgent
    

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

    refund_specialist = FunctionAgent(
        name="refund_specialist",
        description="Checks refund eligibility and prepares proposals.",
        system_prompt=(
            "Check the order and the applicable policy. "
            "Explain your evidence before proposing a refund. "
            "Hand back unrelated requests to the coordinator."
        ),
        tools=[get_order, search_policy, prepare_refund],
        llm=llm,
        can_handoff_to=["coordinator"],
    )
    

    Поле description не является декоративным элементом; оно является частью архитектуры. Такой заголовок, как «полезный ассистент», не несет никакой информации. Фраза «Проверяет возможность возврата средств и готовит предложения» четко определяет обязанности, которые можно протестировать и проверить. В LlamaIndex классы FunctionAgent и AgentWorkflow разработаны именно для такого явного передачи задач. Если вы все еще выбираете фреймворк, статья LangChain и LlamaIndex рассматривает более широкий спектр возможных вариантов.

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

    Подключение координатора к специалисту

    Координатор и рабочий процесс, соединяющий обоих агентов, находятся в одном модуле:

    from llama_index.core.agent.workflow import AgentWorkflow
    

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

    coordinator = FunctionAgent(
        name="coordinator",
        description="Answers general questions and routes refund requests.",
        system_prompt=(
            "Use documentation for general questions. "
            "Hand refund requests to the refund specialist."
        ),
        tools=[search_policy],
        llm=llm,
        can_handoff_to=["refund_specialist"],
    )workflow = AgentWorkflow(
        agents=[coordinator, refund_specialist],
        root_agent="coordinator",
    )
    

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

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

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

    Доказательства перед проверкой соответствия условиям

    Отправка запроса правильному сотруднику не гарантирует правильного вывода со стороны этого сотрудника. Специалист всё равно должен собрать доказательства.

    Предположим, что правила позволяют возвращать нераспакованные товары в течение 30 дней, в то время как записи заказов показывают, что заказ №4821 прибыл 12 дней назад. Эти данные поступают из двух разных систем, и обе необходимы: правила определяют условия, а записи заказов описывают ситуацию, причем пригодность товара для возврата становится очевидной только при их совместном рассмотрении. Следовательно, поиск информации становится частью рабочего процесса. Специалист ищет документы с правилами, одновременно загружая текущую запись заказа.

    Минимальный инструмент для поиска правил начинается с асинхронной подписи и документации, объясняющей агенту назначение инструмента:

    async def search_policy(question: str) -> list[dict]:
        """Find policy passages relevant to a customer request."""
    

    В его теле выполняется поиск соответствующих фрагментов, причем каждый из них возвращается вместе с названием источника и разделом, к которому он относится. Как и в предыдущем примере, вызов функции aretrieve и оператор return должны находиться на разных строках:

        matches = await policy_retriever.aretrieve(question)    return [
            {
                "text": match.node.get_content(),
                "source": match.node.metadata.get("title"),
                "section": match.node.metadata.get("section"),
            }
            for match in matches
        ]
    

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

    Цитата не может доказать правильность толкования; она лишь позволяет кому-то её проверить.

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

    История переписки — это не состояние приложения

    Предположим, что расследование завершено. Специалист сообщает, что заказ №4821 подлежит возврату суммы в размере 79 евро, и спрашивает, следует ли продолжать. Пользователь отвечает «да».

    Маршрутизация сработала, расследование было проведено, а инструменты предоставили необходимые данные, однако остается пробел. Система не может с уверенностью указать, что именно было одобрено: заказ, сумма и валюта, а также версия предложения — всё это лишь предполагается, но не фиксируется. Если в разговоре речь шла о двух заказах или сумма была пересчитана после предложения, ответ «да» становится двусмысленным.

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

    pending_action = {
        "proposal_id": "refund-proposal-17",
        "order_id": "4821",
        "amount_minor": 7900,
        "currency": "EUR",
        "status": "awaiting_approval",
        "evidence": [
            "returns-policy:section-3",
            "order:4821",
        ],
    }
    

    Поле status полезно, но именно proposal_id имеет решающее значение. Отметка об одобрении должна указывать на конкретное предложение, которое видел пользователь. Если меняется сумма — это новое предложение. Если меняется порядок — это новое предложение. Если свежие доказательства меняют рекомендацию — это новое предложение. Сумма хранится в виде amount_minor в целых сантимах, что исключает неожиданные ошибки округления с плавающей точкой, а список evidence фиксирует, какая раздел политики и запись порядка послужили основанием для предложения.

    Транскрипция существует для понимания; структурированный состояние — для действий.

    Беседа помогает модели понять, на что указывает фраза «продолжайте». Сохраненное предложение даёт приложению что-то однозначное для выполнения.

    Одобрение как данные, а не слова

    Наивный способ фиксации согласия — это указание на один факт:

    user said yes
    

    Более безопасная модель связывает идентификатор, предложение, действие и контекст воедино:

    user X approved proposal Y,
    containing action Z,
    under the current conditions.
    

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

    • целевая запись
    • сумма
    • валюта
    • доказательства
    • текущий статус
    • уникальный идентификатор предложения

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

    Ожидание является частью рабочего процесса

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

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

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

    async def approve_refund(proposal_id, user):
        proposal = await proposals.load(proposal_id)
    

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

        await permissions.require(
            user,
            "approve_refund",
            proposal,
        )    await proposals.require_pending(proposal)    await refunds.revalidate(proposal)    receipt = await payments.refund(
            order_id=proposal.order_id,
            amount_minor=proposal.amount_minor,
            idempotency_key=proposal.id,
        )    await proposals.mark_completed(
            proposal.id,
            receipt,
        )    return receipt
    

    В этой короткой функции скрыто несколько важных аспектов:

    • Предложение берётся из сохранённого состояния, а не из хода разговора.
    • Авторизация контролируется вне модели.
    • Код подтверждает, что предложение всё ещё ожидает одобрения, что предотвращает его повторную обработку по этому пути (при одновременных операциях эта проверка должна быть атомарной с обновлением статуса).
    • Подходящее состояние предложения повторно верифицируется непосредственно перед выполнением действий.
    • В вызове на оплату используется стабильный ключ идемпотентности.
    • Результат операции сохраняется.

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

    Повторно проверьте перед действием

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

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

    Согласие даёт разрешение на действие; оно не мешает миру меняться.

    Чем дольше предложение остаётся в ожидании, тем важнее это становится. Сочетание повторной проверки с временем истечения срока действия предложений позволяет ограничить период использования устаревших предположений.

    Повторные попытки не должны повторять действие

    Рассмотрим ещё одну неудачу. Пользователь даёт согласие, приложение отправляет возврат средств, платежный провайдер обрабатывает его, но сеть отключается до того, как приходит ответ. Клиент пытается снова. Должен ли клиент получить ещё одну сумму в 79 евро? Очевидно, что нет.

    Идемпотентность предотвращает это. Стабильный идентификатор операции позволяет любому внешнему сервису, поддерживающему идемпотентность, распознавать две запросы как одну логическую операцию. Вместо того чтобы интерпретировать повторную попытку как «осуществить ещё одну возврат средств», сервис рассматривает её как запрос на получение статуса или результата операции, уже связанной с предложением №17. Использование идентификатора предложения в качестве ключа работает потому, что каждое одобренное предложение должно приводить максимум к одному возврату средств. Механизмы на стороне сервера рассматриваются более подробно в статье «Понимание ключей идемпотентности в Node.js POST-эндпоинтах».

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

    «Неизвестно» — это закономерный исход

    Теперь самый неприятный случай: попытка осуществления оплаты завершается из-за истечения времени ожидания. Возможно, ничего не произошло, или деньги были переведены незадолго до прерывания соединения. Для клиента разница составляет 79 евро.

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

    Эта дисциплина соблюдается на каждом этапе, а не только во время оплаты:

    • Когда поиск заказа завершается с ошибкой, существование заказа остается неизвестным, а не опровергнутым.
  • Когда поиск политики завершается с ошибкой, вопрос о политике остаётся открытым, а не получает ответ «нет».
  • Истечение времени ожидания не обязательно означает сбой операции; завершение работы не подтверждено.
  • Надёжные системы рассматривают эти состояния как отдельные, вместо того чтобы объединять их в категории «сбой» или «не найдено».

    Возврат вызова инструмента не означает завершения

    Здесь архитектура выходит за рамки простой оркестрации. Возврат вызова инструмента не равносилен завершению работы. Ответ с ошибкой означает, что работы не завершено. Истечение времени ожидания также означает, что работы не завершено. Если приложение не может определить, произошло ли возврат средств, значит, работы определённо не завершены.

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

    call refund()
    

    Цель в следующем:

    establish that refund proposal #17
    for order #4821
    was successfully processed exactly once
    

    Это совершенно разные гарантии, и именно из-за этой разницы оркестраторы и специалисты являются лишь началом проектирования.

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

    Прежде чем связывать ИИ-работу с операциями, имеющими реальные последствия, ответьте на следующие вопросы:

    1. Есть ли у каждого агента четко определенная ответственность, которую можно сформулировать одним предложением?
    2. Можно ли проследить важное решение до соответствующих документов и записей?
    3. Может ли отсутствующая информация оставаться явно неизвестной, или неопределенность тайком превращается в уверенную рекомендацию?
    4. Хранится ли предлагаемое действие в виде структурированного состояния, а не существует ли оно только в рамках диалога?
    5. Соглашается ли пользователь с конкретным предложением, так что ответ «да» означает одобрение чего-то определенного?
  • Применяются ли разрешения в коде приложения, или модель ограничивается лишь рекомендациями?
  • Проводится ли повторная проверка предложения непосредственно перед выполнением, учитывая возможные изменения условий?
  • Может ли выполнение безопасно проходить при повторных попытках, при этом идемпотентность рассматривается как часть корректности?
  • Подтверждается ли результат до того, как что-либо будет считаться завершенным, вместо того чтобы рассматривать вызов инструмента как доказательство?
  • Если на некоторые из этих вопросов нет четкого ответа, у вас все еще может быть впечатляющая демонстрация, но, скорее всего, пока еще нет системы, которой можно доверять в принятии решений.

    Заключение

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

    По мере роста системы одни и те же вопросы продолжают оставаться актуальными. Кто отвечает за каждую функцию? Можно ли проверить доказательства, лежащие в основе решения? Сохраняет ли рабочий процесс уровень неопределенности и способен ли восстанавливаться при сбое зависимостей? Может ли человек точно видеть, что он утверждает, и может ли система подтвердить наступление желаемого результата? Ответы на эти вопросы показывают, где необходим специалист-агент для добавления ценности, где достаточно обычной функции и где требуется более четкое разграничение. Лучший опыт работы с агентами может завершиться простым словом «Готово», а архитектура делает этот результат убедительным.