Skip to content

Instantly share code, notes, and snippets.

@ro31337
Last active December 20, 2025 19:50
Show Gist options
  • Select an option

  • Save ro31337/9f2afefe28120761316adff581543610 to your computer and use it in GitHub Desktop.

Select an option

Save ro31337/9f2afefe28120761316adff581543610 to your computer and use it in GitHub Desktop.

LangChain Router + Branch — как это работает (Mermaid)


Полный pipeline (Mermaid)

flowchart LR
    A["User Input: How many users in Neo4j?"] --> B["routerPrompt (ChatPromptTemplate)"]
    B --> C["ChatOpenAI (gpt-4o-mini)"]
    C --> D["StructuredOutputParser (Zod validate)"]
    D --> E["RunnableBranch (conditional routing)"]

    E -->|route == docs| F["docsChain"]
    E -->|route == graph| G["graphChain"]
    E -->|default| H["chatChain"]

    F --> I["Final Answer"]
    G --> I
    H --> I
Loading

Что реально происходит по шагам

1) routerChain — это RunnableSequence (конвейер)

Ты собрал:

  • Prompt → LLM → Parser

На практике это “сначала сформировать строку, потом спросить модель, потом распарсить”.

Псевдо-код, как это ощущается при выполнении:

// invoke({ input }) запускает конвейер слева направо
async function routerChainInvoke({ input }) {
  // 1) prompt: собираем финальный текст промпта
  const promptText = routerPrompt
    .partial({ format_instructions: parser.getFormatInstructions() })
    .format({ input });

  // 2) LLM: получаем сырой текст (строку)
  const raw = await routerModel.invoke(promptText);

  // 3) parser: делаем из сырого текста строгий объект по Zod
  const out = await parser.invoke(raw);
  // out: { route: "docs" | "graph" | "chat" }
  return out;
}

Ключевое: partial() не запускает ничего, он просто заранее подставляет постоянный кусок (format_instructions) в шаблон.


2) StructuredOutputParser.fromZodSchema(schema) — зачем он тут

Он делает 2 вещи:

  1. Генерит format instructions — текст, который говорит модели “верни JSON вот такого вида”
  2. Проверяет результат (валидирует), что реально вернулся JSON нужной формы

Если модель вернула невалидный JSON или route вне enum — parser обычно бросит ошибку.


3) routedChain — это “routerChain → branch”

Ты собрал второй конвейер:

  • Сначала получить { route }
  • Потом выбрать и запустить нужную ветку

Упрощённо:

async function routedChainInvoke({ input }) {
  const out = await routerChain.invoke({ input });  // out = { route: ... }

  // дальше out уходит в branch:
  const answer = await branch.invoke(out);

  return answer;
}

Как именно работает conditional (RunnableBranch)

Вот твой код:

new RunnableBranch([
  [(out) => out.route === "docs", docsChain],
  [(out) => out.route === "graph", graphChain],
  chatChain, // default
])

Это буквально аналог:

  • if route === "docs" → docsChain
  • else if route === "graph" → graphChain
  • else → chatChain

Псевдо-реализация:

async function branchInvoke(out) {
  if (out.route === "docs") {
    return await docsChain.invoke(out);
  }
  if (out.route === "graph") {
    return await graphChain.invoke(out);
  }
  return await chatChain.invoke(out); // default
}

Важная деталь №1: branch получает тот объект, который вышел из routerChain

То есть не { input: ... }, а { route: ... }.

Если твои docsChain/graphChain/chatChain ожидают { input }, то у тебя есть два варианта:

  • Либо сделать так, чтобы routerChain возвращал { route, input }
  • Либо перед branch “приклеить” исходный input обратно

Частая проблема: потеряли input

Сейчас твой поток такой:

  • routedChain.invoke({ input })
    → routerChain возвращает { route }
    → branch видит { route }
    → а downstream цепочке может понадобиться исходный вопрос

Вариант A: routerChain возвращает { route, input }

const routerChain = RunnableSequence.from([
  routerPrompt.partial({ format_instructions: parser.getFormatInstructions() }),
  routerModel,
  parser,
  // добавляем шаг, который сохраняет исходный input
  (out, config) => ({
    route: out.route,
    input: config?.input ?? undefined, // так напрямую обычно не достать
  }),
]);

Но тут проблема: “config?.input” так просто не появится (зависит от того, как ты прокидываешь данные).

Вариант B (нормальный): сделать “merge step” между routerChain и branch

Идея: сохранить исходный input в переменной пайплайна, а потом склеить.

Обычно делают так:

const routedChain = RunnableSequence.from([
  (x) => x, // x = { input }
  {
    decision: routerChain, // вернёт { route }
    original: (x) => x,    // вернёт { input }
  },
  ({ decision, original }) => ({ ...decision, ...original }),
  new RunnableBranch([
    [(x) => x.route === "docs", docsChain],
    [(x) => x.route === "graph", graphChain],
    chatChain,
  ]),
]);

Теперь downstream цепочки получат { route, input }.


Mermaid (детализация: “router output” + “merge input back”)

Если хочешь прям честную схему для продакшена (чтоб ветки имели доступ к input), то вот так:

flowchart LR
    A["Invoke input"] --> B["routerChain"]

    B --> C["decision route"]
    A --> D["original input"]

    C --> E["merge decision + input"]
    D --> E

    E --> F["RunnableBranch"]

    F -->|docs| G["docsChain"]
    F -->|graph| H["graphChain"]
    F -->|default| I["chatChain"]

    G --> J["Answer"]
    H --> J
    I --> J
Loading

“Как там собираются эти объекты” — коротко и по делу

  • ChatPromptTemplate.fromTemplate(...) хранит шаблон с плейсхолдерами {input} и {format_instructions}
  • .partial({ format_instructions: ... }) делает новый PromptTemplate, где один плейсхолдер уже “зашит”
  • RunnableSequence.from([a,b,c]) — это объект “пайплайн”:
    invoke(x) прогоняет x через a → результат в b → результат в c
  • StructuredOutputParser:
    • генерит инструкции для формата
    • парсит и валидирует ответ модели
  • RunnableBranch([...]):
    • хранит список пар [predicate, runnable] + default runnable
    • на invoke(x) пробует предикаты по порядку и запускает первую подходящую ветку

Мини-памятка (самое важное)

  1. LLM не запускает ветки, он только возвращает { route }
  2. RunnableBranch — это чистый кодовый if/else
  3. Следи за формой объекта: ветки получают то, что вышло из routerChain
  4. Если веткам нужен input, добавляй merge-step
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment