使用 LangChain.js 在node端 连接glm大模型示例
使用 LangChain 在后端连接大模型:实践指南 🚀
本文以实战项目代码为例,包含完整后端接入、流式(SSE)实现、前端接收示例与调试方法,读者可直接复制运行。
介绍 ✨
随着大模型在各类应用中的普及,后端如何稳健地接入并把模型能力以 API/流式方式对外提供,成为常见需求。本文基于 LangChain(JS)演示如何在 Node.js 后端(Koa)中:
- 初始化并调用大模型(示例使用智谱 GLM 的接入方式)
- 支持普通请求与流式(Server-Sent Events,SSE)响应
- 在前端用 fetch 读取流并实现打字机效果
适用人群:熟悉 JS/TS、Node.js、前端基本知识,想把模型能力放到后端并对外提供 API 的工程师。
一、准备与依赖 🧩
环境:Node.js 16+。
安装依赖(Koa 示例):
npm install koa koa-router koa-bodyparser @koa/cors dotenv
npm install @langchain/openai @langchain/core
在项目根创建 .env:
ZHIPU_API_KEY=你的_api_key_here
PORT=3001
提示:不同模型提供方的 baseURL 与认证字段会不同,请根据提供方文档调整。
二、后端:服务封装(chatService)🔧
把模型调用封装到服务层,提供普通调用与流式调用接口:
// backend/src/services/chatService.js
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
const chatService = {
llm: null,
init() {
if (!this.llm) {
this.llm = new ChatOpenAI({
openAIApiKey: process.env.ZHIPU_API_KEY,
modelName: "glm-4.5-flash",
temperature: 0.7,
configuration: { baseURL: "https://open.bigmodel.cn/api/paas/v4/" },
});
}
},
async sendMessage(message, conversationHistory = []) {
this.init();
const messages = [
new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
...conversationHistory.map((msg) =>
msg.role === "user"
? new HumanMessage(msg.content)
: new SystemMessage(msg.content),
),
new HumanMessage(message),
];
try {
const response = await this.llm.invoke(messages);
return {
success: true,
content: response.content,
usage: response.usage_metadata,
};
} catch (error) {
console.error("聊天服务错误:", error);
return { success: false, error: error.message };
}
},
async sendMessageStream(message, conversationHistory = []) {
this.init();
const messages = [
new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
...conversationHistory.map((msg) =>
msg.role === "user"
? new HumanMessage(msg.content)
: new SystemMessage(msg.content),
),
new HumanMessage(message),
];
try {
// 假设 llm.stream 返回异步迭代器,逐 chunk 返回 { content }
const stream = await this.llm.stream(messages);
return { success: true, stream };
} catch (error) {
console.error("流式聊天服务错误:", error);
return { success: false, error: error.message };
}
},
};
export default chatService;
说明:实际 SDK 接口名(如 invoke、stream)请依据你所用的 LangChain / provider 版本调整。
三、控制器:普通与 SSE 流式(Koa)🌊
SSE 要点:需要直接写原生 res,并设置 ctx.respond = false,防止 Koa 在中间件链结束时覆盖响应或返回 404。
// backend/src/controllers/chatController.js
import chatService from "../services/chatService.js";
const chatController = {
async sendMessage(ctx) {
try {
const { message, conversationHistory = [] } = ctx.request.body;
if (!message) {
ctx.status = 400;
ctx.body = { success: false, error: "消息内容不能为空" };
return;
}
const result = await chatService.sendMessage(
message,
conversationHistory,
);
if (result.success) ctx.body = result;
else {
ctx.status = 500;
ctx.body = result;
}
} catch (error) {
ctx.status = 500;
ctx.body = { success: false, error: "服务器内部错误" };
}
},
async sendMessageStream(ctx) {
try {
const { message, conversationHistory = [] } = ctx.request.body;
if (!message) {
ctx.status = 400;
ctx.body = { success: false, error: "消息内容不能为空" };
return;
}
const result = await chatService.sendMessageStream(
message,
conversationHistory,
);
if (!result.success) {
ctx.status = 500;
ctx.body = result;
return;
}
ctx.set("Content-Type", "text/event-stream");
ctx.set("Cache-Control", "no-cache");
ctx.set("Connection", "keep-alive");
ctx.status = 200;
// 关键:让我们直接操作 Node 原生 res
ctx.respond = false;
for await (const chunk of result.stream) {
const content = chunk.content;
if (content) ctx.res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
ctx.res.write("data: [DONE]\n\n");
ctx.res.end();
} catch (error) {
console.error("流式控制器错误:", error);
ctx.status = 500;
ctx.body = { success: false, error: "服务器内部错误" };
}
},
};
export default chatController;
四、路由与启动 🌐
// backend/src/routes/index.js
import Router from "koa-router";
import chatController from "../controllers/chatController.js";
const router = new Router({ prefix: "/api" });
router.post("/chat", chatController.sendMessage);
router.post("/chat/stream", chatController.sendMessageStream);
export default router;
// backend/src/app.js
import dotenv from "dotenv";
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import cors from "@koa/cors";
import router from "./routes/index.js";
dotenv.config();
const app = new Koa();
app.use(cors({ origin: "*", credentials: true }));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(process.env.PORT || 3001, () => console.log("Server running"));
五、前端:接收 SSE 流并实现“打字机”效果 ⌨️
前端用 fetch + ReadableStream 读取 SSE 后端发送的 chunk(格式为 data: {...}\n\n)。下面给出简洁示例:
// frontend/src/services/chatService.ts (核心片段)
export const sendMessageStream = async (
message,
conversationHistory,
onChunk,
onComplete,
onError,
) => {
try {
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, conversationHistory }),
});
if (!response.ok) throw new Error("网络请求失败");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// 简单按行解析 SSE data: 行
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
onComplete();
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.content) onChunk(parsed.content);
} catch (e) {}
}
}
}
} catch (err) {
onError(err.message || "流式请求失败");
}
};
打字机效果思路(前端)📌:
- 后端 chunk 通常是按小段返回,前端把每个 chunk 追加到
buffer。 - 用一个定时器以固定速度(如 20–40ms/字符)把
buffer的字符逐个移动到展示内容,使文本逐字出现。 - onComplete 时快速显示剩余字符并停止定时器。
你可以参考项目中 App.tsx 的实现(已实现逐 chunk 追加与打字机渲染逻辑)。
App.tsx
import React, { useState } from "react";
import MessageList from "./components/MessageList";
import ChatInput from "./components/ChatInput";
import { chatService, Message } from "./services/chatService";
import "./App.css";
const App: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const handleSendMessage = async (message: string) => {
const userMessage: Message = { role: "user", content: message };
setMessages((prev) => [...prev, userMessage]);
setIsStreaming(true);
let assistantContent = "";
const conversationHistory = messages;
await chatService.sendMessageStream(
message,
conversationHistory,
(chunk: string) => {
assistantContent += chunk;
setMessages((prev) => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === "assistant") {
lastMessage.content = assistantContent;
} else {
newMessages.push({ role: "assistant", content: assistantContent });
}
return newMessages;
});
},
() => {
setIsStreaming(false);
},
(error: string) => {
console.error("流式响应错误:", error);
setMessages((prev) => [
...prev,
{ role: "assistant", content: "抱歉,发生了错误,请稍后重试。" },
]);
setIsStreaming(false);
},
);
};
return (
<div className="app">
<header className="app-header">
<h1>🤖 LangChain + 智谱 GLM</h1>
<p>AI 聊天助手</p>
</header>
<main className="app-main">
<MessageList messages={messages} isStreaming={isStreaming} />
<ChatInput onSendMessage={handleSendMessage} disabled={isStreaming} />
</main>
</div>
);
};
export default App;
MessageList
import React, { useRef, useEffect } from "react";
import { Message } from "../services/chatService";
interface MessageListProps {
messages: Message[];
isStreaming: boolean;
}
const MessageList: React.FC<MessageListProps> = ({ messages, isStreaming }) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isStreaming]);
return (
<div className="message-list">
{messages.length === 0 ? (
<div className="empty-state">
<p>👋 欢迎使用 LangChain + 智谱 GLM 聊天助手!</p>
<p>开始提问吧,我会尽力帮助你 💬</p>
</div>
) : (
messages.map((msg, index) => (
<div key={index} className={`message ${msg.role}`}>
<div className="message-avatar">
{msg.role === "user" ? "👤" : "🤖"}
</div>
<div className="message-content">
<div className="message-role">
{msg.role === "user" ? "用户" : "AI 助手"}
</div>
<div className="message-text">{msg.content}</div>
</div>
</div>
))
)}
{isStreaming && (
<div className="message assistant">
<div className="message-avatar">🤖</div>
<div className="message-content">
<div className="message-role">AI 助手</div>
<div className="message-text streaming">
<span className="typing-indicator">...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
);
};
export default MessageList;
六、调试建议与常见坑 ⚠️
- 404:确认前端请求路径
/api/chat/stream与路由前缀一致;开发时若使用 Vite,请在vite.config.ts配置 proxy 到后端端口。 - SSE 返回 404/空响应:确认控制器里
ctx.respond = false已设置,并且在设置 header 后立即开始ctx.res.write。 - headers already sent:不要在写入原生
res后再次设置ctx.body或ctx.status。 - CORS:若跨域,确保后端 CORS 配置允许
Content-Type、Authorization等必要 header。
七、快速上手测试命令 🧪
启动后端:
# 在 backend 目录下
node src/app.js
# 或使用 nodemon
npx nodemon src/app.js
用 curl 测试流式(查看快速返回流):
curl -N -H "Content-Type: application/json" -X POST http://localhost:3001/api/chat/stream -d '{"message":"你好","conversationHistory":[]} '
你应能看到类似:
data: {"content":"你"}
data: {"content":"好"}
data: [DONE]