Как LangGraph обрабатывает циклы и петли — как построить агента, который итерирует до выполнения условия?
Циклы в LangGraph строятся через условное ребро, которое возвращает управление назад к предыдущему узлу. Выход из цикла — отдельная ветка, ведущая к END. Для защиты от бесконечного цикла используют счётчик итераций в состоянии.
Как LangGraph реализует циклы
В LangGraph цикл — это граф, в котором одно из условных рёбер ведёт назад к уже посещённому узлу. В отличие от DAG-фреймворков, LangGraph явно поддерживает такие петли: состояние сохраняется между итерациями и обновляется редукторами.
Пример: агент с итерацией до получения корректного ответа
from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
MAX_ITERATIONS = 5
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
iteration: int
solved: bool
def solver_node(state: State) -> dict:
"""Пытается решить задачу. Имитирует постепенное улучшение."""
iteration = state.get("iteration", 0) + 1
if iteration >= 3:
answer = AIMessage(content=f"[Итерация {iteration}] Нашёл решение!")
return {"messages": [answer], "iteration": iteration, "solved": True}
else:
attempt = AIMessage(content=f"[Итерация {iteration}] Пробую ещё...")
return {"messages": [attempt], "iteration": iteration, "solved": False}
def critic_node(state: State) -> dict:
"""Проверяет решение и добавляет обратную связь."""
last = state["messages"][-1].content
feedback = AIMessage(content=f"Критик: '{last}' — нужно доработать.")
return {"messages": [feedback]}
def should_continue(state: State) -> str:
"""Маршрутизатор: продолжать цикл или выходить?"""
if state.get("solved", False):
return "done"
if state.get("iteration", 0) >= MAX_ITERATIONS:
return "done" # защита от бесконечного цикла
return "retry"
builder = StateGraph(State)
builder.add_node("solver", solver_node)
builder.add_node("critic", critic_node)
builder.add_edge(START, "solver")
builder.add_conditional_edges(
"solver",
should_continue,
{"retry": "critic", "done": END} # retry → цикл, done → выход
)
builder.add_edge("critic", "solver") # петля: critic возвращает к solver
graph = builder.compile()
initial_state = {
"messages": [HumanMessage(content="Реши сложную задачу")],
"iteration": 0,
"solved": False
}
result = graph.invoke(initial_state)
for msg in result["messages"]:
print(f"{type(msg).__name__}: {msg.content}")
Паттерн ReAct (Reason + Act)
Классическая агентская петля строится по схеме: LLM решает, нужен ли инструмент → вызывается инструмент → результат возвращается LLM → повторить. Это стандартный цикл в LangGraph:
# llm_node → (tool_calls?) → tool_node → llm_node → ...
builder.add_conditional_edges("llm", route_after_llm, {"tools": "tools", "end": END})
builder.add_edge("tools", "llm") # петля
Визуализация графа
Для отладки структуры цикла полезен метод get_graph().draw_mermaid_png() — он наглядно показывает петли и точки выхода.
Подводные камни
- Бесконечный цикл без safeguard. Если маршрутизатор никогда не вернёт "done", граф будет работать вечно. Всегда добавляйте лимит итераций как отдельное поле в состоянии.
- Состояние растёт с каждой итерацией. Если в цикле добавляются сообщения, через 100 итераций история может превысить контекстное окно LLM. Необходимо реализовать обрезку истории (trim_messages).
- Нет встроенного таймаута. LangGraph не ограничивает время выполнения графа по умолчанию. В production нужно оборачивать вызов в asyncio.wait_for или устанавливать лимит через RunnableConfig.
- Нелинейный порядок сообщений затрудняет отладку. При нескольких итерациях список сообщений становится длинным и запутанным. Рекомендуется хранить отдельное поле
steps: list[str]для краткого лога итераций. - Счётчик итераций нужно инициализировать явно. Если поле
iterationне задано в начальном состоянии,state.get("iteration", 0)вернёт 0, но после первого обновления тип может конфликтовать с аннотацией TypedDict. - Critic-узел не должен изменять solved. Если и solver, и critic обновляют одно поле без редуктора, значение от одного узла перезапишет значение от другого. Проектируйте поля ответственности узлов явно.
- Параллельные ветки и петли несовместимы без синхронизации. Смешение fan-out/fan-in топологии с циклами требует тщательной настройки редукторов — иначе состояние после слияния может быть некорректным.
- recursion_limit. LangGraph имеет защиту от слишком глубокой рекурсии (по умолчанию 25 шагов). Для длинных агентских петель нужно явно поднимать лимит:
graph.invoke(state, {"recursion_limit": 100}).
Common mistakes
- Объяснять
cycles and loopsтолько синтаксисом без shape, dtype, состояния или режима выполнения. - Игнорировать leakage, воспроизводимость, пустые входы и скрытые копии данных.
- Не проверять production-симптомы: latency, память, ретраи, дрейф качества и несовпадение версий.
What the interviewer is testing
- Может ли связать
cycles and loopsс реальным контрактом входов и выходов. - Упоминает ли тесты, метрики, reproducibility и диагностику ошибок.
- Видит ли различие между demo-кодом в ноутбуке и production-пайплайном.