课程目标
理解 RunnableLambda 如何将普通函数包装为 Runnable,以及它对不同返回类型(Promise、Generator、Runnable)的处理策略。
6.1 为什么需要 RunnableLambda?
在 pipe 链中,你经常需要插入一些简单的数据转换逻辑:
const chain = prompt
.pipe(model)
.pipe(parser)
.pipe((text) => text.toUpperCase()); // 普通函数,如何变成 Runnable?
pipe() 接受 RunnableLike 类型,其中包括普通函数。框架内部通过 _coerceToRunnable() 自动将函数包装为 RunnableLambda。
6.2 类定义
源码位置: libs/langchain-core/src/runnables/base.ts:2536
export class RunnableLambda<
RunInput,
RunOutput,
CallOptions extends RunnableConfig = RunnableConfig,
> extends Runnable<RunInput, RunOutput, CallOptions> {
protected func: RunnableFunc<
RunInput,
RunOutput | Runnable<RunInput, RunOutput, CallOptions> // 注意:可以返回 Runnable
>;
constructor(fields: { func: RunnableFunc<...> }) {
super(fields);
this.func = fields.func;
}
}
关键设计:func 的返回类型是 RunOutput | Runnable<...>,意味着函数可以返回一个 Runnable,框架会自动 invoke 它(递归代理模式)。
6.3 invoke 的六种处理路径
_invoke 方法(base.ts:2633)根据函数返回值的类型走不同路径:
async _invoke(input, config, runManager) {
let output = await this.func(input, childConfig);
if (Runnable.isRunnable(output)) {
// 路径 1: 返回 Runnable → 递归 invoke
output = await output.invoke(input, childConfig);
} else if (isAsyncIterable(output)) {
// 路径 2: 返回 AsyncIterable → 收集所有 chunk
let finalOutput;
for await (const chunk of output) {
finalOutput = finalOutput ? concat(finalOutput, chunk) : chunk;
}
output = finalOutput;
} else if (isIterableIterator(output)) {
// 路径 3: 返回同步 Iterator → 收集所有 chunk
let finalOutput;
for (const chunk of output) {
finalOutput = finalOutput ? concat(finalOutput, chunk) : chunk;
}
output = finalOutput;
}
// 路径 4: 返回普通值 → 直接返回
// 路径 5: 返回 Promise → 已被 await 解包
return output;
}
六条路径总结
| 函数返回类型 | 处理方式 | 流式支持 |
|---|---|---|
Promise<T> | await 后返回 | 否(整体 yield) |
T(普通值) | 直接返回 | 否(整体 yield) |
Runnable | 递归 invoke | 取决于返回的 Runnable |
AsyncGenerator | 收集所有 chunk | 是(_transform 中逐 chunk) |
Iterator | 收集所有 chunk | 否 |
AsyncIterable | 收集所有 chunk | 是 |
6.4 流式传播:_transform
RunnableLambda 重写了 _transform()(base.ts:2723)实现更智能的流式处理:
async *_transform(generator, runManager, config) {
// 第一步:收集上游所有 chunk 为一个完整输入
let finalChunk;
for await (const chunk of generator) {
finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk;
}
// 第二步:用完整输入调用函数
const output = await this.func(finalChunk, childConfig);
// 第三步:根据返回类型决定如何 yield
if (Runnable.isRunnable(output)) {
// 返回 Runnable → 用 _streamIterator 流式输出
yield* output._streamIterator(finalChunk, childConfig);
} else if (isAsyncGenerator(output)) {
// 返回 AsyncGenerator → 逐 chunk yield
yield* output;
} else if (isIterator(output)) {
// 返回 Iterator → 逐 chunk yield
yield* output;
} else {
// 返回普通值 → yield 一次
yield output;
}
}
关键洞察:
- 普通函数(返回普通值)在流式管道中表现为"收集所有输入 → 处理 → yield 一次"
- 返回
AsyncGenerator的函数可以实现真正的逐 chunk 流式 - 返回
Runnable的函数会递归,利用返回 Runnable 的流式能力
6.5 创建方式
方式 1:pipe 自动包装
const chain = model.pipe((output) => output.content);
// (output) => output.content 被自动包装为 RunnableLambda
方式 2:显式构造
import { RunnableLambda } from "@langchain/core/runnables";
const upper = new RunnableLambda({
func: (input: string) => input.toUpperCase(),
});
方式 3:静态方法 from
const upper = RunnableLambda.from(
(input: string) => input.toUpperCase()
);
方式 4:返回 Runnable 的动态路由
const router = RunnableLambda.from((input: { type: string }) => {
if (input.type === "joke") return jokeChain; // 返回 Runnable
if (input.type === "poem") return poemChain; // 返回 Runnable
return defaultChain; // 返回 Runnable
});
// router.invoke({ type: "joke" }) 会自动调用 jokeChain.invoke()
方式 5:返回 Generator 的流式函数
const streamingProcessor = RunnableLambda.from(
async function* (input: string) {
for (const word of input.split(" ")) {
yield word.toUpperCase() + " ";
await new Promise((r) => setTimeout(r, 100));
}
}
);
// stream 时逐 word 输出
for await (const chunk of await streamingProcessor.stream("hello world")) {
process.stdout.write(chunk); // "HELLO " → "WORLD "
}
6.6 递归保护
因为 RunnableLambda 可以返回 Runnable(会递归 invoke),框架内置了递归深度限制:
if (config?.recursionLimit === 0) {
throw new Error("Recursion limit reached.");
}
output = await output.invoke(input, {
recursionLimit: (childConfig.recursionLimit ?? DEFAULT_RECURSION_LIMIT) - 1,
});
默认递归限制是 25 层(DEFAULT_RECURSION_LIMIT)。每次递归减 1,到 0 就抛异常。
6.7 上下文传递
RunnableLambda 在调用函数时,通过 AsyncLocalStorage 传递上下文:
void AsyncLocalStorageProviderSingleton.runWithConfig(
pickRunnableConfigKeys(childConfig),
async () => {
let output = await this.func(input, childConfig);
// ...
}
);
这确保了:
- 函数内部创建的子 Runnable 可以继承 callbacks、tags、metadata
- 上下文变量(第 17 课)在函数内部可用
6.8 实战练习
练习 1:基本用法
const toUpper = RunnableLambda.from((s: string) => s.toUpperCase());
const addExclaim = RunnableLambda.from((s: string) => s + "!");
const chain = toUpper.pipe(addExclaim);
const result = await chain.invoke("hello"); // "HELLO!"
练习 2:流式 Generator 函数
const wordByWord = RunnableLambda.from(async function* (text: string) {
for (const word of text.split(" ")) {
yield word;
await new Promise((r) => setTimeout(r, 200));
}
});
for await (const word of await wordByWord.stream("hello world foo bar")) {
console.log("Got:", word);
// Got: hello → Got: world → Got: foo → Got: bar(每个间隔 200ms)
}
练习 3:动态路由
import { FakeChatModel } from "@langchain/core/utils/testing";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
const jokeChain = ChatPromptTemplate.fromMessages([
["human", "Tell me a joke about {topic}"],
]).pipe(new FakeChatModel({})).pipe(new StringOutputParser());
const factChain = ChatPromptTemplate.fromMessages([
["human", "Tell me a fact about {topic}"],
]).pipe(new FakeChatModel({})).pipe(new StringOutputParser());
const router = RunnableLambda.from(
(input: { type: string; topic: string }) => {
if (input.type === "joke") return jokeChain;
return factChain;
}
);
await router.invoke({ type: "joke", topic: "cats" });
6.9 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | runnables/base.ts:2536-2580 | RunnableLambda 类定义、func 字段类型 |
| P0 | runnables/base.ts:2633-2714 | _invoke() 六种返回类型处理路径 |
| P1 | runnables/base.ts:2723-2850 | _transform() 流式传播策略 |
| P1 | runnables/base.ts:2583-2631 | from() 静态工厂方法 |
| P2 | runnables/base.ts:2414-2535 | RunnableTraceable(LangSmith traceable 集成) |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 学会用 RunnableLambda 封装任意函数;理解 pipe 中函数自动包装 |
| 🔵 中阶 | 理解函数返回 Promise / Generator 时的处理差异 |
| 🟡 高阶 | 掌握 _transform 中的流式回退策略;知道何时函数能支持真正流式 |
| 🟠 资深 | 分析 RunnableLambda 在函数式与 OOP 之间的桥梁作用;理解递归保护 |
| 🔴 架构 | 评估"任意函数可组合"带来的灵活性与类型安全的 tension |
下一课预告
第 7 课讲 RunnableParallel — 并行执行多个 Runnable 并合并结果,以及 RunnablePassthrough 在数据编排中的关键角色。