1. 什么是 Runnable?
Runnable 是 LangChain.js 中的一个核心概念,它就像是一个统一的"接口",让不同的组件能够以相同的方式工作。想象一下,如果你有很多不同的工具(比如文本处理、AI模型调用等),它们的工作方式都不一样,使用起来会很麻烦。Runnable 就是来解决这个问题的 - 它让所有组件都遵循相同的规则,这样它们就可以轻松地组合在一起工作。
2.The Runnable Interface
这些方法各自的作用是:
- invoke: 最基础的方法,用于处理单个输入
- stream: 用于流式处理数据
- batch: 用于批量处理多个输入
- pipe: 用于将多个 Runnable 连接起来
- 其他方法用于配置和错误处理
3. 基础实现示例
让我们通过一个简单的例子来理解 Runnable 是如何工作的。
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 就像是一个"函数包装器":
- 它接收一个普通函数
- 把这个函数包装成一个具有统一接口的对象
- 通过 invoke 方法调用这个函数
你可能会问:为什么要这么麻烦?直接调用函数不就好了吗?
Runnable 的价值在于:
- 统一接口:所有组件都使用相同的方式调用
- 可组合性:可以轻松地将多个 Runnable 组合在一起
- 可配置性:通过 RunnableConfig 可以控制行为
- 类型安全:使用 TypeScript 泛型保证类型安全
函数式编程视角
从函数式编程的角度来看,RunnableLambda.from 方法实现了一个重要的概念:函数提升(Lifting) 。它把一个普通函数"提升"到了一个更复杂的抽象结构中,使其具备更强的组合能力。
这种设计让我们可以:
- 把简单的函数转换成具有统一接口的对象
- 保持代码的一致性
- 方便地进行函数组合
- 支持异步操作
通过这种方式,我们可以用统一的方式处理各种不同的操作,无论是简单的文本转换,还是复杂的 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 就像是一个管道系统:
- 每个 Runnable 都是一个处理步骤
- pipe 方法把这些步骤连接起来
- 数据会依次流经每个步骤
- 最终得到处理后的结果
实际使用示例
让我们通过一个简单的例子来理解:
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 系统中,流式处理的特点是:
- 数据以"块"为单位逐步处理
- 每个"块"是 invoke 方法返回的一个完整结果
- 可以通过 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的区别:
- 错误处理:实际实现增加了错误处理机制
- 配置处理:使用 ensureConfig 确保配置正确
- 流转换:将 AsyncGenerator 转换为 IterableReadableStream
- 初始化处理:使用 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() {
// 处理初始化逻辑
}
}
这个类的作用是:
- 包装异步生成器
- 处理初始化逻辑
- 确保配置正确加载
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;
}
}
这个类的关键点:
- 同时实现了 ReadableStream 和 AsyncIterator 接口
- 提供了流式读取和迭代的能力
- 自动管理 reader 的生命周期
- 支持 for await...of 循环
6.2 ReadableStream 详解
6.2.1 什么是 ReadableStream?
ReadableStream 是 Web Streams API 的一部分,它代表了一个可读的数据流。想象一下,就像是一个水管,数据从一端流入,我们可以从另一端读取数据。这个"水管"可以:
- 逐步传输数据
- 控制数据流动的速度
- 处理背压(backpressure)
- 支持异步操作
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);
}