第 35 课: 流式架构与多运行时支持

2 阅读7分钟

课程目标

Part 1: 精读 LangChain.js 的流式能力实现 — stream()_streamIterator()transform()streamEvents("v2")IterableReadableStreamconvertToHttpEventStream()。Part 2: 理解多运行时(Node/Deno/Bun/Browser/Edge)的兼容策略 — ESM+CJS 双输出、构建插件、browser 入口、Universal ChatModel。


Part 1: 流式架构


35.1 流式的用户价值

LLM 生成通常需要数秒时间。流式输出让用户在第一个 token 生成后就能看到结果,将"感知等待时间"从秒级降低到毫秒级。

LangChain.js 的流式设计目标:

  1. 默认可流式 — 即使组件未实现流式,也有合理的降级策略
  2. 管道透传 — 流式数据在 RunnableSequence 中逐节点传播
  3. 事件级监控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);
}

关键设计

  1. _streamIterator() 的默认实现不是真正的流 — 它一次性 yield 整个结果
  2. 子类(如 BaseChatModel)重写 _streamIterator() 调用 _streamResponseChunks() 实现逐 token 流式
  3. 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_startChain/Runnable 开始{ input }
on_chain_endChain/Runnable 结束{ output }
on_chain_streamChain 产出一个 chunk{ chunk }
on_llm_startLLM 开始调用{ input }
on_llm_streamLLM 产出一个 token{ chunk } (AIMessageChunk)
on_llm_endLLM 调用结束{ 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 标准接口
  • AsyncIterablefor 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 可以在以下环境运行:

运行时ESMCJS特殊考虑
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 源码精读路线

优先级文件关注点
P0runnables/base.ts (stream/transform)_streamIterator()stream()_transform()
P0runnables/wrappers.tsconvertToHttpEventStream() SSE 转换
P1runnables/iter.ts迭代器工具、上下文传播
P1langchain/src/chat_models/universal.tsUniversal ChatModel、Provider 映射
P2internal/build/src/index.tsgetBuildConfig()、双格式输出配置
P2internal/build/src/plugins/构建插件体系

35.14 实战练习

  1. 基础: 使用 model.stream() 实现逐 token 输出,用 for await...of 消费
  2. 进阶: 构建一个流式 Web 端点 — 后端用 convertToHttpEventStream() 生成 SSE,前端用 EventSource 消费
  3. 高阶: 用 streamEvents("v2") 监听 RAG 链的完整执行过程,分别展示"检索中..."和逐 token 生成

本课收获总结

级别你应该掌握的
🟢 基础理解流式输出的用户体验价值;能使用 stream()streamEvents()
🔵 中阶掌握 stream()streamEvents() 的区别(chunk 级 vs 事件级)
🟡 高阶理解 _transform() 在 RunnableSequence 中的流式传播机制
🟠 资深分析 convertToHttpEventStream()AsyncGeneratorWithSetup 的设计;理解 ESM+CJS 双输出
🔴 架构能设计端到端流式方案 + 多运行时兼容策略

下一课预告

第 36 课是课程的终章 — 框架架构总结与生产实战,回顾核心设计模式,提供生产化检查清单和贡献指南。