第 5 课: RunnableSequence — 管道组合的核心

0 阅读5分钟

课程目标

理解 pipe() 背后的 RunnableSequence:它的内部结构、invoke 执行流程和流式传播机制。


5.1 从 pipe 到 RunnableSequence

当你写下这行代码:

const chain = prompt.pipe(model).pipe(parser);

背后发生了什么?

  1. prompt.pipe(model) → 创建 RunnableSequence { first: prompt, middle: [], last: model }
  2. .pipe(parser) → 创建 RunnableSequence { first: prompt, middle: [model], last: parser }

RunnableSequence 不是嵌套的 — 它是扁平的。


5.2 内部结构:first / middle / last

源码位置: libs/langchain-core/src/runnables/base.ts:1925

export class RunnableSequence<RunInput = any, RunOutput = any>
  extends Runnable<RunInput, RunOutput> {

  protected first: Runnable<RunInput>;     // 第一个节点
  protected middle: Runnable[] = [];       // 中间节点(0 到 N 个)
  protected last: Runnable<any, RunOutput>; // 最后一个节点

  get steps() {
    return [this.first, ...this.middle, this.last];
  }
}

为什么分三段?

  • first 的输入类型是 RunInput(链的整体输入)
  • last 的输出类型是 RunOutput(链的整体输出)
  • middle 的类型是 Runnable[](内部输入输出由前后节点约束,不需要泛型)

这种设计让 TypeScript 能在编译期推断链的整体类型:Runnable<RunInput, RunOutput>


5.3 pipe 的扁平化优化

连续 pipe() 不会创建嵌套结构:

源码位置: base.ts:2186

pipe<NewRunOutput>(
  coerceable: RunnableLike<RunOutput, NewRunOutput>
): RunnableSequence<RunInput, Exclude<NewRunOutput, Error>> {

  if (RunnableSequence.isRunnableSequence(coerceable)) {
    // 如果 pipe 的目标本身就是 RunnableSequence,展平它
    return new RunnableSequence({
      first: this.first,
      middle: this.middle.concat([
        this.last,
        coerceable.first,
        ...coerceable.middle,
      ]),
      last: coerceable.last,
    });
  } else {
    // 普通情况:当前 last 变成 middle,新节点变成 last
    return new RunnableSequence({
      first: this.first,
      middle: [...this.middle, this.last],
      last: _coerceToRunnable(coerceable),
    });
  }
}

示例追踪

// Step 1: prompt.pipe(model)
// 因为 prompt 是普通 Runnable,不是 RunnableSequence
// 调用 Runnable 基类的 pipe() → 创建新的 RunnableSequence
// { first: prompt, middle: [], last: model }

// Step 2: .pipe(parser)
// 现在 this 是 RunnableSequence { first: prompt, middle: [], last: model }
// parser 不是 RunnableSequence
// → { first: prompt, middle: [model], last: parser }

// Step 3: .pipe(formatter)
// → { first: prompt, middle: [model, parser], last: formatter }

如果两个 RunnableSequence 连接

const chain1 = prompt.pipe(model);   // { first: prompt, middle: [], last: model }
const chain2 = parser.pipe(formatter); // { first: parser, middle: [], last: formatter }

chain1.pipe(chain2);
// 展平为:{ first: prompt, middle: [model, parser], last: formatter }
// 而不是嵌套的 RunnableSequence 套 RunnableSequence

5.4 invoke 执行流程

源码位置: base.ts:1961

async invoke(input: RunInput, options?: RunnableConfig): Promise<RunOutput> {
  const config = ensureConfig(options);
  // 1. 启动回调追踪
  const callbackManager_ = await getCallbackManagerForConfig(config);
  const runManager = await callbackManager_?.handleChainStart(
    this.toJSON(),
    _coerceToDict(input, "input"),
    config.runId,
  );

  let nextStepInput = input;
  let finalOutput: RunOutput;

  try {
    // 2. 依次执行 first + middle
    const initialSteps = [this.first, ...this.middle];
    for (let i = 0; i < initialSteps.length; i++) {
      const step = initialSteps[i];
      const promise = step.invoke(
        nextStepInput,
        patchConfig(config, {
          callbacks: runManager?.getChild(`seq:step:${i + 1}`),
        })
      );
      nextStepInput = await raceWithSignal(promise, config.signal);
    }

    // 3. 执行 last
    finalOutput = await this.last.invoke(nextStepInput, patchConfig(config, {
      callbacks: runManager?.getChild(`seq:step:${this.steps.length}`),
    }));

  } catch (e) {
    // 4. 错误处理:通知回调
    await runManager?.handleChainError(e);
    throw e;
  }

  // 5. 成功:通知回调
  await runManager?.handleChainEnd(_coerceToDict(finalOutput, "output"));
  return finalOutput;
}

执行流程图

输入 ──→ first.invoke() ──→ middle[0].invoke() ──→ ... ──→ last.invoke() ──→ 输出
              │                     │                            │
              ▼                     ▼                            ▼
         handleChainStart    seq:step:1/2/3...            handleChainEnd
              │                                                 │
              └─────────────── Callback 追踪 ───────────────────┘

关键细节

  • 每个 step 的 invoke 都传入了子 runManagergetChild("seq:step:N")),实现层级追踪
  • raceWithSignal() 支持 AbortSignal 中止——如果传入了 signal,可以随时取消执行
  • 前一个 step 的输出直接作为下一个 step 的输入(nextStepInput = await ...

5.5 流式执行:transform 链

invoke 是"等全部完成再传递",stream 是"逐 chunk 传递"。

源码位置: base.ts:2088

async *_streamIterator(input: RunInput, options?: RunnableConfig) {
  const steps = [this.first, ...this.middle, this.last];

  // 把原始输入包装成一个 Generator(只 yield 一次)
  async function* inputGenerator() {
    yield input;
  }

  // 逐步构建 transform 链
  let finalGenerator = steps[0].transform(inputGenerator(), config);

  for (let i = 1; i < steps.length; i++) {
    finalGenerator = await steps[i].transform(finalGenerator, config);
  }

  // 消费最终的 Generator,逐 chunk yield
  for await (const chunk of finalGenerator) {
    yield chunk;
  }
}

流式传播示意

input ──→ [inputGenerator] ──→ step[0].transform ──→ step[1].transform ──→ ... ──→ yield chunk
              yield input        逐 chunk 转换          逐 chunk 转换

transform() 方法是流式传播的核心。每个 Runnable 的默认 transform() 实现是:

// Runnable 基类的默认 transform
async *transform(
  generator: AsyncGenerator<RunInput>,
  options: Partial<CallOptions>
): AsyncGenerator<RunOutput> {
  // 默认:收集所有 chunk,拼接,invoke,yield 结果
  let finalChunk;
  for await (const chunk of generator) {
    finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk;
  }
  yield* this._streamIterator(finalChunk, options);
}

但关键组件会重写 transform() 实现真正的逐 chunk 流式:

  • BaseChatModel.transform() — 逐 token 流式输出
  • StringOutputParser.transform() — 逐 chunk 提取文本
  • RunnableLambda.transform() — 如果包装的是 Generator 函数,逐个 yield

5.6 batch 优化

RunnableSequence.batch() 的实现(base.ts:2028)不是简单地调用 N 次 invoke(),而是逐步骤批量执行

async batch(inputs, options, batchOptions) {
  let nextStepInputs = inputs;

  for (let i = 0; i < this.steps.length; i++) {
    const step = this.steps[i];
    // 每个 step 用 batch 处理所有输入
    nextStepInputs = await step.batch(nextStepInputs, configs, batchOptions);
  }

  return nextStepInputs;
}

对比

// 基类的 batch:每个输入走完整条链
input1: step1 → step2 → step3 → output1
input2: step1 → step2 → step3 → output2
input3: step1 → step2 → step3 → output3

// RunnableSequence 的 batch:逐步骤批量
step1: [input1, input2, input3] → [mid1, mid2, mid3]
step2: [mid1, mid2, mid3]       → [mid1', mid2', mid3']
step3: [mid1', mid2', mid3']    → [output1, output2, output3]

这样每个 step 可以利用自己的 batch 优化(比如 LLM 的批量 API)。


5.7 RunnableSequence.from — 静态工厂

static from<RunInput, RunOutput>(
  [first, ...runnables]: [RunnableLike<RunInput>, ...RunnableLike[], RunnableLike<any, RunOutput>],
  nameOrFields?: string | { name?: string }
)

使用方式

// 方式 1: pipe 链式
const chain = prompt.pipe(model).pipe(parser);

// 方式 2: from 静态方法
const chain = RunnableSequence.from([prompt, model, parser]);

// 方式 3: from + 命名
const chain = RunnableSequence.from([prompt, model, parser], "myChain");

from() 内部调用 _coerceToRunnable() 将非 Runnable 参数自动转换:

  • 函数 → RunnableLambda
  • 对象 → RunnableParallel
// 函数会被自动包装
const chain = RunnableSequence.from([
  prompt,
  model,
  (output: AIMessage) => output.content,  // 自动变成 RunnableLambda
]);

5.8 Graph 生成

RunnableSequence 重写了 getGraph() 方法(base.ts:2153),将链的结构转为有向图:

getGraph(config?: RunnableConfig): Graph {
  const graph = new Graph();
  let currentLastNode = null;

  this.steps.forEach((step, index) => {
    const stepGraph = step.getGraph(config);

    // 去掉中间步骤的首尾虚拟节点(避免冗余)
    if (index !== 0) stepGraph.trimFirstNode();
    if (index !== this.steps.length - 1) stepGraph.trimLastNode();

    graph.extend(stepGraph);

    // 连接前一个步骤的最后节点和当前步骤的第一个节点
    if (currentLastNode) {
      graph.addEdge(currentLastNode, stepGraph.firstNode());
    }
    currentLastNode = stepGraph.lastNode();
  });

  return graph;
}

这个方法支撑了第 23 课的 Mermaid 可视化功能。


5.9 实战练习

练习 1:手动构建 RunnableSequence

import { RunnableSequence, RunnableLambda } from "@langchain/core/runnables";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { FakeChatModel } from "@langchain/core/utils/testing";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = ChatPromptTemplate.fromMessages([
  ["human", "Tell me about {topic}"],
]);
const model = new FakeChatModel({});
const parser = new StringOutputParser();

// 方式 1: pipe
const chain1 = prompt.pipe(model).pipe(parser);

// 方式 2: from
const chain2 = RunnableSequence.from([prompt, model, parser]);

// 两者等价
const result1 = await chain1.invoke({ topic: "cats" });
const result2 = await chain2.invoke({ topic: "cats" });

练习 2:观察流式传播

// 流式执行
const stream = await chain1.stream({ topic: "cats" });
for await (const chunk of stream) {
  console.log("chunk:", JSON.stringify(chunk));
}

练习 3:混入函数

// 函数自动包装为 RunnableLambda
const chain = prompt
  .pipe(model)
  .pipe(parser)
  .pipe((text: string) => text.toUpperCase())  // 自动 RunnableLambda
  .pipe((text: string) => ({ result: text, length: text.length }));  // 自动 RunnableLambda

const output = await chain.invoke({ topic: "cats" });
// { result: "...", length: ... }

5.10 源码精读路线

优先级位置关注点
P0base.ts:1925-1960RunnableSequence 类定义、first/middle/last 结构
P0base.ts:1961-2008invoke() 的执行流程和回调处理
P0base.ts:2088-2151_streamIterator() 的 transform 链实现
P1base.ts:2186-2208pipe() 的扁平化优化
P1base.ts:2216-2243from() 静态工厂方法
P2base.ts:2028-2081batch() 的逐步骤批量优化
P2base.ts:2153-2184getGraph() 图生成

本课收获总结

级别你应该掌握的
🟢 基础学会用 pipe()from() 创建链;理解 first/middle/last 结构
🔵 中阶理解 invoke 的逐步执行流程;理解 pipe 的扁平化优化
🟡 高阶掌握 _streamIterator() 中 transform 链的流式传播机制
🟠 资深分析 batch 的逐步骤优化 vs 基类的逐输入优化;理解 callback 层级传递
🔴 架构对比管道模式 vs 责任链模式;理解 getGraph() 如何将运行时结构转为静态图

下一课预告

第 6 课讲 RunnableLambda — 如何把一个普通函数变成可组合的 Runnable,以及它对流式传播的处理策略。