课程目标
掌握 RunnableBranch 的条件分支和 RouterRunnable 的 key 路由机制,理解动态执行路径的选择策略。
8.1 为什么需要条件路由?
前面学的 RunnableSequence 是线性的(A → B → C),RunnableParallel 是并行的(A, B, C)。但有些场景需要根据条件走不同分支:
┌─→ 笑话链 (joke)
输入 → 意图识别 ──→├─→ 翻译链 (translate)
└─→ 默认链 (default)
LangChain.js 提供两种路由方式:
- RunnableBranch:条件分支(if/else if/else)
- RouterRunnable:key 路由(switch/case)
8.2 RunnableBranch — 条件分支
源码位置: libs/langchain-core/src/runnables/branch.ts
8.2.1 数据结构
export class RunnableBranch<RunInput, RunOutput> extends Runnable<RunInput, RunOutput> {
branches: Branch<RunInput, RunOutput>[]; // [条件, 分支] 对
default: Runnable<RunInput, RunOutput>; // 默认分支
// Branch 类型定义
// type Branch = [Runnable<RunInput, boolean>, Runnable<RunInput, RunOutput>]
}
8.2.2 创建方式
const branch = RunnableBranch.from([
// [条件函数, 执行分支] 对
[
(input: { topic: string }) => input.topic.includes("code"),
codingChain,
],
[
(input: { topic: string }) => input.topic.includes("math"),
mathChain,
],
// 最后一个元素是默认分支(不需要条件)
generalChain,
]);
from() 的签名(branch.ts:118):
static from(
branches: [
...BranchLike<RunInput, RunOutput>[], // 0到N个 [条件, 分支] 对
RunnableLike<RunInput, RunOutput>, // 最后一个是默认分支
]
)
8.2.3 执行逻辑
源码位置: branch.ts:146
async _invoke(input, config, runManager) {
let result;
// 依次检查每个分支的条件
for (let i = 0; i < this.branches.length; i++) {
const [condition, branchRunnable] = this.branches[i];
// 执行条件 Runnable,检查结果是否为 truthy
const conditionValue = await condition.invoke(input, patchConfig(config, {
callbacks: runManager?.getChild(`condition:${i + 1}`),
}));
if (conditionValue) {
// 条件为真 → 执行对应分支
result = await branchRunnable.invoke(input, patchConfig(config, {
callbacks: runManager?.getChild(`branch:${i + 1}`),
}));
break; // 短路:第一个匹配就退出
}
}
if (!result) {
// 所有条件都不满足 → 执行默认分支
result = await this.default.invoke(input, patchConfig(config, {
callbacks: runManager?.getChild("branch:default"),
}));
}
return result;
}
执行流程:
输入 → condition:1 ──→ true? ──→ branch:1.invoke() → 输出
│ false
▼
condition:2 ──→ true? ──→ branch:2.invoke() → 输出
│ false
▼
condition:N ──→ true? ──→ branch:N.invoke() → 输出
│ false
▼
branch:default.invoke() → 输出
8.2.4 流式支持
RunnableBranch 重写了 _streamIterator()(branch.ts:188):
async *_streamIterator(input, config) {
// 同样的条件评估逻辑
for (let i = 0; i < this.branches.length; i++) {
const [condition, branchRunnable] = this.branches[i];
const conditionValue = await condition.invoke(input, config);
if (conditionValue) {
// 条件匹配 → 用 stream 而不是 invoke
const stream = await branchRunnable.stream(input, config);
for await (const chunk of stream) {
yield chunk;
}
return;
}
}
// 默认分支也用 stream
const stream = await this.default.stream(input, config);
for await (const chunk of stream) {
yield chunk;
}
}
注意:条件评估本身不是流式的(必须等条件结果),但选中的分支可以流式输出。
8.3 RouterRunnable — Key 路由
源码位置: libs/langchain-core/src/runnables/router.ts
RouterRunnable 更简单——根据输入的 key 字段直接路由:
export type RouterInput = {
key: string; // 路由键
input: any; // 实际输入
};
export class RouterRunnable extends Runnable<RouterInput, RunOutput> {
runnables: Record<string, Runnable>; // key → Runnable 映射
async invoke(input: RouterInput, options?) {
const { key, input: actualInput } = input;
const runnable = this.runnables[key];
if (runnable === undefined) {
throw new Error(`No runnable associated with key "${key}".`);
}
return runnable.invoke(actualInput, options);
}
}
使用方式
import { RouterRunnable, RunnableLambda } from "@langchain/core/runnables";
const router = new RouterRunnable({
runnables: {
uppercase: RunnableLambda.from((text: string) => text.toUpperCase()),
reverse: RunnableLambda.from((text: string) => text.split("").reverse().join("")),
repeat: RunnableLambda.from((text: string) => text.repeat(3)),
},
});
await router.invoke({ key: "uppercase", input: "hello" }); // "HELLO"
await router.invoke({ key: "reverse", input: "hello" }); // "olleh"
await router.invoke({ key: "unknown", input: "hello" }); // 抛错
batch 优化
RouterRunnable 的 batch() 会按 key 分组,同一 key 的输入一起批处理:
const results = await router.batch([
{ key: "uppercase", input: "a" },
{ key: "uppercase", input: "b" }, // 和上面的一起 batch 给 uppercase
{ key: "reverse", input: "c" },
]);
8.4 RunnableBranch vs RouterRunnable vs RunnableLambda 路由
三种条件路由方式的对比:
| 特性 | RunnableBranch | RouterRunnable | RunnableLambda 返回 Runnable |
|---|---|---|---|
| 路由逻辑 | 条件函数(可复杂) | key 匹配(简单) | 任意 JS 逻辑 |
| 输入形式 | 原始输入 | { key, input } | 原始输入 |
| 默认分支 | 必须有 | 无(key 不存在就报错) | 取决于函数逻辑 |
| 流式支持 | 原生支持 | 无 stream 重写 | 支持(通过返回的 Runnable) |
| 适合场景 | 基于内容的条件判断 | 明确的类型分发 | 复杂动态路由 |
推荐选择:
- 路由 key 已知(如 intent 分类结果)→
RouterRunnable - 需要基于内容判断(如包含某关键词)→
RunnableBranch - 复杂的动态逻辑 →
RunnableLambda返回 Runnable
8.5 组合模式:分类 + 路由
一个常见的模式是用 LLM 做分类,然后路由到不同的专用链:
import { RunnableBranch, RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
// Step 1: 分类链 — 用 LLM 判断意图
const classifierChain = classifierPrompt.pipe(model).pipe(parser);
// 返回: "joke" | "fact" | "poem"
// Step 2: 专用链
const jokeChain = jokePrompt.pipe(model).pipe(parser);
const factChain = factPrompt.pipe(model).pipe(parser);
const poemChain = poemPrompt.pipe(model).pipe(parser);
// Step 3: 分支路由
const branch = RunnableBranch.from([
[(input) => input.type === "joke", jokeChain],
[(input) => input.type === "fact", factChain],
poemChain, // 默认:诗歌
]);
// Step 4: 完整链
const fullChain = RunnableSequence.from([
RunnablePassthrough.assign({
type: classifierChain,
}),
branch,
]);
await fullChain.invoke({ question: "Tell me something funny about cats" });
8.6 实战练习
练习 1:基于长度的路由
const branch = RunnableBranch.from([
[
(input: string) => input.length < 10,
RunnableLambda.from((s: string) => `短文本处理: ${s}`),
],
[
(input: string) => input.length < 100,
RunnableLambda.from((s: string) => `中等文本处理: ${s.substring(0, 50)}...`),
],
RunnableLambda.from((s: string) => `长文本处理: ${s.length} 字符`),
]);
await branch.invoke("hi"); // "短文本处理: hi"
await branch.invoke("hello world, this is a test"); // "中等文本处理: ..."
练习 2:RouterRunnable + 预处理
const router = new RouterRunnable({
runnables: {
en: englishChain,
zh: chineseChain,
ja: japaneseChain,
},
});
// 先检测语言,再路由到对应链
const chain = RunnableLambda.from(
(text: string) => ({
key: detectLanguage(text), // 返回 "en" | "zh" | "ja"
input: text,
})
).pipe(router);
8.7 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | runnables/branch.ts:66-90 | RunnableBranch 类定义、branches + default 结构 |
| P0 | runnables/branch.ts:146-179 | _invoke() 的条件评估和分支执行 |
| P1 | runnables/branch.ts:188+ | _streamIterator() 流式分支 |
| P1 | runnables/branch.ts:118-144 | from() 静态工厂方法 |
| P2 | runnables/router.ts | RouterRunnable 全文(较短) |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 学会用 RunnableBranch 实现 if/else 逻辑;学会用 RouterRunnable 做 key 路由 |
| 🔵 中阶 | 理解 Branch 的条件评估顺序和短路行为;理解 RouterRunnable 的 key 分发 |
| 🟡 高阶 | 掌握 Branch + Parallel + Sequence 的复杂组合模式(分类→路由) |
| 🟠 资深 | 分析条件路由对流式传输的影响:条件评估阻塞 + 分支流式 |
| 🔴 架构 | 对比三种路由方式的 trade-off;设计可扩展的路由策略 |
下一课预告
第 9 课进入核心组件精讲的第一课——Messages 对话数据模型,理解 BaseMessage 继承体系和多模态内容表示。
Runnable 体系总结(第 4-8 课回顾)
至此,Runnable 体系的五大核心组件已经全部讲完:
| 组件 | 用途 | 比喻 |
|---|---|---|
| Runnable (第 4 课) | 统一接口 | 积木的接口标准 |
| RunnableSequence (第 5 课) | 串行管道 | 流水线 |
| RunnableLambda (第 6 课) | 函数包装 | 万能适配器 |
| RunnableParallel (第 7 课) | 并行执行 | 分工协作 |
| RunnableBranch/Router (第 8 课) | 条件路由 | 路由器 |
这五个组合起来,可以表达任意复杂的执行拓扑:
输入 → 预处理(Lambda) → 并行查询(Parallel) → 条件路由(Branch)
├→ 链A(Sequence)
└→ 链B(Sequence)