课程目标
掌握 RunnableParallel(RunnableMap)的并行执行机制、RunnablePassthrough 的透传技巧,以及它们组合形成的数据编排模式。
7.1 RunnableParallel 是什么?
RunnableParallel 让多个 Runnable 并行执行,各自处理同一个输入,然后将结果合并为一个对象:
import { RunnableParallel } from "@langchain/core/runnables";
const parallel = RunnableParallel.from({
joke: jokeChain, // 生成笑话
poem: poemChain, // 生成诗歌
fact: factChain, // 生成事实
});
const result = await parallel.invoke({ topic: "cats" });
// result = {
// joke: "Why did the cat sit on the computer? ...",
// poem: "Soft paws upon the windowsill...",
// fact: "Cats sleep 12-16 hours a day..."
// }
注意:源码中 RunnableParallel 实际上是 RunnableMap 的别名:
// base.ts:2852
export class RunnableParallel<RunInput> extends RunnableMap<RunInput> {}
7.2 内部结构
源码位置: libs/langchain-core/src/runnables/base.ts:2261
export class RunnableMap<RunInput = any, RunOutput = Record<string, any>>
extends Runnable<RunInput, RunOutput> {
protected steps: Record<string, Runnable<RunInput>>;
constructor(fields: { steps: RunnableMapLike<RunInput, RunOutput> }) {
super(fields);
this.steps = {};
for (const [key, value] of Object.entries(fields.steps)) {
this.steps[key] = _coerceToRunnable(value); // 自动包装
}
}
}
关键设计:
steps是一个键值对:{ key: Runnable }- 每个 value 通过
_coerceToRunnable()自动转换(函数 → RunnableLambda) - 所有 step 接收相同的输入
- 输出是
{ key: step.invoke(input) }的合并对象
7.3 invoke 执行:真正的并行
async invoke(input, options) {
const output = {};
const promises = Object.entries(this.steps).map(
async ([key, runnable]) => {
output[key] = await runnable.invoke(
input,
patchConfig(config, {
callbacks: runManager?.getChild(`map:key:${key}`),
})
);
}
);
await raceWithSignal(Promise.all(promises), config.signal);
return output;
}
执行模型:
Promise.all()确保所有 step 并行执行raceWithSignal()支持 AbortSignal 取消- 每个 step 的 callback 使用
map:key:${key}命名,可在追踪中区分
输入 ──┬──→ steps["joke"].invoke(input) ──→ output.joke
├──→ steps["poem"].invoke(input) ──→ output.poem (并行)
└──→ steps["fact"].invoke(input) ──→ output.fact
│
▼
{ joke, poem, fact }
7.4 流式:竞争式输出
RunnableMap 的 _transform() 实现了一个精巧的流式策略(base.ts:2339):
async *_transform(generator, runManager, options) {
const steps = { ...this.steps };
// 1. 用 atee 复制输入流,每个 step 各一份
const inputCopies = atee(generator, Object.keys(steps).length);
// 2. 每个 step 开始 transform
const tasks = new Map(
Object.entries(steps).map(([key, runnable], i) => {
const gen = runnable.transform(inputCopies[i], config);
return [key, gen.next().then((result) => ({ key, gen, result }))];
})
);
// 3. 竞争式 yield:谁先有 chunk,就先 yield 谁
while (tasks.size) {
const { key, result, gen } = await Promise.race(tasks.values());
tasks.delete(key);
if (!result.done) {
yield { [key]: result.value }; // yield 部分结果
tasks.set(key, gen.next().then((result) => ({ key, gen, result })));
}
}
}
流式输出示例:
for await (const chunk of await parallel.stream({ topic: "cats" })) {
console.log(chunk);
}
// 输出顺序取决于哪个 step 先产出 chunk:
// { joke: "Why" } ← joke chain 先产出
// { poem: "Soft" } ← poem chain 紧跟
// { joke: " did" } ← joke 继续
// { fact: "Cats" } ← fact chain 开始
// ...
7.5 RunnablePassthrough — 透传原始输入
源码位置: libs/langchain-core/src/runnables/passthrough.ts
export class RunnablePassthrough<RunInput = any>
extends Runnable<RunInput, RunInput> {
async invoke(input, options) {
if (this.func) {
await this.func(input, config); // 可选的副作用函数
}
return input; // 原样返回
}
}
核心用途:在 RunnableParallel 中保留原始输入:
const chain = RunnableParallel.from({
// 保留原始输入
original: new RunnablePassthrough(),
// 同时做处理
processed: someProcessingChain,
});
const result = await chain.invoke("hello");
// { original: "hello", processed: "..." }
7.6 RunnablePassthrough.assign — 扩展输入
assign() 是一个静态方法,创建 RunnableAssign,它将新的键值对添加到原始输入上:
// 语义:保留原始输入的所有字段,并添加新字段
const chain = RunnablePassthrough.assign({
context: retriever, // 新增 context 字段
timestamp: () => Date.now(), // 新增 timestamp 字段
});
const result = await chain.invoke({ question: "what is LangChain?" });
// {
// question: "what is LangChain?", ← 原始字段保留
// context: [...documents], ← 新增
// timestamp: 1699000000000, ← 新增
// }
RunnableAssign 的实现(base.ts:3210)内部用了 RunnableParallel:
export class RunnableAssign extends Runnable {
mapper: RunnableMap; // 内部用 RunnableMap 并行计算新字段
async invoke(input, options) {
const mapperResult = await this.mapper.invoke(input, config);
return {
...input, // 保留原始输入
...mapperResult, // 合并新字段
};
}
}
7.7 常见数据编排模式
模式 1:RAG 上下文注入
const ragChain = RunnablePassthrough.assign({
context: (input) => retriever.invoke(input.question),
})
.pipe(prompt) // prompt 可以访问 question + context
.pipe(model)
.pipe(parser);
模式 2:并行查询多个数据源
const multiSource = RunnableParallel.from({
faq: faqRetriever,
docs: docRetriever,
history: historyRetriever,
}).pipe((results) => ({
context: [...results.faq, ...results.docs, ...results.history],
}));
模式 3:输入增强
const enriched = RunnablePassthrough.assign({
language: (input) => detectLanguage(input.text),
sentiment: (input) => analyzeSentiment(input.text),
keywords: (input) => extractKeywords(input.text),
});
7.8 实战练习
练习:构建并行查询 + 合并链
import { RunnableParallel, RunnablePassthrough, RunnableLambda } from "@langchain/core/runnables";
const search = RunnableLambda.from(
async (query: string) => `搜索结果: ${query}`
);
const translate = RunnableLambda.from(
async (query: string) => `翻译: ${query}`
);
const summarize = RunnableLambda.from(
async (query: string) => `摘要: ${query}`
);
// 并行执行三个任务
const parallel = RunnableParallel.from({
searchResult: search,
translation: translate,
summary: summarize,
});
// 合并结果
const chain = parallel.pipe(
(results) => `${results.searchResult}\n${results.translation}\n${results.summary}`
);
const result = await chain.invoke("LangChain.js");
// "搜索结果: LangChain.js\n翻译: LangChain.js\n摘要: LangChain.js"
7.9 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | runnables/base.ts:2261-2300 | RunnableMap 类定义、steps 结构、构造函数 |
| P0 | runnables/base.ts:2299-2337 | invoke() 的 Promise.all 并行执行 |
| P1 | runnables/base.ts:2339-2406 | _transform() 竞争式流输出(atee + Promise.race) |
| P1 | runnables/passthrough.ts | RunnablePassthrough、透传 + 可选副作用 |
| P2 | runnables/base.ts:3210-3345 | RunnableAssign(assign() 的实现) |
| P2 | runnables/base.ts:2852 | RunnableParallel(RunnableMap 的别名) |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 学会用 RunnableParallel 并行处理多个子任务;理解 RunnablePassthrough 的透传语义 |
| 🔵 中阶 | 理解 invoke 中 Promise.all 的并行执行;掌握 assign() 的输入扩展模式 |
| 🟡 高阶 | 掌握 _transform 的竞争式流输出(Promise.race)和 atee 输入复制 |
| 🟠 资深 | 分析并行执行中的错误处理策略;理解 RunnableAssign 与 RunnableMap 的组合 |
| 🔴 架构 | 用 Parallel + Passthrough + Assign 实现 DAG 数据流编排 |
下一课预告
第 8 课讲 RunnableBranch 与 RouterRunnable — 根据运行时条件动态选择不同的执行路径。