课程目标
Part 1: 精读 LangChain.js 的流式能力实现 — stream()、_streamIterator()、transform()、streamEvents("v2")、IterableReadableStream、convertToHttpEventStream()。Part 2: 理解多运行时(Node/Deno/Bun/Browser/Edge)的兼容策略 — ESM+CJS 双输出、构建插件、browser 入口、Universal ChatModel。
Part 1: 流式架构
35.1 流式的用户价值
LLM 生成通常需要数秒时间。流式输出让用户在第一个 token 生成后就能看到结果,将"感知等待时间"从秒级降低到毫秒级。
LangChain.js 的流式设计目标:
- 默认可流式 — 即使组件未实现流式,也有合理的降级策略
- 管道透传 — 流式数据在 RunnableSequence 中逐节点传播
- 事件级监控 —
streamEvents暴露每一步的开始/结束/错误
35.2 stream() 与 _streamIterator()
源码位置: libs/langchain-core/src/runnables/base.ts:297
// 默认流式实现 — 直接 yield 整个 invoke 结果
async *_streamIterator(
input: RunInput,
options?: Partial<CallOptions>
): AsyncGenerator<RunOutput> {
yield this.invoke(input, options);
}
// stream() 包装 _streamIterator 为 IterableReadableStream
async stream(
input: RunInput,
options?: Partial<CallOptions>
): Promise<IterableReadableStream<RunOutput>> {
const config = ensureConfig(options);
const wrappedGenerator = new AsyncGeneratorWithSetup({
generator: this._streamIterator(input, config),
config,
});
await wrappedGenerator.setup; // 预消费第一个 chunk,让初始化错误立即暴露
return IterableReadableStream.fromAsyncGenerator(wrappedGenerator);
}
关键设计:
_streamIterator()的默认实现不是真正的流 — 它一次性 yield 整个结果- 子类(如
BaseChatModel)重写_streamIterator()调用_streamResponseChunks()实现逐 token 流式 AsyncGeneratorWithSetup会在stream()返回前消费第一个 chunk,确保连接错误、认证错误等在调用侧立即暴露
35.3 transform() — 管道中的流式传播
transform() 是 RunnableSequence 流式传播的核心:
// 默认 transform 实现
async *_transform(
inputGenerator: AsyncGenerator<RunInput>,
runManager?: CallbackManagerForChainRun,
options?: Partial<CallOptions>
): AsyncGenerator<RunOutput> {
// 默认策略:收集所有输入,然后流式输出
let finalInput: RunInput | undefined;
for await (const chunk of inputGenerator) {
finalInput = finalInput === undefined ? chunk : concat(finalInput, chunk);
}
yield* this._streamIterator(finalInput!, options);
}
RunnableSequence 中的流式传播流程:
用户输入 -> [node1._transform()] -> [node2._transform()] -> [node3._transform()] -> 用户
AsyncGenerator AsyncGenerator AsyncGenerator
每个节点接收上游的 AsyncGenerator,处理后 yield 给下游。支持流式的节点(如 LLM)可以逐 chunk 传播,不支持的节点则先收集完整输入再处理。
35.4 streamEvents("v2") — 事件级流式
streamEvents 提供了比 stream 更细粒度的控制:
const eventStream = chain.streamEvents(input, { version: "v2" });
for await (const event of eventStream) {
// event 结构
// {
// event: "on_chain_start" | "on_chain_end" | "on_llm_start" | "on_llm_stream" | ...
// name: string, // 组件名称
// data: { input?, output?, chunk? },
// run_id: string,
// parent_ids: string[], // 父级 run ID 链
// tags: string[],
// metadata: Record<string, unknown>,
// }
}
事件类型:
| 事件 | 触发时机 | data 内容 |
|---|---|---|
on_chain_start | Chain/Runnable 开始 | { input } |
on_chain_end | Chain/Runnable 结束 | { output } |
on_chain_stream | Chain 产出一个 chunk | { chunk } |
on_llm_start | LLM 开始调用 | { input } |
on_llm_stream | LLM 产出一个 token | { chunk } (AIMessageChunk) |
on_llm_end | LLM 调用结束 | { output } |
on_retriever_start | 检索器开始 | { input } (查询字符串) |
on_retriever_end | 检索器结束 | { output } (文档数组) |
on_tool_start | 工具开始 | { input } |
on_tool_end | 工具结束 | { output } |
实现原理:streamEvents 内部使用 EventStreamCallbackHandler,它是一个特殊的 tracer,将所有 callback 事件转换为 SSE 事件流。
35.5 IterableReadableStream — 流的工具类
LangChain.js 中的流都返回 IterableReadableStream,它同时实现了:
ReadableStream— Web API 标准接口AsyncIterable—for await...of语法
// 两种消费方式都支持
const stream = await model.stream("hello");
// 方式 1: for await...of
for await (const chunk of stream) {
process.stdout.write(chunk.content);
}
// 方式 2: ReadableStream API
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(value.content);
}
35.6 convertToHttpEventStream — SSE 转换
源码位置: libs/langchain-core/src/runnables/wrappers.ts
将 Runnable 的流式输出转换为 HTTP Server-Sent Events 格式:
export function convertToHttpEventStream(stream: AsyncGenerator) {
const encoder = new TextEncoder();
const finalStream = new ReadableStream<Uint8Array>({
async start(controller) {
for await (const chunk of stream) {
controller.enqueue(
encoder.encode(`event: data\ndata: ${JSON.stringify(chunk)}\n\n`)
);
}
controller.enqueue(encoder.encode("event: end\n\n"));
controller.close();
},
});
return IterableReadableStream.fromReadableStream(finalStream);
}
端到端流式架构:
LLM SSE响应 -> BaseChatModel._streamResponseChunks()
-> _streamIterator() -> stream()
-> convertToHttpEventStream()
-> HTTP Response (SSE)
-> 前端 EventSource / fetch
35.7 iter.ts — 迭代器工具
源码位置: libs/langchain-core/src/runnables/iter.ts
提供了在 AsyncLocalStorage 上下文中消费迭代器的工具:
// 在指定上下文中消费异步迭代器
export async function* consumeAsyncIterableInContext<T>(
context: Partial<RunnableConfig> | undefined,
iter: AsyncIterable<T>
): AsyncIterableIterator<T> {
const iterator = iter[Symbol.asyncIterator]();
while (true) {
const { value, done } = await AsyncLocalStorageProviderSingleton.runWithConfig(
pickRunnableConfigKeys(context),
iterator.next.bind(iter),
true
);
if (done) break;
yield value;
}
}
// 类型守卫函数
export function isAsyncIterable(thing: unknown): thing is AsyncIterable<unknown>;
export function isAsyncGenerator(x: unknown): x is AsyncGenerator;
export function isIterableIterator(thing: unknown): thing is IterableIterator<unknown>;
这些工具确保流式数据在跨越异步边界时,RunnableConfig(callbacks, tags, metadata)仍然能正确传播。
Part 2: 多运行时支持
35.8 支持的运行时
LangChain.js 可以在以下环境运行:
| 运行时 | ESM | CJS | 特殊考虑 |
|---|---|---|---|
| Node.js 18+ | 支持 | 支持 | 完整功能 |
| Deno | 支持 | - | 通过 npm: 导入 |
| Bun | 支持 | 支持 | 完整功能 |
| Browser | 支持 | - | 需 browser 入口,无 fs/child_process |
| Cloudflare Workers | 支持 | - | 无 Node API |
| Vercel Edge | 支持 | - | 无 Node API |
35.9 ESM + CJS 双输出
源码位置: internal/build/src/index.ts
getBuildConfig() 为所有 LangChain 包生成标准化的构建配置:
export function getBuildConfig(options?: Partial<BuildOptions>): BuildOptions {
return {
format: ["cjs", "esm"], // 双格式输出
target: "es2022",
platform: "node",
dts: {
parallel: true,
tsgo: true, // 使用 tsgo 加速类型声明生成
build: true,
},
sourcemap: true,
unbundle: true, // 不打包,保持模块结构
exports: {
customExports: async (exports, context) => {
// 从 package.json 读取手写的 exports 条件
// 为每个入口生成 import/require/types 三件套
// 保留 "browser" 等额外条件
return {
".": {
input: "./src/index.ts",
require: { types: "./dist/index.d.cts", default: "./dist/index.cjs" },
import: { types: "./dist/index.d.ts", default: "./dist/index.js" },
},
// ...
};
},
},
attw: { profile: "node16", level: "error" }, // 类型兼容性检查
publint: { level: "error", strict: true }, // 发布质量检查
unused: { level: "error" }, // 未使用导出检查
};
}
构建插件(internal/build/src/plugins/):
| 插件 | 用途 |
|---|---|
cjsCompatPlugin | 确保 CJS 输出的兼容性 |
importConstantsPlugin | 生成 import_constants.ts(可选导入入口列表) |
importMapPlugin | 生成 import_map.ts(可序列化类注册表) |
lcSecretsPlugin | 自动发现并注册 lc_secrets 映射 |
35.10 Browser 入口
某些包提供专门的 browser 入口,排除了 Node.js 特有的依赖:
// package.json exports
{
".": {
"browser": "./dist/browser.js", // 浏览器环境
"import": "./dist/index.js", // Node.js ESM
"require": "./dist/index.cjs" // Node.js CJS
}
}
打包工具(webpack/vite/esbuild)会根据 browser 条件自动选择正确的入口。
35.11 AsyncLocalStorage 的运行时差异
AsyncLocalStorage 是 Node.js 提供的跨异步边界的上下文传递机制。在非 Node 环境中的处理策略:
Node.js -> 原生 AsyncLocalStorage (node:async_hooks)
Deno -> 原生 AsyncLocalStorage (node:async_hooks 兼容层)
Bun -> 原生 AsyncLocalStorage
Browser -> 不可用 -> 降级为同步上下文(无跨异步传播)
Edge -> 部分支持(Cloudflare Workers 已支持)
LangChain.js 通过 AsyncLocalStorageProviderSingleton 单例抽象了这一差异(参见第 17 课)。
35.12 Universal ChatModel — 运行时动态选择 Provider
源码位置: libs/langchain/src/chat_models/universal.ts
Universal ChatModel 支持在运行时根据模型名称动态导入对应的 Provider:
export const MODEL_PROVIDER_CONFIG = {
openai: { package: "@langchain/openai", className: "ChatOpenAI" },
anthropic: { package: "@langchain/anthropic", className: "ChatAnthropic" },
google: { package: "@langchain/google", className: "ChatGoogle" },
ollama: { package: "@langchain/ollama", className: "ChatOllama" },
groq: { package: "@langchain/groq", className: "ChatGroq" },
bedrock: { package: "@langchain/aws", className: "ChatBedrockConverse" },
deepseek: { package: "@langchain/deepseek", className: "ChatDeepSeek" },
xai: { package: "@langchain/xai", className: "ChatXAI" },
// ... 15+ providers
} as const;
// 动态实例化
async function _initChatModelHelper(
model: string,
modelProvider?: string,
params: Record<string, any> = {}
): Promise<BaseChatModel> {
const provider = modelProvider || _inferModelProvider(model);
const config = MODEL_PROVIDER_CONFIG[provider];
const ProviderClass = await getChatModelByClassName(config.className, provider);
return new ProviderClass({ model, ...params });
}
Provider 自动推断:根据模型名称前缀推断 Provider:
"gpt-4o"-> openai"claude-3-opus"-> anthropic"gemini-pro"-> google
使用示例:
import { initChatModel } from "langchain/chat_models/universal";
// 通过模型名自动推断 Provider
const model = await initChatModel("gpt-4o");
// 或显式指定 Provider
const model = await initChatModel("my-model", {
modelProvider: "ollama",
temperature: 0.7,
});
35.13 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | runnables/base.ts (stream/transform) | _streamIterator()、stream()、_transform() |
| P0 | runnables/wrappers.ts | convertToHttpEventStream() SSE 转换 |
| P1 | runnables/iter.ts | 迭代器工具、上下文传播 |
| P1 | langchain/src/chat_models/universal.ts | Universal ChatModel、Provider 映射 |
| P2 | internal/build/src/index.ts | getBuildConfig()、双格式输出配置 |
| P2 | internal/build/src/plugins/ | 构建插件体系 |
35.14 实战练习
- 基础: 使用
model.stream()实现逐 token 输出,用for await...of消费 - 进阶: 构建一个流式 Web 端点 — 后端用
convertToHttpEventStream()生成 SSE,前端用EventSource消费 - 高阶: 用
streamEvents("v2")监听 RAG 链的完整执行过程,分别展示"检索中..."和逐 token 生成
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解流式输出的用户体验价值;能使用 stream() 和 streamEvents() |
| 🔵 中阶 | 掌握 stream() 与 streamEvents() 的区别(chunk 级 vs 事件级) |
| 🟡 高阶 | 理解 _transform() 在 RunnableSequence 中的流式传播机制 |
| 🟠 资深 | 分析 convertToHttpEventStream() 和 AsyncGeneratorWithSetup 的设计;理解 ESM+CJS 双输出 |
| 🔴 架构 | 能设计端到端流式方案 + 多运行时兼容策略 |
下一课预告
第 36 课是课程的终章 — 框架架构总结与生产实战,回顾核心设计模式,提供生产化检查清单和贡献指南。