Галоўная / Артыкулы

Абвязка, цыкл, граф: тры ўзроўні за большасцю збояў прадакшн-агентаў

Збоі прадакшн-агентаў рэдка ідуць ад мадэлі. Раздзяліце абвязку інструментаў, умову спынення цыкла і тапалогію графа 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 і можа свабодна вызываць, чытаць і вяртаць будзь-шта. З яго агент можа робіць толькі тое, што дазволяе харнес.

Большасць праектаў агентаў ніколі не ствараюць такага шару. У навучальных матэрыялах все заканчываецца тады, калі мадэль вызвае інструмент і атрымлеў рэзультат. У іх не адзначаюцца тры сітуацыі: калі інструмент вяртае 10MB-ны блок JSON, які перапоўняе вікно кантэксту; калі інструмент запрашае будзь-яю URL за інструкцыям увведзенага тэксту; і калі падэйсцык, які перапрыбліковываецца, выконваецца два разы, таму што няма ключа ідэмпотэнцыі. Усе тры ўскладненні ўз’язджаюць з проблемамі інтэграцыі, і самэ ўсуненне іх ператварае дэманстрацыйную версію на працоўны сістэма.

Каталаг патэранаў чытальна апісвае прынцыпы выходу у сетка. Домены, з якімі можа зв’язвацца агентскія інструменты, павінны быць обмежаны спіскамі дазволенага і забранага, якія рэалізуеўцы інтэграцыі прыменяюць, чым мадэль выводзіцца за межы сеткавага межовага прастору доверы. Калі мадэль вырашае, чы робіць з’язак, введзення запытанняў можа пахітыльнуць гэты контроль. Перакананне на рэвэлі інтэграцыі ператварае ізоляцыю на структурную характэрыстыку, а не на верагатлівую.

Сама гэта разліка і ўсё, што мае значэння. Мадэлі ўзьмоцьнаявяроўныя і іх можна абмануць, падмесіць інфармацыю або заплутаць. Конструкцыя-фрэймворк ёсць структурным элементам, які выкананае правілі ў кодзе, а не ў запітах. Просіць мадэль ввічліва не запускаць дадзеныя URL-адрэсы — гэта просьба. Зробіць гэта немагчымым — гэта гарантыя.

Цыкл: калі агент зупініцца

Цыкл являе сабою двігун. Агент спазірае на свет, выбірае дзеянне, яго выкананае, пераканальваецца ў выніку і пасля таго альбо продаўжае, альбо зупініцца.

Цыкл «спазіранне-дзеянне-пераканальванне» — гэта простая частка. Складная частка — умова зупінкі, бо без яе агент, які не кантролюецца, будзе вітрачаць токены назаўсёды і не будзе явна выказваць проблемы.

Агент з пошуку паджэроў не меў нічога. Ён вызваў query_suppliers, пераглядаў рэзультаты, вырашаў, што патрэбна ўсё большая колькасць, і зноў вызываў, і зноў. LLM продавжаў казаць, што патрэбны ўсё большыя даны, таму што мовныя мадэлі надаюць прыорытэт ўсё большым вызовам аперацый: яны ўжытковыя, яны працуюць, і не хочуць здавацца. Гэтая якосць ўскладнюе віконанне творчых заданняў, але дапамагае пад час віробнічых задач.

Каталог шаблонаў называе гэта безмежным цыклам. Яго рэалізацыя выглядае так: «ітэравацыя, пакуль мадэль адзвярцаецца, што работа не завершана», без жаднага лімітара, таймера чы кантролю зза меж мадэлі, які мог бы яго зупініць. Неявныя прыяпушчэнні — што мадэль сама адзвярнёцца і паведаміць пра завершэння. Пад час складных заданняў гэта рэдка: мадэль пачынае рашучыцца на суседнія пытанні, павтарае невыконаныя дзеянні і ціклічна страждае ад адных і тых сабе памылак, не усвядомляючы цыклу.

Гэта не гіпотэза:

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

    Таму умова завершэння павінна быць механічная, а не семантычная. Фраза «Велікі мовы на базе ІІ думаюць, што работа завершана» ўсё ў сферы семанікі. Фразы «Было здано тры спробы» або «Было выдана 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, таму жадны інструмент не можа звяжыцца з будзь-якім адрэсам. Рашэнне прымеўае структура, а не мадэль.
    • Обмежэнню выходных дадзеных. Рэзультаты скрацваюцца да 64KB перад тым, як яны прабяжуць у вікно кантэкста. Блоб размерам 10MB прыбывае скрацаны з пазначкай _truncated: true.
  • Автавыключыкі. Кожны інструмент мае свой автавыключык, які выкорыстоўваецца ў всіх працэсах працы агента. Пяць адмов паспяльна запускае автавыключык, і пасля чаго агенты швытаюць працу, замест таго каб продаўляць запуск нефункцыональнага API.
  • Ліміты паралельнай працы. Ліміты паралельнай працы для кожнага інструмента не дазволяюць аднаму повольнаму інструменту займаць усі доступныя рэсурсы працоўніка.
  • Клучы ідэмпатэнтнасці. Для інструментаў, якія маюць побачныя эфекты, сістэма стварае клуч на адной заснове ID працэсу і назвы крока. Практыкаваная знову дзеянне отрымае вялікі рэзультат, які ўжо існуе, і не ствараецца дублікат.
  • Нічога з гэтага не ўзята з Temporal чы LangGraph. Гэта слой, які робіць вызыв інструментаў безпечным, і які большасць праектаў агентаў праігноравае.

    Перакананне: «агент сказаў, што гэта працавае» проты «гэта працавае»

    Трэці крок ціклу — гэта перакананне, і ён павінен быць зовнішнім. Гэта не можа значыць, што агент пераглядае свой сабе вынік, або што LLM оцэнюе сваю сабе працу.

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

    Команда Honk у Spotify сталкалася самэ з гэтым. Іхнія агенты іноды заявлялі „тэсты працавалі, проблем не знайдзена“ і мелі неправу. Яны включылі дэтэрміністычныя верыфікаторы для фармату, зборкі і тэстав у цікл і заблокавалі стварэнне запросаў пры выкарыстоўванні, калі хоць адні з верыфікатораў не працаваў. Тэксты агентаў больш не сталі сігналам успеху; іх заменіў структураваны вынік верыфікатора.

    Іншыя праекты заходзяць да большага. Адзін з інструментаў на адкрытым кодзе альбо мае падтверджэнне пра тое, што «усе тэсты працавалі», альбо пазначае тэзу як непераканае, а сам факт успеху каманды ніколі не выконвае апдэйта семантычнай тэзы. У научным артыкуле гэтае правіле сформулювана так: неабходна перапрацаваць тэзу і не даваць яй паверы, таму што колькасць слоў агента не мае жадных значэнняя як вхідны даны.

    У стаке, які тут опісан, перакананне ўтварае адзінэчную Тэмпаральную дзеяннё, такую як verify_po_exists або verify_payment_initiated, якая безпасэродна запытае базу дадзэння. Агент стварыў замовленне на пакупку. Дзеяннё пераканваецца ў базе дадзэння замовленняў, і калі замовлення няма, тады рабочы працэс збіваецца, незалежна ад таго, што агент задаў.

    Самастаўленне — гэты сігнал, які з наібольшай верыгоднасцю можа быць некорэктным спосабам, якія вельмі складна выявіць, таму перакананне павінна быць зовнішнім. Агент пад назвай create_po аналізав адпаведзь і стварыў утварэнне, што все працавае. Аднак адпаведзь могла быць кодам 200 OK з тэкстам пра бяду, або замовленне могла быць запісанае, а пасля — анульаванае з-за таймауту транзакцыі, або агент могаў прачытаць некарэктны поле і запісаць ID, якога ніколі не існавало. Агент ведае толькі, што вернула аплікацыя, але нічога не ведае пра стан базы дадзенаў. Таму запыт кierуецца да базы дадзенаў.

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

    Прынцып раздзелення

    Кантрольныя правілы: кожны слой павінен быць можлівым да тэставання, замены і розумеў без іншых слоёў.

    • Граф можна пераканалізаваць за дапамою макіяваных вызваў інструментаў, чым паўтарная пераканалізаць, што правыя вузлы запускаюцца у правым парадку.
    • Харнес можна працаваць, вызваючы інструменты безпасяродна і пераканалізаваючы верыфікацыю схемы, механізмы захоплення ціклу і прынцыпы ідэмпотэнцыі.
    • Цікл можна перапрацаваць за дапамою простай актываціі ўнутрь рабочага прайсупутку Temporal, чым паўтарная пераканалізаць, што ён застаецца працэсным пасля збою.

    Змена графа не паўтарная пераканалізаць харнес, змена рэалізацыі інструмента не паўтарная пераканалізаць граф, а змена політыкі павторных спроб ціклу не паўтарная пераканалізаць ні тое, ні другое.

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

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

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

    1. Чым ён можа касацца? (Крэпліца) Калі існуюць інструменты, з якімі схемамі, які об’ём выходу дазволены, дзе знаходзяцца прагі автавыключальніка і якія ключы ідэмпотэнцыі прыменяюцца.
    2. Калі гэта завершыцца? (Цікл) Пярыяякі максімумы — пярыякі максімум крокаў, косту, часу і механічная умова завершэння, якая не залежыць ад рашэння LLM.
    3. Куды гэта можа пайсці? (Граф) Вузлы, рэшткі, умовныя галузі, пункты контролю ад чалавека і форма стану, якая трактуецца між вузламі.

    Калі адпаведны адказ — «мадэль выбірае», значы існуе проблема. LLM дае рэкамендаціі, код прыме рашэнне, інструменты яго рэалізуюць, граф направляе траекторыю, а цікл задае межы.

    Корача кажучы, існуе тры слоі, трое власнікаў і тры пытанні. Якшто іх сумешаць, багі выглядаюць як проблемы мадэлі, хоць на самай працэ не ёсць. Якшо іх трыважыць, ненадзеямасць мадэлі застаецца пад контролем, інструменты можна без апасцярыбаў вядомаць, а агент завершвае работу, калі трэба.

    Супаўзвязаная літэратура

    • Your Agent Graph Belongs in YAML: Compiling a 17-Agent LangGraph at Startup — Як невелікі механізмы выканання кампілююць адны файл YAML у становы граф LangGraph: стваранне стану, плагіны importlib, маршрутызаторы пазначэнняў, адаптываныя вызовы і змены вузлаў за кожную сесію.
    • When the Dashboard Is Green and the Agent Is Wrong: Tracing State — Як неудачы агентаў хаваюцца ў стане, які перадаецца между правільнымі крокамі, і што патрэбна зафіксаваць за дапамогою трэйсінгу, абмежэнняў выканання, запісаў маршрутызаціі і ацэнак.
  • Стварэнне цыклу пасляўжэння агента з аднойчынам LangGraph і аднойчынамі LangSmith — Пасляпасовы працэс работы з LangGraph і LangSmith: збір дадзэнняў, тры слоі ацэнкі, кластэруванне неудач, наборы дадзэнняў для рэгрэсійнага аналізу па зніжанню сіткі і прадзеўственныя аднойчыны.
  • Перш чым AI-агент павінен працаваць у працэсе вырабоцтва: контракты, графы прэтэнзій, тэставанне ў тэневым рэжыме і канаркі — Працэс выпуску AI-агентоў, які спачатку атрымлівае доказы: контракты з чысткім межам, сцэнарыі з станамі, введэнне бягункі, межы неяснасці, паэтапныя канаркі і прымусовая паднятка рангу агента за дапамогою машыны.
  • Аддыяванне агентаў AI да мікросэрвісаў Spring Boot з аднойчы Spring AI — практычныя нарады па абгортаванні інструментаў за дапамойкай Spring AI, аддыяванні прыбораў для захавання стабільнасці і метрык токенаў, а таксама асінхроннам выкананні цыклаў агентаў у існуючых сэрвісах Spring Boot.
  • Агенты, стойкія да павторнага выканання: адсакладжанне вызоў LLM у рамках рабочага прайсупутку за дапамойкай Temporal — чаму агент, які зупініўся пад час выканання, не павінен павторна вызываць свой LLM: актывацыі як межа недэтэрмінізма, механізмы павторнага выканання, перацягванне часу выканання, прабавы і графы без стану.
  • Размешчэнне агентаў AI у працоўным сераўеры: калі выбіраць хмару, самастойнае размешчэнне чы гібрыдную мадэль — перадалі — Пораўнянне спосабаў размешчэння агентаў у воблаку, самастойна і гібрыдна ў аспектах адкрытасці дадзенаў, костава, залежнасці, маршрутацыі і можлівасцяў адзірвання, з чатырма пытаннямі для выбору і памылкамі, якіх трэба утрымівацца.