К курсу

Цикл агента

Один шаг цикла агентаПрактика: один шаг циклаЦикл в кодеПрактика: цикл в кодеОстановка и повторыПрактика: остановка и повторыЧто решает только кодПрактика: что решает код

Надёжность в проде

Таймаут и ключ идемпотентностиПрактика: таймаут и ключ идемпотентностиСказал «готово», а ничего не произошлоПрактика: ложный успехУстойчивый агент: checkpoint и продолжениеПрактика: устойчивый агент

Устройство агентов

Workflow или агентПрактика: workflow или агентReAct и plan-and-executeПрактика: ReAct и plan-and-executePlan-and-SolveПрактика: Plan-and-SolveReflexionПрактика: ReflexionОдин агент или ройПрактика: один агент или рой

Контекст и стоимость

Почему растёт счёт за агентаПрактика: токены и кэшКонтекст-инженерияПрактика: управление окномПамять агентаПрактика: что агент хранит

Инструменты и протоколы

MCPПрактика: MCPОписания инструментов и инъекцииПрактика: описания инструментов и инъекцииКод вместо набора инструментовПрактика: код вместо набора инструментовНавыки агента и A2AПрактика: навыки агента и A2A

Фреймворки

LangGraph или обычный циклПрактика: LangGraph или обычный циклAgent SDK: что вы покупаетеПрактика: agent SDK

Агенты для кода

Агент для кода по спецификацииПрактика: агент для кода по спецификации
←→ENВойти
Агенты › ФреймворкиDeep dive

LangGraph или обычный цикл

Опирается наУстойчивый агент: checkpoint и продолжениеWorkflow или агент

Этот блок показывает, что LangGraph добавляет к уже написанному вами циклу, что оставляет вам и как понять, что он окупится.

Возвраты дороже €200 в Northwind теперь ждут менеджера, а он может ответить завтра утром. Сервер перезапускается при каждой выкатке. Цикл из прошлого блока держит messages в локальной переменной, и возврату, который ждёт до утра, негде жить.

Папка с делом в конторе

Представьте бумажную папку, которая ходит по конторе. Каждый стол читает её, вкладывает лист и отправляет дальше. На столе висит табличка, куда идти потом, и некоторые таблички зависят от содержимого папки. Каждый раз, когда папка уходит дальше, клерк подшивает её копию под номером дела. Если менеджера нет на месте, папка лежит в шкафу, и никому не приходится стоять и держать её в руках.

LangGraph и есть такая контора.

В конторе

В LangGraph

Что это

Папка

Состояние

Общие данные, которые читает любой узел, здесь TypedDict

Стол

Узел

Функция: читает состояние и возвращает изменения

Табличка

Ребро

Какой узел следующий, заранее или по решению функции

Копия

Чекпоинт

Состояние, сохранённое на каждом шаге

Номер дела

thread_id

Какое сохранённое состояние загрузить

Ваш цикл из прошлого блока — это контора с двумя столами. Проверка причины остановки стала табличкой.

Тот же цикл, нарисованный графом. Решение, которое вы писали через if, теперь условное ребро.
Тот же цикл, нарисованный графом. Решение, которое вы писали через if, теперь условное ребро.

В таком простом цикле вы ничего не выигрываете: логика та же, а новых слов прибавилось. Выигрыш начинается с копий и шкафа.

Один возврат через контору

Вот папка для возврата. Часть с моделью опущена, чтобы был виден сам механизм.

class Case(TypedDict):
    order_id: str
    amount_cents: int
    approved: bool
    log: Annotated[list[str], operator.add]

Узел возвращает только то, что меняет. По умолчанию новое значение заменяет старое. У log есть reducer, operator.add, поэтому каждое изменение дописывается. Без него каждый стол выбрасывал бы листы всех предыдущих.

Стол, который ждёт, вызывает interrupt:

def ask_manager(case: Case):
    answer = interrupt({"order_id": case["order_id"],
                        "amount_cents": case["amount_cents"]})
    return {"approved": answer == "approve",
            "log": [f"manager said {answer}"]}

Таблички — это обычные функции, которые называют следующий узел:

def route(case: Case) -> Literal["ask_manager", "refund"]:
    return "ask_manager" if case["amount_cents"] > 20000 else "refund"

builder.add_conditional_edges("propose", route)

Ваш код запускает прогон с номером дела, и граф останавливается у стола менеджера:

config = {"configurable": {"thread_id": "case-4417"}}
result = graph.invoke({"order_id": "4417", "amount_cents": 34900,
                       "approved": False, "log": []}, config)
result["__interrupt__"]   # заказ и сумма для экрана менеджера

Теперь процесс может завершиться. Наутро новый процесс собирает тот же граф с тем же чекпоинтером и передаёт ответ:

graph.invoke(Command(resume="approve"), config)

Значение из Command(resume=...) становится тем, что вернёт interrupt. Документация LangGraph теперь советует вести такие прогоны через stream_events(..., version="v3"). Этот режим пока помечен как экспериментальный, а invoke по-прежнему работает, и здесь он короче.

Чекпоинты настоящего прогона на LangGraph 1.2.20 с SqliteSaver: паузу и продолжение выполнили два разных процесса.
Чекпоинты настоящего прогона на LangGraph 1.2.20 с SqliteSaver: паузу и продолжение выполнили два разных процесса.

На картинке пять чекпоинтов на три узла: один пустой в начале, один со входными данными и по одному после каждого узла. ask_manager выполнился дважды, потому что, как говорилось в блоке о надёжных прогонах, узел начинает работу с самого начала. SqliteSaver годится для локальной работы, а для продакшена документация LangGraph называет PostgresSaver. По умолчанию чекпоинт пишется в фоне, пока идёт следующий шаг (durability="async"), поэтому есть небольшой риск, что при падении последний чекпоинт потеряется. durability="sync" дожидается каждой записи и только потом идёт дальше.

Новый thread_id — это пустая папка. Повтор, который придумывает свежий id, заводит дело заново, а не продолжает его.

Та же пауза в обычном цикле

Обычный цикл справляется с этим лучше, чем принято думать, потому что messages и так простые данные. Сохранили, остановились, продолжили, когда пришёл ответ:

def pause(case_id, messages, results, block):
    db.save(case_id, {"messages": messages, "results": results,
                      "pending": block})

def resume(case_id, decision):
    run = db.load(case_id)
    answer = apply_decision(run["pending"], decision)   # tool_result
    run["messages"].append({"role": "user",
                            "content": run["results"] + [answer]})
    return run_agent(run["messages"])

Результаты остальных вызовов из того же ответа хранятся вместе с паузой, потому что в API Anthropic все они идут одним сообщением. Если точка паузы одна, вам хватит таблицы и двух функций, и все промпты остаются в вашем коде. Сравнение меняется, когда растут требования:

Что нужно

Обычный цикл

LangGraph

Одно одобрение перед одним инструментом

Таблица и две функции

interrupt и чекпоинтер

Продолжить после падения на любом шаге

Сохранять после каждого шага, написать восстановление

Чекпоинт на шаг, а invoke(None, config) повторяет то, что не завершилось

Заданные шаги вперемешку с шагами модели

if, пока путей немного

Узлы и условные рёбра

Параллельные ветки, одна упала

Самим помнить, какие завершились

Завершённые узлы заново не выполняются

Посмотреть или повторить старый прогон

Написать самим

get_state_history, повтор со снимка

Последний вопрос на картинке — о движках надёжного выполнения вроде Temporal из блока о надёжных прогонах.

Правило на глазок. Каждый вопрос добавляет механизмы, только если ответ этого требует.
Правило на глазок. Каждый вопрос добавляет механизмы, только если ответ этого требует.

Чем вы платите и что остаётся за вами

Совет Anthropic 2024 года: начинайте с прямых вызовов API, а если берёте фреймворк, понимайте код под ним, потому что неверные предположения о нём — частая причина ошибок. LangGraph тут стоит низко: в его документации сказано, что он не прячет промпты и не навязывает архитектуру.

Цена всё равно есть. Придётся разобраться в его модели выполнения, состояние должно переживать сериализацию, а старые чекпоинты копятся, пока вы их не почистите или не зададите срок хранения. С 10 ноября 2025 по 10 ноября 2026 у него вышло 50 релизов на PyPI, считая патчи и 10 предварительных версий, так что фиксируйте версию.

Граф не делает возврат правильным. Проверка владельца, суммы и ключ идемпотентности из прошлых блоков по-прежнему живут в вашем узле refund.

Называйте слой правильно. create_agent импортируется из langchain.agents: это готовый цикл агента из LangChain, построенный поверх LangGraph. Собственный конструктор LangGraph называется StateGraph.

Вывод

Глубже

LangGraph: InterruptsКак поставить на паузу и продолжить, включая несколько пауз сразу.LangGraph: CheckpointersКак устроены super-steps и pending writes и как разные вызовы продолжают поток.LangChain: Runtimes, frameworks, and harnessesСобственная карта LangChain: где стоят LangGraph, LangChain и Deep Agents.Anthropic: Building effective agentsРаздел о фреймворках. Текст 2024 года, и в нём самом сказано, что инструменты с тех пор изменились.
‹ Назад
Предложить улучшениеНашёл ошибку