LangChain.js 完全开发手册(四)Callback 机制与事件驱动架构

第4章:Callback 机制与事件驱动架构

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

🎯 本章学习目标

  • 全面理解 LangChain.js 的回调(Callback)体系:生命周期事件、嵌套链路、Run ID 与上下文
  • 掌握流式输出(Streaming)、进度上报(Progress)、指标观测(Metrics)的实现方式
  • 能够自定义 CallbackHandler,与 Runnable、Memory、Agent、Tool、Retriever 等模块协同
  • 在 Next.js 中实现 SSE/WebSocket 实时推送;在前端实现打字机效果、取消与重试
  • 用 LangSmith/自建日志实现链路追踪、错误告警、成本监控与 A/B 评测
  • 通过两个实战项目完成从后端事件驱动到前端实时 UI 的闭环落地

📖 理论:Callback 与事件驱动(约 30%)

4.1 为什么需要 Callback

  • LLM 推理是"黑箱"与"长耗时"的结合,回调可以暴露过程、提升可观测性
  • 复杂链路(Prompt → LLM → Parser → Tool → Retriever)的每一步都需要日志、指标与错误捕捉
  • 流式输出需要"按 token 推送",回调是天然载体

4.2 生命周期事件(概念总览)

LangChain.js 在不同层提供了丰富的回调钩子(具体命名以版本为准):

  • LLM 级:
    • handleLLMStart / handleLLMNewToken / handleLLMEnd / handleLLMError
  • Chain/Runnable 级:
    • handleChainStart / handleChainEnd / handleChainError
    • handleRunnableStart / handleRunnableEnd / handleRunnableError
  • Tool 级:
    • handleToolStart / handleToolEnd / handleToolError
  • Retriever/Loader 级:
    • handleRetrieverStart / handleRetrieverEnd / handleRetrieverError

事件中通常会包含:

  • runId(一次调用的唯一 ID)与 parentRunId(嵌套链路)
  • tagsmetadata(自定义标记)
  • 输入/输出片段、token 用量、耗时等

4.3 嵌套与Run树(Run Tree)

当一个 Runnable 中又调用了多个子 Runnable/Tools 时,回调事件会形成一棵树:

less 复制代码
invoke (runId: A)
 ├─ Prompt.format (runId: B, parent: A)
 ├─ LLM.invoke (runId: C, parent: A)
 │   ├─ token#1
 │   ├─ token#2
 │   └─ ...
 └─ OutputParser.parse (runId: D, parent: A)

这让我们可以精准定位性能瓶颈与错误节点。

4.4 事件驱动架构中的角色

  • 生产者:LLM、Retriever、Tool 等模块不断产出事件
  • 汇聚器:CallbackHandler/Logger 将事件聚合成结构化日志/指标
  • 消费者:UI 进度条、打字机效果、报警器、监控看板

4.5 关键设计点

  • 可插拔:回调应可自由开关与组合,不影响核心逻辑
  • 异步安全:回调应快速、幂等,避免阻塞主执行
  • 隐私与合规:避免日志中泄露敏感信息(API Key、用户隐私)
  • 成本意识:只记录必要的信息;在压测/生产与开发模式中差异化开关

🧩 基础到进阶:Callback 编程模型(约 25%)

4.6 控制台回调(快速上手)

typescript 复制代码
// 文件:src/ch04/console-callback.ts
import { ChatOpenAI } from "@langchain/openai";
import { ConsoleCallbackHandler } from "@langchain/core/callbacks/console";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = PromptTemplate.fromTemplate("解释一下 {topic},要求简洁");
const model = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  callbacks: [new ConsoleCallbackHandler()],
  verbose: true,
});

const chain = prompt.pipe(model).pipe(new StringOutputParser());

export async function run() {
  const out = await chain.invoke({ topic: "虚拟列表(Virtualized List)" });
  console.log("输出:\n", out);
}

if (require.main === module) run();

4.7 自定义 CallbackHandler(收集指标/上报进度)

typescript 复制代码
// 文件:src/ch04/metrics-callback.ts
import type { BaseCallbackHandler } from "@langchain/core/callbacks/base";

export class MetricsHandler implements BaseCallbackHandler {
  name = "metrics-handler";

  async handleLLMStart(event) {
    console.log("[LLMStart]", { runId: event.runId, model: event.invocationParams?.model });
  }

  async handleLLMNewToken(token, _idx, _runId, _parentRunId, _tags, data) {
    // 可推送到 SSE/WebSocket
    process.stdout.write(token);
  }

  async handleLLMEnd(event) {
    console.log("\n[LLMEnd] tokenUsage:", event?.output?.llmOutput?.tokenUsage);
  }

  async handleChainStart(event) {
    console.log("[ChainStart]", { runId: event.runId, name: event.name });
  }

  async handleChainEnd(event) {
    console.log("[ChainEnd]", { runId: event.runId, durationMs: event?.duration });
  }

  async handleChainError(err, _runId) {
    console.error("[ChainError]", err?.message);
  }
}

4.8 将自定义回调注入 Runnable

typescript 复制代码
// 文件:src/ch04/with-metrics.ts
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { MetricsHandler } from "./metrics-callback";

const prompt = PromptTemplate.fromTemplate("将下面文本翻译成英文:{text}");
const model = new ChatOpenAI({ modelName: "gpt-3.5-turbo" });
const chain = prompt.pipe(model).pipe(new StringOutputParser());

export async function run() {
  const out = await chain.invoke(
    { text: "你好,性能优化" },
    { callbacks: [new MetricsHandler()], tags: ["demo", "translate"] }
  );
  console.log("\n结果:\n", out);
}

if (require.main === module) run();

4.9 流式输出到 CLI(打字机效果)

typescript 复制代码
// 文件:src/ch04/stream-cli.ts
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({ modelName: "gpt-3.5-turbo", streaming: true });

export async function run() {
  const stream = await model.stream("请用 3 句话介绍 LangChain.js");
  for await (const chunk of stream) {
    process.stdout.write(chunk.content);
  }
  process.stdout.write("\n--- 完成 ---\n");
}

if (require.main === module) run();

4.10 取消与超时(AbortController)

typescript 复制代码
// 文件:src/ch04/cancel.ts
import { ChatOpenAI } from "@langchain/openai";

export async function run() {
  const ctl = new AbortController();
  const model = new ChatOpenAI({ timeout: 20_000 });

  const p = model.invoke("解释 SSR/CSR/SSG 的区别", { signal: ctl.signal });
  setTimeout(() => ctl.abort(), 200); // 200ms 后取消

  try { await p; } catch (e) { console.log("已取消:", e.name); }
}

if (require.main === module) run();

🔗 Callback 与 Runnable/Memory/Agent 的协作(约 15%)

4.11 Runnable 回调融合

typescript 复制代码
// 文件:src/ch04/runnable-callback.ts
import { RunnableSequence } from "@langchain/core/runnables";
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { MetricsHandler } from "./metrics-callback";

const seq = RunnableSequence.from([
  PromptTemplate.fromTemplate("根据提纲生成 5 条要点:{outline}"),
  new ChatOpenAI({ temperature: 0.3 }),
  new StringOutputParser(),
]);

export async function run() {
  const text = await seq.invoke(
    { outline: "前端性能优化:资源、渲染、交互、网络、监控" },
    { callbacks: [new MetricsHandler()], tags: ["outline"] }
  );
  console.log(text);
}

if (require.main === module) run();

4.12 Agent 工具调用与回调

在 Agent 执行工具(Tool)时,每次工具调用都可触发 handleToolStart/End,便于记录"工具序列""失败重试""耗时/成本"。这对构建"任务时间线/可视化步骤"尤为关键。

伪代码:

typescript 复制代码
// 每个 tool.execute 前后打点;对失败工具触发重试与告警

4.13 Memory 读写上报

MessagesPlaceholder 注入历史前后,通过回调记录"注入了多少条历史""来源(Buffer/Summary/Vector)""Token 占比",便于后续优化与成本控制。


🌐 Next.js 实时推送:SSE 与 WebSocket(约 15%)

4.14 SSE 接口(Route Handlers)

typescript 复制代码
// 文件:src/app/api/stream/route.ts (Next.js 14+)
import { NextRequest } from "next/server";
import { ChatOpenAI } from "@langchain/openai";

export const runtime = "edge"; // 边缘更低延迟(可选)

export async function POST(req: NextRequest) {
  const { question } = await req.json();
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      const model = new ChatOpenAI({ modelName: "gpt-3.5-turbo", streaming: true });
      const s = await model.stream(question);
      for await (const chunk of s) {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ t: chunk.content })}\n\n`));
      }
      controller.close();
    },
  });
  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
  });
}

4.15 前端消费(打字机 + 取消)

tsx 复制代码
// 文件:src/app/page.tsx
"use client";
import { useEffect, useRef, useState } from "react";

export default function Page() {
  const [text, setText] = useState("");
  const esRef = useRef<EventSource | null>(null);

  const start = async () => {
    setText("");
    const res = await fetch("/api/stream", { method: "POST", body: JSON.stringify({ question: "介绍LangChain" }) });
    const url = res.url; // 在 edge 下可直接使用 res.body
    const es = new EventSource("/api/stream"); // 这里简化,生产应改为直接消费 body 流
    esRef.current = es;
    es.onmessage = (e) => {
      const { t } = JSON.parse(e.data);
      setText((prev) => prev + t);
    };
  };

  const stop = () => { esRef.current?.close(); };

  return (
    <main className="p-6 max-w-2xl mx-auto">
      <button onClick={start}>开始</button>
      <button onClick={stop} className="ml-2">停止</button>
      <pre className="mt-4 whitespace-pre-wrap break-words">{text}</pre>
    </main>
  );
}

4.16 WebSocket 方案(服务器推送多类型事件)

typescript 复制代码
// 文件:scripts/ws-server.ts(Node ws 简化示意)
import { WebSocketServer } from "ws";
import { ChatOpenAI } from "@langchain/openai";

const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
  ws.on("message", async (msg) => {
    const { q } = JSON.parse(String(msg));
    const model = new ChatOpenAI({ streaming: true });
    const stream = await model.stream(q);
    ws.send(JSON.stringify({ type: "start" }));
    for await (const chunk of stream) ws.send(JSON.stringify({ type: "token", t: chunk.content }));
    ws.send(JSON.stringify({ type: "end" }));
  });
});

🔍 监控、追踪与评测(约 10%)

4.17 LangSmith 集成

bash 复制代码
# .env
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=xxxx
LANGCHAIN_PROJECT=callback-demo

在回调中打标 tags/metadata,即可在 LangSmith 端聚合成 Run 树、性能分布与错误明细。

4.18 自建指标上报

  • 将回调事件转成结构化 JSON,写入 Kafka/ClickHouse/Elastic
  • 指标:QPS、P95/P99 延迟、Token 成本、错误率、重试率、成功率
  • 报警:基于阈值或异常检测的即时提醒(Slack/飞书/钉钉)

4.19 A/B 与回归评测

  • 基于 tags 在生产环境做灰度:PromptA vs PromptB、模型切换
  • 基于"固定问题集"做回归评测,监控回归率

🚀 实战项目一:实时聊天(SSE + Callback 进度)约 15%

4.20 项目目标

  • 打字机流式聊天;显示"模型思考中/消耗 token/当前步骤"
  • 支持取消、重试、复制、移动端适配

4.21 核心实现思路

  1. 服务端:Route Handler 返回 SSE,将 handleLLMNewToken 的 token 逐条写入
  2. 客户端:EventSource 累积 token 渲染,进度条显示 tokenUsage
  3. 错误:连接断开/超时自动重连;保留最后成功响应

4.22 关键代码(略同 4.14/4.15),增加进度事件

typescript 复制代码
// 服务端 token 计数
let tokens = 0;
handlerLLMNewToken = () => { tokens++; push({ type: "progress", tokens }) };

4.23 移动端体验

  • 输入框吸底、软键盘弹出防遮挡
  • 逐步渲染避免主线程卡顿;长文本断行与分段 append

🧠 实战项目二:Agent 步骤时间线(Callback 驱动)约 15%

4.24 目标

  • 展示 Agent 执行的"思考-工具-结果"时间线;每步耗时、是否重试、是否成功
  • 对失败工具自动重试,超时熔断;提供人工接管入口

4.25 方案要点

  • handleToolStart/End/Error 中写入步骤事件
  • UI 端订阅 WS/SSE 渲染卡片式步骤;失败高亮与重试按钮
  • 持久化步骤日志,支持搜索与回放

4.26 伪代码

typescript 复制代码
// server: on ToolStart -> ws.broadcast({ step: "search", status: "running" })
// on ToolEnd -> ws.broadcast({ step: "search", status: "done", duration })
// on ToolError -> ws.broadcast({ step: "search", status: "error", message })

⚙️ 性能、稳定性与安全(约 5%)

4.27 建议

  • 回调异步处理,避免阻塞主链路;批量刷新 UI 事件
  • 对外推送前做脱敏;不要记录原始密钥/隐私
  • backpressure/心跳保活;断线自动恢复
  • 组件化与依赖注入:可替换的 Handler(Console/Smith/Custom)

🧪 测试与调试(约 5%)

4.28 回调可测试性

  • 用"假模型/假回调"模拟事件序列,断言 UI 状态变化
  • Jest:通过注入 handler,校验 handleLLMNewToken 被调用次数

4.29 常见问题定位

  • 未触发回调:确认 callbacks 注入位置与 Runnable 组合顺序
  • 流式卡顿:检查 Node/边缘运行时的流式支持与 flush 方式
  • Token 统计不准:不同提供商的返回差异,统一转义

📚 参考与扩展

  • LangChain.js 回调文档:https://js.langchain.com/
  • LangSmith:https://docs.smith.langchain.com/
  • SSE 标准:https://developer.mozilla.org/docs/Web/API/Server-sent_events/Using_server-sent_events
  • WebSocket:https://developer.mozilla.org/docs/Web/API/WebSockets_API

✅ 本章小结

  • 掌握了 Callback 生命周期、Run 树与事件驱动设计
  • 实现了控制台/自定义回调、流式输出、取消与超时
  • 在 Next.js 中完成了 SSE/WS 的实时推送与前端打字机
  • 构建了"实时聊天"和"Agent 时间线"两个实战样例
  • 引入监控追踪、A/B 与成本/稳定性优化策略

🎯 下章预告

下一章《Runnable 接口与任务编排系统》中,我们将:

  • 深入 Runnable 的组合、分支、并行与缓存
  • 将复杂工作流抽象为可复用的流水线
  • 与 LangGraph 状态图联动,构建企业级编排

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

相关推荐
前往悬崖下寻宝的神三算11 小时前
Vue Router 也能“强类型”?vite-plugin-vue-typed-router 上手体验
前端·vue.js
鹏多多11 小时前
开发个人微信小程序类目选择/盈利方式/成本控制与服务器接入指南
前端·javascript·程序员
朦胧之11 小时前
前端项目设计
前端·vue.js·react.js
掘金安东尼11 小时前
前端周刊第429期(2025年8月25日–8月31日)
前端·javascript·面试
葫三生11 小时前
三生原理的“阴阳元”能否构造新的代数结构?
前端·人工智能·算法·机器学习·数学建模
大熊猫侯佩11 小时前
Apple 开发初学码农必看:一个 SwiftData 离奇古怪的问题(下)
ai编程·swift·apple
Moment11 小时前
该用 <img> 还是 new Image()?前端图片加载的决策指南 😌😌😌
前端·javascript·面试
小楓120111 小时前
MySQL數據庫開發教學(四) 後端與數據庫的交互
前端·数据库·后端·mysql
Mike_jia12 小时前
SSM平台:Ansible与Docker融合的运维革命——轻量级服务器智能管理指南
前端