LangGraph Intent Router

Практический паттерн маршрутизации: отдельный узел-оркестратор классифицирует намерение запроса и условным ребром направляет его в нужную ветку (RAG / инструмент / обычный диалог). Часто двухуровневый: сначала выбор ветки, затем внутри ветки — следующее решение.

Суть

Вместо одной всеведущей цепочки на входе ставится дешёвый классификатор: его задача — не ответить, а выбрать, какой узел вызвать на основе контекста запроса. Это явный, управляемый и наблюдаемый роутинг в графе (Agent Routing через условные рёбра LangGraph Nodes and Edges).

Зачем это нужно

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

Как работает

  • Узел intent_router_node: по входящему промпту определяет intent (например rag / tool / smalltalk) и кладёт его в state.
  • Условное ребро от роутера: add_conditional_edges("router", route_fn, {"rag": "rag_node", "tool": "tool_node", "smalltalk": "smalltalk_node"}).
  • Двухуровневость: после первого выбора ветка может содержать собственное условное ребро. В разобранном практическом агенте: уровень 1 — роутер выбирает инструмент; уровень 2 — rag_node решает «нашёл / не нашёл» и при неудаче уходит в LangGraph MCP as Node.
  • State обычно содержит user_input, intent, retrieved_docs, tool_result, final_answer, trace.

Пример

START → intent_router_node ─(conditional)→ rag_node      ─(conditional)→ final | mcp_node
                            ├──────────────→ tool_node    ───────────────→ final
                            └──────────────→ smalltalk_node ─────────────→ final

Роутер кладёт в состояние причину, а не только решение

Дешёвое улучшение, которое окупается на первом же разборе инцидента: роутер записывает рядом с intent ещё и короткое объяснение, почему выбран этот маршрут.

state["intent"] = intent
state["manager_reason"] = "В запросе есть слова, связанные с исследованием/обзором."

Одно поле intent отвечает только на вопрос «куда пошли». Когда запрос уехал не в ту ветку, дальше начинается угадывание: сработал не тот ключевой признак, промпт классификатора не покрыл случай или запрос действительно неоднозначен. Причина, зафиксированная в момент решения, отвечает на это сразу.

Два следствия, ради которых это стоит делать.

Причина доступна downstream-узлам. Она лежит в состоянии, а не в логах, поэтому узел в конце ветки может её прочитать: например, приложить к ответу оговорку, если менеджер сам пометил классификацию как неуверенную. Логи такой возможности не дают — они снаружи выполнения.

Причина попадает в трассировку бесплатно. Состояние и так уходит в трейс, отдельного логирования писать не нужно (см. Agent Observability).

Оговорка, без которой приём вреден: у классификатора на правилах причина — это факт (сработало такое-то условие), у классификатора на LLM — это самоотчёт модели, то есть текст, который может не соответствовать настоящему основанию решения. В первом случае причине можно верить как данным, во втором — только как подсказке для человека.

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

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

Отсюда разделение по назначению. Во внутренний трейс писать обязательно — при разборе неверной маршрутизации формулировка причины сокращает поиск, даже будучи приблизительной. Пользователю показывать как объяснение решения — нет, потому что это создаёт ложную подотчётность: человек получает причину, которую нельзя оспорить по существу, так как она не является настоящей.

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

Чем классифицировать намерение: три варианта в цифрах

Сверка 2026-08. Разрыв между вариантами больше, чем принято думать, и по стоимости он на два порядка.

Вариант Задержка Стоимость Точность
Правила / regex микросекунды ноль только на явных формулировках
Эмбеддинг-классификатор 5-10 мс (до 100 мс с сетевым вызовом) практически ноль после загрузки модели 90-95% на чётко определённых намерениях, 92-96% precision в проде после доработки примеров
Вызов LLM 200-500 мс ~$0.65 на 10 000 запросов выше на неоднозначных и составных запросах

Эмбеддинг-роутер выходит примерно в 65 раз дешевле LLM-классификации; в одном из прод-внедрений замена сократила сквозную задержку маршрутизации с 5 000 мс до 100 мс. Дообученная малая модель (SetFit) работает в 56 раз быстрее frontier-модели, уступая ей по F1 всего 8-10 пунктов.

Где эмбеддинг-классификатор ломается — и это ровно то, ради чего держат LLM: запросы вне обучающего распределения и намерения, требующие композиционного рассуждения («сравни отчёт за март с апрельским и объясни разницу»). Он хорош, когда набор намерений фиксирован и хорошо определён, и деградирует, когда набор растёт по ходу.

Отсюда практическая сборка — не выбор одного из трёх, а лестница, та же по устройству, что каскад моделей (Cascade Routing): правила снимают явное бесплатно, эмбеддинг-классификатор закрывает основную массу, а на LLM уходит только неуверенный остаток. Ориентир из замеров: порог уверенности около 0.7 покрывает примерно 80% запросов при точности около 85% — остальные 20% и есть та часть, ради которой стоит платить за вызов модели.

Величина здесь — оценка эмбеддинг-классификатора намерения, и её не надо путать с порогом 0.85 из Agent Routing: тот стоит на само-репортируемой уверенности отвечающей модели, то есть на числе, которое модель называет сама. Шкалы разные и калибруются по-разному, поэтому 0.7 ниже 0.85 не потому, что здесь «мягче».

Связано с

  • Agent Routing — общий паттерн роутинга; здесь — реализация условными рёбрами
  • LangGraph Nodes and Edges — роутер = узел + условное ребро
  • LangGraph MCP as Node — второй уровень: fallback из RAG-ветки
  • LangGraph — каркас, в котором собирается многоветочный агент
  • Agent Observability — куда попадает причина решения вместе с остальным состоянием