第 6 课: RunnableLambda — 函数即组件

0 阅读4分钟

课程目标

理解 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 源码精读路线

优先级文件关注点
P0runnables/base.ts:2536-2580RunnableLambda 类定义、func 字段类型
P0runnables/base.ts:2633-2714_invoke() 六种返回类型处理路径
P1runnables/base.ts:2723-2850_transform() 流式传播策略
P1runnables/base.ts:2583-2631from() 静态工厂方法
P2runnables/base.ts:2414-2535RunnableTraceable(LangSmith traceable 集成)

本课收获总结

级别你应该掌握的
🟢 基础学会用 RunnableLambda 封装任意函数;理解 pipe 中函数自动包装
🔵 中阶理解函数返回 Promise / Generator 时的处理差异
🟡 高阶掌握 _transform 中的流式回退策略;知道何时函数能支持真正流式
🟠 资深分析 RunnableLambda 在函数式与 OOP 之间的桥梁作用;理解递归保护
🔴 架构评估"任意函数可组合"带来的灵活性与类型安全的 tension

下一课预告

第 7 课讲 RunnableParallel — 并行执行多个 Runnable 并合并结果,以及 RunnablePassthrough 在数据编排中的关键角色。