课程目标
理解 pipe() 背后的 RunnableSequence:它的内部结构、invoke 执行流程和流式传播机制。
5.1 从 pipe 到 RunnableSequence
当你写下这行代码:
const chain = prompt.pipe(model).pipe(parser);
背后发生了什么?
prompt.pipe(model)→ 创建RunnableSequence { first: prompt, middle: [], last: model }.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 都传入了子
runManager(getChild("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 源码精读路线
| 优先级 | 位置 | 关注点 |
|---|---|---|
| P0 | base.ts:1925-1960 | RunnableSequence 类定义、first/middle/last 结构 |
| P0 | base.ts:1961-2008 | invoke() 的执行流程和回调处理 |
| P0 | base.ts:2088-2151 | _streamIterator() 的 transform 链实现 |
| P1 | base.ts:2186-2208 | pipe() 的扁平化优化 |
| P1 | base.ts:2216-2243 | from() 静态工厂方法 |
| P2 | base.ts:2028-2081 | batch() 的逐步骤批量优化 |
| P2 | base.ts:2153-2184 | getGraph() 图生成 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 学会用 pipe() 和 from() 创建链;理解 first/middle/last 结构 |
| 🔵 中阶 | 理解 invoke 的逐步执行流程;理解 pipe 的扁平化优化 |
| 🟡 高阶 | 掌握 _streamIterator() 中 transform 链的流式传播机制 |
| 🟠 资深 | 分析 batch 的逐步骤优化 vs 基类的逐输入优化;理解 callback 层级传递 |
| 🔴 架构 | 对比管道模式 vs 责任链模式;理解 getGraph() 如何将运行时结构转为静态图 |
下一课预告
第 6 课讲 RunnableLambda — 如何把一个普通函数变成可组合的 Runnable,以及它对流式传播的处理策略。