课程目标
精读 LangChain.js 的 Callbacks 系统:BaseCallbackHandler 的事件接口、CallbackManager 的分发机制、callback 在 Runnable 链中的传播规则。
21.1 为什么需要 Callbacks
在一条复杂的 Runnable 链中:
Prompt → Model → Tool → Parser
你可能需要:
- 记录每个 LLM 调用的耗时和 token 数
- 在流式输出时逐 token 推送给前端
- 将执行过程发送到 LangSmith 做追踪
- 在出错时触发告警
Callbacks 系统提供了统一的事件观测机制,让这些需求不侵入业务代码。
21.2 BaseCallbackHandler -- 事件接口
源码位置: libs/langchain-core/src/callbacks/base.ts
21.2.1 事件方法体系
abstract class BaseCallbackHandlerMethodsClass {
// ===== LLM/ChatModel 事件 =====
handleLLMStart?(llm, prompts, runId, parentRunId?, ...): Promise<any> | any;
handleLLMNewToken?(token, idx, runId, ...): Promise<any> | any;
handleChatModelStreamEvent?(event, runId, ...): Promise<any> | any;
handleLLMError?(err, runId, ...): Promise<any> | any;
handleLLMEnd?(output, runId, ...): Promise<any> | any;
handleChatModelStart?(llm, messages, runId, ...): Promise<any> | any;
// ===== Chain 事件 =====
handleChainStart?(chain, inputs, runId, ...): Promise<any> | any;
handleChainError?(err, runId, ...): Promise<any> | any;
handleChainEnd?(outputs, runId, ...): Promise<any> | any;
// ===== Tool 事件 =====
handleToolStart?(tool, input, runId, ...): Promise<any> | any;
handleToolError?(err, runId, ...): Promise<any> | any;
handleToolEnd?(output, runId, ...): Promise<any> | any;
handleToolEvent?(chunk, runId, ...): Promise<any> | any;
// ===== Retriever 事件 =====
handleRetrieverStart?(retriever, query, runId, ...): Promise<any> | any;
handleRetrieverEnd?(documents, runId, ...): Promise<any> | any;
handleRetrieverError?(err, runId, ...): Promise<any> | any;
// ===== Agent 事件 =====
handleAgentAction?(action, runId, ...): Promise<void> | void;
handleAgentEnd?(action, runId, ...): Promise<void> | void;
// ===== 通用 =====
handleText?(text, runId, ...): Promise<void> | void;
handleCustomEvent?(eventName, data, runId, ...): Promise<any> | any;
}
设计观察:
- 所有方法都是可选的(
?),handler 只需实现关心的事件 - 每个事件都携带
runId和可选的parentRunId,用于构建调用树 - 事件覆盖了五大组件类型:LLM、Chain、Tool、Retriever、Agent
21.2.2 BaseCallbackHandler 基类
export abstract class BaseCallbackHandler
extends BaseCallbackHandlerMethodsClass
implements BaseCallbackHandlerInput, Serializable
{
abstract name: string; // handler 的唯一标识
ignoreLLM = false; // 是否忽略 LLM 事件
ignoreChain = false; // 是否忽略 Chain 事件
ignoreAgent = false; // 是否忽略 Agent 事件
ignoreRetriever = false; // 是否忽略 Retriever 事件
ignoreCustomEvent = false; // 是否忽略自定义事件
raiseError = false; // handler 出错时是否中断主流程
awaitHandlers = false; // 是否同步等待 handler 执行完毕
}
关键设计:
ignore*系列字段允许 handler 声明只关心特定类型的事件raiseError:默认为false,handler 中的错误只会console.warn而不中断主流程awaitHandlers:默认为异步(后台执行),设为true则同步等待。受环境变量LANGCHAIN_CALLBACKS_BACKGROUND影响
21.2.3 快速创建 Handler
// 方式 1:继承 BaseCallbackHandler
class MyHandler extends BaseCallbackHandler {
name = "my_handler";
handleLLMStart(llm, prompts, runId) {
console.log(`LLM 开始: ${runId}`);
}
handleLLMEnd(output, runId) {
console.log(`LLM 结束: ${runId}`);
}
}
// 方式 2:使用 fromMethods 工厂
const handler = BaseCallbackHandler.fromMethods({
handleLLMStart: (llm, prompts, runId) => {
console.log(`LLM 开始: ${runId}`);
},
handleLLMEnd: (output, runId) => {
console.log(`LLM 结束: ${runId}`);
},
});
21.3 CallbackManager -- 事件分发中枢
源码位置: libs/langchain-core/src/callbacks/manager.ts:670
21.3.1 核心数据结构
export class CallbackManager extends BaseCallbackManager {
handlers: BaseCallbackHandler[] = []; // 当前层的 handlers
inheritableHandlers: BaseCallbackHandler[] = []; // 可继承给子 Runnable 的 handlers
tags: string[] = [];
inheritableTags: string[] = [];
metadata: Record<string, unknown> = {};
inheritableMetadata: Record<string, unknown> = {};
_parentRunId?: string;
}
可继承 vs 不可继承:
inheritableHandlers:注册到这里的 handler 会被子 Runnable 继承handlers:仅在当前 Runnable 生效
21.3.2 事件分发流程
以 handleLLMStart 为例:
async handleLLMStart(
llm: Serialized,
prompts: string[],
runId?: string,
...
): Promise<CallbackManagerForLLMRun[]> {
return Promise.all(
prompts.map(async (prompt, idx) => {
const runId_ = idx === 0 && runId ? runId : uuidv7();
await Promise.all(
this.handlers.map((handler) => {
if (handler.ignoreLLM) return; // 尊重 ignore 标志
if (isBaseTracer(handler)) {
// Tracer 同步创建 Run,避免竞态
handler._createRunForLLMStart(llm, [prompt], runId_, ...);
}
return consumeCallback(async () => {
try {
await handler.handleLLMStart?.(llm, [prompt], runId_, ...);
} catch (err) {
if (handler.raiseError) throw err; // 只有 raiseError=true 才中断
console.warn(`Error in handler: ${err}`);
}
}, handler.awaitHandlers);
})
);
// 返回 RunManager,用于后续事件
return new CallbackManagerForLLMRun(runId_, this.handlers, ...);
})
);
}
关键流程:
- 遍历所有 handler,跳过
ignoreLLM的 - Tracer 类型的 handler 先同步创建 Run(防止异步导致 run map 竞态)
- 通过
consumeCallback执行 handler,根据awaitHandlers决定同步/异步 - 返回
CallbackManagerForLLMRun,后续handleLLMNewToken等事件通过它触发
21.3.3 RunManager 层级
CallbackManager
├── handleLLMStart() → CallbackManagerForLLMRun
├── handleChatModelStart() → CallbackManagerForLLMRun
├── handleChainStart() → CallbackManagerForChainRun
├── handleToolStart() → CallbackManagerForToolRun
└── handleRetrieverStart() → CallbackManagerForRetrieverRun
每种 RunManager 只暴露与该组件类型相关的后续方法(如 CallbackManagerForLLMRun 有 handleLLMNewToken,CallbackManagerForChainRun 则没有)。
21.3.4 子 Runnable 的 callback 继承
// CallbackManagerForChainRun.getChild()
getChild(tag?: string): CallbackManager {
const manager = new CallbackManager(this.runId); // parentRunId = 当前 runId
manager.setHandlers(this.inheritableHandlers); // 只继承可继承的
manager.addTags(this.inheritableTags);
manager.addMetadata(this.inheritableMetadata);
if (tag) {
manager.addTags([tag], false); // 子 Runnable 的额外 tag 不可继承
}
return manager;
}
21.4 consumeCallback -- 异步执行策略
源码位置: libs/langchain-core/src/singletons/callbacks.ts
export async function consumeCallback<T>(
promiseFn: () => Promise<T> | T | void,
wait: boolean
): Promise<void> {
if (wait === true) {
// 同步等待:在清空的上下文中执行
const als = getGlobalAsyncLocalStorageInstance();
if (als !== undefined) {
await als.run(undefined, async () => promiseFn());
} else {
await promiseFn();
}
} else {
// 异步后台执行:加入全局队列
queue = getQueue();
void queue.add(async () => {
// 同样在清空的上下文中执行
const als = getGlobalAsyncLocalStorageInstance();
if (als !== undefined) {
await als.run(undefined, async () => promiseFn());
} else {
await promiseFn();
}
});
}
}
设计要点:
- 不管同步还是异步,都在
als.run(undefined, ...)中执行——清空上下文,防止 callback 错误地使用到 Runnable 链的上下文 - 异步模式使用 p-queue(并发 1),确保 callback 按顺序执行
21.5 CallbackManager.configure -- 自动配置
源码位置: libs/langchain-core/src/callbacks/manager.ts:1248
每次 Runnable 执行前,_configureSync 会自动配置 CallbackManager:
static _configureSync(inheritableHandlers?, localHandlers?, ...): CallbackManager | undefined {
// 1. 合并可继承和本地 handlers
// 2. 如果 LANGCHAIN_VERBOSE=true,自动添加 ConsoleCallbackHandler
// 3. 如果追踪已启用,自动添加 LangChainTracer
// 4. 处理 registerConfigureHook 注册的钩子(第 17 课)
// 5. 合并 tags 和 metadata
}
这就是为什么设置 LANGCHAIN_VERBOSE=true 就能看到详细日志——框架自动注入了 ConsoleCallbackHandler。
21.6 dispatchCustomEvent -- 自定义事件
源码位置: libs/langchain-core/src/callbacks/dispatch/index.ts
import { dispatchCustomEvent } from "@langchain/core/callbacks/dispatch";
const myRunnable = RunnableLambda.from(async (input: string) => {
await dispatchCustomEvent("my_custom_event", { data: "some_value" });
return input;
});
// 监听自定义事件
const callbacks = [{
handleCustomEvent: (eventName: string, payload: any) => {
console.log(eventName, payload);
// "my_custom_event", { data: "some_value" }
}
}];
await myRunnable.invoke("hello", { callbacks });
21.7 Callback 与 RunnableConfig 的协作
Callbacks 通过 RunnableConfig.callbacks 字段沿链传递:
// 在 invoke 时传入 callbacks
await chain.invoke(input, {
callbacks: [new MyHandler()],
tags: ["production"],
metadata: { userId: "user_123" },
});
Runnable 的 invoke 方法会:
- 从 config 中提取 callbacks
- 调用
CallbackManager.configure()构建 manager - 触发
handleChainStart等事件 - 将 manager 传给子 Runnable(通过
getChild()) - 执行完毕后触发
handleChainEnd或handleChainError
结合第 17 课的 AsyncLocalStorage,callbacks 也可以通过上下文隐式传播,不需要显式传递。
21.8 实战练习
自定义一个统计 Handler:
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import { Serialized } from "@langchain/core/load/serializable";
import { LLMResult } from "@langchain/core/outputs";
class StatsCallbackHandler extends BaseCallbackHandler {
name = "stats_handler";
private stats = {
llmCalls: 0,
totalTokens: 0,
errors: 0,
startTime: 0,
};
handleLLMStart(_llm: Serialized, _prompts: string[], runId: string) {
this.stats.llmCalls++;
this.stats.startTime = Date.now();
}
handleLLMEnd(output: LLMResult, runId: string) {
const elapsed = Date.now() - this.stats.startTime;
const tokens = output.llmOutput?.tokenUsage?.totalTokens ?? 0;
this.stats.totalTokens += tokens;
console.log(`[Stats] LLM 调用完成: ${elapsed}ms, ${tokens} tokens`);
}
handleLLMError(_err: any, runId: string) {
this.stats.errors++;
}
getStats() {
return { ...this.stats };
}
}
// 使用
const handler = new StatsCallbackHandler();
await chain.invoke("你好", { callbacks: [handler] });
console.log(handler.getStats());
// { llmCalls: 1, totalTokens: 42, errors: 0, ... }
21.9 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | callbacks/base.ts:58-303 | BaseCallbackHandlerMethodsClass 全部事件方法 |
| P0 | callbacks/base.ts:352-448 | BaseCallbackHandler 基类、ignore/raise 字段 |
| P0 | callbacks/manager.ts:670-800 | CallbackManager 类、handleLLMStart 分发流程 |
| P1 | callbacks/manager.ts:1248-1404 | _configureSync 自动配置逻辑 |
| P1 | callbacks/manager.ts:264-406 | CallbackManagerForLLMRun、token 事件分发 |
| P2 | singletons/callbacks.ts | consumeCallback、后台队列 |
| P2 | callbacks/dispatch/index.ts | dispatchCustomEvent |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 学会注册 CallbackHandler 并传给 Runnable;理解事件方法的命名规律 |
| 🔵 中阶 | 理解 CallbackManager 的事件分发机制:遍历 handlers、ignore 过滤、错误隔离 |
| 🟡 高阶 | 掌握 callback 在链中的传播规则:inheritableHandlers vs handlers、getChild() |
| 🟠 资深 | 分析 consumeCallback 的异步策略;理解 _configureSync 如何自动注入 tracer |
| 🔴 架构 | 设计自定义可观测方案:日志 handler + 指标 handler + 追踪 handler 的组合 |
下一课预告
第 22 课深入 Tracers 和 EventStream——LangChain.js 内置的生产级可观测工具。