Задача: хотим «агента», который делает несколько шагов, помнит контекст, принимает решения и останавливается по условию. Это не один вызов модели — это цикл: подумай → действуй → обнови память → повтори до цели. В этой статье начнём именно с агентной постановки, а затем покажем, как LangGraph помогает это собрать прозрачно и тестопригодно.

План статьи:

  • Что такое агент простыми словами и зачем он нужен
  • LangGraph в 1 минуту: узлы, рёбра, состояние
  • Мини-агент: Plan → Act с критерием остановки (код)
  • Базовая механика без агент��ости: граф A→B (код)
  • Подводные камни и куда двигаться дальше

Что такое агент и зачем он нужен

Агент — это цикл действий с памятью:

  • есть цель (goal),
  • есть память о шагах (мысли, действия, результаты),
  • на каждом шаге он решает «что дальше» и при необходимости вызывает инструменты/функции/модели,
  • он умеет останавливаться, когда цель достигнута или превышен лимит.

Почему это важно:

  • Многошаговые задачи (сбор данных, планирование задач, интеграции) редко решаются одним вызовом LLM;
  • Нужна прозрачность и контроль: где мы находимся, почему агент принял такое решение, когда нужно остановиться.

LangGraph в 1 минуту

LangGraph — это способ собирать таких агентов из простых кусочков кода:

  • Состояние (state) — явная «память» агента (goal, thoughts, actions, result, done…).
  • Узлы (nodes) — функции, которые читают и изменяют состояние (например, Plan, Act).
  • Рёбра (edges) — порядок переходов между узлами (включая циклы и развилки).
  • Точки входа/выхода — где стартуем и где заканчиваем.

В итоге вы получаете исполняемый workflow с методами вроде invoke/stream. Никакой «магии»: это обычные функции и прозрачные переходы.


Мини-агент: Plan → Act → (повтор) с критерием остановки

Соберём минимальную петлю. В реальности Plan может вызывать LLM, а Act — инструменты и API. Здесь используем имитацию, чтобы пример был самодостаточным и сразу работал.

Схема:

  • state: goal, thoughts, actions, result, done
  • Plan: решает следующий шаг; если цель уже достигнута — done=True
  • Act: «выполняет» шаг (заглушка), обновляет result и журнал действий
  • Цикл: Plan → Act ��� Plan, пока не done или не исчерпан лимит итераций
from typing import TypedDict, List
from langgraph.graph import StateGraph


class SimpleAgentState(TypedDict, total=False):
    goal: str
    thoughts: List[str]
    actions: List[str]
    result: str
    done: bool


def node_plan(state: SimpleAgentState) -> SimpleAgentState:
    new = dict(state)
    new["thoughts"] = list(new.get("thoughts") or [])
    new["actions"] = list(new.get("actions") or [])
    new["result"] = new.get("result") or ""

    goal = new.get("goal") or ""
    result = new.get("result") or ""

    # Наивная проверка достижения цели: если goal встречается в result
    if goal and goal.lower() in result.lower():
        new["thoughts"].append("Цель достигнута — останавливаемся.")
        new["done"] = True
        return new  # Без постановки нового шага

    # Иначе запланируем простое действие: 'append_goal'
    new["thoughts"].append("Похоже, цель не достигнута. Действуем: append_goal")
    new["next_action"] = "append_goal"  # служебное поле для act
    new["done"] = False
    return new  # type: ignore[return-value]


def node_act(state: SimpleAgentState) -> SimpleAgentState:
    new = dict(state)
    new["actions"] = list(new.get("actions") or [])
    new["result"] = new.get("result") or ""

    action = new.pop("next_action", None)
    if action == "append_goal":
        goal = new.get("goal") or ""
        new["result"] = (new["result"] or "") + f" {goal}".strip()
        new["actions"].append("append_goal")
    else:
        new["actions"].append("noop")

    return new  # type: ignore[return-value]


def build_agent_graph():
    g = StateGraph(SimpleAgentState)
    g.add_node("Plan", node_plan)
    g.add_node("Act", node_act)

    # Простой цикл: Plan -> Act -> Plan. Остановку контролируем флагом done на уровне вызова.
    g.add_edge("Plan", "Act")
    g.add_edge("Act", "Plan")

    g.set_entry_point("Plan")
    return g.compile()


def run_agent_once(goal: str):
    workflow = build_agent_graph()

    state: SimpleAgentState = {
        "goal": goal,
        "thoughts": [],
        "actions": [],
        "result": "",
        "done": False,
    }

    # Ограничим 5 итерациями, чтобы избежать бесконечного цикла
    for _ in range(5):
        state = workflow.invoke(state)  # Plan -> Act -> Plan
        if state.get("done"):
            break
    return state


if __name__ == "__main__":
    final = run_agent_once("hello")
    print(final)

Идея: за 1–2 итерации в result появится цель (hello), Plan увидит совпадение и выставит done=True. Это и есть «агентность»: последовательное планирование и действие с явной памятью и контролем остановки.

Под реальный LLM: замените логику внутри node_plan на вызов модели (через LangChain/клиенты LLM), а в node_act подключите настоящие инструменты. Прелесть LangGraph — вы управляете потоком (ветвления, повторы, остановки) и храните память в открытом виде, что удобно тестировать и дебажить.


Базовая механика LangGraph без агентной логики: A → B

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

from typing import TypedDict
from langgraph.graph import StateGraph
from rich import print as rprint


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


def node_a(state: SimpleState) -> SimpleState:
    new_state: SimpleState = dict(state)
    new_state["count"] = (new_state.get("count") or 0) + 1
    new_state["message"] = (new_state.get("message") or "") + " -> from A"
    rprint({"node": "A", "state": new_state})
    return new_state


def node_b(state: SimpleState) -> SimpleState:
    new_state: SimpleState = dict(state)
    new_state["message"] = (new_state.get("message") or "") + " -> from B"
    rprint({"node": "B", "state": new_state})
    return new_state


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


def main():
    workflow = build_graph()
    initial: SimpleState = {"message": "hello", "count": 0}
    result = workflow.invoke(initial)
    rprint({"result": result})


if __name__ == "__main__":
    main()

Ожидаемый результат: {"message": "hello -> from A -> from B", "count": 1}. Это показательная «скелетная» версия без планирования и остановок — чистая механика «узлы + рёбра + состояние».


Подводные камни и практические советы

  • Память (state) — это контракт. Сначала продумайте поля: цель, мысли, действия, результаты, флаги. Так проще расширять агента.
  • Иммутабельный стиль обновления состояния упрощает отладку и тесты.
  • Всегда задавайте критерии остановки и/или лимиты итераций, особенно в циклах Plan/Act.
  • Для реальных проектов используйте условные рёбра и явный finish-маршрут (router), чтобы останавливать граф без «внешнего» цикла.
  • Логируйте промежуточные состояния (print/rich/logging) — это эконо��ит часы.

Что дальше (Часть 2)

  • Условная маршрутизация (router): выбор следующего узла по значению из состояния и явное завершение через finish-ребро.
  • Интеграция LLM и инструментов: реально мыслящий Plan и исполняющий Act.
  • Ветвления и циклы: ограничители, счётчики, тайм-ауты, стратегии остановки.
  • Наблюдаемость: stream() для пошагового контроля и трейсинга.

Итого: мы начали с агентной задачи и показали, как LangGraph помогает собрать прозрачного, детерминируемого многошагового агента — из узлов (мини-агентов/инструментов), рёбер (оркестрации) и состояния (памяти). Дальше углубим маршрутизацию, подключим LLM к планировщику и сделаем цикл Plan/Act по-настоящему умным и наблюдаемым.