LangChain.js 中的 Runnable 系统

1. 什么是 Runnable?

Runnable 是 LangChain.js 中的一个核心概念,它就像是一个统一的"接口",让不同的组件能够以相同的方式工作。想象一下,如果你有很多不同的工具(比如文本处理、AI模型调用等),它们的工作方式都不一样,使用起来会很麻烦。Runnable 就是来解决这个问题的 - 它让所有组件都遵循相同的规则,这样它们就可以轻松地组合在一起工作。

2.The Runnable Interface

classDiagram class Runnable { <> +invoke(input, options) Promise~output~ +stream(input, options) Promise~Stream~ +batch(inputs, options) Promise~outputs[]~ +transform(generator, options) AsyncGenerator +pipe(next) Runnable +withConfig(config) Runnable +withRetry(options) RunnableRetry +withFallbacks(fallbacks) RunnableWithFallbacks } Runnable <|-- RunnableSequence Runnable <|-- RunnableMap Runnable <|-- RunnableBranch Runnable <|-- RunnablePassthrough Runnable <|-- RunnableLambda

这些方法各自的作用是:

  • invoke: 最基础的方法,用于处理单个输入
  • stream: 用于流式处理数据
  • batch: 用于批量处理多个输入
  • pipe: 用于将多个 Runnable 连接起来
  • 其他方法用于配置和错误处理

3. 基础实现示例

让我们通过一个简单的例子来理解 Runnable 是如何工作的。

代码:github.com/jianghr-rr/...

typescript 复制代码
// 创建一个将文本转换为大写的 Runnable
const upperCase = RunnableLambda.from((text: string) => text.toUpperCase());

// 使用它
const result = await upperCase.invoke('hello world');
console.log(result); // 输出: "HELLO WORLD"

4. 代码组织结构

python 复制代码
implementation/
├── src/
│   ├── core/
│   │   ├── runnable.ts         # Runnable 接口和基类
│   │   ├── config.ts           # RunnableConfig 配置系统
│   │   └── errors.ts           # 错误处理
│   ├── lambda/
│   │   └── runnable-lambda.ts  # RunnableLambda 实现
│   └── examples/
│       └── basic-usage.ts      # 基础使用示例
├── package.json
├── tsconfig.json
└── README.md

5. 核心功能详解

5.1 invoke

invoke 是 Runnable 系统中最基础的方法,它的作用很简单:接收一个输入,经过处理,然后返回一个输出。就像是一个函数调用,但是被包装成了一个统一的形式。

typescript 复制代码
const upperCase = RunnableLambda.from((text: string) => text.toUpperCase());

const result1 = await upperCase.invoke('hello world');
console.log(`输入: "hello world"`);
console.log(`输出: "${result1}"`); // 输出: "HELLO WORLD"

核心代码结构

typescript 复制代码
/**
 * 配置接口 - 控制 Runnable 的行为
 */
export interface RunnableConfig {}

/**
 * Runnable 核心接口 - LangChain.js 的基石
 *
 * @template Input - 输入类型
 * @template Output - 输出类型
 */
export interface Runnable<Input = any, Output = any> {
    invoke(input: Input, config?: RunnableConfig): Promise<Output>;
}

/**
 * Runnable 抽象基类
 * 提供默认实现和通用功能
 */
export abstract class BaseRunnable<Input = any, Output = any> implements Runnable<Input, Output> {
    abstract invoke(input: Input, config?: RunnableConfig): Promise<Output>;
}

RunnableLambda - 最简单的 Runnable 实现

RunnableLambda 是一个特殊的 Runnable,它可以把普通的函数转换成 Runnable:

typescript 复制代码
import {BaseRunnable, RunnableConfig} from '../core/runnable';

/**
 * RunnableLambda - 将普通函数包装成 Runnable
 * 这是最基础的 Runnable 实现
 */
export class RunnableLambda<Input = any, Output = any> extends BaseRunnable<Input, Output> {
    constructor(private func: (input: Input) => Output | Promise<Output>) {
        super();
    }

    async invoke(input: Input, config?: RunnableConfig): Promise<Output> {
        return this.func(input);
    }

    static from<Input, Output>(func: (input: Input) => Output | Promise<Output>): RunnableLambda<Input, Output> {
        return new RunnableLambda(func);
    }
}

简单理解

想象一下,RunnableLambda 就像是一个"函数包装器":

  1. 它接收一个普通函数
  2. 把这个函数包装成一个具有统一接口的对象
  3. 通过 invoke 方法调用这个函数

你可能会问:为什么要这么麻烦?直接调用函数不就好了吗?

Runnable 的价值在于:

  1. 统一接口:所有组件都使用相同的方式调用
  2. 可组合性:可以轻松地将多个 Runnable 组合在一起
  3. 可配置性:通过 RunnableConfig 可以控制行为
  4. 类型安全:使用 TypeScript 泛型保证类型安全

函数式编程视角

从函数式编程的角度来看,RunnableLambda.from 方法实现了一个重要的概念:函数提升(Lifting) 。它把一个普通函数"提升"到了一个更复杂的抽象结构中,使其具备更强的组合能力。

这种设计让我们可以:

  1. 把简单的函数转换成具有统一接口的对象
  2. 保持代码的一致性
  3. 方便地进行函数组合
  4. 支持异步操作

通过这种方式,我们可以用统一的方式处理各种不同的操作,无论是简单的文本转换,还是复杂的 AI 模型调用。

5.2 pipe

pipe 是 Runnable 系统中的一个强大特性,它允许我们将多个处理步骤连接在一起,形成一个处理管道。就像工厂的流水线一样,数据会依次经过每个处理步骤,最终得到我们想要的结果。

typescript 复制代码
/**
 * 配置接口 - 控制 Runnable 的行为
 */
export interface RunnableConfig {}

/**
 * Runnable 核心接口 - LangChain.js 的基石
 *
 * @template Input - 输入类型
 * @template Output - 输出类型
 */
export interface Runnable<Input = any, Output = any> {
    invoke(input: Input, config?: RunnableConfig): Promise<Output>;
    pipe<NewOutput>(next: Runnable<Output, NewOutput>): Runnable<Input, NewOutput>;
}

/**
 * Runnable 抽象基类
 * 提供默认实现和通用功能
 */
export abstract class BaseRunnable<Input = any, Output = any> implements Runnable<Input, Output> {
    abstract invoke(input: Input, config?: RunnableConfig): Promise<Output>;

    /**
     * 管道组合方法 - LCEL 的基础
     */
    pipe<NewOutput>(next: Runnable<Output, NewOutput>): Runnable<Input, NewOutput> {
        return new RunnableSequence([this, next]);
    }
}

/**
 * RunnableSequence - 序列组合的实现
 * 这是 pipe 方法的核心
 */
export class RunnableSequence<Input = any, Output = any> extends BaseRunnable<Input, Output> {
    constructor(private steps: Runnable<any, any>[]) {
        super();
    }

    async invoke(input: Input, config?: RunnableConfig): Promise<Output> {
        let current: any = input;

        for (const step of this.steps) {
            current = await step.invoke(current, config);
        }

        return current as Output;
    }
}

简单理解

想象一下,pipe 就像是一个管道系统:

  1. 每个 Runnable 都是一个处理步骤
  2. pipe 方法把这些步骤连接起来
  3. 数据会依次流经每个步骤
  4. 最终得到处理后的结果

实际使用示例

让我们通过一个简单的例子来理解:

typescript 复制代码
// 创建两个处理步骤
const toUpperCase = RunnableLambda.from((text: string) => text.toUpperCase());
const addExclamation = RunnableLambda.from((text: string) => text + '!');

// 使用 pipe 连接它们
const pipeline = toUpperCase.pipe(addExclamation);

// 使用组合后的管道
const result = await pipeline.invoke('hello');
console.log(result); // 输出: "HELLO!"

函数式编程视角:

这正是**函数组合(Function Composition)**的典型实现方式:

typescript 复制代码
// 逻辑上等价于:f ∘ g
const composed = f.pipe(g); // g(f(x))

5.3 并发执行

在RunnableConfig里添加并发数

typescript 复制代码
export interface RunnableConfig {
    // 最大并发数(批量处理时)
    maxConcurrency?: number;
}

在Runnable 抽象基类实现

typescript 复制代码
async batch(inputs: Input[], config?: RunnableConfig): Promise<Output[]> {
    const {maxConcurrency = 10} = config || {};
    const results: Output[] = [];

    for (let i = 0; i < inputs.length; i += maxConcurrency) {
        const batch = inputs.slice(i, i + maxConcurrency);
        const batchResults = await Promise.all(batch.map(input => this.invoke(input, config)));
        results.push(...batchResults);
    }

    return results;
}

同时调用多个invoke

5.4 流式输出

流式处理的基本概念

在 Runnable 系统中,流式处理的特点是:

  1. 数据以"块"为单位逐步处理
  2. 每个"块"是 invoke 方法返回的一个完整结果
  3. 可以通过 for await...of 循环逐步获取这些结果

如果你希望实现"一个字符一个字符"地输出,那么你可以在 invoke 返回的 Output(例如一个字符串)上进一步拆分(例如用 split('') 拆成字符数组),然后在 stream 方法中逐个 yield 每个字符,或者在外层迭代时对每个"块"进行进一步处理。

基础实现

让我们看看 Runnable 中的基础流式处理实现:

typescript 复制代码
async *stream(input: Input, config?: RunnableConfig): AsyncIterable<Output> {
    yield await this.invoke(input, config);
}

实际使用示例

让我们通过两个例子来理解流式处理:

示例 1:基本的流式处理

typescript 复制代码
// 创建一个简单的处理管道
const pipeline = RunnableLambda.from((text: string) => text.toUpperCase());

// 使用流式处理
console.log('🌊 流式处理示例:');
for await (const chunk of pipeline.stream('streaming test')) {
    console.log(`  📄 处理结果: ${chunk}`);
}
// 输出: "处理结果: STREAMING TEST"

示例 2:逐字符处理

typescript 复制代码
// 创建一个处理函数
const processor = RunnableLambda.from((input: string) => input + ' (processed)');

// 使用流式处理并逐字符输出
console.log('逐字符处理示例:');
for await (const chunk of processor.stream('hello')) {
    // 对每个处理结果进行逐字符处理
    for (const char of chunk.split('')) {
        console.log(`  📄 字符: ${char}`);
    }
}
// 输出:
// 字符: h
// 字符: e
// 字符: l
// 字符: l
// 字符: o
// 字符:  
// 字符: (
// 字符: p
// 字符: r
// 字符: o
// 字符: c
// 字符: e
// 字符: s
// 字符: s
// 字符: e
// 字符: d
// 字符: )

6. 看看langchin里runnable的部分源代码

typescript 复制代码
export interface RunnableInterface<
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  RunInput = any,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  RunOutput = any,
  CallOptions extends RunnableConfig = RunnableConfig
> extends SerializableInterface {
  lc_serializable: boolean;

  invoke(input: RunInput, options?: Partial<CallOptions>): Promise<RunOutput>;

  batch(
    inputs: RunInput[],
    options?: Partial<CallOptions> | Partial<CallOptions>[],
    batchOptions?: RunnableBatchOptions & { returnExceptions?: false }
  ): Promise<RunOutput[]>;

  batch(
    inputs: RunInput[],
    options?: Partial<CallOptions> | Partial<CallOptions>[],
    batchOptions?: RunnableBatchOptions & { returnExceptions: true }
  ): Promise<(RunOutput | Error)[]>;

  batch(
    inputs: RunInput[],
    options?: Partial<CallOptions> | Partial<CallOptions>[],
    batchOptions?: RunnableBatchOptions
  ): Promise<(RunOutput | Error)[]>;

  stream(
    input: RunInput,
    options?: Partial<CallOptions>
  ): Promise<IterableReadableStreamInterface<RunOutput>>;

  transform(
    generator: AsyncGenerator<RunInput>,
    options: Partial<CallOptions>
  ): AsyncGenerator<RunOutput>;

  getName(suffix?: string): string;
}

export abstract class Runnable<
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    RunInput = any,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    RunOutput = any,
    CallOptions extends RunnableConfig = RunnableConfig
  >
  extends Serializable
  implements RunnableInterface<RunInput, RunOutput, CallOptions>
{
	// ...
	
  abstract invoke(
    input: RunInput,
    options?: Partial<CallOptions>
  ): Promise<RunOutput>;
  
  // ...
  
  
  async batch(
    inputs: RunInput[],
    options?: Partial<CallOptions> | Partial<CallOptions>[],
    batchOptions?: RunnableBatchOptions
  ): Promise<(RunOutput | Error)[]> {
    const configList = this._getOptionsList(options ?? {}, inputs.length);
    const maxConcurrency =
      configList[0]?.maxConcurrency ?? batchOptions?.maxConcurrency;
    const caller = new AsyncCaller({
      maxConcurrency,
      onFailedAttempt: (e) => {
        throw e;
      },
    });
    const batchCalls = inputs.map((input, i) =>
      caller.call(async () => {
        try {
          const result = await this.invoke(input, configList[i]);
          return result;
        } catch (e) {
          if (batchOptions?.returnExceptions) {
            return e as Error;
          }
          throw e;
        }
      })
    );
    return Promise.all(batchCalls);
  }
  
  
  /**
   * Default streaming implementation.
   * Subclasses should override this method if they support streaming output.
   * @param input
   * @param options
   */
  async *_streamIterator(
    input: RunInput,
    options?: Partial<CallOptions>
  ): AsyncGenerator<RunOutput> {
    yield this.invoke(input, options);
  }

  /**
   * Stream output in chunks.
   * @param input
   * @param options
   * @returns A readable stream that is also an iterable.
   */
  async stream(
    input: RunInput,
    options?: Partial<CallOptions>
  ): Promise<IterableReadableStream<RunOutput>> {
    // Buffer the first streamed chunk to allow for initial errors
    // to surface immediately.
    const config = ensureConfig(options);
    const wrappedGenerator = new AsyncGeneratorWithSetup({
      generator: this._streamIterator(input, config),
      config,
    });
    await wrappedGenerator.setup;
    return IterableReadableStream.fromAsyncGenerator(wrappedGenerator);
  }
}

6.1 主要看下stream的区别:

  1. 错误处理:实际实现增加了错误处理机制
  2. 配置处理:使用 ensureConfig 确保配置正确
  3. 流转换:将 AsyncGenerator 转换为 IterableReadableStream
  4. 初始化处理:使用 AsyncGeneratorWithSetup 处理初始化

核心组件解析

AsyncGeneratorWithSetup

typescript 复制代码
class AsyncGeneratorWithSetup<T> {
  constructor({
    generator,
    config
  }: {
    generator: AsyncGenerator<T>;
    config: RunnableConfig;
  }) {
    this.generator = generator;
    this.config = config;
    this.setup = this._setup();
  }

  private async _setup() {
    // 处理初始化逻辑
  }
}

这个类的作用是:

  1. 包装异步生成器
  2. 处理初始化逻辑
  3. 确保配置正确加载

IterableReadableStream

typescript 复制代码
export class IterableReadableStream<T> extends ReadableStream<T> 
  implements IterableReadableStreamInterface<T> {
  
  public reader: ReadableStreamDefaultReader<T>;

  ensureReader() {
    if (!this.reader) {
      this.reader = this.getReader();
    }
  }

  async next(): Promise<IteratorResult<T>> {
    this.ensureReader();
    try {
      const result = await this.reader.read();
      if (result.done) {
        this.reader.releaseLock();
        return { done: true, value: undefined };
      }
      return { done: false, value: result.value };
    } catch (e) {
      this.reader.releaseLock();
      throw e;
    }
  }

  [Symbol.asyncIterator]() {
    return this;
  }
}

这个类的关键点:

  1. 同时实现了 ReadableStream 和 AsyncIterator 接口
  2. 提供了流式读取和迭代的能力
  3. 自动管理 reader 的生命周期
  4. 支持 for await...of 循环

6.2 ReadableStream 详解

6.2.1 什么是 ReadableStream?

ReadableStream 是 Web Streams API 的一部分,它代表了一个可读的数据流。想象一下,就像是一个水管,数据从一端流入,我们可以从另一端读取数据。这个"水管"可以:

  1. 逐步传输数据
  2. 控制数据流动的速度
  3. 处理背压(backpressure)
  4. 支持异步操作

6.2.2 基本概念

核心组件

typescript 复制代码
interface ReadableStream<T> {
  // 获取流的读取器
  getReader(): ReadableStreamDefaultReader<T>;
  
  // 检查流是否被锁定
  readonly locked: boolean;
  
  // 取消流
  cancel(reason?: any): Promise<void>;
  
  // 将流转换为其他格式
  pipeTo(dest: WritableStream<T>): Promise<void>;
  pipeThrough(transform: TransformStream<T, R>): ReadableStream<R>;
}

读取器(Reader)

typescript 复制代码
interface ReadableStreamDefaultReader<T> {
  // 读取下一个数据块
  read(): Promise<ReadableStreamReadResult<T>>;
  
  // 释放读取器
  releaseLock(): void;
  
  // 取消流
  cancel(reason?: any): Promise<void>;
}

interface ReadableStreamReadResult<T> {
  // 是否完成
  done: boolean;
  // 数据值
  value: T;
}

基本使用

typescript 复制代码
// 创建一个简单的 ReadableStream
const stream = new ReadableStream({
  start(controller) {
    // 开始生产数据
    controller.enqueue("Hello");
    controller.enqueue("World");
    controller.close();
  }
});

// 读取数据
const reader = stream.getReader();
while (true) {
  const {done, value} = await reader.read();
  if (done) break;
  console.log(value); // 输出: "Hello", "World"
}

7 LangChain.js 中的 Stream 和 ReadableStream 关系

7.1 核心关系

LangChain.js 中的 IterableReadableStream 是一个特殊的类,它同时继承了 ReadableStream 并实现了 AsyncIterator 接口。这种设计让它既能作为标准的 ReadableStream 使用,又能支持 for await...of 循环。

7.2 转换关系

从 AsyncGenerator 到 ReadableStream

从 ReadableStream 到 AsyncGenerator

让我们通过一个完整的例子来说明这个关系

typescript 复制代码
// 1. 创建一个支持流式处理的 Runnable
const streamRunnable = new RunnableLambda({
  func: async function* (input: string) {
    // 返回 AsyncGenerator
    for (const char of input) {
      await new Promise(resolve => setTimeout(resolve, 100));
      yield char.toUpperCase();
    }
  }
});

// 2. 使用 stream 方法
const stream = await streamRunnable.stream("hello");
// 此时 stream 是 IterableReadableStream

// 3. 使用方式 1:作为 ReadableStream
const reader = stream.getReader();
while (true) {
  const {done, value} = await reader.read();
  if (done) break;
  console.log(value);
}

// 4. 使用方式 2:作为 AsyncIterator
for await (const chunk of stream) {
  console.log(chunk);
}
相关推荐
用户849137175471611 小时前
🚀5 分钟实现 Markdown 智能摘要生成器:LangChain + OpenAI 实战教程
langchain·openai
金汐脉动15 小时前
实践指南:从零开始搭建RAG驱动的智能问答系统
langchain
MrGaoGang1 天前
AI应用开发:LangGraph+MCP
前端·人工智能·langchain
大尾巴青年2 天前
06 一分钟搞懂langchain的Agent是如何工作的
langchain·llm
敲键盘的小夜猫2 天前
LangChain核心之Runnable接口底层实现
langchain
疯狂的小强呀2 天前
基于langchain的简单RAG的实现
python·langchain·rag检索增强
用户711283928472 天前
LangChain(三) LCEL
人工智能·langchain
啾啾大学习2 天前
LangChain快速筑基(带代码)P3-连续对话Memory
langchain
啾啾大学习2 天前
LangChain快速筑基(带代码)P0-DeepSeek对话与联网搜索
langchain