День 3: Planner → Executor → Critic

Цель: Создать первую полноценную multi-agent систему с циклом самоулучшения

Время изучения: 2-3 часа
Код: src/day3_planner_executor_critic.py
Тесты: tests/test_day3_planner_executor_critic.py


🤖 Связь с агентами

Вчера: Научились создавать циклы и условную маршрутизацию
Сегодня: Создаём первую настоящую multi-agent систему!

Что мы строим?

P E C Р l x r е a e i ш n c t е n С u В i О н e о t ы c ц и r з o п е е д r о A н : A а л g и g ё A н e в п e т g я n а о n e е t е в t п n т т т л t ( о ( а ш К р р П н ( а р е и л И г и з т а в с и т у ь н ы п и л и п о п к ь и р о л л ) т л о л н а а и в н и н т щ е т а з и н е а к и л в ) я ь е ) р з ш а и д т а ь ч ? и

Зачем это нужно?

Реальный сценарий: Написание статьи

P E C E C l x r x r a e i e i n c t c t n u i u i e t c t c r o : o : : r r : " : " " Н О 1 В е В т . ы д ы л п о п и И о с о ч с л т л н с н а н о л я т я , е е о е д т ч т п о н е в ш о ш р а а а е т г и г х ь с о 1 т 1 д т о и е ч с м м н н у " и о к , С к в о о а ш 2 б в а . р , г а у Н л н " а у С 2 п 5 ж о " и н б с и о р а с а т т м л ь о и ч н 1 ч н и 2 е и м р к у и н о м с о в т в " 1 о и 0 ч к " н , и к 3 о . в " О т р е д а к т и р о в а т ь "

Это паттерн самоулучшения — агенты работают в цикле, пока не достигнут качественного результата.


Архитектура системы

Три специализированных агента

class AgentState(TypedDict, total=False):
    task: str                    # Исходная задача
    plan: List[str]              # План выполнения
    current_step: int            # Текущий шаг
    step_result: str             # Результат текущего шага
    critique: str                # Оценка критика
    status: str                  # "planning", "executing", "critiquing", "done"
    iteration: int               # Счётчик итераций
    max_iterations: int          # Максимум итераций
    final_result: str            # Финальный результат

Поток выполнения

П E о x в e E ( т c P x C р о u l e r е р t a c i ш и o n u t е т r n t i н ь e o c и r r е ) Г о т о в С В О о о ы ц з п е д о н а л и ё н в т я а е е п т т л а т р н е е к з и у у з щ л и ь з й т а а д ш т а а ч г и

Часть 1: Planner Agent

Задача: Разбить задачу на шаги

def planner_agent(state: AgentState) -> AgentState:
    """
    Planner Agent: создаёт план выполнения задачи.
    
    В реальной системе здесь был бы вызов LLM:
    plan = llm.invoke(f"Create a plan for: {task}")
    """
    updated = dict(state)
    task = state.get("task", "")
    
    # Простая эмуляция планирования
    # В реальности здесь LLM генерирует ��лан
    plan = [
        f"Step 1: Research about '{task}'",
        f"Step 2: Analyze findings for '{task}'",
        f"Step 3: Create summary for '{task}'"
    ]
    
    updated["plan"] = plan
    updated["current_step"] = 0
    updated["status"] = "executing"
    updated["iteration"] = updated.get("iteration", 0) + 1
    
    print(f"\n📋 Planner: Created plan with {len(plan)} steps")
    for i, step in enumerate(plan, 1):
        print(f"   {i}. {step}")
    
    return updated

Ключевые моменты:

  • Получает задачу из state
  • Создаёт список шагов
  • Устанавливает current_step = 0
  • Переводит status в “executing”

Часть 2: Executor Agent

Задача: Выполнить текущий шаг плана

def executor_agent(state: AgentState) -> AgentState:
    """
    Executor Agent: выполняет текущий шаг плана.
    
    В реальной системе здесь был бы вызов LLM с tools:
    result = llm_with_tools.invoke(current_step)
    """
    updated = dict(state)
    plan = state.get("plan", [])
    current_step = state.get("current_step", 0)
    iteration = updated.get("iteration", 0)
    
    if current_step >= len(plan):
        # Все шаги выполнены
        updated["status"] = "done"
        updated["final_result"] = state.get("step_result", "Task completed")
        print(f"\n✅ Executor: All steps completed!")
        return updated
    
    step = plan[current_step]
    
    # Простая эмуляция выполнения
    # В реальности здесь LLM выполняет шаг с использованием tools
    result = f"Completed: {step} (iteration {iteration})"
    
    updated["step_result"] = result
    updated["status"] = "critiquing"
    updated["iteration"] = iteration + 1
    
    print(f"\n⚙️  Executor: Executing step {current_step + 1}/{len(plan)}")
    print(f"   Step: {step}")
    print(f"   Result: {result}")
    
    return updated

Ключевые моменты:

  • Читает текущий шаг из плана
  • Выполняет шаг (в реальности — вызов LLM с tools)
  • Сохраняет результат в step_result
  • Переводит status в “critiquing”

Часть 3: Critic Agent

Задача: Оценить результат и решить, что делать дальше

def critic_agent(state: AgentState) -> AgentState:
    """
    Critic Agent: оценивает результат выполнения шага.
    
    В реальной системе здесь был бы вызов LLM:
    critique = llm.invoke(f"Evaluate: {step_result}")
    """
    updated = dict(state)
    step_result = state.get("step_result", "")
    current_step = state.get("current_step", 0)
    iteration = state.get("iteration", 0)
    
    # Простая эмуляция критики
    # В реальности здесь LLM оценивает качество результата
    
    # Эмулируем: первая попытка всегда требует улучшения
    if iteration % 2 == 1:
        critique = "Good, but needs improvement. Try again."
        decision = "retry"
    else:
        critique = "Excellent! Moving to next step."
        decision = "next"
        updated["current_step"] = current_step + 1
    
    updated["critique"] = critique
    updated["status"] = "executing" if decision == "retry" or updated["current_step"] < len(state.get("plan", [])) else "done"
    
    print(f"\n🔍 Critic: Evaluating step {current_step + 1}")
    print(f"   Critique: {critique}")
    print(f"   Decision: {decision}")
    
    return updated

Ключевые моменты:

  • Оценивает step_result
  • Решает: повторить (retry) или следующий шаг (next)
  • Если retry — status остаётся “executing”
  • Если next — увеличивает current_step
  • Если все шаги выполнены — status = “done”

Часть 4: Роутер (Router)

Задача: Решить, какой агент вызвать следующим

def should_continue(state: AgentState) -> str:
    """
    Роутер: решает, продолжать ли цикл.
    """
    status = state.get("status", "")
    iteration = state.get("iteration", 0)
    max_iterations = state.get("max_iterations", 10)
    
    # Защита от бесконечного цикла
    if iteration >= max_iterations:
        print(f"\n⚠️  Max iterations ({max_iterations}) reached!")
        return "end"
    
    # Проверяем статус
    if status == "done":
        print(f"\n✅ Task completed successfully!")
        return "end"
    elif status == "executing":
        return "execute"
    elif status == "critiquing":
        return "critique"
    else:
        return "end"

Ключевые моменты:

  • Проверяет max_iterations (защита)
  • Маршрутизирует по status
  • Возвращает имя следующего узла

Часть 5: Собираем граф

from langgraph.graph import StateGraph, END

def create_planner_executor_critic_graph():
    """Создаёт граф Planner → Executor → Critic."""
    
    graph = StateGraph(AgentState)
    
    # Добавляем агентов как узлы
    graph.add_node("planner", planner_agent)
    graph.add_node("executor", executor_agent)
    graph.add_node("critic", critic_agent)
    
    # Начинаем с планировщика
    graph.set_entry_point("planner")
    
    # Planner → Executor
    graph.add_edge("planner", "executor")
    
    # Executor → Critic
    graph.add_edge("executor", "critic")
    
    # Critic → (conditional) Executor или END
    graph.add_conditional_edges(
        "critic",
        should_continue,
        {
            "execute": "executor",  # Повторить или следующий шаг
            "critique": "critic",   # На случай, если нужно
            "end": END              # Завершить
        }
    )
    
    return graph.compile()

Полный пример использования

def main():
    """Пример использо��ания Planner → Executor → Critic."""
    
    workflow = create_planner_executor_critic_graph()
    
    # Начальное состояние
    initial_state: AgentState = {
        "task": "Write a blog post about LangGraph",
        "iteration": 0,
        "max_iterations": 10,
        "status": "planning"
    }
    
    # Запускаем граф
    print("="*60)
    print("🚀 Starting Planner → Executor → Critic workflow")
    print("="*60)
    
    result = workflow.invoke(initial_state)
    
    # Выводим результат
    print("\n" + "="*60)
    print("📊 Final Result")
    print("="*60)
    print(f"Task: {result.get('task')}")
    print(f"Plan steps: {len(result.get('plan', []))}")
    print(f"Iterations: {result.get('iteration')}")
    print(f"Status: {result.get('status')}")
    print(f"Final result: {result.get('final_result')}")
    print("="*60)

if __name__ == "__main__":
    main()

Вывод:

= 🚀 = 📋 🔍 🔍 = 📊 = T P I S F = = = = = a l t t i = = S = P C C T = F = s a e a n = = t = l 1 2 3 S R r C D S R r C D a = i = k n r t a = = a = a . . . E t e i r e E t e i r e ( s = n = : a u l = = r = n x e s t i c x e s t i c п k = a = s t s = = t = n S S S e p u i t i e p u i t i р = l = W t i : r = = i = e t t t c : l c i s c : l c i s о c = = r e o e = = n = r e e e u t : q i u t : q i д o = R = i p n d s = = g = : p p p t S : u o t S : u o о m = e = t s s o u = = = o t E e n o t E e n л p = s = e : : n l = = P = C 1 2 3 r e C v : : r e C v : : ж l = u = e t = = l = r : : : : p o a : p o a а e = l = a 3 7 : = = a = e m l G r m l E n е t = t = = = n = a R A C E 1 p u o e E 1 p u x e т e = = b C = = n = t e n r x : l a o t x : l a c x с d = = l o = = e = e s a e e e t d r e e t e t я = = o m = = r = d e l a c R t i , y c R t i l s = = g p = = = a y t u e e n u e e n l д u = = l = = = p r z e t s d g b t s d g e л c = = p e = = = l c e i e : u i e : n я c = = o t = = E = a h s n a s t n a s t e = = s e = = x = n f u g r S t g r S t ! в s = = t d = = e = a i m c t e n c t e с s = = : = = c = w b n m s h e p e s h e p M е f = = a = = u = i o d a t p e t p o х u = = b S = = t = t u i r e a 1 d e a 1 v l = = o t = = o = h t n y p b 1 s p b 1 i ш l = = u e = = r = g o : o : n а y = = t p = = = 3 ' s f 1 u i 1 u g г ! = = = = = W o / t R m / t R о = = L 3 = = = s r f r 3 e p 3 e t в = = a : = = C = t i o ' s r ' s o ) = = n = = r = e t r ' W e o W e = = g C = = i = p e W r a v r a n = = G r = = t = s ' r i r e i r e = = r e = = i = a W i t c m t c x = = a a = = c = r t e h e e h t = = p t = = = b i e n = = h e = = w = l t a a t a a s = = = = o = o e a b . b t = = s = = r = g b o b o e = = u = = k = a b l u T l u p = = m = = f = p l o t r o t . = = m = = l = o b o g y g = = a = = o = s l g ' ' = = r = = w = t o p W a p W = = y = = = g p o r g o r = = = = = a o s i a s i = = f = = = b p s t t i t t = = o = = = o o t e n e = = r = = = u s a . a = = = = = t t a b a b a = = ' = = = b o o = = W = = = L a o u b u b = = r = = = a b u t l t l = = i = = = n o t o o = = t = = = g u L g L g = = e = = = G t L a a = = = = = r a n p n p = = a = a L n g o g o p a g G s G s b h n G r t r t l ' g r a a o G a p a p a g r p h b h b a h ' o ' o p p ' u u o h t t s ' t L L a a a n n b g g o G G u r r t a a p p L h h a ' ' n g ( ( G i i r t t a e e p r r h a a ' t t i i ( o o i n n t e 1 2 r ) ) a t i o n 7 )

Запуск

# Запустить пример
python src/day3_planner_executor_critic.py

# Запустить тесты
make test-day3

# Или напрямую
PYTHONPATH=. pytest tests/test_day3_planner_executor_critic.py -v

Улучшения для реальной системы

1. Интеграция с LLM

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")

def planner_agent_with_llm(state: AgentState) -> AgentState:
    task = state.get("task", "")
    
    prompt = f"""Create a detailed plan to accomplish this task:
    Task: {task}
    
    Return a numbered list of steps."""
    
    response = llm.invoke(prompt)
    plan = response.content.split("\n")
    
    updated = dict(state)
    updated["plan"] = plan
    updated["status"] = "executing"
    return updated

2. Добавление логирования

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def executor_agent(state: AgentState) -> AgentState:
    logger.info(f"Executing step {state.get('current_step')}")
    # ... логика
    logger.info(f"Step result: {result}")
    return updated

3. Сохранение истории

class AgentState(TypedDict, total=False):
    # ... существующие поля
    history: List[Dict[str, str]]  # История всех действий

def executor_agent(state: AgentState) -> AgentState:
    updated = dict(state)
    # ... выполнение
    
    # Сохраняем в историю
    history = updated.get("history", [])
    history.append({
        "agent": "executor",
        "step": step,
        "result": result,
        "iteration": iteration
    })
    updated["history"] = history
    
    return updated

Типичные ошибки

❌ Ошибка 1: Забыли увеличить current_step

def bad_critic(state: AgentState) -> AgentState:
    updated = dict(state)
    updated["status"] = "executing"  # ❌ Не увеличили current_step!
    return updated
# Результат: бесконечное выполнение одного шага

✅ Правильно:

def good_critic(state: AgentState) -> AgentState:
    updated = dict(state)
    if decision == "next":
        updated["current_step"] = state.get("current_step", 0) + 1  # ✓
    updated["status"] = "executing"
    return updated

❌ Ошибка 2: Не проверили границы массива

def bad_executor(state: AgentState) -> AgentState:
    plan = state.get("plan", [])
    step = plan[state.get("current_step")]  # ❌ IndexError!
    # ...

✅ Правильно:

def good_executor(state: AgentState) -> AgentState:
    plan = state.get("plan", [])
    current_step = state.get("current_step", 0)
    
    if current_step >= len(plan):  # ✓ Проверка
        updated["status"] = "done"
        return updated
    
    step = plan[current_step]
    # ...

Ключевые выводы

Planner Agent — разбивает задачу на шаги
Executor Agent — выполняет шаги последовательно
Critic Agent — оценивает результат и решает, что делать
Цикл самоулучшения — повторяем, пока не достигнем качества
MAX_ITERATIONS — защита от бесконечных циклов
current_step — отслеживание прогресса по плану

🤖 Связь с агентами

Сегодня вы создали:

  • ✅ Первую настоящую multi-agent систему
  • ✅ Три специализированных агента
  • ✅ Цикл самоулучшения с обратной связ��ю
  • ✅ Паттерн, который используется в реальных системах

Следующий шаг: Добавим агентам инструменты (tools) — они смогут искать информацию, выполнять вычисления, работать с файлами!


Следующий шаг: День 4

В следующей статье мы изучим:

  • Tool Calling — агенты используют инструменты
  • Structured Outputs — строгая типизация ответов LLM
  • Реальная интеграция с LLM — вместо эмуляции

👉 День 4: Tool Calling и Structured Outputs (скоро)


Ресурсы


Прогресс: 3/10 дней ✅