ГлавнаяБлогУскорение LangGraph с Rust: до 737x на больших состояниях
Python

Ускорение LangGraph с Rust: до 737x на больших состояниях

Узнайте, как Rust-акселераторы ускоряют LangGraph до 737x на сериализации состояний. Замените deepcopy на drop-in компоненты без изменения кода.

Al
Редакция Algolitalgolit.ru
12 мин чтения12 июля 2026 г.

Почему LangGraph тормозит на длинных запусках?

Если вы запускали LangGraph-агента достаточно долго, то заметили: узким местом становится не вызов модели, а инфраструктура. Каждый шаг графа сериализует состояние в чекпоинт для возможности восстановления. LangGraph использует Python-функцию deepcopy. Для маленького словаря это нормально, но для состояния агента в 250 КБ с вложенными сообщениями, выводами инструментов и накопленным контекстом deepcopy работает мучительно медленно — и вы платите за это на каждом шаге.

Я создал fast-langgraph: набор Rust-акселераторов для горячих путей LangGraph, оформленных как заменяемые компоненты с полной совместимостью API.

Числа, которые говорят сами за себя

Вот что дают Rust-пути в сравнении с Python-эквивалентами:

  • Сложный чекпоинт (250 КБ): ускорение в 737 раз быстрее deepcopy (большое состояние агента)
  • Сложный чекпоинт (35 КБ): ускорение в 178 раз (среднее состояние)
  • Постоянные обновления состояния: ускорение в 13–46 раз (длинные графы, много шагов)
  • Кеширование ответов LLM: ускорение в 10 раз при 90% попаданий (повторяющиеся промпты, RAG)
  • Сквозное выполнение графа: ускорение в 2–3 раза (продакшн-нагрузки с чекпоинтами)

Автоматический режим, не требующий изменений кода, даёт около 2.8x для типичного вызова.

Честно: это не история «Rust быстрее во всём». Ускорение чекпоинтов зависит от размера состояния. Для маленького плоского словаря Python-словарь реализован на C и уже быстр. Rust там не выигрывает — и в README это честно указано. 737x — это число для большого сложного состояния, а не заголовок для игрушечного графа.

Идея: переписать критичные пути, сохранить API

LangGraph хорош. Я не хотел форкать или заменять его. Я хотел заменить три операции, которые доминируют в реальной нагрузке:

  1. Сериализация чекпоинтов. deepcopy на сложном вложенном состоянии — главная затрата в длинном запуске. Rust выполняет структурированную сериализацию.
  2. Управление состоянием в масштабе. Высокочастотные обновления накапливают накладные расходы. Rust-путь слияния обрабатывает состояния с добавлением.
  3. Повторные вызовы LLM. Одинаковые промпты тратят реальные деньги API. Кеш перед моделью устраняет избыточные вызовы.

Всё остальное остаётся на Python. Rust-код скрыт за совместимым интерфейсом.

Два способа включения

Автоматическое ускорение

Самый простой способ — один флаг или один вызов функции, и ваш существующий граф работает быстрее без изменения кода:

# В начале вашего приложения
import fast_langgraph
fast_langgraph.shim.patch_langgraph()

# Остальной код без изменений — работает в 2-3 раза быстрее
from langgraph.graph import StateGraph
# ...

Или установите переменную окружения FAST_LANGGRAPH_AUTO_PATCH=1 — это мой предпочтительный путь в продакшне, так как не требует изменения кода.

Ручные компоненты для максимального выигрыша

Для наибольшего ускорения используйте компоненты напрямую. Чекпоинтер — замена «в лоб»:

from fast_langgraph import RustSQLiteCheckpointer

# В 5-6 раз быстрее стандартного чекпоинтера
checkpointer = RustSQLiteCheckpointer("state.db")
graph = graph.compile(checkpointer=checkpointer)

Кеш LLM — это декоратор, который сообщает свой процент попаданий, чтобы вы могли доказать выигрыш:

from fast_langgraph import cached

@cached(max_size=1000)
def call_llm(prompt):
    return llm.invoke(prompt)

# Первый вызов: обращение к API (~500 мс)
response = call_llm("Что такое LangGraph?")
# Второй идентичный вызов: возврат из кеша (~0.01 мс)
response = call_llm("Что такое LangGraph?")
print(call_llm.cache_stats())  # {'hits': 1, 'misses': 1, 'size': 1}

Этот кеш даёт ускорение в 10 раз, но только при высоком проценте попаданий. Если все промпты уникальны, кеш бесполезен. Это оптимизация для RAG и повторных запросов, а не магия.

Как это работает под капотом

Главное ограничение дизайна: никто не будет переписывать своего агента ради ускорителя. Поэтому всё построено как совместимые прослойки над Rust-реализациями.

Прослойка патчит горячие функции LangGraph во время импорта. apply_writes, пакетное обновление каналов, получает Rust-версию. Путь кеширования исполнителя переиспользует ThreadPoolExecutor между вызовами вместо пересоздания. Ни одно из этих изменений не даёт огромного выигрыша по отдельности (1.2x и 2.3x соответственно), но в сумме они дают те самые ~2.8x в автоматическом режиме, потому что находятся на пути каждого вызова.

Ручные компоненты — это где живут большие числа. RustSQLiteCheckpointer заменяет путь сериализации и сохранения. langgraph_state_update выполняет слияние состояния на Rust с явными ключами добавления:

from fast_langgraph import langgraph_state_update

new_state = langgraph_state_update(
    current_state,
    {"messages": [new_message]},
    append_keys=["messages"]
)

Этот путь слияния даёт ускорение в 13–46 раз для длинных графов, которые сотни раз добавляют сообщения.

Также есть профайлер, чтобы вы могли найти своё узкое место до того, как что-то портировать:

from fast_langgraph.profiler import GraphProfiler

profiler = GraphProfiler()
with profiler.profile_run():
    result = graph.invoke(input_data)
profiler.print_report()

Сначала профилируйте. Выигрыш — в конкретных горячих путях, а не «во всём коде».

Где это НЕ подходит

Лучше честно скажу, где это проигрывает, чтобы вы не обнаружили проблему в продакшне.

  • Маленькое простое состояние. Если состояние агента — плоский словарь из нескольких ключей, Python-словарь на C уже быстр. Rust-путь чекпоинтов окупается только на больших и вложенных состояниях. На игрушечном графе вы не увидите разницы.
  • Все промпты уникальны. Декоратор @cached даёт 10x при 90% попаданий. Если каждый промпт разный, кеш — мёртвый груз. Это для повторяющихся промптов и RAG, а не для открытого чата с высокой энтропией.
  • Вы не используете чекпоинты. Половина выгоды — в ускорении сохранения. Если граф запускается один раз и завершается без сохранения состояния, вы пропускаете операцию, которая выигрывает больше всего, и остаётесь с меньшим диапазоном 2–3x.
  • Вам нужен специфический внутренний компонент LangGraph, который не пропатчен. Прослойка покрывает горячие пути, а не всю поверхность. Автоматический режим консервативен и откатывается к Python, если не уверен.

Если ваши агенты короткие и дешёвые, это не для вас. Если вы запускаете длинные, с сохранением состояния и с чекпоинтами в продакшне — сериализация — это то, куда уходит ваше время.

Выводы

Узкое место в длинном запуске агента — обычно инфраструктура, а не модель. deepcopy на большом состоянии — реальный налог, который вы платите на каждом шаге.

Большие ускорения здесь — история о данных и сериализации, а не о языке. 737x — это число для состояния в 250 КБ, и оно масштабируется вниз с размером состояния.

Измеряйте маленькие случаи. Rust проигрывает C-реализациям Python-словарей на плоских состояниях, и притворяться обратным — значит попасть в ловушку.

Сделайте замену «в лоб» с автоматическим откатом, иначе она никогда не будет принята. Один импорт или одна переменная окружения — вот и всё внедрение.

Работает с любой версией LangGraph, Python 3.9+. Код, полный бенчмарк и заметки по архитектуре: https://github.com/neul-labs/fast-langgraph

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

#LangGraph#Rust#ускорение#черепки#сериализация
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →