← Назад к курсу

Пособие по основам работы с LangGraph

📚 Часть 1: Теория графов — Фундамент для понимания

Граф — это математическая структура, которая моделирует отношения между объектами. В компьютерных науках это нелинейная структура данных, где связи могут быть сложнее, чем просто "соседство".

Граф состоит из двух ключевых элементов:

  • Вершины (Nodes): Основные объекты или точки данных. В LangGraph это функции или компоненты вашего агента.
  • Рёбра (Edges): Связи между вершинами, определяющие, как состояние или данные перемещаются по системе. В LangGraph это переходы между узлами.

Для работы с LangGraph полезно понимать несколько базовых концепций:

Концепция Определение Аналог в LangGraph
Путь / Цепь Последовательность смежных вершин и рёбер. Маршрут выполнения агента от старта до финиша.
Цикл Путь, в котором начальная и конечная вершины совпадают. Механизм, позволяющий агенту повторять действия (например, задавать уточняющие вопросы).
Связный граф Граф, где между любыми двумя вершинами существует путь. Корректно построенный workflow, где у каждого узла есть вход и выход.
Ориентированный граф Граф, рёбра которого имеют направление. Все графы в LangGraph ориентированы — состояние течёт по заданному направлению рёбер.
Взвешенный граф Граф, где рёбрам или вершинам присвоены числовые значения ("веса"). Решение о переходе по тому или иному ребру может зависеть от "веса" — результата условной проверки.

⚙️ Часть 2: LangGraph в деталях

LangGraph — это низкоуровневый фреймворк для оркестрации агентов: долгоживущих, сохраняющих состояние и способных выполнять сложные задачи. В отличие от линейных цепочек LangChain, LangGraph позволяет создавать графы любой сложности с ветвлениями, циклами и параллельным выполнением.

Ключевые архитектурные компоненты LangGraph показаны на схеме ниже. Это основа, на которой строятся все агенты:

Давайте подробно разберем, как работает каждый компонент.

1. Состояние (State)

Это центральный объект, который хранит все данные, передаваемые между узлами графа. Обычно это словарь (например, TypedDict в Python). Состояние должно быть изменяемым (mutable), так как каждый узел может его обновлять.

Пример определения состояния для процесса написания шутки:

from typing_extensions import TypedDict
from langgraph.graph import StateGraph

class State(TypedDict):
    topic: str
    joke: str
    improved_joke: str
    final_joke: str

2. Узлы (Nodes)

Это основные вычислительные единицы. Каждый узел — это функция, которая принимает текущее состояние, выполняет логику и возвращает обновление для состояния. Функция может вызывать LLM, работать с инструментами или выполнять любую другую логику.

Пример узла, который генерирует шутку:

def generate_joke(state: State):
    msg = llm.invoke(f"Write a short joke about {state['topic']}")
    return {"joke": msg.content}

3. Рёбра (Edges)

Они определяют поток выполнения. В LangGraph есть три основных типа:

  • Обычные рёбра: Линейный переход от одного узла к другому.
  • Условные рёбра: Решение о следующем узле принимается на основе результата функции-маршрутизатора.
  • Рёбра начала и конца: Специальные константы START и END.

Пример добавления рёбер при построении графа:

workflow = StateGraph(State)
workflow.add_node("generate_joke", generate_joke)
workflow.add_edge(START, "generate_joke")
workflow.add_conditional_edges(
    "generate_joke",
    check_punchline, # Функция, возвращающая "Pass" или "Fail"
    {"Pass": END, "Fail": "improve_joke"}
)

🛠️ Часть 3: Практические шаблоны в LangGraph

Теперь рассмотрим, как компоненты складываются в полезные архитектуры.

Шаблон 1: Последовательная цепочка с условием

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

Процесс выглядит следующим образом:

Шаблон 2: Параллельное выполнение

Используется для ускорения работы или повышения надёжности за счёт одновременного запуска нескольких независимых задач.

Схема параллельного выполнения трех задач с последующей агрегацией результатов:

Шаблон 3: Ветвление (Routing)

Позволяет создавать "интеллектуальных" агентов, которые анализируют входные данные (например, с помощью LLM) и направляют запрос по нужному пути.

Процесс принятия решения о типе запроса и его маршрутизации:

Шаблон 4: Агент с инструментами (Tools) и циклом

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

Схема работы агента с инструментами в цикле:

В этом цикле LLM выступает в роли "мозга", который на каждом шаге решает, какой инструмент применить или дать финальный ответ. Инструменты — это функции, дающие агенту "руки" для работы с внешними системами: выполнения запросов к БД, вызова API, работы с файлами.

🚀 Часть 4: Следующие шаги и рекомендации

  1. Начните с простого. Реализуйте базовый линейный граф, чтобы понять работу со State и узлами.
  2. Добавьте условие. Усложните граф, добавив условное ребро и функцию-маршрутизатор.
  3. Дайте агенту "руки". Создайте простой инструмент (например, калькулятор или поиск в Wikipedia) и интегрируйте его в граф с циклом, как в Шаблоне 4.
  4. Используйте LangSmith для отладки. Этот инструмент предоставляет глубокую визуализацию выполнения графа, что незаменимо для сложных агентов.
  5. Помните о безопасности. Когда агент получает доступ к инструментам, важно продумать систему контроля и проверок (human-in-the-loop), особенно для операций записи или критических действий.

Главное преимущество LangGraph — в контроле и гибкости. Вы проектируете не "чёрный ящик", а детализированную когнитивную архитектуру, которую можно точно настроить под свою задачу.

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

📁 Структура проекта

joke_agent/
├── main.py          # Основной файл с графом
├── requirements.txt # Зависимости
└── .env            # Переменные окружения (API-ключи)

1. requirements.txt

langgraph
langchain-openai
python-dotenv

2. .env (создайте этот файл)

OPENAI_API_KEY=ваш_ключ_здесь

3. main.py — полный код примера

import os
from typing import Literal, TypedDict
from dotenv import load_dotenv
from langgraph.graph import StateGraph, END, START
from langchain_openai import ChatOpenAI

# Загружаем переменные окружения
load_dotenv()

# ==================== 1. ОПРЕДЕЛЕНИЕ СОСТОЯНИЯ ====================
class AgentState(TypedDict):
    """Состояние нашего агента. Все узлы могут читать и записывать эти поля."""
    user_request: str          # Что хочет пользователь
    initial_joke: str          # Первоначальная шутка
    joke_rating: int           # Оценка шутки (1-10)
    analysis: str              # Анализ качества шутки
    improved_joke: str         # Улучшенная версия
    final_output: str          # Финальный результат для пользователя

# ==================== 2. ИНИЦИАЛИЗАЦИЯ LLM ====================
llm = ChatOpenAI(
    model="gpt-3.5-turbo",
    temperature=0.7,
    api_key=os.getenv("OPENAI_API_KEY")
)

# ==================== 3. ОПРЕДЕЛЕНИЕ УЗЛОВ (NODES) ====================

def generate_joke(state: AgentState):
    """Узел 1: Генерация первоначальной шутки"""
    print("🔹 Узел 'generate_joke': Генерирую шутку...")
    
    prompt = f"Придумай короткую смешную шутку на тему: {state['user_request']}. Шутка должна быть на русском языке."
    response = llm.invoke(prompt)
    
    return {"initial_joke": response.content}

def analyze_joke(state: AgentState):
    """Узел 2: Анализ и оценка шутки"""
    print(f"🔹 Узел 'analyze_joke': Анализирую шутку...")
    print(f"   Текст шутки: {state['initial_joke']}")
    
    prompt = f"""Проанализируй эту шутку и дай оценку от 1 до 10:
    
    Шутка: "{state['initial_joke']}"
    
    Верни ответ строго в формате JSON:
    {{
        "rating": число от 1 до 10,
        "analysis": "короткий анализ на русском, почему такая оценка"
    }}
    """
    
    response = llm.invoke(prompt)
    
    # Парсим ответ (упрощённо)
    content = response.content
    rating = 5  # значение по умолчанию
    
    # Простой парсинг
    if '"rating":' in content:
        try:
            # Ищем число после "rating":
            start = content.find('"rating":') + 9
            end = content.find(',', start)
            if end == -1:
                end = content.find('}', start)
            rating_str = content[start:end].strip()
            rating = int(rating_str)
        except:
            pass
    
    analysis = f"Оценка: {rating}/10. " + content[:100] + "..."
    
    return {
        "joke_rating": rating,
        "analysis": analysis
    }

def improve_joke(state: AgentState):
    """Узел 3: Улучшение шутки, если она плохая"""
    print(f"🔹 Узел 'improve_joke': Улучшаю шутку (текущая оценка: {state['joke_rating']}/10)...")
    
    prompt = f"""Улучши эту шутку. Сделай её смешнее и остроумнее.
    
    Исходная шутка: "{state['initial_joke']}"
    
    Анализ проблемы: {state['analysis']}
    
    Верни ТОЛЬКО улучшенную версию шутки, без пояснений."""
    
    response = llm.invoke(prompt)
    
    return {"improved_joke": response.content}

def finalize_output(state: AgentState):
    """Узел 4: Подготовка финального ответа для пользователя"""
    print("🔹 Узел 'finalize_output': Формирую финальный ответ...")
    
    if state.get('improved_joke'):
        # Если шутку улучшали
        output = f"""🎭 **Ваша шутка на тему "{state['user_request']}":**
        
        **Первоначальный вариант:**
        {state['initial_joke']}
        
        ⚠️ *Наш анализ:* {state['analysis']}
        
        **Улучшенный вариант:**
        {state['improved_joke']}
        
        Надеемся, стало смешнее! 😄"""
    else:
        # Если шутка и так хорошая
        output = f"""🎭 **Ваша шутка на тему "{state['user_request']}":**
        
        {state['initial_joke']}
        
        ✅ *Наш анализ:* {state['analysis']}
        
        Отличная шутка! Оценка: {state['joke_rating']}/10 🏆"""
    
    return {"final_output": output}

# ==================== 4. ФУНКЦИЯ МАРШРУТИЗАЦИИ ====================

def should_improve_joke(state: AgentState) -> Literal["good_enough", "needs_improvement"]:
    """
    Условное ребро: решает, нужно ли улучшать шутку.
    Возвращает название следующего узла.
    """
    rating = state.get('joke_rating', 0)
    
    if rating >= 7:
        print(f"    Маршрутизатор: шутка хорошая ({rating}/10), идём к финалу")
        return "good_enough"
    else:
        print(f"    Маршрутизатор: шутка слабая ({rating}/10), нужно улучшить")
        return "needs_improvement"

# ==================== 5. ПОСТРОЕНИЕ ГРАФА ====================

def create_joke_agent():
    """Создаём и конфигурируем граф агента"""
    print("🛠️ Создаю граф агента...")
    
    # 1. Создаём граф с нашим состоянием
    workflow = StateGraph(AgentState)
    
    # 2. Добавляем узлы
    workflow.add_node("generate", generate_joke)
    workflow.add_node("analyze", analyze_joke)
    workflow.add_node("improve", improve_joke)
    workflow.add_node("finalize", finalize_output)
    
    # 3. Добавляем рёбра
    workflow.add_edge(START, "generate")      # Старт -> Генерация
    workflow.add_edge("generate", "analyze")  # Генерация -> Анализ
    
    # 4. УСЛОВНОЕ РЕБРО: после анализа решаем, что делать дальше
    workflow.add_conditional_edges(
        "analyze",
        should_improve_joke,  # Функция-маршрутизатор
        {
            "good_enough": "finalize",        # Если хорошо -> финализируем
            "needs_improvement": "improve"    # Если плохо -> улучшаем
        }
    )
    
    # 5. Линейные рёбра от улучшения к финалу
    workflow.add_edge("improve", "finalize")
    workflow.add_edge("finalize", END)
    
    # 6. Компилируем граф
    print("✅ Граф создан и скомпилирован!")
    return workflow.compile()

# ==================== 6. ЗАПУСК АГЕНТА ====================

def run_agent():
    """Функция запуска агента с примером запроса"""
    # Создаём агента
    agent = create_joke_agent()
    
    # Начальное состояние
    initial_state = AgentState(
        user_request="программисты и кофе",
        initial_joke="",
        joke_rating=0,
        analysis="",
        improved_joke="",
        final_output=""
    )
    
    print("\n" + "="*50)
    print("🚀 ЗАПУСК АГЕНТА")
    print("="*50)
    print(f"Запрос: {initial_state['user_request']}")
    print("="*50 + "\n")
    
    # Запускаем граф
    try:
        result = agent.invoke(initial_state)
        
        print("\n" + "="*50)
        print("📊 РЕЗУЛЬТАТ РАБОТЫ АГЕНТА")
        print("="*50)
        print(result['final_output'])
        print("="*50)
        
        # Дополнительная информация
        print("\n📈 ПРОМЕЖУТОЧНЫЕ РЕЗУЛЬТАТЫ:")
        print(f"• Первоначальная шутка: {result['initial_joke'][:80]}...")
        print(f"• Оценка: {result['joke_rating']}/10")
        if result.get('improved_joke'):
            print(f"• Улучшенная шутка: {result['improved_joke'][:80]}...")
        
    except Exception as e:
        print(f"❌ Ошибка: {e}")
        if "API key" in str(e):
            print("Проверьте, что вы добавили OPENAI_API_KEY в файл .env")

# ==================== 7. ВИЗУАЛИЗАЦИЯ ГРАФА ====================

def visualize_graph():
    """Создаём визуализацию графа (сохраняет в файл)"""
    try:
        agent = create_joke_agent()
        
        # Сохраняем граф в PNG
        from IPython.display import Image, display
        
        # Для сохранения в файл
        image_data = agent.get_graph().draw_mermaid_png()
        
        with open("joke_agent_graph.png", "wb") as f:
            f.write(image_data)
        
        print("📊 Граф сохранён в файл 'joke_agent_graph.png'")
        
        # Если запущено в Jupyter, можно отобразить
        # display(Image(image_data))
        
    except Exception as e:
        print(f"⚠️ Не удалось создать визуализацию: {e}")
        print("Установите: pip install pygraphviz")

# ==================== 8. ТОЧКА ВХОДА ====================

if __name__ == "__main__":
    print("🤖 Агент для генерации и оценки шуток")
    print("Использует LangGraph для управления workflow\n")
    
    # Запускаем агента
    run_agent()
    
    # Создаём визуализацию (опционально)
    # visualize_graph()

🎯 Как запустить:

  1. Установите зависимости:
pip install -r requirements.txt
  1. Получите API-ключ OpenAI и добавьте его в файл .env

  2. Запустите агента:

python main.py

📊 Пример вывода:

🛠️ Создаю граф агента...
✅ Граф создан и скомпилирован!

==================================================
🚀 ЗАПУСК АГЕНТА
==================================================
Запрос: программисты и кофе
==================================================

🔹 Узел 'generate_joke': Генерирую шутку...
🔹 Узел 'analyze_joke': Анализирую шутку...
    Маршрутизатор: шутка слабая (4/10), нужно улучшить
🔹 Узел 'improve_joke': Улучшаю шутку (текущая оценка: 4/10)...
🔹 Узел 'finalize_output': Формирую финальный ответ...

==================================================
📊 РЕЗУЛЬТАТ РАБОТЫ АГЕНТА
==================================================
🎭 **Ваша шутка на тему "программисты и кофе":**
    
**Первоначальный вариант:**
Почему программист пьёт кофе? Потому что Java!

⚠️ *Наш анализ:* Оценка: 4/10. Шутка банальная...

**Улучшенный вариант:**
Почему программист предпочитает кофе без сахара?
Потому что ему хватает sweet синтаксиса в коде!

Надеемся, стало смешнее! 😄
==================================================

🔍 Что происходит в графе:

💡 Ключевые моменты примера:

  1. Полный цикл работы: от запроса пользователя до финального результата
  2. Условная логика: агент решает, нужно ли улучшать шутку
  3. Состояние передаётся: каждый узел видит и может изменять общее состояние
  4. Логирование: на каждом шаге видно, что происходит
  5. Готовность к расширению: можно легко добавить новые узлы или условия

Это основа, на которой можно строить более сложных агентов для любых задач!

Надеюсь, это руководство дало вам чёткое понимание как теории, так и практики. Удачи в создании интеллектуальных агентов