Инструментальный комплекс, цикл, граф: три уровня, лежащие в основе большинства сбоев производственных агентов.
Сбои в агентах производства редко связаны с самой моделью. Необходимо разделить кабельную схему инструмента, условие остановки цикла и топологию графа LangGraph — у каждого из этих элементов должен быть свой ответственный.
Агент погибает в процессе работы, и команда винит в этом модель. Они заменяют её более совершенной, но агент снова погибает. Инструкции для работы с ним переписываются, но проблема остаётся. Вводится логика повторных попыток, но ничего не меняется. Заявка направляется поставщику 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 не принесли ничего нового. Во втором случае субагенты восприняли инструкцию о работе с несколькими независимыми целями как разрешение на создание собственных потомков.
Руководство по проектированию ограниченных агентных циклов выдвигает аналогичное требование: ограничение должно быть частью дизайна с самого начала, а не добавляться в процессе оптимизации после возникновения проблем. В противном случае единственным правилом остановки будет решение самой модели о завершении работы, что является лишь надеждой, а не реальным механизмом контроля.
Поэтому условие остановки должно быть механическим, а не семантическим. Фраза «Большая языковая модель считает, что работа завершена» является семантической. Фразы «Было сделано три попытки» или «Было потрачено 50 центов» — механическими. Механические условия детерминированы, поддаются проверке и остаются стабильными при повторном выполнении. Они не зависят от оценки модели, которая является вероятностной и может отличаться при повторной попытке.
Агент службы поддержки аналитической компании показывает, во что это обходится. Система предназначалась для выявления корневой причины проблемы с простоем платежного шлюза в течение менее двух минут. Она проанализировала журналы событий, получила 4 200 записей, сжала их, пришла к выводу, что требуется дополнительная информация, и проанализировала их снова. После двенадцати попыток в тикете по-прежнему не было краткого обзора, при этом процесс потребовал 380 000 токенов и стоимость использования API составила 47 долларов. Ни модель, ни система выполнения не запрещали продолжение работы, и никто не приказывал остановиться. После того как компания ввела ограничения на количество итераций, механизмы обнаружения застоя (определяющие почти идентичные результаты) и регулятор бюджета токенов, доля неконтролируемых циклов сократилась на 91%, а среднее время решения тикета снизилось с 4,1 до 1,6 минуты. Работу выполнил ограниченный цикл, а не более мощная модель.
Что включает в себя ограниченный цикл
Пример аналитики показывает составляющие цикла, который не может бесконечно работать. Каждая из них проверяется кодом, находящимся вне модели:
- Лимит итераций. Фиксированное количество проходов, поэтому задача, достигшая этого лимита, завершается явной ошибкой, а не бесконечным выполнением.
- Проверка застоя. Анализ недавних результатов, позволяющий считать несколько почти идентичных выходов подряд отсутствием прогресса, даже если количество шагов ещё невелико.
- Лимит расходов. Бюджет в токенах или деньгах, который не может быть превышен в процессе выполнения, причем механизм контроля находится во время выполнения, а не в запросе.
- Тайм-аут. Ограничение по времени выполнения, которое позволяет выявить медленные циклы, не вызывающие срабатывания других контроллеров.
- Лимиты рекурсии для подагентов. Ограничение по глубине и общий лимит создания агентов, чтобы делегированная задача не могла создавать дочерних агентов, которые в свою очередь создают бы новых.
Ни для одного из этих механизмов не требуется сотрудничество модели, и каждый из них можно протестировать отдельно с помощью фиктивной модели, которая никогда не сообщает о завершении работы. Неудачная попытка, заканчивающаяся с ошибкой после трех попыток, стоит несколько центов. Та же попытка, оставленная на усмотрение модели, обходится в столько, сколько позволяет тариф.
Граф: куда может переместиться агент
Граф представляет собой топологию: существующие узлы, связывающие их ребра, точки, где путь разветвляется, места, где происходит одобрение, и состояние, передаваемое от узла к узлу. Если граф — это карта, то цикл — это двигатель, перемещающийся по ней, а ограничения определяют, что может произойти на каждой остановке.
Основным принципом проектирования является явность. Цикл ReAct (подумать, действовать, наблюдать, повторить) представляет собой неявную структуру, в которой один узел обрабатывает собственные данные, а модель больших языковых моделей выполняет роль функции связи между узлами. Для простых задач это подходит. Однако как только становится важна структура, такой подход теряет эффективность. Сравнение ReAct и LangGraph наглядно это иллюстрирует: в первом случае модель сама выбирает следующий шаг на основе вводных данных и истории операций, а во втором — разработчик осуществляет этот выбор с помощью кода и явных маршрутов.
Разница становится заметной сразу, когда требуется этап человеческого утверждения. В неявной сети не существует понятия «между»: агент самостоятельно переходит от одного шага к другому на основе собственных рассуждений. Добавление контрольной точки означает нарушение цикла, введение механизма паузы, внешнее хранение состояния и возобновление работы по сигналу. Это всё равно что приспосабливать сеть к циклу, а цикл сопротивляется, поскольку он создан для автономной работы. В явной сети достаточно добавить новый узел с вызовом функции interrupt(). Сеть меняется, в то время как цикл и связующие элементы остаются прежними.
Один из разработчиков протестировал одну и ту же политику, детерминистическую модель и три сценария с помощью обычного цикла вызовов инструментов и явного графа состояний LangGraph. Поведение оказалось абсолютно идентичным, однако версия с графом занимала 97 строк против 50 у цикла, то есть на 94% больше кода; таким образом, уже на трех шагах обычный цикл превосходит её по всем показателям. Затем разработчик указал на то, что такое сравнение не может учесть. В цикле поведение определяется порядком выполнения операций и должно быть выведено из содержимого кода. В графе рёбра представляют собой данные, что позволяет изучать топологию, визуализировать её и воспроизводить без необходимости открывать отдельные узлы. Значение в цикле равно nil на трёх шагах, большому числу на тридцати шагах, и становится ещё больше, когда задачу берёт на себя другой человек.
Это аргумент в пользу производственной версии. Демо-версии состоят из трех шагов, и они принадлежат тому, кто их написал. Производственная версия включает тридцать шагов и находится в ведении того, кто дежурит в 3 часа ночи. При таком масштабе четкая топология — это условие выживания, а не дополнительная нагрузка.
Практический тест для выбора между этими двумя формами — спросить, кому будет принадлежать агент через шесть месяцев. Короткий агент с одной функцией, поддерживаемый его автором, может оставаться простым циклом. Агент, требующий утверждения человека, имеющий несколько ветвей или другого ответственного за дежурство, требует четкой структуры в виде графа, поскольку топология становится данными, которые можно визуализировать, анализировать и воспроизводить.
Как выглядит путаница между слоями
Указанные ниже способы сбоев возникают во время сборки, которая является безопасной средой для смешивания слоев. Это те же самые проблемы, что и в производственной среде.
Как « Harness » управляет циклом
В некорректной версии проверка на то, «достаточно ли этого», выполняется внутри инструмента, поэтому именно инструмент решает, когда прекратить цикл. Инструмент должен представлять собой определённую функцию, например «возможность запрашивать информацию у поставщиков», а не орган, принимающий решение вроде «хватит, прекратить». Размещение условия остановки внутри инструмента делает цикл невозможным к тестированию отдельно, затрудняет замену инструмента без повторной реализации логики остановки и не оставляет возможности установить лимит количества попыток, поскольку инструмент ничего не знает о попытках.
Исправление касается структуры. Необходимо вынести условие остановки из инструмента и поместить его в узел графа: инструмент возвращает данные, узел решает, требуется ли ещё один проход, механизм обеспечивает соответствующую функцию, а цикл находится в графе. Некорректная версия выглядит в коде следующим образом:
# 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.
- Ограничители одновременности. Лимиты одновременной работы для каждого инструмента предотвращают занятие всего количества доступных ресурсов медленным инструментом.
- Ключи идемпотентности. Для инструментов с побочными эффектами система формирует ключ на основе ID рабочего процесса и названия шага. При повторной попытке выполнения операции возвращается уже существующий результат, и дубликат не создается.
Все это не является частью 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 не требовалось изменять. Поставщик ЯИИ перешел от прямого взаимодействия с OpenAI к использованию слоя маршрутизации, не трогая код графа. Политика повторных попыток из агрессивной стала строго запрещающей их для операций с побочными эффектами, при этом граф и Harness остались без изменений. Разделение компонентов обеспечивает большую гибкость, чем их чистота: возможность изменить одну часть, не нарушая работу трех других.
Та же дисциплина приносит плоды, когда что-то идет не так. Неудачный запуск можно выяснить, задав три узких вопроса. Вернул ли инструмент что-то, что ему никогда не следовало возвращать, или достиг ли он места, куда ему никогда не следовало попадать? Это указывает на систему управления. Продолжал ли агент действовать за пределами разумных ограничений? Это указывает на цикл. Оказался ли агент в узле, к которому ему не следовало попасть, или пропустил ли он контрольную точку? Это указывает на граф. Замена модели не решает ни одной из этих трех проблем.
Три вопроса для проектирования любого агента
- Чего он может коснуться? (Система управления) Какие инструменты существуют, с какими схемами, сколько выходных данных разрешено, где находятся пороги аварийного отключения и какие ключи идемпотентности применяются.
- Когда это прекращается? (Цикл) Потолок количества шагов, потолок затрат, таймаут и механическое условие остановки, не зависящее от оценки LLM.
- Куда это может идти? (Граф) Узлы, рёбра, условные ветви, человеческие точки контроля и форма состояния, передающаяся между узлами.
Когда любой ответ звучит как «модель решает», возникает проблема. LLM дает рекомендации, код принимает решение, механизмы обеспечивают его выполнение, граф определяет маршрут, а цикл задаёт границы.
Короче говоря, существуют три уровня, три ответственных лица и три вопроса. Если их смешивать, ошибки кажутся проблемами модели, хотя на самом деле это не так. Если их разделять, ненадёжность модели остаётся под контролем, инструменты можно использовать безопасно, а агент останавливается в нужный момент.
Связанная литература
- Ваш граф агентов должен находиться в YAML: компиляция графа LangGraph из 17 агентов при запуске — как небольшой движок компилирует один файл с описанием в формате YAML в граф StateGraph LangGraph: генерация состояния, плагины importlib, маршрутизаторы меток, адаптивные вызовы и замена узлов в рамках сессии.
- Когда панель управления показывает зеленый цвет, а агент действует неправильно: отслеживание состояния — как сбои агентов скрываются в состоянии, передаваемом между корректно работающими этапами, и что необходимо зафиксировать с помощью отслеживания, ограничений движка, записей маршрутизации и результатов оценки.