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

Інструментальний комплекс, цикл, граф: три шари, що лежать в основі більшості збоїв агентів у продакшені.

Невдачі продакшн-агента рідко виникають через саму модель. Розділіть кабельну схему інструменту, умову зупинки циклу та топологію графа LangGraph — у кожного з цих елементів має бути власник.

Видавець: REACTAPP.TOP24 хв читання5526 слів

Джерела: The Missing Layer in Production AI Agent Stacks

Агент гине під час виконання завдання, і команда звинувачує модель. Вони замінюють її на більш розумну, але агент знову гине. Умову запиту переписують, але проблема залишається. Додають логіку повторних спроб, але нічого не змінюється. Звертаються до постачальника LLM, який повідомляє, що модель поводиться згідно з проектом. Усі заплутані.

Цей цикл повторюється. Те, що здається дефектом моделі, насправді є недоліком механізмів, які її оточують, і ці механізми складаються з трьох шарів, які команди рідко тримають окремо: інструментів, до яких може дістатися агент (інтерфейс), циклу, який визначає момент завершення роботи (луп), та карти місць, куди може піти агент (граф). Коли ці шари змішуються, помилку в лупі чи інструментах звинувачують у моделі, і зусилля спрямовуються на видимий шар, тоді як пошкоджений залишається під ним.

Два шари замість трьох

Більшість стеків агентів для виробництва у 2026 році працюють у два шари. У них є граф, створений за допомогою LangGraph, CrewAI чи OpenAI Agents SDK, та шар стійкості, такий як Temporal, або його зовсім немає. Що опускається, — це механізм керування: шар, який визначає, які інструменти може використовувати агент, які URL-адреси він може викликати, що він може повертати та коли має зупинитися.

Постачальники також рухаються в тому ж напрямку. Claude Code отримав параметр sandbox.network.deniedDomains. GitHub впровадив контроль за брандмауером на рівні організації для свого агента Copilot, а VS Code отримав ключі групової політики, які фільтрують те, до чого може дістатися агент у мережі. У публічному каталозі шаблонів агентів політика виходу в мережу вказана як окремий шаблон. Дослідження також вказують на цей напрямок: дослідження arXiv 2026 року залишало модель незмінною під час кожного запуску, замінюючи лише інструментальне середовище у шести передових моделях, і виявило зниження вартості виконання завдання на 41% та затримки на 44%. Лише кілька команд інвестували в цей рівень.

Жоден з цих елементів не існує ізольовано. Навколо трьох основних шарів розташовані шлюз, який направляє запити та контролює бюджети, пам’ять, яка забезпечує отримання даних, та семантичний кеш, система відстеження кожного кроку, механізми управління, які застосовують квоти та обмеження швидкості, а також система оцінки, яка дозволяє або забороняє розгортання на ідеальному наборі даних. Кожен з цих елементів має значення. Проте жоден з них зазвичай не є причиною збою агента. Це роблять заплутані межі між компонентами, цикли та графи, оскільки наслідком таких проблем легко є помилкове тлумачення як несправності моделі.

Як були знайдені три шари

Причиною стало те, що агент з пошуку постачальників постійно повторював виклик функції query_suppliers. Штучний інтелект постійно повідомляв про необхідність більше даних, цикл продовжувався, а рахунок за токени зростав. Замість вивчення навчальних матеріалів корисними виявилися аналізи після інцидентів, документація з архітектури та звіти про проблеми, і в різних непов’язаних проектах постійно з’являлися ті самі три слова: harness, loop, graph.

У популярних текстах з архітектури агентів цей розподіл описується так: harness охоплює взаємозв’язок одного агента з його оточенням; loop — спосіб керування виконанням окремого агента з плином часу; graph — кількість агентів, інструментів та контрольних точок, які пов’язані між собою в усій програмі. Кожен рівень ґрунтується на рівні нижче, і кожен з’являється лише тоді, коли рівень нижче стає достатньо стабільним.

Коли імена елементів стали відомі, первісна проблема стала очевидною: умова зупинки знаходилася всередині інструменту, а не в циклі. Елементи Harness та цикл були переплутані. Модель не була пошкоджена, а проблема була у межах шарів. Інші також описують подібну ситуацію. Один з експертів прямо сказав: агент, який вважається ненадійним через неправильну модель, зазвичай має проблеми через неправильні шари. У одному інформаційному бюлетені розповідали про засновника, який видалив одну модель, замінив її на, нібито, кращу, але отримав знову той самий несправний агент, оскільки проблема полягала у сусідніх елементах системи.

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

Harness: те, до чого може дістатися агент

Конструкція є межею можливостей. Вона визначає, які інструменти має агент, які у них схеми вхідних та вихідних даних, які дозволи він має, яку пам’ять може читати, які URL-адреси може викликати та які дані може бачити.

Уявіть агента як підрядника. Конструкція — це бейдж, що висить на шнурку: він визначає, які двері відкриваються, які системи є видимими та що дозволено після входу. Він нічого не говорить про інтелект. Він визначає область дії, і саме там також знаходиться безпека. Без неї агент — це лише виклик функції з ключем API, який може вільно викликати, читати та повертати будь-що. З нею агент може робити лише те, що дозволяє конструкція.

Більшість проєктів агентів ніколи не створюють цей шар. Туторіали закінчуються одразу після того, як модель викликає інструмент та отримує результат. Вони ігнорують три ситуації: коли інструмент повертає 10-МБ об’єкт JSON, який переповнює вікно контексту; коли інструмент запитує довільну URL за вказівкою введеного тексту; та коли побічний ефект виконується двічі через відсутність ключа ідемпотентності. Усі ці випадки є проблемами інфраструктури, і саме їх вирішення перетворює демо-версію на продакшн-систему.

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

Саме в цьому й полягає різниця. Моделі є ймовірнісними та можуть бути обдурені, піддані впливу ззовні чи заплутані. Конструкція ж є структурною та забезпечує дотримання правил у коді, а не у запитах. Ввічливе прохання до моделі не відкривати довільні URL — це просьба. Зробити це неможливим — це гарантія.

Цикл: коли агент зупиняється

Цикл є двигуном. Агент спостерігає за світом, обирає дію, виконує її, перевіряє результат, а потім або продовжує, або зупиняється.

Цикл спостереження-дії-перевірки — це проста частина. Складною є умова зупинки, адже без неї агент без нагляду витрачатиме токени вічно та не дасть явного сигналу про збій.

У агента з пошуку постачальників їх не було. Він викликав функцію query_suppliers, переглянув результати, вирішив, що потрібно більше інформації, і знову її викликав, знову та знову. Штучний інтелект постійно повідомляв про необхідність додаткових даних, адже мовні моделі схильні до ще одного виклику інструменту: вони корисні, намагаються та не хочуть здаватися. Ця риса є перевагою під час творчих завдань, але недоліком під час операційних.

Каталог шаблонів називає це безмежним циклом. Реалізація полягає у «ітерації, поки модель повідомляє про незавершену роботу», без лічильника, таймера чи ліміту часу ззовні, які могли б його зупинити. Неявною передумовою є те, що модель сама оголосить про завершення. При складних завданнях це рідко трапляється: модель відволікається на суміжні питання, повторює невдалі дії та постійно повертається до помилок, не усвідомлюючи цього циклу.

Це не гіпотетичний сценарій:

  • У липні 2025 року неконтрольована рекурсія в Claude Code спожила від 16 000 до 50 000 доларів протягом п’яти годин. Краху чи помилки не сталося. Агенти просто виконували інструкції без кінцевої мети.
  • У публічних звітах про баги описується ситуація, коли підагенти створюють нащадків на глибині понад 50 рівнів. Одна сесія використала 4 мільйони токенів протягом п’яти хвилин. Інший користувач заплатив 27,60 долара понад ліміт плану, коли кредити на використання активувалися посеред сесії, і не було можливості зупинити її. У першому випадку корисні дослідження завершилися після трьох-чотирьох агентів, а решта 44 не додали нічого нового. У другому випадку підагенти інтерпретували інструкцію щодо обробки кількох незалежних цілей як дозвіл на створення власних нащадків.
  • У статті від липня 2026 року цю проблему назвали «Нескінченні агентні цикли». Їхній інструмент статичного аналізу IAL-SCAN оглянув 6 549 репозиторії агентів на основі LLM та виявив 68 несправностей у 47 проектах. Шістдесят вісім агентів, які ніколи не завершують свою роботу, — це скоріше бомба з годинником для обліку коштів, ніж просто кількість помилок.
  • Керівництво з проектування обмежених агентних циклів висуває таку саму вимогу: обмеження має бути частиною проектування ще на етапі створення циклу, а не оптимізацією, яку додають після виникнення проблем. Інакше єдиним правилом зупинки буде рішення самої моделі про завершення роботи, що є лише надією, а не реальним контролем.

    Тому умова зупинки має бути механічною, а не семантичною. Фраза «LLM вважає, що робота завершена» є семантичною. Фрази «Було здійснено три спроби» або «Було витрачено 50 центів» є механічними. Механічні умови є детермінованими, піддаються перевірці та залишаються стабільними під час повторного виконання. Вони не залежать від оцінки моделі, яка є ймовірнісною та може відрізнятися під час повторної спроби.

    Агент компанії з аналітики, який працює у режимі чергування, показує, скільки це коштує. Цей інструмент мав на меті виявити первинну причину затримки в роботі платіжного шлюзу протягом менше ніж двох хвилин. Він переглянув журнали подій, отримав 4 200 рядків, стиснув їх, дійшов висновку, що потрібно більше контексту, та почав пошук знову. Після дванадцяти спроб у тикеті все ще не було резюме, тоді як процес використав 380 000 токенів та коштував 47 доларів за використання API. У моделі не було нічого, що б завадило продовженню роботи, і ніщо під час виконання не наказувало зупинитися. Після того, як компанія ввела обмеження на кількість ітерацій, механізми виявлення застою (для виявлення повторюваних майже ідентичних результатів) та контроль бюджету токенів, частка неконтрольованих циклів знизилася на 91%, а середній час вирішення тикета скоротився з 4,1 до 1,6 хвилини. Роботу виконав обмежений цикл, а не більш потужна модель.

    Що містить обмежений цикл

    Приклад аналітики вказує на складові циклу, який не може безкінечно працювати. Кожну з них перевіряє код, що знаходиться поза моделлю:

    • Ліміт ітерацій. Фіксована кількість спроб, тож завдання, яке досягає цього ліміту, закінчується явною невдачею, а не беззвучним продовженням роботи.
    • Перевірка застою. Аналіз останніх результатів, щоб кілька майже ідентичних вихідних даних поспіль вважалися відсутністю прогресу, навіть якщо кількість кроків ще невелика.
    • Ліміт витрат. Бюджет у токенах або грошах, який не може бути перевищений під час виконання, причому механізм контролю знаходиться під час виконання, а не у запиті.
    • Тайм-аут. Ліміт на пройдений час, який допомагає виявити повільні цикли, що ніколи не активують інші лічильники.
    • Обмеження рекурсії для підагентів. Ліміт глибини та загальний ліміт створення екземплярів, щоб делеговане завдання не могло створювати нащадків, які б також створювали своїх нащадків.

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

    Граф: куди може рухатися агент

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

    Основним принципом проектування є чіткість. Цикл ReAct (думати, діяти, спостерігати, повторювати) — це неявна структура з одним вузлом, який сам себе живить, а LLM виступає у ролі функції кроку. Для простих завдань це підходить. Однак як тільки починає мати значення структура, цей підхід перестає працювати. Порівняння ReAct з LangGraph чітко це ілюструє: у першому випадку модель обирає наступний крок на основі запиту та історії, а у другому — розробник обирає його через код та чітке спрямування.

    Різниця стає помітною як тільки потрібен крок людського схвалення. У неявній графі немає поняття „між“: агент переходить від кроку до кроку завдяки власному міркуванню. Додавання контрольної точки означає розрив циклу, введення механізму паузи, екстерналізацію стану та продовження роботи за сигналом. Це фактично адаптація графа під цикл, проте цикл чинить опір, адже він створений для самостійної роботи. У явній графі достатньо додати новий вузол із викликом interrupt(). Граф змінюється, тоді як цикл та супутні елементи залишаються незмінними.

    Один розробник протестував одну й ту саму політику, детерміністичну модель та три сценарії за допомогою звичайного циклу викликів інструментів та явного графа станів LangGraph. Поведінка була абсолютно ідентичною, проте версія з графом складалася з 97 рядків проти 50, що на 94% більше коду; тож на трьох кроках звичайний цикл перемагає за всіма показниками. Потім розробник вказав на те, що таке порівняння не може врахувати. У циклі поведінка випливає з порядку операторів та мусить бути з’ясована шляхом читання їхнього тіла. У графі краї оголошуються як дані, що дозволяє переглядати топологію, її зображувати та відтворювати без необхідності відкривати будь-яку вузол. Значення дорівнює nil на трьох кроках, великому значенню на тридцяти, а ще більшому — коли роботу бере на себе інша особа.

    Це аргумент щодо виробництва. Демонстраційні версії мають три кроки, і вони належать тому, хто їх написав. Виробнича версія має тридцять кроків та належить тому, хто чергує о 3 годині ранку. У такому масштабі чітка топологія є умовою виживання, а не додатковим тягарем.

    Практичний тест для вибору між цими двома формами — це запитання про те, хто буде володіти агентом через шість місяців. Короткий агент з однією функцією, який підтримується його автором, може залишатися простою циклічною структурою. Агент, який потребує людського схвалення, має кілька гілок чи іншого власника, який чергує, вимагає чіткої графічної структури, оскільки топологія стає даними, які можна намалювати, переглянути та повторно використати.

    Як виглядає плутанина між шарами

    Наведені нижче способи збою виникають під час створення продукту, що є безпечним місцем для змішування шарів. Це ті самі проблеми, які з’являються у виробничій среді.

    Конструкція, яка керує циклом

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

    Виправлення є структурним. Вийміть умову зупинки з інструменту та розмістіть її у вузлі графа: інструмент повертає дані, вузол вирішує, чи потрібна ще одна спроба, механізм забезпечує необхідну функціональність, а цикл знаходиться у графі. У заплутаній версії це виглядає так у коді:

    # CONFUSED LAYER — Harness owns Loop logic
    async def query_suppliers(item: str, budget: float, quantity: int, attempts: int) -> dict:
        """Tool decides when the loop stops. Capability + control flow in one function."""
        results = query_mock_marketplace(item, budget, quantity)
    
        # The tool decides when to stop — couples execution logic with capability
        if len(results) > 5 or attempts >= 3:
            return {"status": "STOP", "suppliers": results}
        return {"status": "CONTINUE", "suppliers": results}
    
    
    async def research_node(state: AgentState) -> dict:
        # The node just calls the tool and forwards whatever the tool decided.
        # It has no idea why the loop stopped — that logic is hidden inside the tool.
        result = await query_suppliers(
            state["brief"].item, state["brief"].budget, state["brief"].quantity,
            state.get("attempts", 0) + 1,
        )
        return {"suppliers": result["suppliers"], "status": result["status"].lower()}
    
    # ✅ CLEAN SEPARATION — Harness is pure capability, Loop lives in the Node
    MAX_RESEARCH_ATTEMPTS = 3
    
    async def query_suppliers_impl(item: str, budget: float, quantity: int) -> dict:
        """Pure Harness capability: schema validation, circuit breaker, marketplace call.
    
        The tool has no idea a loop exists. It returns data. It does not decide
        when to stop. Circuit breaker + fault injection live here because they are
        capability concerns (is the downstream reachable?), not control-flow concerns.
        """
        breaker = get_circuit_breaker("query_suppliers")
        if not breaker.allow_call():
            raise CircuitBreakerOpenError("query_suppliers", breaker.state)
    
        maybe_inject("query_suppliers")  # fault injection for chaos tests
        try:
            results = query_mock_marketplace(item, budget, quantity)
            breaker.record_success()
            return {"suppliers": results}
        except Exception as e:
            breaker.record_failure()
            raise
    
    
    async def research_node(state: AgentState) -> dict:
        """Graph Node / Loop: mechanical stop conditions and routing decisions.
    
        The node reads state, calls the tool through the registry, and decides
        whether to stop — based on counters in state, NOT on the LLM's judgment
        and NOT on the tool's opinion.
        """
        brief = state["brief"]
        attempts = state.get("attempts", 0) + 1
    
        # Harness call — schema validation, timeout, output cap all happen inside registry.call
        query_result = await registry.call("query_suppliers", {
            "item": brief.item,
            "budget": brief.budget,
            "quantity": brief.quantity,
        })
        suppliers = query_result.get("suppliers", [])
    
        # Mechanical stop condition — enforced OUTSIDE the tool, in code, on state.
        # Deterministic. Survives replay. Testable without the tool being live.
        if not suppliers and attempts >= MAX_RESEARCH_ATTEMPTS:
            logger.info(
                "STOP_CONDITION_FIRED path=no_matches item=%s attempts=%d max=%d",
                brief.item, attempts, MAX_RESEARCH_ATTEMPTS,
            )
            return {"suppliers": [], "attempts": attempts, "status": "no_matches"}
    
        if not suppliers:
            return {"suppliers": [], "attempts": attempts, "status": "running"}
    
        # Enrich, then mark completed
        enriched = []
        for s in suppliers:
            quote = await registry.call("get_price_quote", {
                "supplier_name": s["name"], "item": brief.item, "quantity": brief.quantity,
            })
            rating = await registry.call("check_seller_rating", {"supplier_name": s["name"]})
            enriched.append({**s, "quote": quote, "rating_info": rating})
    
        return {"suppliers": enriched, "attempts": attempts, "status": "completed"}
    
    
    # The Graph owns WHERE the agent can go — conditional edges, not the node body.
    def _should_retry_or_end(state: AgentState) -> str:
        """After Research: loop back if still running, else Score or END."""
        if state["status"] == "running":
            return "research"          # self-loop — the retry
        if state["status"] == "no_matches":
            return END                 # mechanical stop → graph terminates
        return "score"                 # success → next node
    
    
    # Wiring (in build_sourcing_graph):
    #   graph.add_conditional_edges("research", _should_retry_or_end,
    #       {"research": "research", "score": "score", END: END})
    

    Цикл, який керує графом

    Багато фреймворків надають цикл ReAct, у якому топологія є неявною: агент вирішує наступний крок на основі результатів роботи моделі, і жодної явної карти не існує. Це триває доти, доки між прийняттям рішення та підтвердженням не потрібен крок людського схвалення, після чого починається модифікація, проти якої бореться цикл.

    Рішенням є спроєктування графа явно з самого початку, де вузли є функціями, а ребра — переходами; деякі з них є умовними, а інші виконують функцію переривань. Цикл все ще допомагає агенту просуватися вперед, але граф визначає досяжні місця, а не цикл. Монолітна форма ReAct, у якій цикл є графом, а LLM — функцією ребра, виглядає так:

    # ❌ CONFUSED LAYER — Implicit topology inside a monolithic ReAct loop
    # The loop IS the graph. The LLM is the edge function. There is no "between."
    while not agent.is_done():
        action = llm.decide_next_step(history)
        if action == "query":
            data = query_suppliers()
        elif action == "approve":
            # Retrofitting human approval into an implicit loop requires messy pauses.
            # You have to break the loop, externalize state, park the worker, and
            # resume on a webhook — the loop fights you because it was designed to
            # be self-driving.
            pause_execution_and_wait_for_webhook()
        elif action == "stop":
            break
    
    
    # ✅ CLEAN SEPARATION — Declarative graph with explicit nodes and human interrupt
    from langgraph.graph import StateGraph, START, END
    from langgraph.types import interrupt
    
    
    def approve_node(state: AgentState) -> dict:
        """Human checkpoint via interrupt() — a STRUCTURAL pause, not a prompt.
    
        The graph genuinely suspends here. interrupt() pauses execution and waits
        for a human to call Command(resume=approval_data). The worker is freed.
        If the worker crashes while waiting, Temporal replays the activity, the
        graph re-runs from the last checkpoint, and interrupt() fires again —
        the human re-approves. No data loss.
        """
        selected = state.get("selected_supplier", {})
        brief = state.get("brief", {})
    
        approval_request = {
            "item": brief.item,
            "quantity": brief.quantity,
            "supplier": selected.get("name", ""),
            "price": selected.get("price", 0),
            "total_cost": selected.get("price", 0) * brief.quantity,
        }
    
        # interrupt() pauses the graph HERE. The human must call /approve
        # with a resume value to continue.
        approval = interrupt(approval_request)
    
        approved = approval.get("approved", False)
        comment = approval.get("comment", "")
    
        return {
            "approval_status": "approved" if approved else "rejected",
            "approval_comment": comment,
            "rejection_count": state.get("rejection_count", 0) + (0 if approved else 1),
        }
    
    
    def _after_approve(state: AgentState) -> str:
        """After Approve: go to Confirm if approved, retry Research if rejected."""
        if state.get("approval_status") == "approved":
            return "confirm"
        if state.get("rejection_count", 0) + 1 >= 3:
            return END
        return "research"
    
    
    def build_sourcing_graph(checkpointer=None):
        """Topology is DECLARED DATA, not statement order in a loop body.
    
            START → Research → (retry) → Score → Decide → Approve → Confirm → END
                                                  ↓ (rejected, < 3)
                                                 Research
                                                  ↓ (rejected, = 3)
                                                 END
        """
        graph = StateGraph(AgentState)
    
        graph.add_node("research", research_node)
        graph.add_node("score", score_node)
        graph.add_node("decide", decide_node)
        graph.add_node("approve", approve_node)      # explicit checkpoint node
        graph.add_node("confirm", confirm_node)
    
        graph.add_edge(START, "research")
        graph.add_conditional_edges("research", _should_retry_or_end,
            {"research": "research", "score": "score", END: END})
        graph.add_edge("score", "decide")
        graph.add_edge("decide", "approve")
        graph.add_conditional_edges("approve", _after_approve,
            {"confirm": "confirm", "research": "research", END: END})
        graph.add_edge("confirm", END)
    
        return graph.compile(checkpointer=checkpointer)
    

    Граф, який керує інструментом

    Цей варіант є рідкіснішим, хоча трапляється під час перегляду коду. Вузол графа безпосередньо викликає зовнішній API, наприклад, HTTP GET до кінцевої точки постачальника всередині функції вузла, тож граф контролює цей виклик інструменту.

    Це неправильно, оскільки граф є топологією та не повинен нічого знати про HTTP, автентифікацію, повторні спроби чи механізми захисту від збоїв. Якщо виклик знаходиться всередині вузла, додавання такого механізму означає редагування графа, зміна реалізації також означає редагування графа, а тестування графа вимагає наявності робочого інструменту.

    Щоб виправити ситуацію, виклик направляється через інструментальний комплекс. Вузол запитує call_tool("query_suppliers", {...}) та отримує перевірений, типізований результат. Інструментальний комплекс обробляє HTTP-запити, повторні спроби, механізми захисту та перевірку схеми. Граф не знає про існування HTTP, а інструментальний комплекс — про існування графа:

    # ❌ CONFUSED LAYER — Graph node owns the tool call (Harness concern)
    import httpx
    
    
    def supplier_node(state: AgentState) -> dict:
        """The graph node reaches directly into HTTP, auth, retries, and parsing.
    
        Why that's wrong: the graph is topology. It should not know about HTTP,
        authentication, retries, circuit breakers, or schema validation. Those are
        harness concerns. When the graph owns the tool call:
          - You can't add a circuit breaker without modifying the graph.
          - You can't swap the tool implementation without modifying the graph.
          - You can't test the graph without the tool being live.
          - You can't enforce an egress allowlist — the URL is hardcoded here.
        """
        resp = httpx.get(
            f"https://supplier-api.example.com/suppliers",
            params={"item": state["brief"].item},
            headers={"Authorization": f"Bearer {SUPPLIER_API_KEY}"},
            timeout=10.0,
        )
        resp.raise_for_status()
        suppliers = resp.json()["suppliers"]  # no schema validation, no output cap
        return {"suppliers": suppliers}
    
    
    # ✅ CLEAN SEPARATION — Graph node calls through the harness; harness owns the call
    async def supplier_node(state: AgentState) -> dict:
        """The graph node says 'call this tool with these args' and gets back a
        validated, typed result. The graph doesn't know there's HTTP involved.
        The harness doesn't know there's a graph involved.
        """
        query_result = await registry.call("query_suppliers", {
            "item": state["brief"].item,
            "budget": state["brief"].budget,
            "quantity": state["brief"].quantity,
        })
        suppliers = query_result.get("suppliers", [])
    
        # Mechanical stop condition — the node's job, not the tool's
        if state.get("attempts", 0) + 1 >= MAX_RESEARCH_ATTEMPTS and not suppliers:
            return {"suppliers": [], "attempts": state.get("attempts", 0) + 1, "status": "no_matches"}
    
        return {"suppliers": suppliers, "attempts": state.get("attempts", 0) + 1, "status": "completed"}
    
    
    # What the harness does — the graph never sees this:
    #
    #   registry.call("query_suppliers", {...})
    #     │
    #     ├─ 1. validate_input(QuerySuppliersInput, args)        # Pydantic v2
    #     ├─ 2. EgressFilter(spec.allowed_egress)                # URL allowlist
    #     ├─ 3. SandboxedHTTPClient(egress_filter)               # blocked domains = structural
    #     ├─ 4. run_with_timeout(fn, kwargs, spec.timeout_seconds)
    #     ├─ 5. enforce_output_cap(result, spec.max_output_bytes)  # 64KB cap
    #     └─ 6. validate_output(QuerySuppliersOutput, result)    # garbage caught here
    #
    # The graph node gets back a validated dict. It never sees the HTTP call,
    # the auth header, the retry, the circuit breaker, or the egress filter.
    # Swap httpx for aiohttp, swap the supplier API for a new one, add a circuit
    # breaker — the graph doesn't change. The harness doesn't know a graph exists.
    

    Хто володіє кожним шаром

    Кожному шару потрібна відповідна інфраструктура. Коли власність незрозуміла, шари починають «кровоточити».

    Temporal відповідає за безпеку при збоях циклу

    Якщо двигун зупиняється на півдорозі циклу, Temporal забезпечує відновлення роботи. Він зберігає кожен крок у історії подій. Після збою працівника він повторює робочий процес, не запускаючи дії заново, а вставляючи їхні зазначені результати, тож цикл продовжується з останнього завершеного кроку, а не з нуля.

    Ось чому умова зупинки має бути детермінованою. Умова на кшталт «модель оцінює виконану роботу» є недетермінованою, тож повторний запуск може призвести до іншого рішення. Лічильник кроків, ліміт витрат чи поріг якості, отриманий з результатів інструментів, працюють ефективно під час повторного запуску. Модель може вплинути на рішення, але перевірка if attempts >= 3: stop має бути в коді.

    LangGraph контролює топологію графа

    У LangGraph вузли — це звичайні функції, ребра — це переходи, а умовні ребра — це функції, які повертають назву наступного вузла. LangGraph компілює все це у запусканий граф. Оскільки граф є явним, його можна переглядати, кожен вузол можна перевірити окремо, а вузол можна додати, не порушуючи роботу решти. Точка людського схвалення — це просто вузол, який викликає interrupt(), що змінює граф, залишаючи цикли та інші елементи незмінними.

    LangGraph також контролює форму стану — TypedDict, який передається між вузлами. Кожен вузол читає те, що йому потрібно, та записує те, що він генерує, тож стан виступає у ролі контракту між ними. Вузол, який ніколи не записує дані suppliers, унеможливлює їх читання наступним вузлом, і перевірник типів виявляє це ще до запуску програми.

    Інструментальний механізм, створений вручну, керує всім іншим

    Це той рівень, який не надається ні Temporal, ні LangGraph, тому його потрібно створювати з нуля. Він обробляє:

    • Реєстрацію. Кожен інструмент вказує назву, схему вхідних даних у форматі Pydantic, схему вихідних даних, таймаут та чи потрібна йому ідемпотентність.
    • Валідацію. Вхідні дані перевіряються перед запуском інструменту та після його виконання, щоб уникнути некоректних відповідей ще до того, як агент їх побачить.
    • Фільтрацію вихідних даних. Інструменти, які здійснюють HTTP-запити, проходять через клієнта в ізольованому середовищі з списком дозволених URL, тож жоден інструмент не може звернутися до довільної адреси. Рішення приймає механізм, а не модель.
    • Обмеження обсягу вихідних даних. Результати обрізаються до 64 КБ перед тим, як вони потраплять у вікно контексту. Блок даних обсягом 10 МБ надсилається у скороченому вигляді з позначкою _truncated: true.
    • Вимикачі ланцюгів. Кожен інструмент має свій вимикач, який використовується у всіх робочих процесах агентів. П’ять невдалих спроб поспіль призводять до вимкнення ланцюга, після чого агенти швидко зазнають невдачі замість того, щоб безуспішно намагатися взаємодіяти з несправною API.
    • Обмеження одночасності за інструментом. Ліміти одночасності для кожного інструменту запобігають тому, щоб один повільний інструмент займав усі доступні ресурси працівника.
    • Ключі ідемпотентності. Для інструментів із побічними ефектами система створює ключ на основі ідентифікатора робочого процесу та назви кроку. При повторній спробі виконання завдання повертається вже наявний результат, і дублікат не створюється.

    Усе це не походить від Temporal чи LangGraph. Це шар, який робить виклик інструментів безпечним, і саме його зазвичай пропускають у більшості проектів агентів.

    Перевірка: „агент сказав, що це спрацювало“ проти „це справді спрацювало“

    Третім кроком циклу є перевірка, і вона має бути зовнішньою. Це не може означати, що агент перевіряє власний вихід чи що ШІ оцінює власну роботу.

    У каталозі шаблонів це виправлення називається журналом перевірок: самостійно надані повідомлення на кшталт „збірка пройшла“ поступаються місцем структурованим записам, де кожна перевірка є операцією INSERT у базу даних, а кожен набір доказів — операцією SELECT. Якщо залишити все як є, агент може уявити, що перевірки пройдені, пропустити кроки, не зазначаючи цього, або стверджувати про результати від інструменту, який він ніколи не використовував.

    Команда Honk у Spotify зіткнулася саме з цією проблемою. Їхні агенти іноді стверджували, що „тести пройшли успішно, проблем не виявлено“, але помилялися. Вони вбудували в цикл детерміністичні перевірювачі формату, збірки та тестів та заблокували створення запитів на зміни, як тільки будь-який з перевірювачів дає негативний результат. Текстові повідомлення агента більше не були сигналом успіху — їх замінив структурований вихід перевірювача.

    Інші проекти йдуть ще далі. Один із інструментів з відкритим кодом або містить підтвердження про „все зроблено, усі тести пройдені“, або позначає твердження як неперевірене, причому сам факт пройдення команди ніколи не призводить до оновлення семантичного твердження. У науковій статті це правило сформульовано так: необхідно заново обчислити твердження та не довіряти йому, оскільки кількість слів агента не має жодної цінності як вхідні дані.

    У стеку, описаному тут, перевірка є окремою темпоральною діяльністю, такою як verify_po_exists або verify_payment_initiated, яка безпосередньо запитує базу даних. Агент стверджує, що створив замовлення на покупку. Ця діяльність перевіряє базу даних замовлень, і якщо замовлення відсутнє, робочий процес завершується незалежно від того, що повідомив агент.

    Саморепорт є сигналом, який найчастіше може бути неправильним у способи, які найважче виявити, тому перевірка має бути зовнішньою. Агент під назвою create_po обробив відповідь та заявив, що все працює. Проте відповідь могла бути кодом 200 OK із тілом помилки, або замовлення могло бути створене, а потім скасоване через тайм-аут транзакції, або агент міг прочитати неправильне поле та повідомити про ID, якого ніколи не існувало. Агент знає лише те, що повернуло інструмент, і нічого про стан бази даних. Тому запитується база даних.

    Докази з процесу виробництва підтверджують це. У статті від липня 2026 року розглядалася Лені — бізнес-аналітик ШІ, чиї цикли верифікації виконують завдання шляхом аналізу, спостереження, порівняння та корекції за допомогою невеликих моделей, спеціалізованих на конкретних завданнях. Повна система перевершила свою базову модель на 11 відсоткових пунктів у тесті SpreadsheetBench: 91,25% проти 80,25%. Сама верифікація додала лише незначні 1,5 пункту, але вони зосередилися у верхній частині діапазону оцінок, де дозволили виконати завдання, які інакше б зазнали невдачі. Більша частина загального покращення походить від структурних елементів, механізмів направлення завдань та спеціалізованих моделей. Верифікація не рятує кожен випадок; вона допомагає у межових ситуаціях, коли агент стверджує про завершення завдання, а реальність це заперечує.

    Принцип розділення

    Керівний принцип: кожен шар має бути піддаваним тестуванню, заміні та зрозумілим без інших шарів.

    • Граф можна перевірити за допомогою імітації викликів інструментів, щоб підтвердити, що відповідні вузли запускаються у правильному порядку.
    • Комплект інструментів можна протестувати шляхом прямого виклику інструментів та перевірки валідації схеми, механізмів захисту від збоїв та властивостей ідемпотентності.
    • Цикл можна протестувати за допомогою простої дії всередині робочого процесу Temporal, щоб підтвердити, що він продовжує функціонувати після збою.

    Зміна графа не впливає на комплект інструментів, заміна реалізації інструменту не впливає на граф, а зміна політики повторних спроб циклу не впливає ні на один з них.

    Користь є практичною. Граф агента з пошуку джерел змінився тричі: з’явився вузол оцінки, цикл повторних спроб із врахуванням відхилень та контрольний пункт схвалення людиною, причому код harness не змінювався. Постачальник LLM перейшов від прямого використання OpenAI до використання шару маршрутизації, не торкаючись коду графа. Політика повторних спроб змінилась від агресивної на суворо заборонену для операцій із побічними ефектами, без змін у графі чи harness. Розділення елементів забезпечує більшу гнучкість, ніж чистоту: можливість змінити одну річ, не пошкодивши трьох інших.

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

    Три запитання для проектування будь-якого агента

    1. Що він може торкатися? (Кріплення) Які інструменти існують, з якими схемами, який обсяг вихідних даних дозволений, де розташовані пороги автоматичного вимкнення та які ключі ідемпотентності застосовуються.
    2. Коли це припиняється? (Цикл) Максимальна кількість кроків, максимальна вартість, таймаут та механічна умова зупинки, яка не залежить від рішення ШІ.
    3. Куди це може піти? (Граф) Вузли, ребра, умовні гілки, людські контрольні точки та форма стану, яка передається між вузлами.

    Якщо будь-яка відповідь — „модель вирішує“, це ознака проблеми. ШІ дає рекомендації, код приймає рішення, механізми забезпечують його виконання, граф спрямовує потік даних, а цикл обмежує кількість кроків.

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

    Пов’язана література

  • Створення циклу вдосконалення агента за допомогою LangGraph та оцінок LangSmith — Поетапний процес використання LangGraph та LangSmith: збір даних, три рівні оцінювання, групування помилок, набори даних для регресійного аналізу офлайн та попередні тести перед випуском.
  • Перш ніж AI-агент отримає право на роботу в продакшені: контракти, графи тверджень, тестування в тіні та канарки — Процес випуску AI-агентів, заснований на доказах: контракти з чіткими обмеженнями, сценарії зі змінною станом, введення помилок, межі невизначеності, поетапне тестування канарками та автоматичне підвищення рівня доступу.
  • Додавання AI-агентів до мікросервісів Spring Boot за допомогою Spring AI — практичний посібник з використання інструментів Spring AI, додавання механізмів захисту та метрик, а також асинхронної роботи агентських циклів у наявних сервісах Spring Boot.
  • Агенти, стійкі до повторного виконання: уникнення використання LLM у робочому процесі за допомогою Temporal — чому агент, який зупиняється під час роботи, не повинен знову запускати свої запити до LLM: функції як межа недетермінізму, механізми повторного виконання, перевірки стану, спроби повтору та безстанові структури.
  • Розгортання AI-агентів у продакшн: вибір між хмарою, самостійним розгортанням чи гібридним підходом — Порівняння способів розгортання агентів у хмарі, самостійно чи в гібридному форматі з точки зору рівня відкритості даних, витрат, залежності від певних рішень, маршрутизації та можливостей моніторингу, разом із чотирма запитаннями для вибору та помилками, яких слід уникати.