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
Ты собрал:
- 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 вещи:
- Генерит format instructions — текст, который говорит модели “верни JSON вот такого вида”
- Проверяет результат (валидирует), что реально вернулся JSON нужной формы
Если модель вернула невалидный JSON или route вне enum — parser обычно бросит ошибку.
Ты собрал второй конвейер:
- Сначала получить
{ route } - Потом выбрать и запустить нужную ветку
Упрощённо:
async function routedChainInvoke({ input }) {
const out = await routerChain.invoke({ input }); // out = { route: ... }
// дальше out уходит в branch:
const answer = await branch.invoke(out);
return answer;
}Вот твой код:
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
}То есть не { input: ... }, а { route: ... }.
Если твои docsChain/graphChain/chatChain ожидают { input }, то у тебя есть два варианта:
- Либо сделать так, чтобы routerChain возвращал
{ route, input } - Либо перед branch “приклеить” исходный input обратно
Сейчас твой поток такой:
- routedChain.invoke({ input })
→ routerChain возвращает{ route }
→ branch видит{ route }
→ а downstream цепочке может понадобиться исходный вопрос
const routerChain = RunnableSequence.from([
routerPrompt.partial({ format_instructions: parser.getFormatInstructions() }),
routerModel,
parser,
// добавляем шаг, который сохраняет исходный input
(out, config) => ({
route: out.route,
input: config?.input ?? undefined, // так напрямую обычно не достать
}),
]);Но тут проблема: “config?.input” так просто не появится (зависит от того, как ты прокидываешь данные).
Идея: сохранить исходный 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 }.
Если хочешь прям честную схему для продакшена (чтоб ветки имели доступ к 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
ChatPromptTemplate.fromTemplate(...)хранит шаблон с плейсхолдерами{input}и{format_instructions}.partial({ format_instructions: ... })делает новый PromptTemplate, где один плейсхолдер уже “зашит”RunnableSequence.from([a,b,c])— это объект “пайплайн”:
invoke(x)прогоняет x через a → результат в b → результат в cStructuredOutputParser:- генерит инструкции для формата
- парсит и валидирует ответ модели
RunnableBranch([...]):- хранит список пар
[predicate, runnable]+ default runnable - на
invoke(x)пробует предикаты по порядку и запускает первую подходящую ветку
- хранит список пар
- LLM не запускает ветки, он только возвращает
{ route } RunnableBranch— это чистый кодовыйif/else- Следи за формой объекта: ветки получают то, что вышло из routerChain
- Если веткам нужен
input, добавляй merge-step