第 21 课: Callbacks 系统 -- 框架的神经网络

1 阅读3分钟

课程目标

精读 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, ...);
    })
  );
}

关键流程

  1. 遍历所有 handler,跳过 ignoreLLM
  2. Tracer 类型的 handler 先同步创建 Run(防止异步导致 run map 竞态)
  3. 通过 consumeCallback 执行 handler,根据 awaitHandlers 决定同步/异步
  4. 返回 CallbackManagerForLLMRun,后续 handleLLMNewToken 等事件通过它触发

21.3.3 RunManager 层级

CallbackManager
  ├── handleLLMStart()     → CallbackManagerForLLMRun
  ├── handleChatModelStart() → CallbackManagerForLLMRun
  ├── handleChainStart()   → CallbackManagerForChainRun
  ├── handleToolStart()    → CallbackManagerForToolRun
  └── handleRetrieverStart() → CallbackManagerForRetrieverRun

每种 RunManager 只暴露与该组件类型相关的后续方法(如 CallbackManagerForLLMRunhandleLLMNewTokenCallbackManagerForChainRun 则没有)。

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 方法会:

  1. 从 config 中提取 callbacks
  2. 调用 CallbackManager.configure() 构建 manager
  3. 触发 handleChainStart 等事件
  4. 将 manager 传给子 Runnable(通过 getChild()
  5. 执行完毕后触发 handleChainEndhandleChainError

结合第 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 源码精读路线

优先级文件关注点
P0callbacks/base.ts:58-303BaseCallbackHandlerMethodsClass 全部事件方法
P0callbacks/base.ts:352-448BaseCallbackHandler 基类、ignore/raise 字段
P0callbacks/manager.ts:670-800CallbackManager 类、handleLLMStart 分发流程
P1callbacks/manager.ts:1248-1404_configureSync 自动配置逻辑
P1callbacks/manager.ts:264-406CallbackManagerForLLMRun、token 事件分发
P2singletons/callbacks.tsconsumeCallback、后台队列
P2callbacks/dispatch/index.tsdispatchCustomEvent

本课收获总结

级别你应该掌握的
🟢 基础学会注册 CallbackHandler 并传给 Runnable;理解事件方法的命名规律
🔵 中阶理解 CallbackManager 的事件分发机制:遍历 handlers、ignore 过滤、错误隔离
🟡 高阶掌握 callback 在链中的传播规则:inheritableHandlers vs handlersgetChild()
🟠 资深分析 consumeCallback 的异步策略;理解 _configureSync 如何自动注入 tracer
🔴 架构设计自定义可观测方案:日志 handler + 指标 handler + 追踪 handler 的组合

下一课预告

第 22 课深入 Tracers 和 EventStream——LangChain.js 内置的生产级可观测工具。