课程目标
精读 LangChain.js 的运行时基础设施:AsyncLocalStorage 驱动的上下文变量系统、全局单例管理,以及结构化错误类型体系。这三者共同构成了 Runnable 链的"隐式通信层"和"异常治理层"。
17.1 问题背景:深层嵌套中的状态共享
考虑一条复杂的 Runnable 链:
Prompt → Model → Tool → SubChain → Parser
在这条链中,每个节点可能需要访问同一个 trace ID、同一个 callback handler 或同一份配置。显式地逐层传递这些信息既繁琐又容易出错。LangChain.js 通过 AsyncLocalStorage 提供了一种隐式的跨层通信机制。
17.2 AsyncLocalStorage 单例系统
17.2.1 全局接口定义
源码位置: libs/langchain-core/src/singletons/async_local_storage/globals.ts
export interface AsyncLocalStorageInterface {
getStore: () => any | undefined;
run: <T>(store: any, callback: () => T) => T;
enterWith: (store: any) => void;
}
export const TRACING_ALS_KEY = Symbol.for("ls:tracing_async_local_storage");
export const _CONTEXT_VARIABLES_KEY = Symbol.for("lc:context_variables");
关键设计:
- 使用
Symbol.for()而非Symbol()—— 确保跨模块/跨包引用同一个 Symbol - 接口抽象了三个核心方法:
getStore(读取当前上下文)、run(在新上下文中执行回调)、enterWith(替换当前上下文) - 全局实例存储在
globalThis上,避免模块系统导致的多实例问题
17.2.2 Provider 单例
源码位置: libs/langchain-core/src/singletons/async_local_storage/index.ts
class AsyncLocalStorageProvider {
getInstance(): AsyncLocalStorageInterface {
return getGlobalAsyncLocalStorageInstance() ?? mockAsyncLocalStorage;
}
runWithConfig<T>(config: any, callback: () => T, avoidCreatingRootRunTree?: boolean): T {
const callbackManager = CallbackManager._configureSync(
config?.callbacks, undefined, config?.tags, undefined, config?.metadata
);
const storage = this.getInstance();
// ... 构建 RunTree,传播上下文变量
return storage.run(runTree, callback);
}
initializeGlobalInstance(instance: AsyncLocalStorageInterface) {
if (getGlobalAsyncLocalStorageInstance() === undefined) {
setGlobalAsyncLocalStorageInstance(instance);
}
}
}
const AsyncLocalStorageProviderSingleton = new AsyncLocalStorageProvider();
设计要点:
- 懒初始化:
initializeGlobalInstance采用 "first-wins" 策略,只有首次调用生效 - MockAsyncLocalStorage:在不支持
AsyncLocalStorage的环境中提供无操作的 fallback - 上下文变量传播:
runWithConfig会从父上下文复制_CONTEXT_VARIABLES_KEY,保证子 Runnable 能访问父级设置的变量
17.2.3 初始化入口
源码位置: libs/langchain-core/src/context.ts
import { AsyncLocalStorage } from "node:async_hooks";
import { AsyncLocalStorageProviderSingleton } from "./singletons/index.js";
// 模块加载时自动初始化
AsyncLocalStorageProviderSingleton.initializeGlobalInstance(
new AsyncLocalStorage()
);
这是一个带副作用的入口文件——import "@langchain/core/context" 会自动初始化全局 AsyncLocalStorage 实例。
17.3 上下文变量 API
源码位置: libs/langchain-core/src/singletons/async_local_storage/context.ts
17.3.1 setContextVariable
export function setContextVariable<T>(name: PropertyKey, value: T): void {
const asyncLocalStorageInstance = getGlobalAsyncLocalStorageInstance();
if (asyncLocalStorageInstance === undefined) {
throw new Error("Global shared async local storage instance has not been initialized.");
}
const runTree = asyncLocalStorageInstance.getStore();
const contextVars = { ...runTree?.[_CONTEXT_VARIABLES_KEY] }; // 不可变拷贝
contextVars[name] = value;
let newValue = {};
if (isRunTree(runTree)) {
newValue = new RunTree(runTree); // 保留 RunTree 结构
}
(newValue as any)[_CONTEXT_VARIABLES_KEY] = contextVars;
asyncLocalStorageInstance.enterWith(newValue); // 替换当前上下文
}
17.3.2 getContextVariable
export function getContextVariable<T = any>(name: PropertyKey): T | undefined {
const asyncLocalStorageInstance = getGlobalAsyncLocalStorageInstance();
if (asyncLocalStorageInstance === undefined) {
return undefined; // 优雅降级,不报错
}
const runTree = asyncLocalStorageInstance.getStore();
return runTree?.[_CONTEXT_VARIABLES_KEY]?.[name];
}
关键差异:set 找不到实例会抛错,get 找不到实例会返回 undefined。这种不对称设计合理——写入失败是严重问题,读取未初始化则是可预期的情况。
17.3.3 作用域规则
const nested = RunnableLambda.from(() => {
// 能读取父级设置的变量
console.log(getContextVariable("foo")); // "bar"
// 修改只影响当前及子 Runnable
setContextVariable("foo", "baz");
return getContextVariable("foo"); // "baz"
});
const runnable = RunnableLambda.from(async () => {
setContextVariable("foo", "bar");
const res = await nested.invoke({});
// 子 Runnable 的修改不影响父级
console.log(getContextVariable("foo")); // 仍然是 "bar"
return res;
});
作用域模型:子 Runnable 继承父级上下文,但修改不会反向传播。这类似于闭包的变量捕获语义。
17.4 Configure Hook:自动注入回调
源码位置: libs/langchain-core/src/singletons/async_local_storage/context.ts:188
export const registerConfigureHook = (config: ConfigureHook) => {
if (config.envVar && !config.handlerClass) {
throw new Error("If envVar is set, handlerClass must also be set.");
}
setContextVariable(LC_CONFIGURE_HOOKS_KEY, [..._getConfigureHooks(), config]);
};
export type ConfigureHook = {
contextVar?: string; // 上下文变量名
inheritable?: boolean; // 子 Runnable 是否继承
handlerClass?: new (...args: any[]) => BaseCallbackHandler;
envVar?: string; // 环境变量开关
};
两种使用方式:
// 方式 1:通过上下文变量
registerConfigureHook({ contextVar: "my_tracer" });
setContextVariable("my_tracer", new MyCallbackHandler());
// 方式 2:通过环境变量
registerConfigureHook({
handlerClass: MyCallbackHandler,
envVar: "MY_TRACER_ENABLED",
});
// 当 process.env.MY_TRACER_ENABLED === "true" 时自动实例化
CallbackManager._configureSync() 在每次 Runnable 执行时会读取所有已注册的 hook,自动将 handler 注入到 callback 链中。
17.5 其他单例
17.5.1 Tracer 客户端单例
源码位置: libs/langchain-core/src/singletons/tracer.ts
let client: Client;
export const getDefaultLangChainClientSingleton = () => {
if (client === undefined) {
const clientParams =
getEnvironmentVariable("LANGCHAIN_CALLBACKS_BACKGROUND") === "false"
? { blockOnRootRunFinalization: true }
: {};
client = new Client(clientParams);
}
return client;
};
17.5.2 Callback 队列单例
源码位置: libs/langchain-core/src/singletons/callbacks.ts
使用 p-queue 创建一个并发为 1 的队列,所有非阻塞 callback 通过此队列串行执行,防止并发导致的乱序。
17.6 错误类型体系
源码位置: libs/langchain-core/src/errors/index.ts
17.6.1 基类 LangChainError
export class LangChainError extends ns.brand(Error) {
readonly name: string = "LangChainError";
constructor(message?: string) {
super(message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
ns.brand 是 LangChain.js 的命名空间品牌机制,使得 LangChainError.isInstance(obj) 可以安全地检查错误类型,即使跨包/跨版本也能正确识别。
17.6.2 错误码体系
export type LangChainErrorCodes =
| "CONTEXT_OVERFLOW"
| "INVALID_PROMPT_INPUT"
| "INVALID_TOOL_RESULTS"
| "MESSAGE_COERCION_FAILURE"
| "MODEL_AUTHENTICATION"
| "MODEL_NOT_FOUND"
| "MODEL_RATE_LIMIT"
| "OUTPUT_PARSING_FAILURE"
| "MODEL_ABORTED";
17.6.3 ModelAbortError
export class ModelAbortError extends ns.brand(LangChainError, "model-abort") {
readonly partialOutput?: AIMessageChunk; // 中断前的部分输出
constructor(message: string, partialOutput?: AIMessageChunk) {
super(message);
this.partialOutput = partialOutput;
}
}
关键特性:携带 partialOutput,允许调用方获取中断前已生成的内容。
17.6.4 ContextOverflowError
export class ContextOverflowError extends ns.brand(LangChainError, "context-overflow") {
cause?: Error;
constructor(message?: string) {
super(message ?? "Input exceeded the model's context window.");
}
static fromError(obj: Error): ContextOverflowError {
const error = new ContextOverflowError(obj.message);
error.cause = obj;
return error;
}
}
Provider 层会捕获底层 API 的错误,包装为 ContextOverflowError 抛出,上层应用可以据此做截断或摘要压缩。
17.6.5 错误处理模式
try {
await model.invoke(input, { signal: controller.signal });
} catch (err) {
if (ModelAbortError.isInstance(err)) {
// 用户主动取消,可以使用 err.partialOutput
} else if (ContextOverflowError.isInstance(err)) {
// 上下文溢出,需要截断输入或切换大窗口模型
} else if (LangChainError.isInstance(err)) {
// 其他 LangChain 错误
} else {
throw err; // 非框架错误,继续抛出
}
}
17.7 上下文变量 vs RunnableConfig
| 维度 | 上下文变量 | RunnableConfig |
|---|---|---|
| 传递方式 | 隐式(AsyncLocalStorage) | 显式(参数传递) |
| 作用域 | 当前调用链的所有层级 | 仅传给直接子 Runnable |
| 类型安全 | 弱(any) | 强(泛型约束) |
| 适用场景 | 全局 trace ID、日志上下文 | callbacks、tags、timeout |
| 环境依赖 | 需要 AsyncLocalStorage | 无环境依赖 |
17.8 实战练习
利用上下文变量实现"请求级 Trace ID":
import { RunnableLambda } from "@langchain/core/runnables";
import { setContextVariable, getContextVariable } from "@langchain/core/context";
import { v4 as uuidv4 } from "uuid";
const step1 = RunnableLambda.from((input: string) => {
const traceId = getContextVariable("traceId");
console.log(`[${traceId}] Step1 处理: ${input}`);
return input.toUpperCase();
});
const step2 = RunnableLambda.from((input: string) => {
const traceId = getContextVariable("traceId");
console.log(`[${traceId}] Step2 处理: ${input}`);
return `结果: ${input}`;
});
const chain = step1.pipe(step2);
// 在链执行前设置 traceId
const main = RunnableLambda.from(async (input: string) => {
setContextVariable("traceId", uuidv4());
return chain.invoke(input);
});
await main.invoke("hello");
// [abc-123-...] Step1 处理: hello
// [abc-123-...] Step2 处理: HELLO
17.9 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | singletons/async_local_storage/context.ts | setContextVariable/getContextVariable 实现、registerConfigureHook |
| P0 | errors/index.ts | LangChainError、ModelAbortError、ContextOverflowError |
| P1 | singletons/async_local_storage/globals.ts | AsyncLocalStorageInterface、全局 Symbol |
| P1 | singletons/async_local_storage/index.ts | AsyncLocalStorageProvider、runWithConfig |
| P2 | context.ts | 入口文件、副作用初始化 |
| P2 | singletons/callbacks.ts | p-queue 回调队列 |
| P2 | singletons/tracer.ts | LangSmith 客户端单例 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解"上下文传递"的问题:深层嵌套中如何不逐层传参地共享状态 |
| 🔵 中阶 | 掌握 getContextVariable() / setContextVariable() 的使用方式和作用域规则 |
| 🟡 高阶 | 理解 AsyncLocalStorage 如何在异步调用链中保持上下文;理解 registerConfigureHook 的工作机制 |
| 🟠 资深 | 分析错误类型体系的品牌机制(ns.brand)和 isInstance 的跨包安全性;理解 partialOutput 的设计价值 |
| 🔴 架构 | 评估 AsyncLocalStorage 的多运行时兼容性策略(Mock fallback);设计基于上下文变量的跨组件通信方案 |
下一课预告
第 18 课进入 Provider 集成模块,以 OpenAI 为例,深入 ChatOpenAI._generate() 的实现——看核心抽象如何被具体 Provider 填充。