После релиза LangGraph не может продолжить согласование: как менять имена узлов

Агент готовит отчёт и останавливается, пока руководитель его согласует. За ночь разработчики выпускают новую версию графа и переименовывают узел review в approve_report. Утром руководитель нажимает кнопку. Обработчик нашёл нужный thread_id и передал решение. Теперь важно, сможет ли новая версия вообще найти место, куда его подставить.

Имя узла попадает в контракт с сохранённым состоянием

При interrupt() LangGraph сохраняет точку исполнения через checkpointer. Вызов с тем же thread_id и Command(resume=...) адресует существующее выполнение. При продолжении прерванный узел начинает выполняться с начала. Значение из команды возвращается из соответствующего вызова interrupt(). По этой причине нельзя бездумно менять порядок нескольких interrupt() внутри узла. Состояние сохранённого потока привязано и к имени узла, в который исполнителю предстоит вернуться.

Проверьте точку продолжения через get_state(config).next. Если в сохранённом снимке ожидается review, новое имя само по себе не меняет эту запись. Вход нового запроса и продолжение старого потока требуют разных проверок. Если запускать интеграционные тесты только на новых идентификаторах, переименование пройдёт незамеченным.

Выпускаем граф с двумя точками входа для согласования

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

Пример запускается на langgraph==1.2.20. Один объект InMemorySaver моделирует общее хранилище снимков для обеих сборок. Сначала создадим прерванный поток старого графа. Затем покажем, какой узел ему нужен и присутствует ли он в двух сборках. Наконец продолжим старый поток совместимой сборкой и запустим новый.

from typing import TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt

class State(TypedDict, total=False):
    report: str
    approved: bool

def review(state: State):
    approved = interrupt({"report": state["report"]})
    return {"approved": approved}

def build(entry, keep_old=False):
    graph = StateGraph(State)
    graph.add_node(entry, review)
    graph.add_edge(START, entry)
    graph.add_edge(entry, END)
    if keep_old:
        graph.add_node("review", review)
        graph.add_edge("review", END)
    return graph.compile(checkpointer=saver)

saver = InMemorySaver()
old_cfg = {"configurable": {"thread_id": "old-report"}}
new_cfg = {"configurable": {"thread_id": "new-report"}}
old = build("review")
old.invoke({"report": "Отчёт за октябрь"}, old_cfg)
waiting = old.get_state(old_cfg).next
renamed = build("approve_report")
print("Старый поток ждёт:", waiting)
assert waiting == ("review",)
assert not set(waiting) <= renamed.get_graph().nodes.keys()

compatible = build("approve_report", keep_old=True)
result = compatible.invoke(Command(resume=True), old_cfg)
assert result["approved"] is True
print("Старый отчёт согласован:", result["approved"])
compatible.invoke({"report": "Отчёт за ноябрь"}, new_cfg)
next_node = compatible.get_state(new_cfg).next
assert next_node == ("approve_report",)
print("Новый поток ждёт:", next_node)

У старого потока в next остаётся review. У новой несовместимой сборки этого узла нет. Совместимая сборка успешно завершает старое согласование, а для нового отчёта сохраняет approve_report. На своём графе сделайте такой тест для каждой пользовательской паузы: сохраните снимок кодом текущего релиза, загрузите его кандидатом и проверьте ожидаемый результат после ответа пользователя.

Когда можно удалить старый узел

Перед удалением найдите потоки, которые ещё могут возобновиться, и проверьте их ожидаемые узлы. Учитывайте срок действия согласований и возможность вернуться к старому снимку по правилам приложения. Если граф получает новые типы состояния, оставьте чтение прежних полей и проверьте восстановление с историческими значениями, включая пропуски в новых полях. Для эксплуатации храните связь между потоком и версией workflow рядом с записью задачи. Так после релиза можно восстановить маршрут согласования до вызова графа.

Практика на Latorn

В пробном уроке AI Agents PRO можно посмотреть, как устроен цикл агентного приложения, прежде чем добавлять сохранение состояния и согласование.

В платном AI Agents PRO вы соберёте граф состояний на LangGraph, подключите инструменты и упакуете агентный сервис для запуска через FastAPI и Docker. После этого у вас будет граф, на котором можно отработать выпуск новой версии с сохранёнными задачами. Открыть программу AI Agents PRO.

Продолжить на практике

  • AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшне ↗

    Курс разработки AI-агентов на Python: соберёте сервис с поиском по документам, инструментами и внешними API. LangGraph, RAG, оценка качества, контроль расходов и запуск с Docker. Самостоятельная практика для разработчиков; пробные уроки доступны до покупки.