День 1: Минимальный граф в LangGraph

Цель: Создать первый работающий граф с двумя узлами и понять основы LangGraph

Время изучения: 30-60 минут
Код: src/day1_min_graph.py
Тесты: tests/test_day1_min_graph.py


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

Важно понять: Каждый узел в графе — это будущий агент!

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

С е г У ( У ( о з ф з ф д е у е у н л н л н я к к : A ц B ц и и я я ) ) Ч е P ( E ( р l п x в е a л e ы з n а c п n н u о н e и t л е r р o н д у r я е A е е л g т A т ю e ) g ) : n e t n t

Зачем начинать с графов?

Граф — это структура для организации агентов:

  • 🎯 Кто за что отвечает (узлы = агенты)
  • 🔄 В каком порядке работают (рёбра = связи)
  • 📦 Как передают данные (state = общая память)

Аналогия: Сначала учимся строить комнаты (узлы), потом строим дом (multi-agent систему).


Что такое LangGraph?

LangGraph — это фреймворк для создания графов вычислений с состоянием. Представьте конвейер, где:

  • State (состояние) — данные, которые передаются между узлами
  • Nodes (узлы) — функции, которые обрабатывают и обновляют состояние
  • Edges (рёбра) — связи между узлами, определяющие порядок выполнения
[ В х о д ] S [ t У a з t е e л A ] [ S У t з a е t л e B ] [ В ы х о д ]

В контексте агентов:

  • State = общая память всех агентов
  • Node = отдельный агент со сво��й задачей
  • Edge = передача управления от агента к агенту

Мин��мальный пример: A → B

Создадим простой граф из двух узлов, где каждый узел добавляет свою пометку в сообщение.

Шаг 1: Определяем состояние

from typing import TypedDict

class SimpleState(TypedDict, total=False):
    message: str  # Накапливаем сообщения
    count: int    # Счётчик проходов

Зачем TypedDict?

  • ✅ Строгая типизация — IDE подсказывает поля
  • ✅ Проверка типов — меньше ошибок
  • ✅ Документация — явно видно, что в состоянии

total=False — можно не инициализировать все поля сразу

Шаг 2: Создаём узлы

def node_a(state: SimpleState) -> SimpleState:
    """Узел A: увеличивает счётчик и добавляет пометку."""
    new_state = dict(state)  # ✓ Создаём копию!
    new_state["count"] = (new_state.get("count") or 0) + 1
    new_state["message"] = (new_state.get("message") or "") + " → A"
    return new_state

def node_b(state: SimpleState) -> SimpleState:
    """Узел B: добавляет свою пометку."""
    new_state = dict(state)
    new_state["message"] = (new_state.get("message") or "") + " → B"
    return new_state

Важные правила:

  1. Не мутируйте входной state — создавайте копию
  2. Используйте .get() с дефолтами — защита от KeyError
  3. Возвращайте полное состояние — даже если изменили одно поле

Шаг 3: Собираем граф

from langgraph.graph import StateGraph

def build_graph():
    graph = StateGraph(SimpleState)
    
    # Добавляем узлы
    graph.add_node("A", node_a)
    graph.add_node("B", node_b)
    
    # Соединяем узлы
    graph.add_edge("A", "B")
    
    # Определяем вход и выход
    graph.set_entry_point("A")
    graph.set_finish_point("B")
    
    # Компилируем в исполняемый workflow
    return graph.compile()

Что происходит:

  1. StateGraph(SimpleState) — создаём граф с типизированным состоянием
  2. add_node(name, function) — регистрируем узлы
  3. add_edge(from, to) — создаём связь между узлами
  4. set_entry_point / set_finish_point — обязательные точки входа/выхода
  5. compile() — превращаем граф в исполняемый workflow

Шаг 4: Запускаем

def main():
    workflow = build_graph()
    
    # Начальное состояние
    initial = {"message": "Start", "count": 0}
    
    # Запускаем граф
    result = workflow.invoke(initial)
    
    print(result)
    # {"message": "Start → A → B", "count": 1}

Полный код

from typing import TypedDict
from langgraph.graph import StateGraph

class SimpleState(TypedDict, total=False):
    message: str
    count: int

def node_a(state: SimpleState) -> SimpleState:
    new_state = dict(state)
    new_state["count"] = (new_state.get("count") or 0) + 1
    new_state["message"] = (new_state.get("message") or "") + " → A"
    print(f"Node A: {new_state}")
    return new_state

def node_b(state: SimpleState) -> SimpleState:
    new_state = dict(state)
    new_state["message"] = (new_state.get("message") or "") + " → B"
    print(f"Node B: {new_state}")
    return new_state

def build_graph():
    graph = StateGraph(SimpleState)
    graph.add_node("A", node_a)
    graph.add_node("B", node_b)
    graph.add_edge("A", "B")
    graph.set_entry_point("A")
    graph.set_finish_point("B")
    return graph.compile()

def main():
    workflow = build_graph()
    result = workflow.invoke({"message": "Start", "count": 0})
    print(f"Final: {result}")

if __name__ == "__main__":
    main()

Запуск

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

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

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

Вывод:

N N F o o i d d n e e a l A B : : : { { { ' ' ' m m m e e e s s s s s s a a a g g g e e e ' ' ' : : : ' ' ' S S S t t t a a a r r r t t t A A A ' , B ' B ' c ' , o , u ' n ' c t c o ' o u : u n n t 1 t ' } ' : : 1 1 } }

Отладка: stream() для пошагового выполнения

Если нужно видеть каждый шаг:

workflow = build_graph()
initial = {"message": "Start", "count": 0}

for step in workflow.stream(initial):
    print(f"Step: {step}")

Вывод:

S S t t e e p p : : { { ' ' A B ' ' : : { { ' ' m m e e s s s s a a g g e e ' ' : : ' ' S S t t a a r r t t A A ' , ' B c ' o , u n ' t c ' o : u n 1 t } ' } : 1 } }

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

❌ Ошибка 1: Мутация состояния

def bad_node(state: SimpleState) -> SimpleState:
    state["message"] += " → Bad"  # Мутирует оригинал!
    return state

Проблема: Изменяет входной state, сложно отлаживать

✅ Правильно:

def good_node(state: SimpleState) -> SimpleState:
    new_state = dict(state)  # Копия
    new_state["message"] = (new_state.get("message") or "") + " → Good"
    return new_state

❌ Ошибка 2: Забыли entry/finish point

graph = StateGraph(SimpleState)
graph.add_node("A", node_a)
graph.compile()  # ❌ Ошибка: нет entry_point!

✅ Правильно:

graph.set_entry_point("A")
graph.set_finish_point("A")
graph.compile()  # ✅ Работает

❌ Ошибка 3: KeyError на несуществующем поле

def bad_node(state: SimpleState) -> SimpleState:
    new_state = dict(state)
    new_state["count"] = state["count"] + 1  # ❌ KeyError если count не задан!
    return new_state

✅ Правильно:

def good_node(state: SimpleState) -> SimpleState:
    new_state = dict(state)
    new_state["count"] = (new_state.get("count") or 0) + 1  # ✅ Дефолт
    return new_state

Тестирование

import pytest
from src.day1_min_graph import node_a, node_b, SimpleState

def test_node_a():
    state: SimpleState = {"message": "Test", "count": 0}
    result = node_a(state)
    
    assert result["count"] == 1
    assert "→ A" in result["message"]

def test_node_b():
    state: SimpleState = {"message": "Test"}
    result = node_b(state)
    
    assert "→ B" in result["message"]

def test_full_graph():
    from src.day1_min_graph import build_graph
    
    workflow = build_graph()
    result = workflow.invoke({"message": "Start", "count": 0})
    
    assert result["count"] == 1
    assert result["message"] == "Start → A → B"

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

StateGraph — основа LangGraph, граф с типизированным состоянием
TypedDict — строгая типизация для state
Неизменяемость — всегда создавайте копию state
entry/finish points — обязательны для компиляции
invoke() — запуск графа, возвращает финальное состояние
stream() — пошаговое выполнение для отла��ки

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

Сегодня вы научились:

  • ✅ Создавать узлы (будущие агенты)
  • ✅ Передавать состояние (общая память агентов)
  • ✅ Связывать узлы (координация агентов)

Следующий шаг: Научимся делать агентов умнее — добавим условную логику и циклы.


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

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

  • Conditional Edges — выбор следующего узла на основе state
  • Циклы — как создать узел, который вызывает сам себя
  • Защита от бесконечных циклов — MAX_ITERATIONS

👉 День 2: State и Conditional Edges


Ресурсы


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