第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
(嵌套链路)tags
、metadata
(自定义标记)- 输入/输出片段、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 核心实现思路
- 服务端:Route Handler 返回 SSE,将
handleLLMNewToken
的 token 逐条写入 - 客户端:EventSource 累积 token 渲染,进度条显示
tokenUsage
- 错误:连接断开/超时自动重连;保留最后成功响应
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 状态图联动,构建企业级编排
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!