Умовна зупінка інструментаў: адмініструемыя рэсурсы перамагаюць метод return_direct у LangGraph.
Зупініце або продовжыце выконанне за кожны вызов інструмента ў LangGraph за дапамою артэфактов інструмента, у ручна створаным цыкле ReAct і ў проміжнім модулі create_agent, калі return_direct не можа прымець рашэння.
Крыніцы: **Conditional Tool Stopping in LangGraph: Routing on Tool Results**
Калі статычны параметр return_direct ўзьміцца не прыдатна
Кожны, хто выканаў розпакаванне агента для вызову інструментаў у LangGraph, стыкаўся з параметрам return_direct=True: у такім случыку рэзультат інструмента не адправляецца назад праз мадэль, і цыкл завершаецца. Усё выглядае ідэальна, пакуль рашэнне пра зупінку не должна завісіць ад рэзультата данага вызову, а не ад таго, які інструмент быў зарэгістраваны.
У данай інструкцыі мы сталкнуліся з гэтым працоўным абмежэнням, вручную створылі мінімальны цыкл ReAct, а пасля перадаў тое жа поведанне за дапамогою create_agent і мідлвэра. Короткая адказ: так, гэта працюе — але першы падход з вікорыстанням мідлвэра можа зазнаць бягучых проблем з порадкам выдавання паведамленняў, а не таму, што фрэймворк таямніча ігнаруе абнаўленні.
Для яснасці: «legacy» абярае langgraph==0.6.6 (пасляльнія лінія перад create_react_agent была заменена на create_agent); «current» абярае langchain==1.4.2, які выкарыстоўвае langgraph==1.2.11. Кожны агент ReAct — гэта цыкл модэлю ↔ інструментам; тут акцэнт ставіцца на звязак ад інструментаў назад да модэлю, а таксама на тое, калі гэты звязак павінен з’яўіцца для конкрэтнага вызову.
Тыя выкліканні, якія спакусілі разбіць стандартны цыкл
Інтэграція інструмента пошуку здавалася звычайной: модэль выклікае search(query), чытае рэзультаты, адпавядае або продаўжае работу. Две характэрыстыкі зробілі стандартны цыкл непадходзячым.
Калі пошук быў успешны, інструмент вяртаў большы JSON-файл. Перадача гэтага файла знову ў кантэкст для наступнага апрацоўкі модэлю ёсць дорогай і зазвычай беспраганной — калі пошук ужо адпавяў на запыт, другі вызов пераважна толькі перафразуе адпаведзь за вялікія затраты.
Калі спроба не ўспех, адмовы дзеляцца на працупакі: справжня тупікавая сітуацыя (нема чаго падбіраць — перапрыткі марныя) проты тымчасовага перыяду чакання або коду 503 (перапрыткі маюць сэнс). Таму правілы залежнаць ад кожнага вызову: успех → завершыць, адмова, яку можна перапрыткі → продаваць, фатальная адмова → завершыць — рашэнне вырахоўваецца на адвярсе дадзеных, а не на статычным тыпе інструмента.
Чаму return_direct не можа гэта сказаць
У файле langgraph.prebuilt.chat_agent_executor на зафіксаванай версіі легасі-програмы маршрутызацыя выглядае так:
should_return_direct = {t.name for t in tool_classes if t.return_direct}
...
def route_tool_responses(state):
for m in reversed(_get_state_value(state, "messages")):
if not isinstance(m, ToolMessage):
break
if m.name in should_return_direct:
return END
...
return entrypoint
should_return_direct вырахоўваецца аднойчы на час стварэння графа з атрыбута .return_direct інструмента. Штандарт означае “гэты інструмент завжды завершае цыкл”. У яго няма рэжыму для кожнага вызову. Гэта несасвяранасць катаграфій, а не дыфект штандарта.
У тэпах форума адгукуюць тыя ж проблемы: вельмі большыя рэзультаты пры выкарыстоўванні інструмента змушваюць до непатрэбных дадзеных вызоў модэля, а адпаведальныя за тыя інструменты часта прасіць ручна падключыць tool_node → END. Аднаэтажныя дыскусія ўжо пра абнаўленні Command і return_direct (заўключаючы langgraph#5496) дапамагаюць у розумэнні кантэксту; галоўная ідэя не выклікае патрабавання ў выявленні бага — статычныя флагі проста не можуць падтрымваць дынамічныя рэзультаты.
Форма контрольнага прайму
Як сказаць проста:
Tool call
├── success or unfixable failure → stop, use the tool's result
└── fixable failure → let the model decide
Две меты, якія выбіраюцца кожны раз пад час вызоў. Рэшта часткі коду рэалізуе гэтую форму два разы — аднойчы ручна, аднойчы за дапамогою мідлвэра.
Раздзельныя каналы: content і artifact
У LangChain функцыя @tool ужо раздзеляе тое, што бачыць мадэль, ад таго, што прымае код аплікацыі праз response_format="content_and_artifact". Адказчык вяртае (content, artifact). ToolMessage.content падае модэлі; artifact застаецца ў паведамленні для керування і ніколі не прабрацоўваецца модэллю LLM.
@tool(response_format="content_and_artifact")
def search(query: str):
return "the content the LLM sees", {"stop": True, "debug": "extra stuff"}
node = ToolNode([search])
result = node.invoke(state)
msg = result["messages"][0]
# msg.content -> "the content the LLM sees"
# msg.artifact -> {"stop": True, "debug": "extra stuff"}
Бажаны патэрн:
tool result
│
┌──────────┴──────────┐
↓ ↓
content artifact
│ │
↓ ↓
model router
│
continue / stop
працоўна з тым, калі return_direct спраўляецца ў аднай статычной адказі:
return_direct content_and_artifact
│ │
└── tool content → model
definition artifact → routing metadata
→ routing
Адна фіксаваная пазнака, якая намагаецца адказаць на запытанні як «што бачыць корыстнік?», так і «чы гэты цыкл трэба завершыць?» — альбо два каналы, кожны з якіх адказвае на адна запытанне.
Старыя, ручна створаныя ReAct
У зямест на стварэнне ціклу, трэба скасаваць усе зайвое ў create_react_agent, залішыўшы толькі аснову: зберагчы назвы вузлаў і цікл, адмовіцца ад хуків для запытанняў, структураваных фарматаў адпаведзей, дынамічнага расшуку модэля, ведачэння заставшыхся крокаў, стварэння пунктавых контролі, перэрываў і паралельнаг адправлення пакетаў за дапамогою Send.
Заставаліся тры вузлы:
agent— вызывае модэль; калі існуюцьtool_calls, працягваецца, інакш — завершаецца.tools— простыToolNode; да яго дадаюцца рэзультатыToolMessage.finalize— без вызыва модэля; выбраныя вузлам тэксты паказваюцца у фармацеAIMessageбез жадных змян.
Два маршрутазаводы:
should_continueпасляagent: якшчы ёсць вызывы інструментаў — пераходзіце даtools, інакш —END.
route_after_tools пасля tools: аналізуецца рэзультат работы інструмента, пасля чаго можна або вернуцца да agent, або перайсці да finalize (заменяючы первасны статычны чэк return_direct). finalize — гэта свяжаны з тым компроміс: ён працюе без вызову LLM і паказвае самэўсёлкі рэзультат работы інструмента, але інструмент павінен выдаваць чытальны тэкст, а мадэль не можа суягчы гэты рэзультат з іншымі дапамогамі. Калі “рэзультат інструмента ўжо яўляецца адказам”, такі компроміс ёсць выгодным.
Рэзультаты пошуку як метаданы, а не команды графа
Інструмент пошуку задае значэнне artifact["stop"] на аднойчынку з тым, што выйшла гэтая вызова. Параметр stop ёсць метаданамі прыкладнення, а не полем, зарэзерваваным для LangChain. Важнае: інструмент фіксуе рэзультат, а сістэма керавання яго інтэрпрэтуе. Гэта дазволяе заставіць механізм направлення працаваць у співвязі з правіламі, якіх інструмент нават не бачыць.
@tool(response_format="content_and_artifact")
def search(query: str) -> tuple[str, dict]:
"""Search a knowledge base for information about the query."""
outcome = force_outcome or rng.choices(
list(resolved_weights), weights=list(resolved_weights.values())
)[0]
if outcome == "retryable":
return rng.choice(_RETRYABLE_MESSAGES), {"stop": False} if outcome == "fatal":
return rng.choice(_FATAL_MESSAGES), {"stop": True} query_lower = query.lower()
for topic, page in _INDEX.items():
if topic in query_lower or query_lower in topic:
return page, {"stop": True}
return "Nothing in the index overlaps with this query.", {"stop": True}
Тры разлікі:
- Успех з рэальным кантэнтам →
stop=True(яшчэ адна спроба мадэлі будзе толькі перапрацаваць тэкст). - Памошчыма невяснае рашэнне →
stop=False(даць мадэлі ўсё ж адну яшчэ спробу). - Катастрофічна невяснае рашэнне →
stop=True(паціклаванне вітрачае токены на тое ж самае нерашэнне).
stop=False не значыць “спробаваць зноў зараз” — гэта толькі не дазволяе абмовіцца ад працы. Мадэль все равно можа знову запрашаць даныя, спробаваць іншы варыянт або даты рашэнне. Маршрутызатор сведаецца да перагляду рэзультатаў у адной лініі:
def route_after_tools(self, state: AgentState) -> str:
last_message = state["messages"][-1]
if (
isinstance(last_message, ToolMessage)
and isinstance(last_message.artifact, dict)
and last_message.artifact.get("stop")
):
return "finalize"
return "agent"
Конструкцыя, якая прымушае выбіраць "success" | "retryable" | "fatal", робіць маршруты дэтэрміністычныя для рэальной мадэлі Groq: у выпадку успеху і катастрофы робіцца пераход tools → finalize → END без яшчэ адной спробы мадэлі; у выпадку памошчы мадэль вяртаецца да agent.
Граніцы: калі маршрутазаванне таксама залежыць ад застаўшыхся крокаў, пярэйшых спроб чы сігналаў аутанацыі, сама артыфактная інформацыя недастатня — маршрутазатор павінен чытаць стан большога графа. content_and_artifact падходзіць тады, калі рэзультат гэтага інструмента вяліць за выбор наступнага хопу.
За межамі пошуку
Падходзяць любыя інструменты, чыё рэзультат ўсё багачэ за простае «да/нет»: інструмент write_record можа задаць значэнне already_applied; інструмент для збору даных можа задаць progress для інтэрфейсу, які мадэль ніколі не адображае. Артыфакт — цэла простая даныя, якую можна выкарыстоўваць у умовных рэшыткі, мідлварах чы інтэрфейсе, які ніколі не з’яўляецца ў графе. return_direct являе сабою рашэнне пра маршрутазаванне, ўбудованае ў саму вялічыню; у яго нема рэжыму «зберагчы інфармацыю, адлучыць рашэнне пазней».
Тая ж ідэя ў create_agent
Піны: Python 3.12, langchain==1.4.2 / langgraph==1.2.11, langchain-groq==1.1.3. Функцыя create_agent заменяе ручнае схематызаванне графа на дэкларатывныя з’ўязкі плюс мідлвэр.
Першы інстынкт: выкарыстаць wrap_tool_call і вернуць Command(goto=END), калі задана настройка stop.
class StopOnArtifact(AgentMiddleware):
def wrap_tool_call(self, request, handler):
result = handler(request)
if isinstance(result, ToolMessage):
stop = isinstance(result.artifact, dict) and result.artifact.get("stop")
if stop:
relay = AIMessage(content=str(result.content))
return Command(goto=END, update={"messages": [result, relay]})
return Command(goto="model", update={"messages": [result]})
return result
У працаваным варыянце гэты шлях функцыонуе толькі тады, калі до END ужо можна дасячы за дапамою способу return_direct. Мідлвэр можа вырахаваць stop=True, але цыкл продовжвае вяртацца да модэлю, пакуль той нарэшце не дасьведзіць адказу без вікорыстання інструментаў. Гэта выглядала як #5496 у чынных конфігураціях — пакуль два скрыптаваныя варыянты не паказалі іншага:
A: update={"messages": [result]} -> stops correctly
B: update={"messages": [result, relay]} -> loops back to the model
Версія A працюе. Версія B даджуе ў той жа апдэйт рэлея AIMessage без tool_calls. Пераканалоўванне прабывае назад да последнага AIMessage, каб ацэніць return_direct; яно знаходзіце рэлея, не бачыць жадных вызоў інструментаў і продовжвае ціклізаваць. Command быў застосоўаны — парадакт прыемных звесцей сховаў аслівныя звескі з вызамі інструментаў ад пераканалоўвання. Цэ не быў нераспрацаванный апдэйт, і не #5496.
Нават пасля выправлення гэтага, іспользуемы падход аддае before_model заместа гэтага: ён узагалі не выкарыстоўвае return_direct.
class StopOnArtifact(AgentMiddleware):
@hook_config(can_jump_to=["end"])
def before_model(self, state, runtime):
last = state["messages"][-1]
if isinstance(last, ToolMessage) and isinstance(last.artifact, dict) and last.artifact.get("stop"):
relay = AIMessage(content=str(last.content))
return {"jump_to": "end", "messages": [relay]}
return None
before_model выконваецца адразу перад кожным вызовам модэлю — у пазнейшых ітэрацыйях — адразу пасля выкарыстоўвання інструментаў. @hook_config(can_jump_to=["end"]) дазваляе перайсці да END незалежна ад будзь-якіх флагаў інструментаў. Вернуць {"jump_to": "end", ...} означае проста апডейт стану, які чытаецца на рэштах графа. Адны хук як выявляе такі артыфакт, так і стварае рэлеюцыйны сігнал AIMessage — задача далей раздэляецца між route_after_tools і finalize.
Прымусовыя рэзультаты падаюць пад прыметнік ручна створанага графа: успех або фатальная першачасна адсцягвання з точным тэкстам інструмента; можлівасць перапрытку вяртае модэль да наступнага крока.
Вывад
content_and_artifact не быў створаны як прымітыва для маршрутацыі. Ён роздзеляе аўдытарыя — контэнт, відны модэлю, і метаданы, доступны толькі для дапрыгнутка — і гэты ж роздзелленне чыста перадае пытанне «чы гэта трэба зупініць?», не прасячы модэлю аналізаваць поток керування. return_direct сплівае прынтаванне і завершэння ў адну статычную пазнаку і не функціонуе тады, калі адказы на гэтыя пытанні павінны разлічвацца для кожнага вызову.
Калі патрабуецца умовная зупінка, трэба роздзеляць рэзультат работы інструмента і рашэння пра маршрутацыю: прадставіць метаданы разам з адказам, дазволіўшы сістэме керування прымусіць рашэнне. content_and_artifact вялікі час спромагаецца ў гэтым.
Прыметкі да дызайну, якія команды забываюць пасля першага успешнага тэсту
Умовна зупінка вважаецца рэшанойя, калі прыйдзець троха прымусовых рэзультатаў. Працэс вырабніцтва дагаходзіць да паралельнасці: два вызовы інструменту за адну партыю модэлю, або група пошукав, дзе павінен быць завершаны толькі адзін. Павінна быць вярнута рашэнне, чы будзь-які факт зупінкі спрачынае першачасны завершэння всіх дзеянь, чы все павінны згадзіцца, чы прыменяецца прапорцыйнасць. Гэтую політыку трэба зафіксаваць у рутэры, а не ў традыцыйных знаёмствах.
Візуабельнае адображэнне павінна паказваць рэзультат пры імені ToolMessage, не фіксуючы таёмныя даны з content. Калі выйшла зупінка, трэба зафіксаваць, якое правіло паспела — успех, фатальная памялка чы перакрыцча політыкай — каб падтрэмка могла поясніць, чаму асістэнт не «думаў далей». Гэта павінна быць супакоўвана з урахоўваннем токенаў: весь сэнс завершэння пасля успеху — меньшы колькісць вызоўв модэлю; панелі керування павінны падтвердзіць эты эконамічны эфект.
Будзьце абярожны пад час пераносу шаблонаў між разнаманітнымі версіямі LangGraph. Назвы хукав мідлвэра, можлівасць выканання Command і пераконтроль пры выходзе з функцыі змяніліся межа 0.6 і 1.x версый. Зберагуйце тэсты, якія змусваюць сістэму працаваць у рэжыме успех/перапрыбутку/фатальнай памылкі пасля кожнага апдэйта. Калі хук раптам пачне бесканечна ціклізаваць, спачатку пераканайцеся ў формacie ліста паведамленняяў, перш чым адзначаць багі фрэймворку — перадача паведамленняяў часта становіць прычыну такіх проблем.
На заканчанне, утрымайцеся ад дадавання флагаў кантролю ў content “толькі раз”. Калі модэль пазнае stop=true у тэксце, ён можа опісваць логіку кантролю або прадаваць внутранія коды корыстувачам. Артыфакт існуе ў тым, каб організацыя працы магла быць рашучай, а канал для корыстувача заставаўся чыстым.
Спраўленне шаблона ў суседнія фрэймворкі
Той самы разлік межаючы контэнтам і кантролем выступае і за межам LangGraph. Кожны рэжым работы агента, які з’еднае выхідны даныя інструменту ў едынственны канал паведамленняў, зрэшты стварае спецыяльныя маркеры, JSON-обгорткі або дадзеныя метаінфармацыі. Калі платформа адказвае, лепш выбраць офіцыяльны додатковы канал; калі такога няма, трэба стварыць задокументаваную обгортку; ніколі не пакладацца на тое, што мадэль занедбае кантрольныя токены, захаваныя ў тэксце.
Калі кампанія должна падрываць як старыя графікі create_react_agent, так і новыя прыкладніцы create_agent, неабходна ўтрымка ідэнтычнага фармата артыфакта інструмента, пры чым трэба зменіць толькі рэалізацыю маршрута. Чыгунне так памагае абмежыць змены версій толькі да тэстаў оркастрацыі. Калі мідлвэр расшырваецца — праз перагляд автанацый, лімітаў вітрачэння, маскавання персональных дадзеных — трэба запускаць этыя хукі перад адпрацоўкай команды stop, каб адмова ў выпаленні політыкі не была спылена з успешным перэрывам. Парадак хукіў ёсць часткай публічнага поведзення агента, нават калі гэта выглядае як тэхнічныя налады.
Дакумент для будучых чытачоў, якіяяснюе, чаму існуе finalize (альбо перэхад before_model): гэта явны выбар продукту, па якому тэкст інструмента можа быць відным для карыстальніка без додатковай працы над яго фарматаваннем. Калі пазней продукт захоча стыль уснаея апісу, трэба знова дадзіць вузол мадэлі на шляху завершэння, а не перавантажваць інструмент для напісання двух стылёў адразу. Раз’ядрэнне «вычыслення рэзультата» ад «апісу рэзультата» дазволяе інструментам быць відновліванымі для выкарыстоўвання ў голасавых сэрвісах, чаты і кліентах API.
Інтуўіцыя пра розлік завершэння і продажу
Уявіце саблей для адміністрування, якая іноды вяртае готавы квітанцый, іноды паведамленне пра „затрымку у апрацоўцы платэжа“, а іноды — „карта назаўседы адхілена“. Гэтыя тры варыянты чыста адпавядаюць станам успеху, можлівасці перапрыбутку і фатальнай адмінастрацыі. content квітанцыі можа быць HTML, гатовым для клёянта; артыфакт мае значэнне { "stop": true, "reason": "completed" }. У разе затрымкі ў content залічваецца короткая адпаведзь для модэлю, а ў артыфакце — { "stop": false, "reason": "transient" }. У разе назаўседы адхіленай карты цыкл зупіняецца за дапамогою паведамлення, безпечнага для клёянта, і значэння { "stop": true, "reason": "fatal" }, ўбачліва, каб агент не продаваў прабыткі ў апрацоўшчыку. Той самы шэма можа быць застосаваны для пошуку, стварэння тыкетаў чы выеўналення дакументаў без перапісвы рутэра — толькі змена способу супарабаткі саблея.
Звычкі пераканання супутніх элементаў
Зберагаюце прыстрой для вынуджанага рэзультата ў CI з фальшывым мадэлем чату, які выдае запланаваныя вызовы інструментаў. Рэальныя запускі Groq выкарыстоўваюцца толькі для перыядычнай пераканальванасці на всіх этапах, а не для кожнага коміту. Пяракладзіце точныя последовальнасці шляхоў: якія вузлы працавалі, чы рэгулярна выкананы другі вызов мадэлі, і чы контэнт падзея finalize збігаецца з контэнтом інструмента на шляхах завершэння. Калі хтось «спрасцавае» мідлвэр і знову вводзіць return_direct, прыстрой павінен яскрава паказаць аб бягучых проблемах. Зберагаюце ідэальныя транскрыпты праза прыстрой, каб можна было адзначыць разніцы ў рэзультатах. Умовнае завершэння ўскладненняў — гэта контракт па прыемнасці; тэсты служаюць для таго, каб гэты контракт застаўся чынным праз перэработкі ў разных версіях LangGraph і серэд інжынераў, якія толькі каротка прачытаюць первісныя нотаткі па праекту.
Калі пазлу пасуюць модэль, якая трэба з’еднае выходы інструменту з попярэднімі раундамі нават у разе успеху, дадзіце неабяжлівы вузол для дапраўкі пасля фіналізацыі, а не выдалейце схему з короткам дзвяненням. Флагі функцый працуюць лепшая за перапісвы: stop_mode=hard|polish|never дазволяе працаваць над эксперыментамі без адхыленняя ад умов контракту. Перад выборам стандартнага рэжыму пераканайцеся, сколькі токенав вітрачаецца кожны рэжым на аднай і той жа сэтце запітоў.
Контракт чытача для адпрацоўкі гэтага патэрану
Перад тым, як копіюваць сам тэкст, скопіюйце схему артэфакта і тэсты рутэра. Ценнае ў эсэі — раздзеленыя функцыі, а не прыгадкі про інструменты пошуку. Калі ваш домэн вікорыстоўвае іншыя пазначкі для абэранцэй, перакладзіце іх у тыя ж тры катэгорыі і залейкайце рутэр простым. Утрымваюцца ад дадзення чэтыртай катэгорыі, пакуль гэта не будзе неабходна ў зв’язку з рэальным інцыдэнтам. Калі ёсць сумневы, лепшая продаважваць працэю мадэлі, чым зупіняцца праз нечысткія бягі — тыхі короткі замыканні, якія маскуюць частковыя абэранцыі, ўжо горшыя, чым дадзенне додатковага, недорогага вызову мадэлі, які пояснюе корыстніку нэвядомасць.
Падаюце комплект разам з статыяй, каб чытальнікі моглі самі пераканацца ў працы з крайнімі варыянтамі на сваёй інфраструктуры, перш чым паверыць у гэты патэрн у рэальным трафіку.