Головна / Статті

Від «Продовжити» до завершення: стан, схвалення та ідемпотентність для діючих агентів

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

Видавець: 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 було оброблено, додається підтвердження. Це твердження є надійним завдяки всьому, що стоїть за ним.

    У міру зростання системи ті самі запитання продовжують залишатися актуальними. Чи є кожна функціональність у власності конкретної особи? Чи можна перевірити докази, які лежать в основі рішення? Чи зберігає процес роботи невизначеність та може відновитися у разі збою залежностей? Чи може людина бачити саме те, що схвалює, і чи може система довести, що досягнутий бажаний результат? Відповіді показують, де інший спеціаліст-агент може додати цінності, де достатньо звичайної функції та де потрібні більш чіткі межі. Найкращий досвід роботи з агентами може завершуватися простим словом „Готово“, а сама архітектура робить це переконливим.