第 8 课: RunnableBranch 与 Router — 条件路由

0 阅读3分钟

课程目标

掌握 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 路由

三种条件路由方式的对比:

特性RunnableBranchRouterRunnableRunnableLambda 返回 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 源码精读路线

优先级文件关注点
P0runnables/branch.ts:66-90RunnableBranch 类定义、branches + default 结构
P0runnables/branch.ts:146-179_invoke() 的条件评估和分支执行
P1runnables/branch.ts:188+_streamIterator() 流式分支
P1runnables/branch.ts:118-144from() 静态工厂方法
P2runnables/router.tsRouterRunnable 全文(较短)

本课收获总结

级别你应该掌握的
🟢 基础学会用 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)