第 4 课: Runnable 接口与核心契约

0 阅读6分钟

课程目标

精读 Runnable 抽象类的核心设计:invoke / stream / batch 三大方法,以及 withRetry / withFallbacks / withConfig 三大增强方法。


4.1 Runnable 的设计哲学

Runnable 是整个 LangChain.js 的万物基石。一个简单但深刻的设计决策:

框架中的一切组件 — 模型、提示词、解析器、工具、检索器 — 都是 Runnable。

这意味着任何两个组件都可以通过 pipe() 组合,不需要特殊的适配器或胶水代码。


4.2 类定义与继承关系

Serializable (序列化基类)
  └── Runnable<RunInput, RunOutput, CallOptions>  (核心抽象)
        ├── BaseChatModel      (模型)
        ├── BasePromptTemplate (提示词)
        ├── BaseOutputParser   (解析器)
        ├── StructuredTool     (工具)
        ├── BaseRetriever      (检索器)
        ├── RunnableSequence   (管道组合)
        ├── RunnableParallel   (并行执行)
        ├── RunnableLambda     (函数包装)
        ├── RunnableBranch     (条件分支)
        ├── RunnableBinding    (配置绑定)
        ├── RunnableRetry      (重试包装)
        ├── RunnableWithFallbacks (降级包装)
        └── ...

源码位置: libs/langchain-core/src/runnables/base.ts:124

export abstract class Runnable<
  RunInput = any,
  RunOutput = any,
  CallOptions extends RunnableConfig = RunnableConfig,
>
  extends Serializable
  implements RunnableInterface<RunInput, RunOutput, CallOptions>
{
  protected lc_runnable = true;  // 标识这是一个 Runnable
  name?: string;                 // 可选的名称(用于调试和追踪)

  // 唯一的抽象方法 — 子类必须实现
  abstract invoke(
    input: RunInput,
    options?: Partial<CallOptions>
  ): Promise<RunOutput>;

  // 下面的方法都有默认实现,子类可选择重写
  // ...
}

关键观察invoke() 是唯一的抽象方法。batch()stream()pipe() 等都有默认实现。这意味着实现一个自定义 Runnable 的最低要求是只实现 invoke()


4.3 三大核心方法

4.3.1 invoke — 单次调用

abstract invoke(
  input: RunInput,
  options?: Partial<CallOptions>
): Promise<RunOutput>;
  • 语义:接收输入,返回输出。最基本的调用方式。
  • 异步:始终返回 Promise,支持异步操作(如 HTTP 调用 LLM)。
  • options:可选的运行时配置(callbacks、timeout、signal 等)。

4.3.2 batch — 批量调用

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

默认实现base.ts:261):

async batch(inputs, options, batchOptions) {
  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);
}

设计要点

  • 默认实现是"并行调用 N 次 invoke()",通过 AsyncCaller 控制并发
  • maxConcurrency 限制并发数(防止 API 限流)
  • returnExceptions: true 模式:收集异常而不是 fail-fast
  • 子类可以重写 batch() 提供更高效的批处理(如一次 API 调用处理多个请求)

4.3.3 stream — 流式调用

async stream(
  input: RunInput,
  options?: Partial<CallOptions>
): Promise<IterableReadableStream<RunOutput>>

默认实现base.ts:310):

async stream(input, options) {
  const config = ensureConfig(options);
  const wrappedGenerator = new AsyncGeneratorWithSetup({
    generator: this._streamIterator(input, config),
    config,
  });
  await wrappedGenerator.setup;
  return IterableReadableStream.fromAsyncGenerator(wrappedGenerator);
}

实际的流式逻辑在 _streamIterator() 中(base.ts:297):

async *_streamIterator(input, options) {
  // 默认实现:直接 yield invoke 的结果(一次性输出)
  yield this.invoke(input, options);
}

设计要点

  • 默认的 _streamIterator() 没有真正的"流"——它 yield 了整个 invoke 结果
  • 子类(如 BaseChatModel)重写 _streamIterator() 实现真正的逐 token 流式
  • stream()AsyncGenerator 包装为 IterableReadableStream,兼容 Web API
  • AsyncGeneratorWithSetup 会预先消费第一个 chunk,让初始化错误立即暴露

4.4 三大增强方法

4.4.1 withRetry — 自动重试

withRetry(fields?: {
  stopAfterAttempt?: number;
  onFailedAttempt?: RunnableRetryFailedAttemptHandler;
}): RunnableRetry<RunInput, RunOutput, CallOptions>

使用方式

const reliableModel = model.withRetry({
  stopAfterAttempt: 3,
  onFailedAttempt: (error, input) => {
    console.log(`Attempt failed: ${error.message}`);
  },
});

实现原理base.ts:156):

  • 返回一个 RunnableRetry 包装器
  • RunnableRetry 持有原始 Runnable 的引用
  • 调用时,如果失败就重试,直到达到 stopAfterAttempt 次数
  • 底层使用 p-retry

4.4.2 withFallbacks — 降级策略

withFallbacks(
  fields: { fallbacks: Runnable<RunInput, RunOutput>[] } | Runnable<RunInput, RunOutput>[]
): RunnableWithFallbacks<RunInput, RunOutput>

使用方式

const resilientModel = primaryModel.withFallbacks([backupModel1, backupModel2]);

// 调用时:
// 1. 先尝试 primaryModel
// 2. 如果失败,尝试 backupModel1
// 3. 如果还失败,尝试 backupModel2
// 4. 全部失败才抛异常

实现原理base.ts:192):

  • 返回 RunnableWithFallbacks 包装器
  • 依次尝试 primary + fallbacks,第一个成功就返回
  • 常见场景:主力模型超时/限流时降级到备选模型

4.4.3 withConfig — 配置绑定

withConfig(
  config: Partial<CallOptions>
): Runnable<RunInput, RunOutput, CallOptions>

使用方式

const verboseModel = model.withConfig({
  tags: ["production"],
  metadata: { user: "test" },
  maxConcurrency: 5,
});

实现原理base.ts:175):

  • 返回 RunnableBinding 包装器
  • RunnableBinding 在每次调用时将绑定的 config 与运行时 config 合并
  • 不改变原始 Runnable

4.5 pipe — 管道组合

pipe<NewRunOutput>(
  coerceable: RunnableLike<RunOutput, NewRunOutput>
): Runnable<RunInput, Exclude<NewRunOutput, Error>>

这是 LangChain.js 最重要的方法之一。

const chain = prompt.pipe(model).pipe(parser);
// 等价于
const chain = RunnableSequence.from([prompt, model, parser]);

pipe() 接受一个 RunnableLike,这意味着它可以接受:

  • RunnableInterface — 标准 Runnable 对象
  • RunnableFunc — 普通函数(自动包装为 RunnableLambda
  • RunnableMapLike — 对象字面量(自动包装为 RunnableParallel
// 三种都可以
chain.pipe(parser);                              // Runnable 对象
chain.pipe((output) => output.toUpperCase());    // 普通函数
chain.pipe({ a: runnableA, b: runnableB });      // 对象字面量

4.6 RunnableConfig 详解

每次调用 Runnable 时,都可以传入运行时配置:

export interface RunnableConfig<ConfigurableFieldType = Record<string, any>>
  extends BaseCallbackConfig {
  configurable?: ConfigurableFieldType;  // 可配置字段(自定义 key-value)
  recursionLimit?: number;               // 递归深度限制(默认 25)
  maxConcurrency?: number;               // 最大并发数
  timeout?: number;                      // 超时时间(毫秒)
  signal?: AbortSignal;                  // 中止信号
}

// 继承自 BaseCallbackConfig
interface BaseCallbackConfig {
  callbacks?: CallbackManager | Callbacks;  // 回调处理器
  tags?: string[];                          // 标签(用于过滤和追踪)
  metadata?: Record<string, unknown>;       // 元数据(附加到追踪记录)
  runName?: string;                         // 运行名称(用于追踪)
  runId?: string;                           // 运行 ID
}

config 合并策略config.ts:49 mergeConfigs()):

  • metadata:浅合并(...spread
  • tags:去重合并(Set
  • configurable:浅合并
  • timeout:取最小值(更严格的约束)
  • signal:用 AbortSignal.any() 组合(任一信号触发就中止)
  • callbacks:链式传递

4.7 Serializable 基类

Runnable 继承自 Serializable,获得序列化能力:

abstract class Serializable {
  lc_serializable = false;  // 默认不可序列化,子类按需开启
  lc_namespace: string[];   // 命名空间(如 ["langchain_core", "prompts"])

  toJSON(): SerializedNotImplemented | SerializedConstructor {
    // 序列化为 JSON
  }

  toJSONNotImplemented(): SerializedNotImplemented {
    // 不支持序列化时的回退
  }
}

这让 Runnable 可以被保存、传输和恢复(第 34 课详讲)。


4.8 实战练习:自定义 Runnable

实现一个简单的 UpperCaseRunnable

import { Runnable, RunnableConfig } from "@langchain/core/runnables";

class UpperCaseRunnable extends Runnable<string, string> {
  lc_namespace = ["custom"];

  async invoke(input: string, _options?: Partial<RunnableConfig>): Promise<string> {
    return input.toUpperCase();
  }

  // 可选:重写 _streamIterator 实现真正的流式
  async *_streamIterator(input: string): AsyncGenerator<string> {
    for (const char of input) {
      yield char.toUpperCase();
      await new Promise((resolve) => setTimeout(resolve, 50)); // 模拟延迟
    }
  }
}

// 使用
const upper = new UpperCaseRunnable();

// invoke
const result = await upper.invoke("hello world");
console.log(result); // "HELLO WORLD"

// stream
for await (const chunk of await upper.stream("hello")) {
  process.stdout.write(chunk); // H E L L O(逐字符)
}

// batch
const results = await upper.batch(["hello", "world"]);
console.log(results); // ["HELLO", "WORLD"]

// pipe 组合
const chain = upper.pipe((s) => s.split("").reverse().join(""));
const reversed = await chain.invoke("hello");
console.log(reversed); // "OLLEH"

// withRetry
const reliable = upper.withRetry({ stopAfterAttempt: 3 });

// withFallbacks
const fallback = upper.withFallbacks([new UpperCaseRunnable()]);

观察

  • 只实现了 invoke(),但 batch()stream()pipe() 等全部可用
  • 重写 _streamIterator() 就获得了真正的流式能力
  • withRetry()withFallbacks() 无需额外代码

4.9 源码精读路线

优先级文件关注点
P0runnables/types.tsRunnableInterface 接口定义、RunnableConfig 类型
P0runnables/base.ts:120-320Runnable 抽象类、invoke/batch/stream 实现
P1runnables/config.tsmergeConfigs()ensureConfig()、config 合并策略
P2runnables/base.ts:1302RunnableBinding(withConfig 的实现)
P2runnables/base.ts:1727RunnableRetry(withRetry 的实现)
P2runnables/base.ts:2922RunnableWithFallbacks(withFallbacks 的实现)

本课收获总结

级别你应该掌握的
🟢 基础理解 invoke/stream/batch 三大核心方法的语义;能自定义一个简单 Runnable
🔵 中阶掌握 RunnableConfig 的字段和作用;理解 withRetry/withFallbacks/withConfig
🟡 高阶理解 _streamIterator() 的默认实现与重写机制;理解模板方法模式
🟠 资深分析 Runnable 继承 Serializable 的设计决策;理解 config 合并策略的细节
🔴 架构能评估"统一接口 + 默认实现"模式的扩展性边界

下一课预告

第 5 课深入 RunnableSequence — 管道组合的核心。理解 pipe() 背后的 first → middle → last 结构和流式传播机制。