Суть
Вместо одной всеведущей цепочки на входе ставится дешёвый классификатор: его задача — не ответить, а выбрать, какой узел вызвать на основе контекста запроса. Это явный, управляемый и наблюдаемый роутинг в графе (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 — куда попадает причина решения вместе с остальным состоянием