День 2: State и Conditional Edges
Цель: Научиться работать с условной маршрутизацией и создавать безопасные циклы
Время изучения: 1-2 часа
Код: src/day2_state_conditional.py
Тесты: tests/test_day2_state_conditional.py
🤖 Связь с агентами
Вчера: Создали простой граф A → B (линейный поток)
Сегодня: Добавляем интеллект — агенты принимают решения!
Что мы строим?
Зачем это нужно для агентов?
Реальный сценарий:
Conditional Edges = способность агента принимать решения!
Что мы изучим
В День 1 мы создали простой линейный граф A → B. Сегодня усложним:
- Расширенная state-схема — больше полей, Optional типы
- Conditional Edges — выбор следующего узла на основе state
- Циклы — узел может вызывать сам себя
- Защита от бесконечности — MAX_ITERATIONS
Часть 1: Расширенная State-схема
Простая схема (День 1)
class SimpleState(TypedDict, total=False):
message: str
count: int
Расширенная схема (День 2)
from typing import TypedDict, List, Optional
class AgentState(TypedDict, total=False):
messages: List[str] # История всех сообщений
plan: Optional[str] # Текущий план действий
result: Optional[str] # Результат выполнения
iteration: int # Счётчик итераций
status: str # Статус: "planning", "executing", "done", "error"
Зачем больше полей?
messages— накапливаем историю для отладкиplan/result— разделяем планирование и выполнениеiteration— защита от бесконечных цикловstatus— управление потоком выполнения
Часть 2: Граф с тремя узлами
Создадим граф: Planner → Executor → Finalizer
def node_planner(state: AgentState) -> AgentState:
"""Узел планирования: создаёт план."""
updated = dict(state)
updated["plan"] = "Execute task X"
updated["status"] = "planning"
updated["iteration"] = updated.get("iteration", 0) + 1
updated["messages"] = updated.get("messages", []) + ["Planning"]
print(f"Planner: iteration={updated['iteration']}")
return updated
def node_executor(state: AgentState) -> AgentState:
"""Узел выполнения: выполняет план."""
updated = dict(state)
plan = state.get("plan", "No plan")
updated["result"] = f"Executed: {plan}"
updated["status"] = "executing"
updated["iteration"] = updated.get("iteration", 0) + 1
updated["messages"] = updated.get("messages", []) + ["Executing"]
print(f"Executor: iteration={updated['iteration']}")
return updated
def node_finalizer(state: AgentState) -> AgentState:
"""Узел финализации: завершает работу."""
updated = dict(state)
updated["status"] = "done"
updated["iteration"] = updated.get("iteration", 0) + 1
updated["messages"] = updated.get("messages", []) + ["Done"]
print(f"Finalizer: iteration={updated['iteration']}")
return updated
Собираем граф
from langgraph.graph import StateGraph
def create_simple_graph():
graph = StateGraph(AgentState)
graph.add_node("planner", node_planner)
graph.add_node("executor", node_executor)
graph.add_node("finalizer", node_finalizer)
graph.add_edge("planner", "executor")
graph.add_edge("executor", "finalizer")
graph.set_entry_point("planner")
graph.set_finish_point("finalizer")
return graph.compile()
Запуск
workflow = create_simple_graph()
result = workflow.invoke({"messages": [], "iteration": 0})
print(f"Status: {result['status']}") # done
print(f"Iterations: {result['iteration']}") # 3
print(f"Messages: {result['messages']}") # ['Planning', 'Executing', 'Done']
Часть 3: Conditional Edges (условная маршрутизация)
Теперь добавим выбор пути на основе состояния.
Сценарий
После планирования проверяем status:
- Если
status == "error"→ идём в error_handler - Если
status == "done"→ завершаем - Иначе → продолжаем выполнение
Функция-роутер
def router_function(state: AgentState) -> str:
"""Выбирает следующий узел на основе status."""
status = state.get("status", "")
if status == "error":
return "error"
elif status == "done":
return "finish"
else:
return "continue"
Важно: Функция возвращает строку — имя следующего пути
Обработчики
def error_handler(state: AgentState) -> AgentState:
"""Обрабатывает ошибки."""
updated = dict(state)
updated["messages"] = updated.get("messages", []) + ["Error handled"]
updated["status"] = "recovered"
print("Error Handler: recovered")
return updated
def success_handler(state: AgentState) -> AgentState:
"""Обрабатывает успех."""
updated = dict(state)
updated["messages"] = updated.get("messages", []) + ["Success!"]
updated["status"] = "done"
print("Success Handler: done")
return updated
Граф с conditional edge
from langgraph.graph import StateGraph, END
def create_conditional_graph():
graph = StateGraph(AgentState)
graph.add_node("start", node_planner)
graph.add_node("error_handler", error_handler)
graph.add_node("success_handler", success_handler)
graph.set_entry_point("start")
# Conditional edge: выбор пути на основе router_function
graph.add_conditional_edges(
"start", # Из какого узла
router_function, # Функция-роутер
{
"error": "error_handler", # Если вернула "error"
"finish": END, # Если вернула "finish"
"continue": "success_handler" # Если вернула "continue"
}
)
graph.add_edge("error_handler", END)
graph.add_edge("success_handler", END)
return graph.compile()
Тестируем разные пути
workflow = create_conditional_graph()
# Тест 1: Успешный путь
result = workflow.invoke({
"messages": [],
"iteration": 0,
"status": "start"
})
print(result["status"]) # done
print(result["messages"]) # ['Planning', 'Success!']
# Тест 2: Путь с ошибкой
result = workflow.invoke({
"messages": [],
"iteration": 0,
"status": "error"
})
print(result["status"]) # recovered
print(result["messages"]) # ['Planning', 'Error handled']
Часть 4: Циклы с защитой
Самое интересное — создадим узел, который может вызывать сам себя.
Проблема: бесконечный цикл
# ❌ Опасно!
graph.add_conditional_edges(
"loop_node",
lambda state: "continue", # Всегда продолжаем
{"continue": "loop_node"} # Возврат к себе
)
# Это зациклится навсегда!
Решение: MAX_ITERATIONS
MAX_ITERATIONS = 10
def loop_node(state: AgentState) -> AgentState:
"""Узел, который выполняется в цикле."""
updated = dict(state)
iteration = updated.get("iteration", 0) + 1
updated["iteration"] = iteration
updated["messages"] = updated.get("messages", []) + [f"Loop {iteration}"]
# Условие завершения: после 5 итераций
if iteration >= 5:
updated["status"] = "done"
else:
updated["status"] = "continue"
print(f"Loop iteration {iteration}: status={updated['status']}")
return updated
def should_continue(state: AgentState) -> str:
"""Решает, продолжать ли цикл."""
iteration = state.get("iteration", 0)
status = state.get("status", "")
# Защита 1: Максимум итераций
if iteration >= MAX_ITERATIONS:
print(f"⚠️ Max iterations ({MAX_ITERATIONS}) reached!")
return "stop"
# Защита 2: Проверка статуса
if status == "done":
print("✓ Task completed!")
return "stop"
return "continue"
Граф с циклом
from langgraph.graph import StateGraph, END
def create_loop_graph():
graph = StateGraph(AgentState)
graph.add_node("loop", loop_node)
graph.set_entry_point("loop")
graph.add_conditional_edges(
"loop",
should_continue,
{
"continue": "loop", # Возврат к самому себе
"stop": END # Завершение
}
)
return graph.compile()
Запуск
workflow = create_loop_graph()
result = workflow.invoke({
"messages": [],
"iteration": 0,
"status": "start"
})
print(f"Iterations: {result['iteration']}") # 5
print(f"Status: {result['status']}") # done
print(f"Messages: {result['messages']}") # ['Loop 1', 'Loop 2', ..., 'Loop 5']
Вывод:
Полный пример: Все вместе
from typing import TypedDict, List, Optional
from langgraph.graph import StateGraph, END
MAX_ITERATIONS = 10
class AgentState(TypedDict, total=False):
messages: List[str]
plan: Optional[str]
result: Optional[str]
iteration: int
status: str
def loop_node(state: AgentState) -> AgentState:
updated = dict(state)
iteration = updated.get("iteration", 0) + 1
updated["iteration"] = iteration
updated["messages"] = updated.get("messages", []) + [f"Loop {iteration}"]
if iteration >= 5:
updated["status"] = "done"
else:
updated["status"] = "continue"
return updated
def should_continue(state: AgentState) -> str:
if state.get("iteration", 0) >= MAX_ITERATIONS:
return "stop"
if state.get("status") == "done":
return "stop"
return "continue"
def create_loop_graph():
graph = StateGraph(AgentState)
graph.add_node("loop", loop_node)
graph.set_entry_point("loop")
graph.add_conditional_edges(
"loop",
should_continue,
{"continue": "loop", "stop": END}
)
return graph.compile()
# Запуск
workflow = create_loop_graph()
result = workflow.invoke({"messages": [], "iteration": 0, "status": "start"})
print(result)
Запуск ��римеров
# Запустить все примеры дня 2
python src/day2_state_conditional.py
# Запустить тесты
make test-day2
# Или напрямую
PYTHONPATH=. pytest tests/test_day2_state_conditional.py -v
Типичные ошибки
❌ Ошибка 1: Забыли проверить MAX_ITERATIONS
def bad_should_continue(state: AgentState) -> str:
if state.get("status") == "done":
return "stop"
return "continue" # ❌ Может зациклиться!
✅ Правильно:
def good_should_continue(state: AgentState) -> str:
if state.get("iteration", 0) >= MAX_ITERATIONS: # ✓ Защита
return "stop"
if state.get("status") == "done":
return "stop"
return "continue"
❌ Ошибка 2: Роутер возвращает несуществующий путь
def bad_router(state: AgentState) -> str:
return "unknown_path" # ❌ Нет в маппинге!
graph.add_conditional_edges(
"node",
bad_router,
{"path1": "node1", "path2": "node2"} # "unknown_path" отсутствует!
)
✅ Правильно:
def good_router(state: AgentState) -> str:
status = state.get("status", "")
if status == "error":
return "error"
return "continue" # ✓ Дефолтный путь
graph.add_conditional_edges(
"node",
good_router,
{"error": "error_node", "continue": "next_node"} # Все пути покрыты
)
Ключевые выводы
✅ TypedDict с Optional — гибкая типизация для сложных состояний
✅ Conditional Edges — выбор пути на основе state через функцию-роутер
✅ Циклы �� узел может вызывать сам себя через conditional edge
✅ MAX_ITERATIONS — обязательная защита от бесконечных циклов
✅ END — специальный маркер завершения графа
✅ Логирование — добавляйте print-ы для отладки
🤖 Связь с агентами
Сегодня вы научились:
- ✅ Агенты принимают решения (conditional routing)
- ✅ Агенты могут повторять действия (циклы)
- ✅ Агенты защищены от зацикливания (MAX_ITERATIONS)
Следующий шаг: Создадим первую полноценную multi-agent систему с тремя специализированными агентами!
Следующий шаг: День 3
В следующей статье мы создадим полноценный цикл Planner → Executor → Critic:
- Planner генерирует план из задачи
- Executor выполняет шаги плана
- Critic оценивает результат и решает, продолжать или завершить
- Цикл с обратной связью и самоул��чшением
👉 День 3: Planner → Executor → Critic (скоро)
Ресурсы
Прогресс: 2/10 дней ✅