课程目标
掌握 LangChain.js 中使用的 TypeScript 高级特性,理解这些特性如何支撑框架的类型安全和可组合性。
3.1 泛型 — 框架的类型骨架
3.1.1 Runnable 的泛型定义
LangChain.js 最核心的类型定义:
// libs/langchain-core/src/runnables/types.ts
export interface RunnableInterface<
RunInput = any, // 输入类型
RunOutput = any, // 输出类型
CallOptions extends RunnableConfig = RunnableConfig, // 调用选项
> extends SerializableInterface {
invoke(input: RunInput, options?: Partial<CallOptions>): Promise<RunOutput>;
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>;
}
三个泛型参数的作用:
RunInput:这个 Runnable 接受什么类型的输入RunOutput:这个 Runnable 产出什么类型的输出CallOptions:调用时可以传哪些额外配置
3.1.2 泛型如何在 pipe 链中传递
当你写 prompt.pipe(model).pipe(parser) 时,TypeScript 在编译期做了什么:
// prompt 是 Runnable<{topic: string}, BaseMessage[]>
// model 是 Runnable<BaseMessage[], AIMessage>
// parser 是 Runnable<AIMessage, string>
const chain = prompt // Runnable<{topic: string}, BaseMessage[]>
.pipe(model) // Runnable<{topic: string}, AIMessage>
.pipe(parser); // Runnable<{topic: string}, string>
pipe() 的签名确保了类型传递:
// 简化版 pipe 签名
class Runnable<RunInput, RunOutput> {
pipe<NewOutput>(
next: Runnable<RunOutput, NewOutput> // 注意:next 的输入必须匹配 this 的输出
): Runnable<RunInput, NewOutput> // 返回:输入保持不变,输出变为 next 的输出
}
如果类型不匹配,编译器直接报错:
const parser = new StringOutputParser(); // Runnable<AIMessage, string>
const model = new ChatOpenAI(); // Runnable<BaseMessage[], AIMessage>
// ❌ 编译错误:string 不能赋值给 BaseMessage[]
const broken = parser.pipe(model);
// ~~~~~
// Type 'string' is not assignable to type 'BaseMessage[]'
3.1.3 泛型约束
CallOptions 使用了 extends 约束:
CallOptions extends RunnableConfig = RunnableConfig
这意味着:
CallOptions必须是RunnableConfig的子类型(可以更具体,不能更宽泛)- 默认值是
RunnableConfig
实际例子:BaseChatModel 的 CallOptions 扩展了 RunnableConfig,添加了 tools、stop 等模型特有选项。
3.2 RunnableConfig — 运行时配置的类型设计
// libs/langchain-core/src/runnables/types.ts
export interface RunnableConfig<
ConfigurableFieldType extends Record<string, any> = Record<string, any>,
> extends BaseCallbackConfig {
configurable?: ConfigurableFieldType; // 可配置字段
recursionLimit?: number; // 递归深度限制(默认 25)
maxConcurrency?: number; // 最大并发数
timeout?: number; // 超时(毫秒)
signal?: AbortSignal; // 中止信号
}
设计要点:
ConfigurableFieldType泛型让不同 Runnable 可以定义自己的可配置字段- 继承
BaseCallbackConfig获得 callbacks、tags、metadata 等通用配置 Partial<CallOptions>— 所有配置都是可选的
3.3 抽象类 vs 接口
3.3.1 为什么 Runnable 是抽象类而不是接口?
LangChain.js 选择了抽象类:
abstract class Runnable<RunInput, RunOutput> extends Serializable {
// 可以提供默认实现
async batch(inputs: RunInput[]): Promise<RunOutput[]> {
return Promise.all(inputs.map((input) => this.invoke(input)));
}
// 可以提供组合方法
pipe<NewOutput>(next: Runnable<RunOutput, NewOutput>): RunnableSequence {
return new RunnableSequence({ first: this, last: next });
}
// 子类只需实现核心方法
abstract invoke(input: RunInput): Promise<RunOutput>;
}
如果用接口:
// ❌ 接口无法提供默认实现
interface Runnable<RunInput, RunOutput> {
invoke(input: RunInput): Promise<RunOutput>;
batch(inputs: RunInput[]): Promise<RunOutput[]>; // 每个实现者都得写
pipe(next: Runnable): RunnableSequence; // 每个实现者都得写
stream(input: RunInput): AsyncGenerator<RunOutput>; // 每个实现者都得写
}
抽象类的优势:
- 提供
batch、pipe、stream等方法的默认实现,子类不用重复写 - 可以继承
Serializable,获得序列化能力 - 可以在基类中统一处理 callbacks、config 传播等横切关注点
trade-off:
- TypeScript 不支持多继承,选择继承
Runnable就不能继承其他类 - 实际上 LangChain.js 的所有组件都继承
Runnable,这个约束可以接受
3.4 AsyncGenerator 与流式输出
3.4.1 AsyncGenerator 基础
LangChain.js 的 stream() 方法返回异步可迭代对象,底层基于 AsyncGenerator:
// AsyncGenerator 是一个可以异步逐个产出值的函数
async function* countToThree(): AsyncGenerator<number> {
yield 1;
await sleep(100);
yield 2;
await sleep(100);
yield 3;
}
// 消费
for await (const num of countToThree()) {
console.log(num); // 1, 2, 3(每个间隔 100ms)
}
3.4.2 在 LangChain.js 中的应用
transform() 方法是流式管道的核心:
// RunnableInterface 中的 transform 签名
transform(
generator: AsyncGenerator<RunInput>, // 接收上游的流
options: Partial<CallOptions>
): AsyncGenerator<RunOutput>; // 产出下游的流
RunnableSequence 利用 transform 实现逐节点流式传播:
// 简化版实现
class RunnableSequence {
async *_streamIterator(input) {
// 第一个节点:把 input 变成流
let currentStream = this.first._streamIterator(input);
// 中间节点:每个节点 transform 上游的流
for (const middle of this.middle) {
currentStream = middle.transform(currentStream, config);
}
// 最后一个节点
yield* this.last.transform(currentStream, config);
}
}
3.4.3 IterableReadableStream
LangChain.js 封装了一个工具类,让 AsyncGenerator 和 Web ReadableStream 互通:
// 既是 AsyncIterable(可以 for await)
// 又是 ReadableStream(可以 pipe 到 HTTP Response)
class IterableReadableStream<T> extends ReadableStream<T> {
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
const reader = this.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}
}
3.5 条件类型与映射类型
3.5.1 batch 方法的重载签名
batch() 方法根据 returnExceptions 参数返回不同类型:
// 不返回异常时,结果全是 RunOutput
batch(
inputs: RunInput[],
options?: Partial<CallOptions>,
batchOptions?: { returnExceptions?: false }
): Promise<RunOutput[]>;
// 返回异常时,结果可能包含 Error
batch(
inputs: RunInput[],
options?: Partial<CallOptions>,
batchOptions?: { returnExceptions: true }
): Promise<(RunOutput | Error)[]>;
这是 TypeScript 的函数重载,根据传入参数的字面量类型缩窄返回类型。
3.5.2 Tool 系统的类型推断
tool() 函数从 Zod schema 自动推断参数类型:
import { tool } from "@langchain/core/tools";
import { z } from "zod/v3";
const weatherTool = tool(
async ({ city, unit }) => {
// city 被推断为 string
// unit 被推断为 "celsius" | "fahrenheit"
return `${city}: 25°${unit === "celsius" ? "C" : "F"}`;
},
{
name: "get_weather",
description: "获取天气",
schema: z.object({
city: z.string().describe("城市名"),
unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
}
);
这背后的类型推断链:
schema的类型是z.ZodObject<{ city: ZodString, unit: ZodEnum<...> }>z.infer<typeof schema>推断出{ city: string, unit: "celsius" | "fahrenheit" }tool()的泛型将 schema 推断结果传递给回调函数的参数类型
3.6 Zod v3 + v4 双版本兼容
3.6.1 为什么要支持双版本?
- Zod v4 是新版本,有更好的性能和 API
- 大量现有用户代码用的是 Zod v3
- 作为框架,必须同时支持,让用户自己决定迁移时机
3.6.2 import 路径约定
import { z } from "zod/v3"; // Zod v3
import { z as z4 } from "zod/v4"; // Zod v4
3.6.3 互操作类型
框架内部定义了 InteropZodType 等类型来抹平差异:
// libs/langchain-core/src/utils/types/zod.ts (简化)
// 可以是 v3 或 v4 的 ZodType
type InteropZodType = ZodTypeV3 | ZodTypeV4;
// 统一的 parse 函数
async function interopParseAsync(schema: InteropZodType, data: unknown) {
if (isZodV3(schema)) {
return schema.parseAsync(data);
} else {
return z4.parseAsync(schema, data); // v4 用静态方法
}
}
3.6.4 对开发者的意义
// 你可以用 v3 定义工具
const toolV3 = tool(fn, {
schema: z.object({ query: z.string() }), // Zod v3
});
// 也可以用 v4
const toolV4 = tool(fn, {
schema: z4.object({ query: z4.string() }), // Zod v4
});
// 框架内部统一处理
3.7 模板方法模式的类型体现
3.7.1 public 方法 vs protected 方法
abstract class BaseChatModel extends BaseLanguageModel {
// public 方法:用户调用,包含通用逻辑(callbacks、config、重试等)
async invoke(
input: BaseMessageLike[],
options?: Partial<CallOptions>
): Promise<AIMessage> {
// 1. 处理 config、callbacks
// 2. 调用 _generate()(子类实现)
// 3. 触发 callbacks
// 4. 返回结果
}
// protected 抽象方法:Provider 实现,只关注核心逻辑
protected abstract _generate(
messages: BaseMessage[],
options: this["ParsedCallOptions"],
runManager?: CallbackManagerForLLMRun
): Promise<ChatResult>;
}
类型设计的妙处:
this["ParsedCallOptions"]— 使用this类型,让子类的选项类型自动传递- 子类只需实现
_generate(),所有通用能力(callbacks、retry、stream)由基类提供
3.8 关键类型工具集
3.8.1 常见类型速查
| 类型 | 定义位置 | 用途 |
|---|---|---|
RunnableInterface<I, O> | runnables/types.ts | 所有 Runnable 的接口 |
RunnableConfig | runnables/types.ts | 运行时配置 |
BaseMessage | messages/base.ts | 消息基类 |
AIMessage | messages/ai.ts | AI 回复消息 |
AIMessageChunk | messages/ai.ts | 流式 AI 消息分片 |
InteropZodType | utils/types/zod.ts | Zod v3/v4 兼容类型 |
StructuredToolInterface | tools/types.ts | 工具接口 |
3.8.2 类型推断链示例
完整追踪一条链的类型传递:
// Step 1: ChatPromptTemplate
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a {role}"],
["human", "{question}"],
]);
// 类型: Runnable<{ role: string, question: string }, ChatPromptValue>
// Step 2: ChatOpenAI
const model = new ChatOpenAI();
// 类型: BaseChatModel = Runnable<BaseMessageLike[], AIMessage>
// ChatPromptValue 可以转为 BaseMessage[],所以 pipe 成功
// Step 3: StringOutputParser
const parser = new StringOutputParser();
// 类型: Runnable<AIMessage | AIMessageChunk, string>
// Step 4: 组合
const chain = prompt.pipe(model).pipe(parser);
// 类型: Runnable<{ role: string, question: string }, string>
// Step 5: 调用
const result: string = await chain.invoke({
role: "expert", // ✅ 类型检查通过
question: "hello", // ✅ 类型检查通过
// typo: "hell", // ❌ 编译错误:Object literal may only specify known properties
});
3.9 实用技巧
3.9.1 利用类型系统调试
当 pipe() 报错时,检查前一个 Runnable 的输出类型和后一个的输入类型是否匹配。
3.9.2 从测试看类型用法
# 查看类型测试文件
ls libs/langchain-core/src/runnables/tests/
# 其中 *.test-d.ts 是类型测试
类型测试用 expectTypeOf:
import { expectTypeOf } from "vitest";
expectTypeOf(chain.invoke).parameter(0).toMatchTypeOf<{ topic: string }>();
expectTypeOf(chain.invoke).returns.resolves.toBeString();
3.9.3 strict mode 的保护
tsconfig.json 开启了 strict: true,这意味着:
strictNullChecks:null和undefined不能随意赋值noImplicitAny:不允许隐式anystrictFunctionTypes:函数参数类型严格检查
这些检查在编译期阻止了大量运行时错误。
3.10 实战练习
练习 1:类型推断验证
在项目中创建一个测试文件,验证 pipe 链的类型推断:
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { FakeChatModel } from "@langchain/core/utils/testing";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { expectTypeOf } from "vitest";
const prompt = ChatPromptTemplate.fromMessages([["human", "{topic}"]]);
const model = new FakeChatModel({});
const parser = new StringOutputParser();
const chain = prompt.pipe(model).pipe(parser);
// 验证链的输入输出类型
expectTypeOf(chain.invoke).parameter(0).toHaveProperty("topic");
expectTypeOf(chain.invoke).returns.resolves.toBeString();
练习 2:实现泛型 Runnable
尝试实现一个泛型转换 Runnable,体会泛型约束:
import { Runnable, RunnableConfig } from "@langchain/core/runnables";
class MapRunnable<T, U> extends Runnable<T[], U[]> {
lc_namespace = ["custom"];
private mapFn: (item: T) => U;
constructor(mapFn: (item: T) => U) {
super({});
this.mapFn = mapFn;
}
async invoke(input: T[], _options?: Partial<RunnableConfig>): Promise<U[]> {
return input.map(this.mapFn);
}
}
const doubler = new MapRunnable<number, number>((n) => n * 2);
const result = await doubler.invoke([1, 2, 3]); // [2, 4, 6]
3.11 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | runnables/types.ts | RunnableInterface 泛型定义、RunnableConfig 类型 |
| P1 | tools/index.ts:1-70 | tool() 函数的泛型推断链 |
| P1 | utils/types/zod.ts | InteropZodType、interopParseAsync() Zod 双版本兼容 |
| P2 | runnables/base.ts:72-100 | RunnableFunc、RunnableLike、RunnableMapLike 类型别名 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解泛型的基本概念;知道 AsyncGenerator 用于流式输出;理解 Runnable<I, O> 的含义 |
| 🔵 中阶 | 掌握泛型在 pipe() 链中的传递规则;理解抽象类 vs 接口的 trade-off |
| 🟡 高阶 | 掌握条件类型、函数重载在框架 API 中的运用;理解 tool() 函数的类型推断链 |
| 🟠 资深 | 理解 Zod v3/v4 互操作层的设计;分析 InteropZodType 的类型抹平策略 |
| 🔴 架构 | 能评估类型系统如何在编译期阻止非法组合;理解"类型即文档"的框架设计理念 |
下一课预告
第 4 课开始深入 Runnable 体系的第一课——Runnable 接口与核心契约。我们将精读 runnables/base.ts 的核心 3542 行。